##// END OF EJS Templates
hgweb: refactor hgweb code
Dirkjan Ochtman -
r6393:894875ea default
parent child Browse files
Show More
@@ -1,98 +1,97 b''
1 1 """
2 2 This is Mercurial extension for syntax highlighting in the file
3 3 revision view of hgweb.
4 4
5 5 It depends on the pygments syntax highlighting library:
6 6 http://pygments.org/
7 7
8 8 To enable the extension add this to hgrc:
9 9
10 10 [extensions]
11 11 hgext.highlight =
12 12
13 13 There is a single configuration option:
14 14
15 15 [web]
16 16 pygments_style = <style>
17 17
18 18 The default is 'colorful'. If this is changed the corresponding CSS
19 19 file should be re-generated by running
20 20
21 21 # pygmentize -f html -S <newstyle>
22 22
23 23
24 24 -- Adam Hupp <adam@hupp.org>
25 25
26 26
27 27 """
28 28
29 29 from mercurial import demandimport
30 30 demandimport.ignore.extend(['pkgutil',
31 31 'pkg_resources',
32 32 '__main__',])
33 33
34 from mercurial.hgweb.hgweb_mod import hgweb
34 from mercurial.hgweb import webcommands, webutil
35 35 from mercurial import util
36 36 from mercurial.templatefilters import filters
37 37
38 38 from pygments import highlight
39 39 from pygments.util import ClassNotFound
40 40 from pygments.lexers import guess_lexer, guess_lexer_for_filename, TextLexer
41 41 from pygments.formatters import HtmlFormatter
42 42
43 43 SYNTAX_CSS = ('\n<link rel="stylesheet" href="#staticurl#highlight.css" '
44 44 'type="text/css" />')
45 45
46 46 def pygmentize(self, tmpl, fctx, field):
47 47 # append a <link ...> to the syntax highlighting css
48 48 old_header = ''.join(tmpl('header'))
49 49 if SYNTAX_CSS not in old_header:
50 50 new_header = old_header + SYNTAX_CSS
51 51 tmpl.cache['header'] = new_header
52 52
53 53 text = fctx.data()
54 54 if util.binary(text):
55 55 return
56 56
57 57 style = self.config("web", "pygments_style", "colorful")
58 58 # To get multi-line strings right, we can't format line-by-line
59 59 try:
60 60 lexer = guess_lexer_for_filename(fctx.path(), text,
61 61 encoding=util._encoding)
62 62 except ClassNotFound:
63 63 try:
64 64 lexer = guess_lexer(text, encoding=util._encoding)
65 65 except ClassNotFound:
66 66 lexer = TextLexer(encoding=util._encoding)
67 67
68 68 formatter = HtmlFormatter(style=style, encoding=util._encoding)
69 69
70 70 colorized = highlight(text, lexer, formatter)
71 71 # strip wrapping div
72 72 colorized = colorized[:colorized.find('\n</pre>')]
73 73 colorized = colorized[colorized.find('<pre>')+5:]
74 74 coloriter = iter(colorized.splitlines())
75 75
76 76 filters['colorize'] = lambda x: coloriter.next()
77 77
78 78 oldl = tmpl.cache[field]
79 79 newl = oldl.replace('line|escape', 'line|colorize')
80 80 tmpl.cache[field] = newl
81 81
82 def filerevision_highlight(self, tmpl, fctx):
83 pygmentize(self, tmpl, fctx, 'fileline')
84
85 return realrevision(self, tmpl, fctx)
82 web_filerevision = webcommands._filerevision
83 web_annotate = webcommands.annotate
86 84
87 def fileannotate_highlight(self, tmpl, fctx):
88 pygmentize(self, tmpl, fctx, 'annotateline')
85 def filerevision_highlight(web, tmpl, fctx):
86 pygmentize(web, tmpl, fctx, 'fileline')
87 return web_filerevision(web, tmpl, fctx)
89 88
90 return realannotate(self, tmpl, fctx)
89 def annotate_highlight(web, req, tmpl):
90 fctx = webutil.filectx(web.repo, req)
91 pygmentize(web, tmpl, fctx, 'annotateline')
92 return web_annotate(web, req, tmpl)
91 93
92 94 # monkeypatch in the new version
93 # should be safer than overriding the method in a derived class
94 # and then patching the class
95 realrevision = hgweb.filerevision
96 hgweb.filerevision = filerevision_highlight
97 realannotate = hgweb.fileannotate
98 hgweb.fileannotate = fileannotate_highlight
95
96 webcommands._filerevision = filerevision_highlight
97 webcommands.annotate = annotate_highlight
@@ -1,556 +1,562 b''
1 1 # keyword.py - $Keyword$ expansion for Mercurial
2 2 #
3 3 # Copyright 2007, 2008 Christian Ebert <blacktrash@gmx.net>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7 #
8 8 # $Id$
9 9 #
10 10 # Keyword expansion hack against the grain of a DSCM
11 11 #
12 12 # There are many good reasons why this is not needed in a distributed
13 13 # SCM, still it may be useful in very small projects based on single
14 14 # files (like LaTeX packages), that are mostly addressed to an audience
15 15 # not running a version control system.
16 16 #
17 17 # For in-depth discussion refer to
18 18 # <http://www.selenic.com/mercurial/wiki/index.cgi/KeywordPlan>.
19 19 #
20 20 # Keyword expansion is based on Mercurial's changeset template mappings.
21 21 #
22 22 # Binary files are not touched.
23 23 #
24 24 # Setup in hgrc:
25 25 #
26 26 # [extensions]
27 27 # # enable extension
28 28 # hgext.keyword =
29 29 #
30 30 # Files to act upon/ignore are specified in the [keyword] section.
31 31 # Customized keyword template mappings in the [keywordmaps] section.
32 32 #
33 33 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
34 34
35 35 '''keyword expansion in local repositories
36 36
37 37 This extension expands RCS/CVS-like or self-customized $Keywords$
38 38 in tracked text files selected by your configuration.
39 39
40 40 Keywords are only expanded in local repositories and not stored in
41 41 the change history. The mechanism can be regarded as a convenience
42 42 for the current user or for archive distribution.
43 43
44 44 Configuration is done in the [keyword] and [keywordmaps] sections
45 45 of hgrc files.
46 46
47 47 Example:
48 48
49 49 [keyword]
50 50 # expand keywords in every python file except those matching "x*"
51 51 **.py =
52 52 x* = ignore
53 53
54 54 Note: the more specific you are in your filename patterns
55 55 the less you lose speed in huge repos.
56 56
57 57 For [keywordmaps] template mapping and expansion demonstration and
58 58 control run "hg kwdemo".
59 59
60 60 An additional date template filter {date|utcdate} is provided.
61 61
62 62 The default template mappings (view with "hg kwdemo -d") can be replaced
63 63 with customized keywords and templates.
64 64 Again, run "hg kwdemo" to control the results of your config changes.
65 65
66 66 Before changing/disabling active keywords, run "hg kwshrink" to avoid
67 67 the risk of inadvertedly storing expanded keywords in the change history.
68 68
69 69 To force expansion after enabling it, or a configuration change, run
70 70 "hg kwexpand".
71 71
72 72 Also, when committing with the record extension or using mq's qrecord, be aware
73 73 that keywords cannot be updated. Again, run "hg kwexpand" on the files in
74 74 question to update keyword expansions after all changes have been checked in.
75 75
76 76 Expansions spanning more than one line and incremental expansions,
77 77 like CVS' $Log$, are not supported. A keyword template map
78 78 "Log = {desc}" expands to the first line of the changeset description.
79 79 '''
80 80
81 81 from mercurial import commands, cmdutil, context, dispatch, filelog, revlog
82 82 from mercurial import patch, localrepo, templater, templatefilters, util
83 83 from mercurial.hgweb import webcommands
84 84 from mercurial.node import nullid, hex
85 85 from mercurial.i18n import _
86 86 import re, shutil, tempfile, time
87 87
88 88 commands.optionalrepo += ' kwdemo'
89 89
90 90 # hg commands that do not act on keywords
91 91 nokwcommands = ('add addremove bundle copy export grep incoming init'
92 92 ' log outgoing push rename rollback tip'
93 93 ' convert email glog')
94 94
95 95 # hg commands that trigger expansion only when writing to working dir,
96 96 # not when reading filelog, and unexpand when reading from working dir
97 97 restricted = 'record qfold qimport qnew qpush qrefresh qrecord'
98 98
99 99 def utcdate(date):
100 100 '''Returns hgdate in cvs-like UTC format.'''
101 101 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
102 102
103 103
104 104 # make keyword tools accessible
105 105 kwtools = {'templater': None, 'hgcmd': None}
106 106
107 107 # store originals of monkeypatches
108 108 _patchfile_init = patch.patchfile.__init__
109 109 _patch_diff = patch.diff
110 110 _dispatch_parse = dispatch._parse
111 111
112 112 def _kwpatchfile_init(self, ui, fname, missing=False):
113 113 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
114 114 rejects or conflicts due to expanded keywords in working dir.'''
115 115 _patchfile_init(self, ui, fname, missing=missing)
116 116 # shrink keywords read from working dir
117 117 kwt = kwtools['templater']
118 118 self.lines = kwt.shrinklines(self.fname, self.lines)
119 119
120 120 def _kw_diff(repo, node1=None, node2=None, files=None, match=util.always,
121 121 fp=None, changes=None, opts=None):
122 122 '''Monkeypatch patch.diff to avoid expansion except when
123 123 comparing against working dir.'''
124 124 if node2 is not None:
125 125 kwtools['templater'].matcher = util.never
126 126 elif node1 is not None and node1 != repo.changectx().node():
127 127 kwtools['templater'].restrict = True
128 128 _patch_diff(repo, node1=node1, node2=node2, files=files, match=match,
129 129 fp=fp, changes=changes, opts=opts)
130 130
131 # monkeypatching hgweb functions changeset and filediff
132 # actual monkeypatching is done at the bottom of reposetup()
133
134 web_changeset = webcommands.changeset
135 web_filediff = webcommands.filediff
136
131 137 def _kwweb_changeset(web, req, tmpl):
132 138 '''Wraps webcommands.changeset turning off keyword expansion.'''
133 139 kwtools['templater'].matcher = util.never
134 return web.changeset(tmpl, web.changectx(req))
140 return web_changeset(web, req, tmpl)
135 141
136 142 def _kwweb_filediff(web, req, tmpl):
137 143 '''Wraps webcommands.filediff turning off keyword expansion.'''
138 144 kwtools['templater'].matcher = util.never
139 return web.filediff(tmpl, web.filectx(req))
145 return web_filediff(web, req, tmpl)
140 146
141 147 def _kwdispatch_parse(ui, args):
142 148 '''Monkeypatch dispatch._parse to obtain running hg command.'''
143 149 cmd, func, args, options, cmdoptions = _dispatch_parse(ui, args)
144 150 kwtools['hgcmd'] = cmd
145 151 return cmd, func, args, options, cmdoptions
146 152
147 153 # dispatch._parse is run before reposetup, so wrap it here
148 154 dispatch._parse = _kwdispatch_parse
149 155
150 156
151 157 class kwtemplater(object):
152 158 '''
153 159 Sets up keyword templates, corresponding keyword regex, and
154 160 provides keyword substitution functions.
155 161 '''
156 162 templates = {
157 163 'Revision': '{node|short}',
158 164 'Author': '{author|user}',
159 165 'Date': '{date|utcdate}',
160 166 'RCSFile': '{file|basename},v',
161 167 'Source': '{root}/{file},v',
162 168 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
163 169 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
164 170 }
165 171
166 172 def __init__(self, ui, repo, inc, exc):
167 173 self.ui = ui
168 174 self.repo = repo
169 175 self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
170 176 self.restrict = kwtools['hgcmd'] in restricted.split()
171 177
172 178 kwmaps = self.ui.configitems('keywordmaps')
173 179 if kwmaps: # override default templates
174 180 kwmaps = [(k, templater.parsestring(v, quoted=False))
175 181 for (k, v) in kwmaps]
176 182 self.templates = dict(kwmaps)
177 183 escaped = map(re.escape, self.templates.keys())
178 184 kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
179 185 self.re_kw = re.compile(kwpat)
180 186
181 187 templatefilters.filters['utcdate'] = utcdate
182 188 self.ct = cmdutil.changeset_templater(self.ui, self.repo,
183 189 False, '', False)
184 190
185 191 def getnode(self, path, fnode):
186 192 '''Derives changenode from file path and filenode.'''
187 193 # used by kwfilelog.read and kwexpand
188 194 c = context.filectx(self.repo, path, fileid=fnode)
189 195 return c.node()
190 196
191 197 def substitute(self, data, path, node, subfunc):
192 198 '''Replaces keywords in data with expanded template.'''
193 199 def kwsub(mobj):
194 200 kw = mobj.group(1)
195 201 self.ct.use_template(self.templates[kw])
196 202 self.ui.pushbuffer()
197 203 self.ct.show(changenode=node, root=self.repo.root, file=path)
198 204 ekw = templatefilters.firstline(self.ui.popbuffer())
199 205 return '$%s: %s $' % (kw, ekw)
200 206 return subfunc(kwsub, data)
201 207
202 208 def expand(self, path, node, data):
203 209 '''Returns data with keywords expanded.'''
204 210 if not self.restrict and self.matcher(path) and not util.binary(data):
205 211 changenode = self.getnode(path, node)
206 212 return self.substitute(data, path, changenode, self.re_kw.sub)
207 213 return data
208 214
209 215 def iskwfile(self, path, islink):
210 216 '''Returns true if path matches [keyword] pattern
211 217 and is not a symbolic link.
212 218 Caveat: localrepository._link fails on Windows.'''
213 219 return self.matcher(path) and not islink(path)
214 220
215 221 def overwrite(self, node=None, expand=True, files=None):
216 222 '''Overwrites selected files expanding/shrinking keywords.'''
217 223 ctx = self.repo.changectx(node)
218 224 mf = ctx.manifest()
219 225 if node is not None: # commit
220 226 files = [f for f in ctx.files() if f in mf]
221 227 notify = self.ui.debug
222 228 else: # kwexpand/kwshrink
223 229 notify = self.ui.note
224 230 candidates = [f for f in files if self.iskwfile(f, mf.linkf)]
225 231 if candidates:
226 232 self.restrict = True # do not expand when reading
227 233 candidates.sort()
228 234 action = expand and 'expanding' or 'shrinking'
229 235 for f in candidates:
230 236 fp = self.repo.file(f)
231 237 data = fp.read(mf[f])
232 238 if util.binary(data):
233 239 continue
234 240 if expand:
235 241 changenode = node or self.getnode(f, mf[f])
236 242 data, found = self.substitute(data, f, changenode,
237 243 self.re_kw.subn)
238 244 else:
239 245 found = self.re_kw.search(data)
240 246 if found:
241 247 notify(_('overwriting %s %s keywords\n') % (f, action))
242 248 self.repo.wwrite(f, data, mf.flags(f))
243 249 self.repo.dirstate.normal(f)
244 250 self.restrict = False
245 251
246 252 def shrinktext(self, text):
247 253 '''Unconditionally removes all keyword substitutions from text.'''
248 254 return self.re_kw.sub(r'$\1$', text)
249 255
250 256 def shrink(self, fname, text):
251 257 '''Returns text with all keyword substitutions removed.'''
252 258 if self.matcher(fname) and not util.binary(text):
253 259 return self.shrinktext(text)
254 260 return text
255 261
256 262 def shrinklines(self, fname, lines):
257 263 '''Returns lines with keyword substitutions removed.'''
258 264 if self.matcher(fname):
259 265 text = ''.join(lines)
260 266 if not util.binary(text):
261 267 return self.shrinktext(text).splitlines(True)
262 268 return lines
263 269
264 270 def wread(self, fname, data):
265 271 '''If in restricted mode returns data read from wdir with
266 272 keyword substitutions removed.'''
267 273 return self.restrict and self.shrink(fname, data) or data
268 274
269 275 class kwfilelog(filelog.filelog):
270 276 '''
271 277 Subclass of filelog to hook into its read, add, cmp methods.
272 278 Keywords are "stored" unexpanded, and processed on reading.
273 279 '''
274 280 def __init__(self, opener, path):
275 281 super(kwfilelog, self).__init__(opener, path)
276 282 self.kwt = kwtools['templater']
277 283 self.path = path
278 284
279 285 def read(self, node):
280 286 '''Expands keywords when reading filelog.'''
281 287 data = super(kwfilelog, self).read(node)
282 288 return self.kwt.expand(self.path, node, data)
283 289
284 290 def add(self, text, meta, tr, link, p1=None, p2=None):
285 291 '''Removes keyword substitutions when adding to filelog.'''
286 292 text = self.kwt.shrink(self.path, text)
287 293 return super(kwfilelog, self).add(text, meta, tr, link, p1=p1, p2=p2)
288 294
289 295 def cmp(self, node, text):
290 296 '''Removes keyword substitutions for comparison.'''
291 297 text = self.kwt.shrink(self.path, text)
292 298 if self.renamed(node):
293 299 t2 = super(kwfilelog, self).read(node)
294 300 return t2 != text
295 301 return revlog.revlog.cmp(self, node, text)
296 302
297 303 def _status(ui, repo, kwt, *pats, **opts):
298 304 '''Bails out if [keyword] configuration is not active.
299 305 Returns status of working directory.'''
300 306 if kwt:
301 307 files, match, anypats = cmdutil.matchpats(repo, pats, opts)
302 308 return repo.status(files=files, match=match, list_clean=True)
303 309 if ui.configitems('keyword'):
304 310 raise util.Abort(_('[keyword] patterns cannot match'))
305 311 raise util.Abort(_('no [keyword] patterns configured'))
306 312
307 313 def _kwfwrite(ui, repo, expand, *pats, **opts):
308 314 '''Selects files and passes them to kwtemplater.overwrite.'''
309 315 kwt = kwtools['templater']
310 316 status = _status(ui, repo, kwt, *pats, **opts)
311 317 modified, added, removed, deleted, unknown, ignored, clean = status
312 318 if modified or added or removed or deleted:
313 319 raise util.Abort(_('outstanding uncommitted changes in given files'))
314 320 wlock = lock = None
315 321 try:
316 322 wlock = repo.wlock()
317 323 lock = repo.lock()
318 324 kwt.overwrite(expand=expand, files=clean)
319 325 finally:
320 326 del wlock, lock
321 327
322 328
323 329 def demo(ui, repo, *args, **opts):
324 330 '''print [keywordmaps] configuration and an expansion example
325 331
326 332 Show current, custom, or default keyword template maps
327 333 and their expansion.
328 334
329 335 Extend current configuration by specifying maps as arguments
330 336 and optionally by reading from an additional hgrc file.
331 337
332 338 Override current keyword template maps with "default" option.
333 339 '''
334 340 def demostatus(stat):
335 341 ui.status(_('\n\t%s\n') % stat)
336 342
337 343 def demoitems(section, items):
338 344 ui.write('[%s]\n' % section)
339 345 for k, v in items:
340 346 ui.write('%s = %s\n' % (k, v))
341 347
342 348 msg = 'hg keyword config and expansion example'
343 349 kwstatus = 'current'
344 350 fn = 'demo.txt'
345 351 branchname = 'demobranch'
346 352 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
347 353 ui.note(_('creating temporary repo at %s\n') % tmpdir)
348 354 repo = localrepo.localrepository(ui, path=tmpdir, create=True)
349 355 ui.setconfig('keyword', fn, '')
350 356 if args or opts.get('rcfile'):
351 357 kwstatus = 'custom'
352 358 if opts.get('rcfile'):
353 359 ui.readconfig(opts.get('rcfile'))
354 360 if opts.get('default'):
355 361 kwstatus = 'default'
356 362 kwmaps = kwtemplater.templates
357 363 if ui.configitems('keywordmaps'):
358 364 # override maps from optional rcfile
359 365 for k, v in kwmaps.iteritems():
360 366 ui.setconfig('keywordmaps', k, v)
361 367 elif args:
362 368 # simulate hgrc parsing
363 369 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
364 370 fp = repo.opener('hgrc', 'w')
365 371 fp.writelines(rcmaps)
366 372 fp.close()
367 373 ui.readconfig(repo.join('hgrc'))
368 374 if not opts.get('default'):
369 375 kwmaps = dict(ui.configitems('keywordmaps')) or kwtemplater.templates
370 376 reposetup(ui, repo)
371 377 for k, v in ui.configitems('extensions'):
372 378 if k.endswith('keyword'):
373 379 extension = '%s = %s' % (k, v)
374 380 break
375 381 demostatus('config using %s keyword template maps' % kwstatus)
376 382 ui.write('[extensions]\n%s\n' % extension)
377 383 demoitems('keyword', ui.configitems('keyword'))
378 384 demoitems('keywordmaps', kwmaps.iteritems())
379 385 keywords = '$' + '$\n$'.join(kwmaps.keys()) + '$\n'
380 386 repo.wopener(fn, 'w').write(keywords)
381 387 repo.add([fn])
382 388 path = repo.wjoin(fn)
383 389 ui.note(_('\n%s keywords written to %s:\n') % (kwstatus, path))
384 390 ui.note(keywords)
385 391 ui.note('\nhg -R "%s" branch "%s"\n' % (tmpdir, branchname))
386 392 # silence branch command if not verbose
387 393 quiet = ui.quiet
388 394 ui.quiet = not ui.verbose
389 395 commands.branch(ui, repo, branchname)
390 396 ui.quiet = quiet
391 397 for name, cmd in ui.configitems('hooks'):
392 398 if name.split('.', 1)[0].find('commit') > -1:
393 399 repo.ui.setconfig('hooks', name, '')
394 400 ui.note(_('unhooked all commit hooks\n'))
395 401 ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg))
396 402 repo.commit(text=msg)
397 403 format = ui.verbose and ' in %s' % path or ''
398 404 demostatus('%s keywords expanded%s' % (kwstatus, format))
399 405 ui.write(repo.wread(fn))
400 406 ui.debug(_('\nremoving temporary repo %s\n') % tmpdir)
401 407 shutil.rmtree(tmpdir, ignore_errors=True)
402 408
403 409 def expand(ui, repo, *pats, **opts):
404 410 '''expand keywords in working directory
405 411
406 412 Run after (re)enabling keyword expansion.
407 413
408 414 kwexpand refuses to run if given files contain local changes.
409 415 '''
410 416 # 3rd argument sets expansion to True
411 417 _kwfwrite(ui, repo, True, *pats, **opts)
412 418
413 419 def files(ui, repo, *pats, **opts):
414 420 '''print files currently configured for keyword expansion
415 421
416 422 Crosscheck which files in working directory are potential targets for
417 423 keyword expansion.
418 424 That is, files matched by [keyword] config patterns but not symlinks.
419 425 '''
420 426 kwt = kwtools['templater']
421 427 status = _status(ui, repo, kwt, *pats, **opts)
422 428 modified, added, removed, deleted, unknown, ignored, clean = status
423 429 files = modified + added + clean
424 430 if opts.get('untracked'):
425 431 files += unknown
426 432 files.sort()
427 433 wctx = repo.workingctx()
428 434 islink = lambda p: 'l' in wctx.fileflags(p)
429 435 kwfiles = [f for f in files if kwt.iskwfile(f, islink)]
430 436 cwd = pats and repo.getcwd() or ''
431 437 kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
432 438 if opts.get('all') or opts.get('ignore'):
433 439 kwfstats += (('I', [f for f in files if f not in kwfiles]),)
434 440 for char, filenames in kwfstats:
435 441 format = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
436 442 for f in filenames:
437 443 ui.write(format % repo.pathto(f, cwd))
438 444
439 445 def shrink(ui, repo, *pats, **opts):
440 446 '''revert expanded keywords in working directory
441 447
442 448 Run before changing/disabling active keywords
443 449 or if you experience problems with "hg import" or "hg merge".
444 450
445 451 kwshrink refuses to run if given files contain local changes.
446 452 '''
447 453 # 3rd argument sets expansion to False
448 454 _kwfwrite(ui, repo, False, *pats, **opts)
449 455
450 456
451 457 def reposetup(ui, repo):
452 458 '''Sets up repo as kwrepo for keyword substitution.
453 459 Overrides file method to return kwfilelog instead of filelog
454 460 if file matches user configuration.
455 461 Wraps commit to overwrite configured files with updated
456 462 keyword substitutions.
457 463 This is done for local repos only, and only if there are
458 464 files configured at all for keyword substitution.'''
459 465
460 466 try:
461 467 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
462 468 or '.hg' in util.splitpath(repo.root)
463 469 or repo._url.startswith('bundle:')):
464 470 return
465 471 except AttributeError:
466 472 pass
467 473
468 474 inc, exc = [], ['.hg*']
469 475 for pat, opt in ui.configitems('keyword'):
470 476 if opt != 'ignore':
471 477 inc.append(pat)
472 478 else:
473 479 exc.append(pat)
474 480 if not inc:
475 481 return
476 482
477 483 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
478 484
479 485 class kwrepo(repo.__class__):
480 486 def file(self, f):
481 487 if f[0] == '/':
482 488 f = f[1:]
483 489 return kwfilelog(self.sopener, f)
484 490
485 491 def wread(self, filename):
486 492 data = super(kwrepo, self).wread(filename)
487 493 return kwt.wread(filename, data)
488 494
489 495 def commit(self, files=None, text='', user=None, date=None,
490 496 match=util.always, force=False, force_editor=False,
491 497 p1=None, p2=None, extra={}, empty_ok=False):
492 498 wlock = lock = None
493 499 _p1 = _p2 = None
494 500 try:
495 501 wlock = self.wlock()
496 502 lock = self.lock()
497 503 # store and postpone commit hooks
498 504 commithooks = {}
499 505 for name, cmd in ui.configitems('hooks'):
500 506 if name.split('.', 1)[0] == 'commit':
501 507 commithooks[name] = cmd
502 508 ui.setconfig('hooks', name, None)
503 509 if commithooks:
504 510 # store parents for commit hook environment
505 511 if p1 is None:
506 512 _p1, _p2 = repo.dirstate.parents()
507 513 else:
508 514 _p1, _p2 = p1, p2 or nullid
509 515 _p1 = hex(_p1)
510 516 if _p2 == nullid:
511 517 _p2 = ''
512 518 else:
513 519 _p2 = hex(_p2)
514 520
515 521 node = super(kwrepo,
516 522 self).commit(files=files, text=text, user=user,
517 523 date=date, match=match, force=force,
518 524 force_editor=force_editor,
519 525 p1=p1, p2=p2, extra=extra,
520 526 empty_ok=empty_ok)
521 527
522 528 # restore commit hooks
523 529 for name, cmd in commithooks.iteritems():
524 530 ui.setconfig('hooks', name, cmd)
525 531 if node is not None:
526 532 kwt.overwrite(node=node)
527 533 repo.hook('commit', node=node, parent1=_p1, parent2=_p2)
528 534 return node
529 535 finally:
530 536 del wlock, lock
531 537
532 538 repo.__class__ = kwrepo
533 539 patch.patchfile.__init__ = _kwpatchfile_init
534 540 patch.diff = _kw_diff
535 541 webcommands.changeset = webcommands.rev = _kwweb_changeset
536 542 webcommands.filediff = webcommands.diff = _kwweb_filediff
537 543
538 544
539 545 cmdtable = {
540 546 'kwdemo':
541 547 (demo,
542 548 [('d', 'default', None, _('show default keyword template maps')),
543 549 ('f', 'rcfile', [], _('read maps from rcfile'))],
544 550 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
545 551 'kwexpand': (expand, commands.walkopts,
546 552 _('hg kwexpand [OPTION]... [FILE]...')),
547 553 'kwfiles':
548 554 (files,
549 555 [('a', 'all', None, _('show keyword status flags of all files')),
550 556 ('i', 'ignore', None, _('show files excluded from expansion')),
551 557 ('u', 'untracked', None, _('additionally show untracked files')),
552 558 ] + commands.walkopts,
553 559 _('hg kwfiles [OPTION]... [FILE]...')),
554 560 'kwshrink': (shrink, commands.walkopts,
555 561 _('hg kwshrink [OPTION]... [FILE]...')),
556 562 }
This diff has been collapsed as it changes many lines, (521 lines changed) Show them Hide them
@@ -1,892 +1,379 b''
1 1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 import os, mimetypes, re
10 from mercurial.node import hex, nullid, short
9 import os, mimetypes
10 from mercurial.node import hex, nullid
11 11 from mercurial.repo import RepoError
12 from mercurial import mdiff, ui, hg, util, archival, patch, hook
12 from mercurial import mdiff, ui, hg, util, patch, hook
13 13 from mercurial import revlog, templater, templatefilters, changegroup
14 from common import get_mtime, style_map, paritygen, countgen, get_contact
15 from common import ErrorResponse
14 from common import get_mtime, style_map, paritygen, countgen, ErrorResponse
16 15 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
17 16 from request import wsgirequest
18 17 import webcommands, protocol, webutil
19 18
20 19 shortcuts = {
21 20 'cl': [('cmd', ['changelog']), ('rev', None)],
22 21 'sl': [('cmd', ['shortlog']), ('rev', None)],
23 22 'cs': [('cmd', ['changeset']), ('node', None)],
24 23 'f': [('cmd', ['file']), ('filenode', None)],
25 24 'fl': [('cmd', ['filelog']), ('filenode', None)],
26 25 'fd': [('cmd', ['filediff']), ('node', None)],
27 26 'fa': [('cmd', ['annotate']), ('filenode', None)],
28 27 'mf': [('cmd', ['manifest']), ('manifest', None)],
29 28 'ca': [('cmd', ['archive']), ('node', None)],
30 29 'tags': [('cmd', ['tags'])],
31 30 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
32 31 'static': [('cmd', ['static']), ('file', None)]
33 32 }
34 33
35 def _up(p):
36 if p[0] != "/":
37 p = "/" + p
38 if p[-1] == "/":
39 p = p[:-1]
40 up = os.path.dirname(p)
41 if up == "/":
42 return "/"
43 return up + "/"
44
45 def revnavgen(pos, pagelen, limit, nodefunc):
46 def seq(factor, limit=None):
47 if limit:
48 yield limit
49 if limit >= 20 and limit <= 40:
50 yield 50
51 else:
52 yield 1 * factor
53 yield 3 * factor
54 for f in seq(factor * 10):
55 yield f
56
57 def nav(**map):
58 l = []
59 last = 0
60 for f in seq(1, pagelen):
61 if f < pagelen or f <= last:
62 continue
63 if f > limit:
64 break
65 last = f
66 if pos + f < limit:
67 l.append(("+%d" % f, hex(nodefunc(pos + f).node())))
68 if pos - f >= 0:
69 l.insert(0, ("-%d" % f, hex(nodefunc(pos - f).node())))
70
71 try:
72 yield {"label": "(0)", "node": hex(nodefunc('0').node())}
73
74 for label, node in l:
75 yield {"label": label, "node": node}
76
77 yield {"label": "tip", "node": "tip"}
78 except RepoError:
79 pass
80
81 return nav
82
83 34 class hgweb(object):
84 35 def __init__(self, repo, name=None):
85 36 if isinstance(repo, str):
86 37 parentui = ui.ui(report_untrusted=False, interactive=False)
87 38 self.repo = hg.repository(parentui, repo)
88 39 else:
89 40 self.repo = repo
90 41
91 42 hook.redirect(True)
92 43 self.mtime = -1
93 44 self.reponame = name
94 45 self.archives = 'zip', 'gz', 'bz2'
95 46 self.stripecount = 1
96 47 self._capabilities = None
97 48 # a repo owner may set web.templates in .hg/hgrc to get any file
98 49 # readable by the user running the CGI script
99 50 self.templatepath = self.config("web", "templates",
100 51 templater.templatepath(),
101 52 untrusted=False)
102 53
103 54 # The CGI scripts are often run by a user different from the repo owner.
104 55 # Trust the settings from the .hg/hgrc files by default.
105 56 def config(self, section, name, default=None, untrusted=True):
106 57 return self.repo.ui.config(section, name, default,
107 58 untrusted=untrusted)
108 59
109 60 def configbool(self, section, name, default=False, untrusted=True):
110 61 return self.repo.ui.configbool(section, name, default,
111 62 untrusted=untrusted)
112 63
113 64 def configlist(self, section, name, default=None, untrusted=True):
114 65 return self.repo.ui.configlist(section, name, default,
115 66 untrusted=untrusted)
116 67
117 68 def refresh(self):
118 69 mtime = get_mtime(self.repo.root)
119 70 if mtime != self.mtime:
120 71 self.mtime = mtime
121 72 self.repo = hg.repository(self.repo.ui, self.repo.root)
122 73 self.maxchanges = int(self.config("web", "maxchanges", 10))
123 74 self.stripecount = int(self.config("web", "stripes", 1))
124 75 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
125 76 self.maxfiles = int(self.config("web", "maxfiles", 10))
126 77 self.allowpull = self.configbool("web", "allowpull", True)
127 78 self.encoding = self.config("web", "encoding", util._encoding)
128 79 self._capabilities = None
129 80
130 81 def capabilities(self):
131 82 if self._capabilities is not None:
132 83 return self._capabilities
133 84 caps = ['lookup', 'changegroupsubset']
134 85 if self.configbool('server', 'uncompressed'):
135 86 caps.append('stream=%d' % self.repo.changelog.version)
136 87 if changegroup.bundlepriority:
137 88 caps.append('unbundle=%s' % ','.join(changegroup.bundlepriority))
138 89 self._capabilities = caps
139 90 return caps
140 91
141 92 def run(self):
142 93 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
143 94 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
144 95 import mercurial.hgweb.wsgicgi as wsgicgi
145 96 wsgicgi.launch(self)
146 97
147 98 def __call__(self, env, respond):
148 99 req = wsgirequest(env, respond)
149 100 self.run_wsgi(req)
150 101 return req
151 102
152 103 def run_wsgi(self, req):
153 104
154 105 self.refresh()
155 106
156 107 # expand form shortcuts
157 108
158 109 for k in shortcuts.iterkeys():
159 110 if k in req.form:
160 111 for name, value in shortcuts[k]:
161 112 if value is None:
162 113 value = req.form[k]
163 114 req.form[name] = value
164 115 del req.form[k]
165 116
166 117 # work with CGI variables to create coherent structure
167 118 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
168 119
169 120 req.url = req.env['SCRIPT_NAME']
170 121 if not req.url.endswith('/'):
171 122 req.url += '/'
172 123 if 'REPO_NAME' in req.env:
173 124 req.url += req.env['REPO_NAME'] + '/'
174 125
175 126 if req.env.get('PATH_INFO'):
176 127 parts = req.env.get('PATH_INFO').strip('/').split('/')
177 128 repo_parts = req.env.get('REPO_NAME', '').split('/')
178 129 if parts[:len(repo_parts)] == repo_parts:
179 130 parts = parts[len(repo_parts):]
180 131 query = '/'.join(parts)
181 132 else:
182 133 query = req.env['QUERY_STRING'].split('&', 1)[0]
183 134 query = query.split(';', 1)[0]
184 135
185 136 # translate user-visible url structure to internal structure
186 137
187 138 args = query.split('/', 2)
188 139 if 'cmd' not in req.form and args and args[0]:
189 140
190 141 cmd = args.pop(0)
191 142 style = cmd.rfind('-')
192 143 if style != -1:
193 144 req.form['style'] = [cmd[:style]]
194 145 cmd = cmd[style+1:]
195 146
196 147 # avoid accepting e.g. style parameter as command
197 148 if hasattr(webcommands, cmd) or hasattr(protocol, cmd):
198 149 req.form['cmd'] = [cmd]
199 150
200 151 if args and args[0]:
201 152 node = args.pop(0)
202 153 req.form['node'] = [node]
203 154 if args:
204 155 req.form['file'] = args
205 156
206 157 if cmd == 'static':
207 158 req.form['file'] = req.form['node']
208 159 elif cmd == 'archive':
209 160 fn = req.form['node'][0]
210 161 for type_, spec in self.archive_specs.iteritems():
211 162 ext = spec[2]
212 163 if fn.endswith(ext):
213 164 req.form['node'] = [fn[:-len(ext)]]
214 165 req.form['type'] = [type_]
215 166
216 167 # process this if it's a protocol request
217 168
218 169 cmd = req.form.get('cmd', [''])[0]
219 170 if cmd in protocol.__all__:
220 171 method = getattr(protocol, cmd)
221 172 method(self, req)
222 173 return
223 174
224 175 # process the web interface request
225 176
226 177 try:
227 178
228 179 tmpl = self.templater(req)
229 180 ctype = tmpl('mimetype', encoding=self.encoding)
230 181 ctype = templater.stringify(ctype)
231 182
232 183 if cmd == '':
233 184 req.form['cmd'] = [tmpl.cache['default']]
234 185 cmd = req.form['cmd'][0]
235 186
236 187 if cmd not in webcommands.__all__:
237 188 msg = 'no such method: %s' % cmd
238 189 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
239 190 elif cmd == 'file' and 'raw' in req.form.get('style', []):
240 191 self.ctype = ctype
241 192 content = webcommands.rawfile(self, req, tmpl)
242 193 else:
243 194 content = getattr(webcommands, cmd)(self, req, tmpl)
244 195 req.respond(HTTP_OK, ctype)
245 196
246 197 req.write(content)
247 198 del tmpl
248 199
249 200 except revlog.LookupError, err:
250 201 req.respond(HTTP_NOT_FOUND, ctype)
251 202 msg = str(err)
252 203 if 'manifest' not in msg:
253 204 msg = 'revision not found: %s' % err.name
254 205 req.write(tmpl('error', error=msg))
255 206 except (RepoError, revlog.RevlogError), inst:
256 207 req.respond(HTTP_SERVER_ERROR, ctype)
257 208 req.write(tmpl('error', error=str(inst)))
258 209 except ErrorResponse, inst:
259 210 req.respond(inst.code, ctype)
260 211 req.write(tmpl('error', error=inst.message))
261 212
262 213 def templater(self, req):
263 214
264 215 # determine scheme, port and server name
265 216 # this is needed to create absolute urls
266 217
267 218 proto = req.env.get('wsgi.url_scheme')
268 219 if proto == 'https':
269 220 proto = 'https'
270 221 default_port = "443"
271 222 else:
272 223 proto = 'http'
273 224 default_port = "80"
274 225
275 226 port = req.env["SERVER_PORT"]
276 227 port = port != default_port and (":" + port) or ""
277 228 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
278 229 staticurl = self.config("web", "staticurl") or req.url + 'static/'
279 230 if not staticurl.endswith('/'):
280 231 staticurl += '/'
281 232
282 233 # some functions for the templater
283 234
284 235 def header(**map):
285 236 yield tmpl('header', encoding=self.encoding, **map)
286 237
287 238 def footer(**map):
288 239 yield tmpl("footer", **map)
289 240
290 241 def motd(**map):
291 242 yield self.config("web", "motd", "")
292 243
293 244 def sessionvars(**map):
294 245 fields = []
295 246 if 'style' in req.form:
296 247 style = req.form['style'][0]
297 248 if style != self.config('web', 'style', ''):
298 249 fields.append(('style', style))
299 250
300 251 separator = req.url[-1] == '?' and ';' or '?'
301 252 for name, value in fields:
302 253 yield dict(name=name, value=value, separator=separator)
303 254 separator = ';'
304 255
305 256 # figure out which style to use
306 257
307 258 style = self.config("web", "style", "")
308 259 if 'style' in req.form:
309 260 style = req.form['style'][0]
310 261 mapfile = style_map(self.templatepath, style)
311 262
312 263 if not self.reponame:
313 264 self.reponame = (self.config("web", "name")
314 265 or req.env.get('REPO_NAME')
315 266 or req.url.strip('/') or self.repo.root)
316 267
317 268 # create the templater
318 269
319 270 tmpl = templater.templater(mapfile, templatefilters.filters,
320 271 defaults={"url": req.url,
321 272 "staticurl": staticurl,
322 273 "urlbase": urlbase,
323 274 "repo": self.reponame,
324 275 "header": header,
325 276 "footer": footer,
326 277 "motd": motd,
327 278 "sessionvars": sessionvars
328 279 })
329 280 return tmpl
330 281
331 282 def archivelist(self, nodeid):
332 283 allowed = self.configlist("web", "allow_archive")
333 284 for i, spec in self.archive_specs.iteritems():
334 285 if i in allowed or self.configbool("web", "allow" + i):
335 286 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
336 287
337 288 def listfilediffs(self, tmpl, files, changeset):
338 289 for f in files[:self.maxfiles]:
339 290 yield tmpl("filedifflink", node=hex(changeset), file=f)
340 291 if len(files) > self.maxfiles:
341 292 yield tmpl("fileellipses")
342 293
343 294 def diff(self, tmpl, node1, node2, files):
344 295 def filterfiles(filters, files):
345 296 l = [x for x in files if x in filters]
346 297
347 298 for t in filters:
348 299 if t and t[-1] != os.sep:
349 300 t += os.sep
350 301 l += [x for x in files if x.startswith(t)]
351 302 return l
352 303
353 304 parity = paritygen(self.stripecount)
354 305 def diffblock(diff, f, fn):
355 306 yield tmpl("diffblock",
356 307 lines=prettyprintlines(diff),
357 308 parity=parity.next(),
358 309 file=f,
359 310 filenode=hex(fn or nullid))
360 311
361 312 blockcount = countgen()
362 313 def prettyprintlines(diff):
363 314 blockno = blockcount.next()
364 315 for lineno, l in enumerate(diff.splitlines(1)):
365 316 if blockno == 0:
366 317 lineno = lineno + 1
367 318 else:
368 319 lineno = "%d.%d" % (blockno, lineno + 1)
369 320 if l.startswith('+'):
370 321 ltype = "difflineplus"
371 322 elif l.startswith('-'):
372 323 ltype = "difflineminus"
373 324 elif l.startswith('@'):
374 325 ltype = "difflineat"
375 326 else:
376 327 ltype = "diffline"
377 328 yield tmpl(ltype,
378 329 line=l,
379 330 lineid="l%s" % lineno,
380 331 linenumber="% 8s" % lineno)
381 332
382 333 r = self.repo
383 334 c1 = r.changectx(node1)
384 335 c2 = r.changectx(node2)
385 336 date1 = util.datestr(c1.date())
386 337 date2 = util.datestr(c2.date())
387 338
388 339 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
389 340 if files:
390 341 modified, added, removed = map(lambda x: filterfiles(files, x),
391 342 (modified, added, removed))
392 343
393 344 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
394 345 for f in modified:
395 346 to = c1.filectx(f).data()
396 347 tn = c2.filectx(f).data()
397 348 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
398 349 opts=diffopts), f, tn)
399 350 for f in added:
400 351 to = None
401 352 tn = c2.filectx(f).data()
402 353 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
403 354 opts=diffopts), f, tn)
404 355 for f in removed:
405 356 to = c1.filectx(f).data()
406 357 tn = None
407 358 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
408 359 opts=diffopts), f, tn)
409 360
410 def changelog(self, tmpl, ctx, shortlog=False):
411 def changelist(limit=0,**map):
412 cl = self.repo.changelog
413 l = [] # build a list in forward order for efficiency
414 for i in xrange(start, end):
415 ctx = self.repo.changectx(i)
416 n = ctx.node()
417 showtags = webutil.showtag(self.repo, tmpl, 'changelogtag', n)
418
419 l.insert(0, {"parity": parity.next(),
420 "author": ctx.user(),
421 "parent": webutil.siblings(ctx.parents(), i - 1),
422 "child": webutil.siblings(ctx.children(), i + 1),
423 "changelogtag": showtags,
424 "desc": ctx.description(),
425 "date": ctx.date(),
426 "files": self.listfilediffs(tmpl, ctx.files(), n),
427 "rev": i,
428 "node": hex(n),
429 "tags": webutil.nodetagsdict(self.repo, n),
430 "inbranch": webutil.nodeinbranch(self.repo, ctx),
431 "branches": webutil.nodebranchdict(self.repo, ctx)
432 })
433
434 if limit > 0:
435 l = l[:limit]
436
437 for e in l:
438 yield e
439
440 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
441 cl = self.repo.changelog
442 count = cl.count()
443 pos = ctx.rev()
444 start = max(0, pos - maxchanges + 1)
445 end = min(count, start + maxchanges)
446 pos = end - 1
447 parity = paritygen(self.stripecount, offset=start-end)
448
449 changenav = revnavgen(pos, maxchanges, count, self.repo.changectx)
450
451 return tmpl(shortlog and 'shortlog' or 'changelog',
452 changenav=changenav,
453 node=hex(cl.tip()),
454 rev=pos, changesets=count,
455 entries=lambda **x: changelist(limit=0,**x),
456 latestentry=lambda **x: changelist(limit=1,**x),
457 archives=self.archivelist("tip"))
458
459 def search(self, tmpl, query):
460
461 def changelist(**map):
462 cl = self.repo.changelog
463 count = 0
464 qw = query.lower().split()
465
466 def revgen():
467 for i in xrange(cl.count() - 1, 0, -100):
468 l = []
469 for j in xrange(max(0, i - 100), i + 1):
470 ctx = self.repo.changectx(j)
471 l.append(ctx)
472 l.reverse()
473 for e in l:
474 yield e
475
476 for ctx in revgen():
477 miss = 0
478 for q in qw:
479 if not (q in ctx.user().lower() or
480 q in ctx.description().lower() or
481 q in " ".join(ctx.files()).lower()):
482 miss = 1
483 break
484 if miss:
485 continue
486
487 count += 1
488 n = ctx.node()
489 showtags = webutil.showtag(self.repo, tmpl, 'changelogtag', n)
490
491 yield tmpl('searchentry',
492 parity=parity.next(),
493 author=ctx.user(),
494 parent=webutil.siblings(ctx.parents()),
495 child=webutil.siblings(ctx.children()),
496 changelogtag=showtags,
497 desc=ctx.description(),
498 date=ctx.date(),
499 files=self.listfilediffs(tmpl, ctx.files(), n),
500 rev=ctx.rev(),
501 node=hex(n),
502 tags=webutil.nodetagsdict(self.repo, n),
503 inbranch=webutil.nodeinbranch(self.repo, ctx),
504 branches=webutil.nodebranchdict(self.repo, ctx))
505
506 if count >= self.maxchanges:
507 break
508
509 cl = self.repo.changelog
510 parity = paritygen(self.stripecount)
511
512 return tmpl('search',
513 query=query,
514 node=hex(cl.tip()),
515 entries=changelist,
516 archives=self.archivelist("tip"))
517
518 def changeset(self, tmpl, ctx):
519 n = ctx.node()
520 showtags = webutil.showtag(self.repo, tmpl, 'changesettag', n)
521 parents = ctx.parents()
522 p1 = parents[0].node()
523
524 files = []
525 parity = paritygen(self.stripecount)
526 for f in ctx.files():
527 files.append(tmpl("filenodelink",
528 node=hex(n), file=f,
529 parity=parity.next()))
530
531 def diff(**map):
532 yield self.diff(tmpl, p1, n, None)
533
534 return tmpl('changeset',
535 diff=diff,
536 rev=ctx.rev(),
537 node=hex(n),
538 parent=webutil.siblings(parents),
539 child=webutil.siblings(ctx.children()),
540 changesettag=showtags,
541 author=ctx.user(),
542 desc=ctx.description(),
543 date=ctx.date(),
544 files=files,
545 archives=self.archivelist(hex(n)),
546 tags=webutil.nodetagsdict(self.repo, n),
547 branch=webutil.nodebranchnodefault(ctx),
548 inbranch=webutil.nodeinbranch(self.repo, ctx),
549 branches=webutil.nodebranchdict(self.repo, ctx))
550
551 def filelog(self, tmpl, fctx):
552 f = fctx.path()
553 fl = fctx.filelog()
554 count = fl.count()
555 pagelen = self.maxshortchanges
556 pos = fctx.filerev()
557 start = max(0, pos - pagelen + 1)
558 end = min(count, start + pagelen)
559 pos = end - 1
560 parity = paritygen(self.stripecount, offset=start-end)
561
562 def entries(limit=0, **map):
563 l = []
564
565 for i in xrange(start, end):
566 ctx = fctx.filectx(i)
567 n = fl.node(i)
568
569 l.insert(0, {"parity": parity.next(),
570 "filerev": i,
571 "file": f,
572 "node": hex(ctx.node()),
573 "author": ctx.user(),
574 "date": ctx.date(),
575 "rename": webutil.renamelink(fl, n),
576 "parent": webutil.siblings(fctx.parents()),
577 "child": webutil.siblings(fctx.children()),
578 "desc": ctx.description()})
579
580 if limit > 0:
581 l = l[:limit]
582
583 for e in l:
584 yield e
585
586 nodefunc = lambda x: fctx.filectx(fileid=x)
587 nav = revnavgen(pos, pagelen, count, nodefunc)
588 return tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav,
589 entries=lambda **x: entries(limit=0, **x),
590 latestentry=lambda **x: entries(limit=1, **x))
591
592 def filerevision(self, tmpl, fctx):
593 f = fctx.path()
594 text = fctx.data()
595 fl = fctx.filelog()
596 n = fctx.filenode()
597 parity = paritygen(self.stripecount)
598
599 if util.binary(text):
600 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
601 text = '(binary:%s)' % mt
602
603 def lines():
604 for lineno, t in enumerate(text.splitlines(1)):
605 yield {"line": t,
606 "lineid": "l%d" % (lineno + 1),
607 "linenumber": "% 6d" % (lineno + 1),
608 "parity": parity.next()}
609
610 return tmpl("filerevision",
611 file=f,
612 path=_up(f),
613 text=lines(),
614 rev=fctx.rev(),
615 node=hex(fctx.node()),
616 author=fctx.user(),
617 date=fctx.date(),
618 desc=fctx.description(),
619 branch=webutil.nodebranchnodefault(fctx),
620 parent=webutil.siblings(fctx.parents()),
621 child=webutil.siblings(fctx.children()),
622 rename=webutil.renamelink(fl, n),
623 permissions=fctx.manifest().flags(f))
624
625 def fileannotate(self, tmpl, fctx):
626 f = fctx.path()
627 n = fctx.filenode()
628 fl = fctx.filelog()
629 parity = paritygen(self.stripecount)
630
631 def annotate(**map):
632 last = None
633 if util.binary(fctx.data()):
634 mt = (mimetypes.guess_type(fctx.path())[0]
635 or 'application/octet-stream')
636 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
637 '(binary:%s)' % mt)])
638 else:
639 lines = enumerate(fctx.annotate(follow=True, linenumber=True))
640 for lineno, ((f, targetline), l) in lines:
641 fnode = f.filenode()
642 name = self.repo.ui.shortuser(f.user())
643
644 if last != fnode:
645 last = fnode
646
647 yield {"parity": parity.next(),
648 "node": hex(f.node()),
649 "rev": f.rev(),
650 "author": name,
651 "file": f.path(),
652 "targetline": targetline,
653 "line": l,
654 "lineid": "l%d" % (lineno + 1),
655 "linenumber": "% 6d" % (lineno + 1)}
656
657 return tmpl("fileannotate",
658 file=f,
659 annotate=annotate,
660 path=_up(f),
661 rev=fctx.rev(),
662 node=hex(fctx.node()),
663 author=fctx.user(),
664 date=fctx.date(),
665 desc=fctx.description(),
666 rename=webutil.renamelink(fl, n),
667 branch=webutil.nodebranchnodefault(fctx),
668 parent=webutil.siblings(fctx.parents()),
669 child=webutil.siblings(fctx.children()),
670 permissions=fctx.manifest().flags(f))
671
672 def manifest(self, tmpl, ctx, path):
673 mf = ctx.manifest()
674 node = ctx.node()
675
676 files = {}
677 parity = paritygen(self.stripecount)
678
679 if path and path[-1] != "/":
680 path += "/"
681 l = len(path)
682 abspath = "/" + path
683
684 for f, n in mf.items():
685 if f[:l] != path:
686 continue
687 remain = f[l:]
688 if "/" in remain:
689 short = remain[:remain.index("/") + 1] # bleah
690 files[short] = (f, None)
691 else:
692 short = os.path.basename(remain)
693 files[short] = (f, n)
694
695 if not files:
696 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
697
698 def filelist(**map):
699 fl = files.keys()
700 fl.sort()
701 for f in fl:
702 full, fnode = files[f]
703 if not fnode:
704 continue
705
706 fctx = ctx.filectx(full)
707 yield {"file": full,
708 "parity": parity.next(),
709 "basename": f,
710 "date": fctx.changectx().date(),
711 "size": fctx.size(),
712 "permissions": mf.flags(full)}
713
714 def dirlist(**map):
715 fl = files.keys()
716 fl.sort()
717 for f in fl:
718 full, fnode = files[f]
719 if fnode:
720 continue
721
722 yield {"parity": parity.next(),
723 "path": "%s%s" % (abspath, f),
724 "basename": f[:-1]}
725
726 return tmpl("manifest",
727 rev=ctx.rev(),
728 node=hex(node),
729 path=abspath,
730 up=_up(abspath),
731 upparity=parity.next(),
732 fentries=filelist,
733 dentries=dirlist,
734 archives=self.archivelist(hex(node)),
735 tags=webutil.nodetagsdict(self.repo, node),
736 inbranch=webutil.nodeinbranch(self.repo, ctx),
737 branches=webutil.nodebranchdict(self.repo, ctx))
738
739 def tags(self, tmpl):
740 i = self.repo.tagslist()
741 i.reverse()
742 parity = paritygen(self.stripecount)
743
744 def entries(notip=False,limit=0, **map):
745 count = 0
746 for k, n in i:
747 if notip and k == "tip":
748 continue
749 if limit > 0 and count >= limit:
750 continue
751 count = count + 1
752 yield {"parity": parity.next(),
753 "tag": k,
754 "date": self.repo.changectx(n).date(),
755 "node": hex(n)}
756
757 return tmpl("tags",
758 node=hex(self.repo.changelog.tip()),
759 entries=lambda **x: entries(False,0, **x),
760 entriesnotip=lambda **x: entries(True,0, **x),
761 latestentry=lambda **x: entries(True,1, **x))
762
763 def summary(self, tmpl):
764 i = self.repo.tagslist()
765 i.reverse()
766
767 def tagentries(**map):
768 parity = paritygen(self.stripecount)
769 count = 0
770 for k, n in i:
771 if k == "tip": # skip tip
772 continue;
773
774 count += 1
775 if count > 10: # limit to 10 tags
776 break;
777
778 yield tmpl("tagentry",
779 parity=parity.next(),
780 tag=k,
781 node=hex(n),
782 date=self.repo.changectx(n).date())
783
784
785 def branches(**map):
786 parity = paritygen(self.stripecount)
787
788 b = self.repo.branchtags()
789 l = [(-self.repo.changelog.rev(n), n, t) for t, n in b.items()]
790 l.sort()
791
792 for r,n,t in l:
793 ctx = self.repo.changectx(n)
794
795 yield {'parity': parity.next(),
796 'branch': t,
797 'node': hex(n),
798 'date': ctx.date()}
799
800 def changelist(**map):
801 parity = paritygen(self.stripecount, offset=start-end)
802 l = [] # build a list in forward order for efficiency
803 for i in xrange(start, end):
804 ctx = self.repo.changectx(i)
805 n = ctx.node()
806 hn = hex(n)
807
808 l.insert(0, tmpl(
809 'shortlogentry',
810 parity=parity.next(),
811 author=ctx.user(),
812 desc=ctx.description(),
813 date=ctx.date(),
814 rev=i,
815 node=hn,
816 tags=webutil.nodetagsdict(self.repo, n),
817 inbranch=webutil.nodeinbranch(self.repo, ctx),
818 branches=webutil.nodebranchdict(self.repo, ctx)))
819
820 yield l
821
822 cl = self.repo.changelog
823 count = cl.count()
824 start = max(0, count - self.maxchanges)
825 end = min(count, start + self.maxchanges)
826
827 return tmpl("summary",
828 desc=self.config("web", "description", "unknown"),
829 owner=get_contact(self.config) or "unknown",
830 lastchange=cl.read(cl.tip())[2],
831 tags=tagentries,
832 branches=branches,
833 shortlog=changelist,
834 node=hex(cl.tip()),
835 archives=self.archivelist("tip"))
836
837 def filediff(self, tmpl, fctx):
838 n = fctx.node()
839 path = fctx.path()
840 parents = fctx.parents()
841 p1 = parents and parents[0].node() or nullid
842
843 def diff(**map):
844 yield self.diff(tmpl, p1, n, [path])
845
846 return tmpl("filediff",
847 file=path,
848 node=hex(n),
849 rev=fctx.rev(),
850 branch=webutil.nodebranchnodefault(fctx),
851 parent=webutil.siblings(parents),
852 child=webutil.siblings(fctx.children()),
853 diff=diff)
854
855 361 archive_specs = {
856 362 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
857 363 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
858 364 'zip': ('application/zip', 'zip', '.zip', None),
859 365 }
860 366
861 def archive(self, tmpl, req, key, type_):
862 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
863 cnode = self.repo.lookup(key)
864 arch_version = key
865 if cnode == key or key == 'tip':
866 arch_version = short(cnode)
867 name = "%s-%s" % (reponame, arch_version)
868 mimetype, artype, extension, encoding = self.archive_specs[type_]
869 headers = [
870 ('Content-Type', mimetype),
871 ('Content-Disposition', 'attachment; filename=%s%s' %
872 (name, extension))
873 ]
874 if encoding:
875 headers.append(('Content-Encoding', encoding))
876 req.header(headers)
877 req.respond(HTTP_OK)
878 archival.archive(self.repo, req, cnode, artype, prefix=name)
879
880 367 def check_perm(self, req, op, default):
881 368 '''check permission for operation based on user auth.
882 369 return true if op allowed, else false.
883 370 default is policy to use if no config given.'''
884 371
885 372 user = req.env.get('REMOTE_USER')
886 373
887 374 deny = self.configlist('web', 'deny_' + op)
888 375 if deny and (not user or deny == ['*'] or user in deny):
889 376 return False
890 377
891 378 allow = self.configlist('web', 'allow_' + op)
892 379 return (allow and (allow == ['*'] or user in allow)) or default
@@ -1,129 +1,572 b''
1 1 #
2 2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 import os, mimetypes
8 import os, mimetypes, re
9 9 import webutil
10 from mercurial import revlog
10 from mercurial import revlog, archival
11 from mercurial.node import hex, nullid
11 12 from mercurial.util import binary
12 13 from mercurial.repo import RepoError
13 from common import staticfile, ErrorResponse, HTTP_OK, HTTP_NOT_FOUND
14 from common import paritygen, staticfile, get_contact, ErrorResponse
15 from common import HTTP_OK, HTTP_NOT_FOUND
14 16
15 17 # __all__ is populated with the allowed commands. Be sure to add to it if
16 18 # you're adding a new command, or the new command won't work.
17 19
18 20 __all__ = [
19 21 'log', 'rawfile', 'file', 'changelog', 'shortlog', 'changeset', 'rev',
20 22 'manifest', 'tags', 'summary', 'filediff', 'diff', 'annotate', 'filelog',
21 23 'archive', 'static',
22 24 ]
23 25
24 26 def log(web, req, tmpl):
25 27 if 'file' in req.form and req.form['file'][0]:
26 28 return filelog(web, req, tmpl)
27 29 else:
28 30 return changelog(web, req, tmpl)
29 31
30 32 def rawfile(web, req, tmpl):
31 33 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
32 34 if not path:
33 content = web.manifest(tmpl, webutil.changectx(web.repo, req), path)
35 content = manifest(web, req, tmpl)
34 36 req.respond(HTTP_OK, web.ctype)
35 37 return content
36 38
37 39 try:
38 40 fctx = webutil.filectx(web.repo, req)
39 41 except revlog.LookupError, inst:
40 42 try:
41 content = web.manifest(tmpl, webutil.changectx(web.repo, req), path)
43 content = manifest(web, req, tmpl)
42 44 req.respond(HTTP_OK, web.ctype)
43 45 return content
44 46 except ErrorResponse:
45 47 raise inst
46 48
47 49 path = fctx.path()
48 50 text = fctx.data()
49 51 mt = mimetypes.guess_type(path)[0]
50 52 if mt is None or binary(text):
51 53 mt = mt or 'application/octet-stream'
52 54
53 55 req.respond(HTTP_OK, mt, path, len(text))
54 56 return [text]
55 57
58 def _filerevision(web, tmpl, fctx):
59 f = fctx.path()
60 text = fctx.data()
61 fl = fctx.filelog()
62 n = fctx.filenode()
63 parity = paritygen(web.stripecount)
64
65 if binary(text):
66 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
67 text = '(binary:%s)' % mt
68
69 def lines():
70 for lineno, t in enumerate(text.splitlines(1)):
71 yield {"line": t,
72 "lineid": "l%d" % (lineno + 1),
73 "linenumber": "% 6d" % (lineno + 1),
74 "parity": parity.next()}
75
76 return tmpl("filerevision",
77 file=f,
78 path=webutil.up(f),
79 text=lines(),
80 rev=fctx.rev(),
81 node=hex(fctx.node()),
82 author=fctx.user(),
83 date=fctx.date(),
84 desc=fctx.description(),
85 branch=webutil.nodebranchnodefault(fctx),
86 parent=webutil.siblings(fctx.parents()),
87 child=webutil.siblings(fctx.children()),
88 rename=webutil.renamelink(fl, n),
89 permissions=fctx.manifest().flags(f))
90
56 91 def file(web, req, tmpl):
57 92 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
58 93 if path:
59 94 try:
60 return web.filerevision(tmpl, webutil.filectx(web.repo, req))
95 return _filerevision(web, tmpl, webutil.filectx(web.repo, req))
61 96 except revlog.LookupError, inst:
62 97 pass
63 98
64 99 try:
65 return web.manifest(tmpl, webutil.changectx(web.repo, req), path)
100 return manifest(web, req, tmpl)
66 101 except ErrorResponse:
67 102 raise inst
68 103
104 def _search(web, tmpl, query):
105
106 def changelist(**map):
107 cl = web.repo.changelog
108 count = 0
109 qw = query.lower().split()
110
111 def revgen():
112 for i in xrange(cl.count() - 1, 0, -100):
113 l = []
114 for j in xrange(max(0, i - 100), i + 1):
115 ctx = web.repo.changectx(j)
116 l.append(ctx)
117 l.reverse()
118 for e in l:
119 yield e
120
121 for ctx in revgen():
122 miss = 0
123 for q in qw:
124 if not (q in ctx.user().lower() or
125 q in ctx.description().lower() or
126 q in " ".join(ctx.files()).lower()):
127 miss = 1
128 break
129 if miss:
130 continue
131
132 count = 1
133 n = ctx.node()
134 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
135
136 yield tmpl('searchentry',
137 parity=parity.next(),
138 author=ctx.user(),
139 parent=webutil.siblings(ctx.parents()),
140 child=webutil.siblings(ctx.children()),
141 changelogtag=showtags,
142 desc=ctx.description(),
143 date=ctx.date(),
144 files=web.listfilediffs(tmpl, ctx.files(), n),
145 rev=ctx.rev(),
146 node=hex(n),
147 tags=webutil.nodetagsdict(web.repo, n),
148 inbranch=webutil.nodeinbranch(web.repo, ctx),
149 branches=webutil.nodebranchdict(web.repo, ctx))
150
151 if count >= web.maxchanges:
152 break
153
154 cl = web.repo.changelog
155 parity = paritygen(web.stripecount)
156
157 return tmpl('search',
158 query=query,
159 node=hex(cl.tip()),
160 entries=changelist,
161 archives=web.archivelist("tip"))
162
69 163 def changelog(web, req, tmpl, shortlog = False):
70 164 if 'node' in req.form:
71 165 ctx = webutil.changectx(web.repo, req)
72 166 else:
73 167 if 'rev' in req.form:
74 168 hi = req.form['rev'][0]
75 169 else:
76 170 hi = web.repo.changelog.count() - 1
77 171 try:
78 172 ctx = web.repo.changectx(hi)
79 173 except RepoError:
80 return web.search(tmpl, hi) # XXX redirect to 404 page?
174 return _search(web, tmpl, hi) # XXX redirect to 404 page?
175
176 def changelist(limit=0, **map):
177 cl = web.repo.changelog
178 l = [] # build a list in forward order for efficiency
179 for i in xrange(start, end):
180 ctx = web.repo.changectx(i)
181 n = ctx.node()
182 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
183
184 l.insert(0, {"parity": parity.next(),
185 "author": ctx.user(),
186 "parent": webutil.siblings(ctx.parents(), i - 1),
187 "child": webutil.siblings(ctx.children(), i + 1),
188 "changelogtag": showtags,
189 "desc": ctx.description(),
190 "date": ctx.date(),
191 "files": web.listfilediffs(tmpl, ctx.files(), n),
192 "rev": i,
193 "node": hex(n),
194 "tags": webutil.nodetagsdict(web.repo, n),
195 "inbranch": webutil.nodeinbranch(web.repo, ctx),
196 "branches": webutil.nodebranchdict(web.repo, ctx)
197 })
81 198
82 return web.changelog(tmpl, ctx, shortlog = shortlog)
199 if limit > 0:
200 l = l[:limit]
201
202 for e in l:
203 yield e
204
205 maxchanges = shortlog and web.maxshortchanges or web.maxchanges
206 cl = web.repo.changelog
207 count = cl.count()
208 pos = ctx.rev()
209 start = max(0, pos - maxchanges + 1)
210 end = min(count, start + maxchanges)
211 pos = end - 1
212 parity = paritygen(web.stripecount, offset=start-end)
213
214 changenav = webutil.revnavgen(pos, maxchanges, count, web.repo.changectx)
215
216 return tmpl(shortlog and 'shortlog' or 'changelog',
217 changenav=changenav,
218 node=hex(cl.tip()),
219 rev=pos, changesets=count,
220 entries=lambda **x: changelist(limit=0,**x),
221 latestentry=lambda **x: changelist(limit=1,**x),
222 archives=web.archivelist("tip"))
83 223
84 224 def shortlog(web, req, tmpl):
85 225 return changelog(web, req, tmpl, shortlog = True)
86 226
87 227 def changeset(web, req, tmpl):
88 return web.changeset(tmpl, webutil.changectx(web.repo, req))
228 ctx = webutil.changectx(web.repo, req)
229 n = ctx.node()
230 showtags = webutil.showtag(web.repo, tmpl, 'changesettag', n)
231 parents = ctx.parents()
232 p1 = parents[0].node()
233
234 files = []
235 parity = paritygen(web.stripecount)
236 for f in ctx.files():
237 files.append(tmpl("filenodelink",
238 node=hex(n), file=f,
239 parity=parity.next()))
240
241 diffs = web.diff(tmpl, p1, n, None)
242 return tmpl('changeset',
243 diff=diffs,
244 rev=ctx.rev(),
245 node=hex(n),
246 parent=webutil.siblings(parents),
247 child=webutil.siblings(ctx.children()),
248 changesettag=showtags,
249 author=ctx.user(),
250 desc=ctx.description(),
251 date=ctx.date(),
252 files=files,
253 archives=web.archivelist(hex(n)),
254 tags=webutil.nodetagsdict(web.repo, n),
255 branch=webutil.nodebranchnodefault(ctx),
256 inbranch=webutil.nodeinbranch(web.repo, ctx),
257 branches=webutil.nodebranchdict(web.repo, ctx))
89 258
90 259 rev = changeset
91 260
92 261 def manifest(web, req, tmpl):
93 return web.manifest(tmpl, webutil.changectx(web.repo, req),
94 webutil.cleanpath(web.repo, req.form['path'][0]))
262 ctx = webutil.changectx(web.repo, req)
263 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
264 mf = ctx.manifest()
265 node = ctx.node()
266
267 files = {}
268 parity = paritygen(web.stripecount)
269
270 if path and path[-1] != "/":
271 path += "/"
272 l = len(path)
273 abspath = "/" + path
274
275 for f, n in mf.items():
276 if f[:l] != path:
277 continue
278 remain = f[l:]
279 if "/" in remain:
280 short = remain[:remain.index("/") + 1] # bleah
281 files[short] = (f, None)
282 else:
283 short = os.path.basename(remain)
284 files[short] = (f, n)
285
286 if not files:
287 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
288
289 def filelist(**map):
290 fl = files.keys()
291 fl.sort()
292 for f in fl:
293 full, fnode = files[f]
294 if not fnode:
295 continue
296
297 fctx = ctx.filectx(full)
298 yield {"file": full,
299 "parity": parity.next(),
300 "basename": f,
301 "date": fctx.changectx().date(),
302 "size": fctx.size(),
303 "permissions": mf.flags(full)}
304
305 def dirlist(**map):
306 fl = files.keys()
307 fl.sort()
308 for f in fl:
309 full, fnode = files[f]
310 if fnode:
311 continue
312
313 yield {"parity": parity.next(),
314 "path": "%s%s" % (abspath, f),
315 "basename": f[:-1]}
316
317 return tmpl("manifest",
318 rev=ctx.rev(),
319 node=hex(node),
320 path=abspath,
321 up=webutil.up(abspath),
322 upparity=parity.next(),
323 fentries=filelist,
324 dentries=dirlist,
325 archives=web.archivelist(hex(node)),
326 tags=webutil.nodetagsdict(web.repo, node),
327 inbranch=webutil.nodeinbranch(web.repo, ctx),
328 branches=webutil.nodebranchdict(web.repo, ctx))
95 329
96 330 def tags(web, req, tmpl):
97 return web.tags(tmpl)
331 i = web.repo.tagslist()
332 i.reverse()
333 parity = paritygen(web.stripecount)
334
335 def entries(notip=False,limit=0, **map):
336 count = 0
337 for k, n in i:
338 if notip and k == "tip":
339 continue
340 if limit > 0 and count >= limit:
341 continue
342 count = count + 1
343 yield {"parity": parity.next(),
344 "tag": k,
345 "date": web.repo.changectx(n).date(),
346 "node": hex(n)}
347
348 return tmpl("tags",
349 node=hex(web.repo.changelog.tip()),
350 entries=lambda **x: entries(False,0, **x),
351 entriesnotip=lambda **x: entries(True,0, **x),
352 latestentry=lambda **x: entries(True,1, **x))
98 353
99 354 def summary(web, req, tmpl):
100 return web.summary(tmpl)
355 i = web.repo.tagslist()
356 i.reverse()
357
358 def tagentries(**map):
359 parity = paritygen(web.stripecount)
360 count = 0
361 for k, n in i:
362 if k == "tip": # skip tip
363 continue
364
365 count = 1
366 if count > 10: # limit to 10 tags
367 break
368
369 yield tmpl("tagentry",
370 parity=parity.next(),
371 tag=k,
372 node=hex(n),
373 date=web.repo.changectx(n).date())
374
375 def branches(**map):
376 parity = paritygen(web.stripecount)
377
378 b = web.repo.branchtags()
379 l = [(-web.repo.changelog.rev(n), n, t) for t, n in b.items()]
380 l.sort()
381
382 for r,n,t in l:
383 ctx = web.repo.changectx(n)
384 yield {'parity': parity.next(),
385 'branch': t,
386 'node': hex(n),
387 'date': ctx.date()}
388
389 def changelist(**map):
390 parity = paritygen(web.stripecount, offset=start-end)
391 l = [] # build a list in forward order for efficiency
392 for i in xrange(start, end):
393 ctx = web.repo.changectx(i)
394 n = ctx.node()
395 hn = hex(n)
396
397 l.insert(0, tmpl(
398 'shortlogentry',
399 parity=parity.next(),
400 author=ctx.user(),
401 desc=ctx.description(),
402 date=ctx.date(),
403 rev=i,
404 node=hn,
405 tags=webutil.nodetagsdict(web.repo, n),
406 inbranch=webutil.nodeinbranch(web.repo, ctx),
407 branches=webutil.nodebranchdict(web.repo, ctx)))
408
409 yield l
410
411 cl = web.repo.changelog
412 count = cl.count()
413 start = max(0, count - web.maxchanges)
414 end = min(count, start + web.maxchanges)
415
416 return tmpl("summary",
417 desc=web.config("web", "description", "unknown"),
418 owner=get_contact(web.config) or "unknown",
419 lastchange=cl.read(cl.tip())[2],
420 tags=tagentries,
421 branches=branches,
422 shortlog=changelist,
423 node=hex(cl.tip()),
424 archives=web.archivelist("tip"))
101 425
102 426 def filediff(web, req, tmpl):
103 return web.filediff(tmpl, webutil.filectx(web.repo, req))
427 fctx = webutil.filectx(web.repo, req)
428 n = fctx.node()
429 path = fctx.path()
430 parents = fctx.parents()
431 p1 = parents and parents[0].node() or nullid
432
433 diffs = web.diff(tmpl, p1, n, [path])
434 return tmpl("filediff",
435 file=path,
436 node=hex(n),
437 rev=fctx.rev(),
438 branch=webutil.nodebranchnodefault(fctx),
439 parent=webutil.siblings(parents),
440 child=webutil.siblings(fctx.children()),
441 diff=diffs)
104 442
105 443 diff = filediff
106 444
107 445 def annotate(web, req, tmpl):
108 return web.fileannotate(tmpl, webutil.filectx(web.repo, req))
446 fctx = webutil.filectx(web.repo, req)
447 f = fctx.path()
448 n = fctx.filenode()
449 fl = fctx.filelog()
450 parity = paritygen(web.stripecount)
451
452 def annotate(**map):
453 last = None
454 if binary(fctx.data()):
455 mt = (mimetypes.guess_type(fctx.path())[0]
456 or 'application/octet-stream')
457 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
458 '(binary:%s)' % mt)])
459 else:
460 lines = enumerate(fctx.annotate(follow=True, linenumber=True))
461 for lineno, ((f, targetline), l) in lines:
462 fnode = f.filenode()
463 name = web.repo.ui.shortuser(f.user())
464
465 if last != fnode:
466 last = fnode
467
468 yield {"parity": parity.next(),
469 "node": hex(f.node()),
470 "rev": f.rev(),
471 "author": name,
472 "file": f.path(),
473 "targetline": targetline,
474 "line": l,
475 "lineid": "l%d" % (lineno + 1),
476 "linenumber": "% 6d" % (lineno + 1)}
477
478 return tmpl("fileannotate",
479 file=f,
480 annotate=annotate,
481 path=webutil.up(f),
482 rev=fctx.rev(),
483 node=hex(fctx.node()),
484 author=fctx.user(),
485 date=fctx.date(),
486 desc=fctx.description(),
487 rename=webutil.renamelink(fl, n),
488 branch=webutil.nodebranchnodefault(fctx),
489 parent=webutil.siblings(fctx.parents()),
490 child=webutil.siblings(fctx.children()),
491 permissions=fctx.manifest().flags(f))
109 492
110 493 def filelog(web, req, tmpl):
111 return web.filelog(tmpl, webutil.filectx(web.repo, req))
494 fctx = webutil.filectx(web.repo, req)
495 f = fctx.path()
496 fl = fctx.filelog()
497 count = fl.count()
498 pagelen = web.maxshortchanges
499 pos = fctx.filerev()
500 start = max(0, pos - pagelen + 1)
501 end = min(count, start + pagelen)
502 pos = end - 1
503 parity = paritygen(web.stripecount, offset=start-end)
504
505 def entries(limit=0, **map):
506 l = []
507
508 for i in xrange(start, end):
509 ctx = fctx.filectx(i)
510 n = fl.node(i)
511
512 l.insert(0, {"parity": parity.next(),
513 "filerev": i,
514 "file": f,
515 "node": hex(ctx.node()),
516 "author": ctx.user(),
517 "date": ctx.date(),
518 "rename": webutil.renamelink(fl, n),
519 "parent": webutil.siblings(fctx.parents()),
520 "child": webutil.siblings(fctx.children()),
521 "desc": ctx.description()})
522
523 if limit > 0:
524 l = l[:limit]
525
526 for e in l:
527 yield e
528
529 nodefunc = lambda x: fctx.filectx(fileid=x)
530 nav = webutil.revnavgen(pos, pagelen, count, nodefunc)
531 return tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav,
532 entries=lambda **x: entries(limit=0, **x),
533 latestentry=lambda **x: entries(limit=1, **x))
534
112 535
113 536 def archive(web, req, tmpl):
114 537 type_ = req.form['type'][0]
115 538 allowed = web.configlist("web", "allow_archive")
116 if (type_ in web.archives and (type_ in allowed or
539 key = req.form['node'][0]
540
541 if not (type_ in web.archives and (type_ in allowed or
117 542 web.configbool("web", "allow" + type_, False))):
118 web.archive(tmpl, req, req.form['node'][0], type_)
119 return []
120 raise ErrorResponse(HTTP_NOT_FOUND, 'unsupported archive type: %s' % type_)
543 msg = 'Unsupported archive type: %s' % type_
544 raise ErrorResponse(HTTP_NOT_FOUND, msg)
545
546 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
547 cnode = web.repo.lookup(key)
548 arch_version = key
549 if cnode == key or key == 'tip':
550 arch_version = short(cnode)
551 name = "%s-%s" % (reponame, arch_version)
552 mimetype, artype, extension, encoding = web.archive_specs[type_]
553 headers = [
554 ('Content-Type', mimetype),
555 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
556 ]
557 if encoding:
558 headers.append(('Content-Encoding', encoding))
559 req.header(headers)
560 req.respond(HTTP_OK)
561 archival.archive(web.repo, req, cnode, artype, prefix=name)
562 return []
563
121 564
122 565 def static(web, req, tmpl):
123 566 fname = req.form['file'][0]
124 567 # a repo owner may set web.static in .hg/hgrc to get any file
125 568 # readable by the user running the CGI script
126 569 static = web.config("web", "static",
127 570 os.path.join(web.templatepath, "static"),
128 571 untrusted=False)
129 572 return [staticfile(static, fname, req)]
@@ -1,94 +1,143 b''
1 1 # hgweb/webutil.py - utility library for the web interface.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 import os
9 10 from mercurial.node import hex, nullid
10 11 from mercurial.repo import RepoError
11 12 from mercurial import util
12 13
14 def up(p):
15 if p[0] != "/":
16 p = "/" + p
17 if p[-1] == "/":
18 p = p[:-1]
19 up = os.path.dirname(p)
20 if up == "/":
21 return "/"
22 return up + "/"
23
24 def revnavgen(pos, pagelen, limit, nodefunc):
25 def seq(factor, limit=None):
26 if limit:
27 yield limit
28 if limit >= 20 and limit <= 40:
29 yield 50
30 else:
31 yield 1 * factor
32 yield 3 * factor
33 for f in seq(factor * 10):
34 yield f
35
36 def nav(**map):
37 l = []
38 last = 0
39 for f in seq(1, pagelen):
40 if f < pagelen or f <= last:
41 continue
42 if f > limit:
43 break
44 last = f
45 if pos + f < limit:
46 l.append(("+%d" % f, hex(nodefunc(pos + f).node())))
47 if pos - f >= 0:
48 l.insert(0, ("-%d" % f, hex(nodefunc(pos - f).node())))
49
50 try:
51 yield {"label": "(0)", "node": hex(nodefunc('0').node())}
52
53 for label, node in l:
54 yield {"label": label, "node": node}
55
56 yield {"label": "tip", "node": "tip"}
57 except RepoError:
58 pass
59
60 return nav
61
13 62 def siblings(siblings=[], hiderev=None, **args):
14 63 siblings = [s for s in siblings if s.node() != nullid]
15 64 if len(siblings) == 1 and siblings[0].rev() == hiderev:
16 65 return
17 66 for s in siblings:
18 67 d = {'node': hex(s.node()), 'rev': s.rev()}
19 68 if hasattr(s, 'path'):
20 69 d['file'] = s.path()
21 70 d.update(args)
22 71 yield d
23 72
24 73 def renamelink(fl, node):
25 74 r = fl.renamed(node)
26 75 if r:
27 76 return [dict(file=r[0], node=hex(r[1]))]
28 77 return []
29 78
30 79 def nodetagsdict(repo, node):
31 80 return [{"name": i} for i in repo.nodetags(node)]
32 81
33 82 def nodebranchdict(repo, ctx):
34 83 branches = []
35 84 branch = ctx.branch()
36 85 # If this is an empty repo, ctx.node() == nullid,
37 86 # ctx.branch() == 'default', but branchtags() is
38 87 # an empty dict. Using dict.get avoids a traceback.
39 88 if repo.branchtags().get(branch) == ctx.node():
40 89 branches.append({"name": branch})
41 90 return branches
42 91
43 92 def nodeinbranch(repo, ctx):
44 93 branches = []
45 94 branch = ctx.branch()
46 95 if branch != 'default' and repo.branchtags().get(branch) != ctx.node():
47 96 branches.append({"name": branch})
48 97 return branches
49 98
50 99 def nodebranchnodefault(ctx):
51 100 branches = []
52 101 branch = ctx.branch()
53 102 if branch != 'default':
54 103 branches.append({"name": branch})
55 104 return branches
56 105
57 106 def showtag(repo, tmpl, t1, node=nullid, **args):
58 107 for t in repo.nodetags(node):
59 108 yield tmpl(t1, tag=t, **args)
60 109
61 110 def cleanpath(repo, path):
62 111 path = path.lstrip('/')
63 112 return util.canonpath(repo.root, '', path)
64 113
65 114 def changectx(repo, req):
66 115 if 'node' in req.form:
67 116 changeid = req.form['node'][0]
68 117 elif 'manifest' in req.form:
69 118 changeid = req.form['manifest'][0]
70 119 else:
71 120 changeid = self.repo.changelog.count() - 1
72 121
73 122 try:
74 123 ctx = repo.changectx(changeid)
75 124 except RepoError:
76 125 man = repo.manifest
77 126 mn = man.lookup(changeid)
78 127 ctx = repo.changectx(man.linkrev(mn))
79 128
80 129 return ctx
81 130
82 131 def filectx(repo, req):
83 132 path = cleanpath(repo, req.form['file'][0])
84 133 if 'node' in req.form:
85 134 changeid = req.form['node'][0]
86 135 else:
87 136 changeid = req.form['filenode'][0]
88 137 try:
89 138 ctx = repo.changectx(changeid)
90 139 fctx = ctx.filectx(path)
91 140 except RepoError:
92 141 fctx = repo.filectx(path, fileid=changeid)
93 142
94 143 return fctx
General Comments 0
You need to be logged in to leave comments. Login now