##// END OF EJS Templates
templater: provide the standard template filters by default
Dirkjan Ochtman -
r8360:acc202b7 default
parent child Browse files
Show More
@@ -1,62 +1,60 b''
1 1 # highlight.py - highlight extension implementation file
2 2 #
3 3 # Copyright 2007-2009 Adam Hupp <adam@hupp.org> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2, incorporated herein by reference.
7 7 #
8 8 # The original module was split in an interface and an implementation
9 9 # file to defer pygments loading and speedup extension setup.
10 10
11 11 from mercurial import demandimport
12 12 demandimport.ignore.extend(['pkgutil', 'pkg_resources', '__main__',])
13
14 13 from mercurial import util, encoding
15 from mercurial.templatefilters import filters
16 14
17 15 from pygments import highlight
18 16 from pygments.util import ClassNotFound
19 17 from pygments.lexers import guess_lexer, guess_lexer_for_filename, TextLexer
20 18 from pygments.formatters import HtmlFormatter
21 19
22 20 SYNTAX_CSS = ('\n<link rel="stylesheet" href="{url}highlightcss" '
23 21 'type="text/css" />')
24 22
25 23 def pygmentize(field, fctx, style, tmpl):
26 24
27 25 # append a <link ...> to the syntax highlighting css
28 26 old_header = ''.join(tmpl('header'))
29 27 if SYNTAX_CSS not in old_header:
30 28 new_header = old_header + SYNTAX_CSS
31 29 tmpl.cache['header'] = new_header
32 30
33 31 text = fctx.data()
34 32 if util.binary(text):
35 33 return
36 34
37 35 # avoid UnicodeDecodeError in pygments
38 36 text = encoding.tolocal(text)
39 37
40 38 # To get multi-line strings right, we can't format line-by-line
41 39 try:
42 40 lexer = guess_lexer_for_filename(fctx.path(), text[:1024],
43 41 encoding=encoding.encoding)
44 42 except (ClassNotFound, ValueError):
45 43 try:
46 44 lexer = guess_lexer(text[:1024], encoding=encoding.encoding)
47 45 except (ClassNotFound, ValueError):
48 46 lexer = TextLexer(encoding=encoding.encoding)
49 47
50 48 formatter = HtmlFormatter(style=style, encoding=encoding.encoding)
51 49
52 50 colorized = highlight(text, lexer, formatter)
53 51 # strip wrapping div
54 52 colorized = colorized[:colorized.find('\n</pre>')]
55 53 colorized = colorized[colorized.find('<pre>')+5:]
56 54 coloriter = iter(colorized.splitlines())
57 55
58 filters['colorize'] = lambda x: coloriter.next()
56 tmpl.filters['colorize'] = lambda x: coloriter.next()
59 57
60 58 oldl = tmpl.cache[field]
61 59 newl = oldl.replace('line|escape', 'line|colorize')
62 60 tmpl.cache[field] = newl
@@ -1,1225 +1,1223 b''
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2, incorporated herein by reference.
7 7
8 8 from node import hex, nullid, nullrev, short
9 9 from i18n import _
10 10 import os, sys, bisect, stat, errno
11 11 import mdiff, bdiff, util, templater, templatefilters, patch, error, encoding
12 12 import match as _match
13 13
14 14 revrangesep = ':'
15 15
16 16 def findpossible(cmd, table, strict=False):
17 17 """
18 18 Return cmd -> (aliases, command table entry)
19 19 for each matching command.
20 20 Return debug commands (or their aliases) only if no normal command matches.
21 21 """
22 22 choice = {}
23 23 debugchoice = {}
24 24 for e in table.keys():
25 25 aliases = e.lstrip("^").split("|")
26 26 found = None
27 27 if cmd in aliases:
28 28 found = cmd
29 29 elif not strict:
30 30 for a in aliases:
31 31 if a.startswith(cmd):
32 32 found = a
33 33 break
34 34 if found is not None:
35 35 if aliases[0].startswith("debug") or found.startswith("debug"):
36 36 debugchoice[found] = (aliases, table[e])
37 37 else:
38 38 choice[found] = (aliases, table[e])
39 39
40 40 if not choice and debugchoice:
41 41 choice = debugchoice
42 42
43 43 return choice
44 44
45 45 def findcmd(cmd, table, strict=True):
46 46 """Return (aliases, command table entry) for command string."""
47 47 choice = findpossible(cmd, table, strict)
48 48
49 49 if cmd in choice:
50 50 return choice[cmd]
51 51
52 52 if len(choice) > 1:
53 53 clist = choice.keys()
54 54 clist.sort()
55 55 raise error.AmbiguousCommand(cmd, clist)
56 56
57 57 if choice:
58 58 return choice.values()[0]
59 59
60 60 raise error.UnknownCommand(cmd)
61 61
62 62 def bail_if_changed(repo):
63 63 if repo.dirstate.parents()[1] != nullid:
64 64 raise util.Abort(_('outstanding uncommitted merge'))
65 65 modified, added, removed, deleted = repo.status()[:4]
66 66 if modified or added or removed or deleted:
67 67 raise util.Abort(_("outstanding uncommitted changes"))
68 68
69 69 def logmessage(opts):
70 70 """ get the log message according to -m and -l option """
71 71 message = opts.get('message')
72 72 logfile = opts.get('logfile')
73 73
74 74 if message and logfile:
75 75 raise util.Abort(_('options --message and --logfile are mutually '
76 76 'exclusive'))
77 77 if not message and logfile:
78 78 try:
79 79 if logfile == '-':
80 80 message = sys.stdin.read()
81 81 else:
82 82 message = open(logfile).read()
83 83 except IOError, inst:
84 84 raise util.Abort(_("can't read commit message '%s': %s") %
85 85 (logfile, inst.strerror))
86 86 return message
87 87
88 88 def loglimit(opts):
89 89 """get the log limit according to option -l/--limit"""
90 90 limit = opts.get('limit')
91 91 if limit:
92 92 try:
93 93 limit = int(limit)
94 94 except ValueError:
95 95 raise util.Abort(_('limit must be a positive integer'))
96 96 if limit <= 0: raise util.Abort(_('limit must be positive'))
97 97 else:
98 98 limit = sys.maxint
99 99 return limit
100 100
101 101 def remoteui(src, opts):
102 102 'build a remote ui from ui or repo and opts'
103 103 if hasattr(src, 'baseui'): # looks like a repository
104 104 dst = src.baseui # drop repo-specific config
105 105 src = src.ui # copy target options from repo
106 106 else: # assume it's a global ui object
107 107 dst = src # keep all global options
108 108
109 109 # copy ssh-specific options
110 110 for o in 'ssh', 'remotecmd':
111 111 v = opts.get(o) or src.config('ui', o)
112 112 if v:
113 113 dst.setconfig("ui", o, v)
114 114 # copy bundle-specific options
115 115 r = src.config('bundle', 'mainreporoot')
116 116 if r:
117 117 dst.setconfig('bundle', 'mainreporoot', r)
118 118
119 119 return dst
120 120
121 121 def revpair(repo, revs):
122 122 '''return pair of nodes, given list of revisions. second item can
123 123 be None, meaning use working dir.'''
124 124
125 125 def revfix(repo, val, defval):
126 126 if not val and val != 0 and defval is not None:
127 127 val = defval
128 128 return repo.lookup(val)
129 129
130 130 if not revs:
131 131 return repo.dirstate.parents()[0], None
132 132 end = None
133 133 if len(revs) == 1:
134 134 if revrangesep in revs[0]:
135 135 start, end = revs[0].split(revrangesep, 1)
136 136 start = revfix(repo, start, 0)
137 137 end = revfix(repo, end, len(repo) - 1)
138 138 else:
139 139 start = revfix(repo, revs[0], None)
140 140 elif len(revs) == 2:
141 141 if revrangesep in revs[0] or revrangesep in revs[1]:
142 142 raise util.Abort(_('too many revisions specified'))
143 143 start = revfix(repo, revs[0], None)
144 144 end = revfix(repo, revs[1], None)
145 145 else:
146 146 raise util.Abort(_('too many revisions specified'))
147 147 return start, end
148 148
149 149 def revrange(repo, revs):
150 150 """Yield revision as strings from a list of revision specifications."""
151 151
152 152 def revfix(repo, val, defval):
153 153 if not val and val != 0 and defval is not None:
154 154 return defval
155 155 return repo.changelog.rev(repo.lookup(val))
156 156
157 157 seen, l = {}, []
158 158 for spec in revs:
159 159 if revrangesep in spec:
160 160 start, end = spec.split(revrangesep, 1)
161 161 start = revfix(repo, start, 0)
162 162 end = revfix(repo, end, len(repo) - 1)
163 163 step = start > end and -1 or 1
164 164 for rev in xrange(start, end+step, step):
165 165 if rev in seen:
166 166 continue
167 167 seen[rev] = 1
168 168 l.append(rev)
169 169 else:
170 170 rev = revfix(repo, spec, None)
171 171 if rev in seen:
172 172 continue
173 173 seen[rev] = 1
174 174 l.append(rev)
175 175
176 176 return l
177 177
178 178 def make_filename(repo, pat, node,
179 179 total=None, seqno=None, revwidth=None, pathname=None):
180 180 node_expander = {
181 181 'H': lambda: hex(node),
182 182 'R': lambda: str(repo.changelog.rev(node)),
183 183 'h': lambda: short(node),
184 184 }
185 185 expander = {
186 186 '%': lambda: '%',
187 187 'b': lambda: os.path.basename(repo.root),
188 188 }
189 189
190 190 try:
191 191 if node:
192 192 expander.update(node_expander)
193 193 if node:
194 194 expander['r'] = (lambda:
195 195 str(repo.changelog.rev(node)).zfill(revwidth or 0))
196 196 if total is not None:
197 197 expander['N'] = lambda: str(total)
198 198 if seqno is not None:
199 199 expander['n'] = lambda: str(seqno)
200 200 if total is not None and seqno is not None:
201 201 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
202 202 if pathname is not None:
203 203 expander['s'] = lambda: os.path.basename(pathname)
204 204 expander['d'] = lambda: os.path.dirname(pathname) or '.'
205 205 expander['p'] = lambda: pathname
206 206
207 207 newname = []
208 208 patlen = len(pat)
209 209 i = 0
210 210 while i < patlen:
211 211 c = pat[i]
212 212 if c == '%':
213 213 i += 1
214 214 c = pat[i]
215 215 c = expander[c]()
216 216 newname.append(c)
217 217 i += 1
218 218 return ''.join(newname)
219 219 except KeyError, inst:
220 220 raise util.Abort(_("invalid format spec '%%%s' in output file name") %
221 221 inst.args[0])
222 222
223 223 def make_file(repo, pat, node=None,
224 224 total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
225 225
226 226 writable = 'w' in mode or 'a' in mode
227 227
228 228 if not pat or pat == '-':
229 229 return writable and sys.stdout or sys.stdin
230 230 if hasattr(pat, 'write') and writable:
231 231 return pat
232 232 if hasattr(pat, 'read') and 'r' in mode:
233 233 return pat
234 234 return open(make_filename(repo, pat, node, total, seqno, revwidth,
235 235 pathname),
236 236 mode)
237 237
238 238 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
239 239 if not globbed and default == 'relpath':
240 240 pats = util.expand_glob(pats or [])
241 241 m = _match.match(repo.root, repo.getcwd(), pats,
242 242 opts.get('include'), opts.get('exclude'), default)
243 243 def badfn(f, msg):
244 244 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
245 245 return False
246 246 m.bad = badfn
247 247 return m
248 248
249 249 def matchall(repo):
250 250 return _match.always(repo.root, repo.getcwd())
251 251
252 252 def matchfiles(repo, files):
253 253 return _match.exact(repo.root, repo.getcwd(), files)
254 254
255 255 def findrenames(repo, added=None, removed=None, threshold=0.5):
256 256 '''find renamed files -- yields (before, after, score) tuples'''
257 257 if added is None or removed is None:
258 258 added, removed = repo.status()[1:3]
259 259 ctx = repo['.']
260 260 for a in added:
261 261 aa = repo.wread(a)
262 262 bestname, bestscore = None, threshold
263 263 for r in removed:
264 264 rr = ctx.filectx(r).data()
265 265
266 266 # bdiff.blocks() returns blocks of matching lines
267 267 # count the number of bytes in each
268 268 equal = 0
269 269 alines = mdiff.splitnewlines(aa)
270 270 matches = bdiff.blocks(aa, rr)
271 271 for x1,x2,y1,y2 in matches:
272 272 for line in alines[x1:x2]:
273 273 equal += len(line)
274 274
275 275 lengths = len(aa) + len(rr)
276 276 if lengths:
277 277 myscore = equal*2.0 / lengths
278 278 if myscore >= bestscore:
279 279 bestname, bestscore = r, myscore
280 280 if bestname:
281 281 yield bestname, a, bestscore
282 282
283 283 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
284 284 if dry_run is None:
285 285 dry_run = opts.get('dry_run')
286 286 if similarity is None:
287 287 similarity = float(opts.get('similarity') or 0)
288 288 add, remove = [], []
289 289 mapping = {}
290 290 audit_path = util.path_auditor(repo.root)
291 291 m = match(repo, pats, opts)
292 292 for abs in repo.walk(m):
293 293 target = repo.wjoin(abs)
294 294 good = True
295 295 try:
296 296 audit_path(abs)
297 297 except:
298 298 good = False
299 299 rel = m.rel(abs)
300 300 exact = m.exact(abs)
301 301 if good and abs not in repo.dirstate:
302 302 add.append(abs)
303 303 mapping[abs] = rel, m.exact(abs)
304 304 if repo.ui.verbose or not exact:
305 305 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
306 306 if repo.dirstate[abs] != 'r' and (not good or not util.lexists(target)
307 307 or (os.path.isdir(target) and not os.path.islink(target))):
308 308 remove.append(abs)
309 309 mapping[abs] = rel, exact
310 310 if repo.ui.verbose or not exact:
311 311 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
312 312 if not dry_run:
313 313 repo.remove(remove)
314 314 repo.add(add)
315 315 if similarity > 0:
316 316 for old, new, score in findrenames(repo, add, remove, similarity):
317 317 oldrel, oldexact = mapping[old]
318 318 newrel, newexact = mapping[new]
319 319 if repo.ui.verbose or not oldexact or not newexact:
320 320 repo.ui.status(_('recording removal of %s as rename to %s '
321 321 '(%d%% similar)\n') %
322 322 (oldrel, newrel, score * 100))
323 323 if not dry_run:
324 324 repo.copy(old, new)
325 325
326 326 def copy(ui, repo, pats, opts, rename=False):
327 327 # called with the repo lock held
328 328 #
329 329 # hgsep => pathname that uses "/" to separate directories
330 330 # ossep => pathname that uses os.sep to separate directories
331 331 cwd = repo.getcwd()
332 332 targets = {}
333 333 after = opts.get("after")
334 334 dryrun = opts.get("dry_run")
335 335
336 336 def walkpat(pat):
337 337 srcs = []
338 338 m = match(repo, [pat], opts, globbed=True)
339 339 for abs in repo.walk(m):
340 340 state = repo.dirstate[abs]
341 341 rel = m.rel(abs)
342 342 exact = m.exact(abs)
343 343 if state in '?r':
344 344 if exact and state == '?':
345 345 ui.warn(_('%s: not copying - file is not managed\n') % rel)
346 346 if exact and state == 'r':
347 347 ui.warn(_('%s: not copying - file has been marked for'
348 348 ' remove\n') % rel)
349 349 continue
350 350 # abs: hgsep
351 351 # rel: ossep
352 352 srcs.append((abs, rel, exact))
353 353 return srcs
354 354
355 355 # abssrc: hgsep
356 356 # relsrc: ossep
357 357 # otarget: ossep
358 358 def copyfile(abssrc, relsrc, otarget, exact):
359 359 abstarget = util.canonpath(repo.root, cwd, otarget)
360 360 reltarget = repo.pathto(abstarget, cwd)
361 361 target = repo.wjoin(abstarget)
362 362 src = repo.wjoin(abssrc)
363 363 state = repo.dirstate[abstarget]
364 364
365 365 # check for collisions
366 366 prevsrc = targets.get(abstarget)
367 367 if prevsrc is not None:
368 368 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
369 369 (reltarget, repo.pathto(abssrc, cwd),
370 370 repo.pathto(prevsrc, cwd)))
371 371 return
372 372
373 373 # check for overwrites
374 374 exists = os.path.exists(target)
375 375 if not after and exists or after and state in 'mn':
376 376 if not opts['force']:
377 377 ui.warn(_('%s: not overwriting - file exists\n') %
378 378 reltarget)
379 379 return
380 380
381 381 if after:
382 382 if not exists:
383 383 return
384 384 elif not dryrun:
385 385 try:
386 386 if exists:
387 387 os.unlink(target)
388 388 targetdir = os.path.dirname(target) or '.'
389 389 if not os.path.isdir(targetdir):
390 390 os.makedirs(targetdir)
391 391 util.copyfile(src, target)
392 392 except IOError, inst:
393 393 if inst.errno == errno.ENOENT:
394 394 ui.warn(_('%s: deleted in working copy\n') % relsrc)
395 395 else:
396 396 ui.warn(_('%s: cannot copy - %s\n') %
397 397 (relsrc, inst.strerror))
398 398 return True # report a failure
399 399
400 400 if ui.verbose or not exact:
401 401 if rename:
402 402 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
403 403 else:
404 404 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
405 405
406 406 targets[abstarget] = abssrc
407 407
408 408 # fix up dirstate
409 409 origsrc = repo.dirstate.copied(abssrc) or abssrc
410 410 if abstarget == origsrc: # copying back a copy?
411 411 if state not in 'mn' and not dryrun:
412 412 repo.dirstate.normallookup(abstarget)
413 413 else:
414 414 if repo.dirstate[origsrc] == 'a' and origsrc == abssrc:
415 415 if not ui.quiet:
416 416 ui.warn(_("%s has not been committed yet, so no copy "
417 417 "data will be stored for %s.\n")
418 418 % (repo.pathto(origsrc, cwd), reltarget))
419 419 if repo.dirstate[abstarget] in '?r' and not dryrun:
420 420 repo.add([abstarget])
421 421 elif not dryrun:
422 422 repo.copy(origsrc, abstarget)
423 423
424 424 if rename and not dryrun:
425 425 repo.remove([abssrc], not after)
426 426
427 427 # pat: ossep
428 428 # dest ossep
429 429 # srcs: list of (hgsep, hgsep, ossep, bool)
430 430 # return: function that takes hgsep and returns ossep
431 431 def targetpathfn(pat, dest, srcs):
432 432 if os.path.isdir(pat):
433 433 abspfx = util.canonpath(repo.root, cwd, pat)
434 434 abspfx = util.localpath(abspfx)
435 435 if destdirexists:
436 436 striplen = len(os.path.split(abspfx)[0])
437 437 else:
438 438 striplen = len(abspfx)
439 439 if striplen:
440 440 striplen += len(os.sep)
441 441 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
442 442 elif destdirexists:
443 443 res = lambda p: os.path.join(dest,
444 444 os.path.basename(util.localpath(p)))
445 445 else:
446 446 res = lambda p: dest
447 447 return res
448 448
449 449 # pat: ossep
450 450 # dest ossep
451 451 # srcs: list of (hgsep, hgsep, ossep, bool)
452 452 # return: function that takes hgsep and returns ossep
453 453 def targetpathafterfn(pat, dest, srcs):
454 454 if util.patkind(pat, None)[0]:
455 455 # a mercurial pattern
456 456 res = lambda p: os.path.join(dest,
457 457 os.path.basename(util.localpath(p)))
458 458 else:
459 459 abspfx = util.canonpath(repo.root, cwd, pat)
460 460 if len(abspfx) < len(srcs[0][0]):
461 461 # A directory. Either the target path contains the last
462 462 # component of the source path or it does not.
463 463 def evalpath(striplen):
464 464 score = 0
465 465 for s in srcs:
466 466 t = os.path.join(dest, util.localpath(s[0])[striplen:])
467 467 if os.path.exists(t):
468 468 score += 1
469 469 return score
470 470
471 471 abspfx = util.localpath(abspfx)
472 472 striplen = len(abspfx)
473 473 if striplen:
474 474 striplen += len(os.sep)
475 475 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
476 476 score = evalpath(striplen)
477 477 striplen1 = len(os.path.split(abspfx)[0])
478 478 if striplen1:
479 479 striplen1 += len(os.sep)
480 480 if evalpath(striplen1) > score:
481 481 striplen = striplen1
482 482 res = lambda p: os.path.join(dest,
483 483 util.localpath(p)[striplen:])
484 484 else:
485 485 # a file
486 486 if destdirexists:
487 487 res = lambda p: os.path.join(dest,
488 488 os.path.basename(util.localpath(p)))
489 489 else:
490 490 res = lambda p: dest
491 491 return res
492 492
493 493
494 494 pats = util.expand_glob(pats)
495 495 if not pats:
496 496 raise util.Abort(_('no source or destination specified'))
497 497 if len(pats) == 1:
498 498 raise util.Abort(_('no destination specified'))
499 499 dest = pats.pop()
500 500 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
501 501 if not destdirexists:
502 502 if len(pats) > 1 or util.patkind(pats[0], None)[0]:
503 503 raise util.Abort(_('with multiple sources, destination must be an '
504 504 'existing directory'))
505 505 if util.endswithsep(dest):
506 506 raise util.Abort(_('destination %s is not a directory') % dest)
507 507
508 508 tfn = targetpathfn
509 509 if after:
510 510 tfn = targetpathafterfn
511 511 copylist = []
512 512 for pat in pats:
513 513 srcs = walkpat(pat)
514 514 if not srcs:
515 515 continue
516 516 copylist.append((tfn(pat, dest, srcs), srcs))
517 517 if not copylist:
518 518 raise util.Abort(_('no files to copy'))
519 519
520 520 errors = 0
521 521 for targetpath, srcs in copylist:
522 522 for abssrc, relsrc, exact in srcs:
523 523 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
524 524 errors += 1
525 525
526 526 if errors:
527 527 ui.warn(_('(consider using --after)\n'))
528 528
529 529 return errors
530 530
531 531 def service(opts, parentfn=None, initfn=None, runfn=None):
532 532 '''Run a command as a service.'''
533 533
534 534 if opts['daemon'] and not opts['daemon_pipefds']:
535 535 rfd, wfd = os.pipe()
536 536 args = sys.argv[:]
537 537 args.append('--daemon-pipefds=%d,%d' % (rfd, wfd))
538 538 # Don't pass --cwd to the child process, because we've already
539 539 # changed directory.
540 540 for i in xrange(1,len(args)):
541 541 if args[i].startswith('--cwd='):
542 542 del args[i]
543 543 break
544 544 elif args[i].startswith('--cwd'):
545 545 del args[i:i+2]
546 546 break
547 547 pid = os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
548 548 args[0], args)
549 549 os.close(wfd)
550 550 os.read(rfd, 1)
551 551 if parentfn:
552 552 return parentfn(pid)
553 553 else:
554 554 os._exit(0)
555 555
556 556 if initfn:
557 557 initfn()
558 558
559 559 if opts['pid_file']:
560 560 fp = open(opts['pid_file'], 'w')
561 561 fp.write(str(os.getpid()) + '\n')
562 562 fp.close()
563 563
564 564 if opts['daemon_pipefds']:
565 565 rfd, wfd = [int(x) for x in opts['daemon_pipefds'].split(',')]
566 566 os.close(rfd)
567 567 try:
568 568 os.setsid()
569 569 except AttributeError:
570 570 pass
571 571 os.write(wfd, 'y')
572 572 os.close(wfd)
573 573 sys.stdout.flush()
574 574 sys.stderr.flush()
575 575 fd = os.open(util.nulldev, os.O_RDWR)
576 576 if fd != 0: os.dup2(fd, 0)
577 577 if fd != 1: os.dup2(fd, 1)
578 578 if fd != 2: os.dup2(fd, 2)
579 579 if fd not in (0, 1, 2): os.close(fd)
580 580
581 581 if runfn:
582 582 return runfn()
583 583
584 584 class changeset_printer(object):
585 585 '''show changeset information when templating not requested.'''
586 586
587 587 def __init__(self, ui, repo, patch, diffopts, buffered):
588 588 self.ui = ui
589 589 self.repo = repo
590 590 self.buffered = buffered
591 591 self.patch = patch
592 592 self.diffopts = diffopts
593 593 self.header = {}
594 594 self.hunk = {}
595 595 self.lastheader = None
596 596
597 597 def flush(self, rev):
598 598 if rev in self.header:
599 599 h = self.header[rev]
600 600 if h != self.lastheader:
601 601 self.lastheader = h
602 602 self.ui.write(h)
603 603 del self.header[rev]
604 604 if rev in self.hunk:
605 605 self.ui.write(self.hunk[rev])
606 606 del self.hunk[rev]
607 607 return 1
608 608 return 0
609 609
610 610 def show(self, ctx, copies=(), **props):
611 611 if self.buffered:
612 612 self.ui.pushbuffer()
613 613 self._show(ctx, copies, props)
614 614 self.hunk[ctx.rev()] = self.ui.popbuffer()
615 615 else:
616 616 self._show(ctx, copies, props)
617 617
618 618 def _show(self, ctx, copies, props):
619 619 '''show a single changeset or file revision'''
620 620 changenode = ctx.node()
621 621 rev = ctx.rev()
622 622
623 623 if self.ui.quiet:
624 624 self.ui.write("%d:%s\n" % (rev, short(changenode)))
625 625 return
626 626
627 627 log = self.repo.changelog
628 628 changes = log.read(changenode)
629 629 date = util.datestr(changes[2])
630 630 extra = changes[5]
631 631 branch = extra.get("branch")
632 632
633 633 hexfunc = self.ui.debugflag and hex or short
634 634
635 635 parents = [(p, hexfunc(log.node(p)))
636 636 for p in self._meaningful_parentrevs(log, rev)]
637 637
638 638 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)))
639 639
640 640 # don't show the default branch name
641 641 if branch != 'default':
642 642 branch = encoding.tolocal(branch)
643 643 self.ui.write(_("branch: %s\n") % branch)
644 644 for tag in self.repo.nodetags(changenode):
645 645 self.ui.write(_("tag: %s\n") % tag)
646 646 for parent in parents:
647 647 self.ui.write(_("parent: %d:%s\n") % parent)
648 648
649 649 if self.ui.debugflag:
650 650 self.ui.write(_("manifest: %d:%s\n") %
651 651 (self.repo.manifest.rev(changes[0]), hex(changes[0])))
652 652 self.ui.write(_("user: %s\n") % changes[1])
653 653 self.ui.write(_("date: %s\n") % date)
654 654
655 655 if self.ui.debugflag:
656 656 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
657 657 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
658 658 files):
659 659 if value:
660 660 self.ui.write("%-12s %s\n" % (key, " ".join(value)))
661 661 elif changes[3] and self.ui.verbose:
662 662 self.ui.write(_("files: %s\n") % " ".join(changes[3]))
663 663 if copies and self.ui.verbose:
664 664 copies = ['%s (%s)' % c for c in copies]
665 665 self.ui.write(_("copies: %s\n") % ' '.join(copies))
666 666
667 667 if extra and self.ui.debugflag:
668 668 for key, value in sorted(extra.items()):
669 669 self.ui.write(_("extra: %s=%s\n")
670 670 % (key, value.encode('string_escape')))
671 671
672 672 description = changes[4].strip()
673 673 if description:
674 674 if self.ui.verbose:
675 675 self.ui.write(_("description:\n"))
676 676 self.ui.write(description)
677 677 self.ui.write("\n\n")
678 678 else:
679 679 self.ui.write(_("summary: %s\n") %
680 680 description.splitlines()[0])
681 681 self.ui.write("\n")
682 682
683 683 self.showpatch(changenode)
684 684
685 685 def showpatch(self, node):
686 686 if self.patch:
687 687 prev = self.repo.changelog.parents(node)[0]
688 688 chunks = patch.diff(self.repo, prev, node, match=self.patch,
689 689 opts=patch.diffopts(self.ui, self.diffopts))
690 690 for chunk in chunks:
691 691 self.ui.write(chunk)
692 692 self.ui.write("\n")
693 693
694 694 def _meaningful_parentrevs(self, log, rev):
695 695 """Return list of meaningful (or all if debug) parentrevs for rev.
696 696
697 697 For merges (two non-nullrev revisions) both parents are meaningful.
698 698 Otherwise the first parent revision is considered meaningful if it
699 699 is not the preceding revision.
700 700 """
701 701 parents = log.parentrevs(rev)
702 702 if not self.ui.debugflag and parents[1] == nullrev:
703 703 if parents[0] >= rev - 1:
704 704 parents = []
705 705 else:
706 706 parents = [parents[0]]
707 707 return parents
708 708
709 709
710 710 class changeset_templater(changeset_printer):
711 711 '''format changeset information.'''
712 712
713 713 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
714 714 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
715 filters = templatefilters.filters.copy()
716 filters['formatnode'] = (ui.debugflag and (lambda x: x)
717 or (lambda x: x[:12]))
718 self.t = templater.templater(mapfile, filters,
715 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
716 self.t = templater.templater(mapfile, {'formatnode': formatnode},
719 717 cache={
720 718 'parent': '{rev}:{node|formatnode} ',
721 719 'manifest': '{rev}:{node|formatnode}',
722 720 'filecopy': '{name} ({source})'})
723 721
724 722 def use_template(self, t):
725 723 '''set template string to use'''
726 724 self.t.cache['changeset'] = t
727 725
728 726 def _meaningful_parentrevs(self, ctx):
729 727 """Return list of meaningful (or all if debug) parentrevs for rev.
730 728 """
731 729 parents = ctx.parents()
732 730 if len(parents) > 1:
733 731 return parents
734 732 if self.ui.debugflag:
735 733 return [parents[0], self.repo['null']]
736 734 if parents[0].rev() >= ctx.rev() - 1:
737 735 return []
738 736 return parents
739 737
740 738 def _show(self, ctx, copies, props):
741 739 '''show a single changeset or file revision'''
742 740
743 741 def showlist(name, values, plural=None, **args):
744 742 '''expand set of values.
745 743 name is name of key in template map.
746 744 values is list of strings or dicts.
747 745 plural is plural of name, if not simply name + 's'.
748 746
749 747 expansion works like this, given name 'foo'.
750 748
751 749 if values is empty, expand 'no_foos'.
752 750
753 751 if 'foo' not in template map, return values as a string,
754 752 joined by space.
755 753
756 754 expand 'start_foos'.
757 755
758 756 for each value, expand 'foo'. if 'last_foo' in template
759 757 map, expand it instead of 'foo' for last key.
760 758
761 759 expand 'end_foos'.
762 760 '''
763 761 if plural: names = plural
764 762 else: names = name + 's'
765 763 if not values:
766 764 noname = 'no_' + names
767 765 if noname in self.t:
768 766 yield self.t(noname, **args)
769 767 return
770 768 if name not in self.t:
771 769 if isinstance(values[0], str):
772 770 yield ' '.join(values)
773 771 else:
774 772 for v in values:
775 773 yield dict(v, **args)
776 774 return
777 775 startname = 'start_' + names
778 776 if startname in self.t:
779 777 yield self.t(startname, **args)
780 778 vargs = args.copy()
781 779 def one(v, tag=name):
782 780 try:
783 781 vargs.update(v)
784 782 except (AttributeError, ValueError):
785 783 try:
786 784 for a, b in v:
787 785 vargs[a] = b
788 786 except ValueError:
789 787 vargs[name] = v
790 788 return self.t(tag, **vargs)
791 789 lastname = 'last_' + name
792 790 if lastname in self.t:
793 791 last = values.pop()
794 792 else:
795 793 last = None
796 794 for v in values:
797 795 yield one(v)
798 796 if last is not None:
799 797 yield one(last, tag=lastname)
800 798 endname = 'end_' + names
801 799 if endname in self.t:
802 800 yield self.t(endname, **args)
803 801
804 802 def showbranches(**args):
805 803 branch = ctx.branch()
806 804 if branch != 'default':
807 805 branch = encoding.tolocal(branch)
808 806 return showlist('branch', [branch], plural='branches', **args)
809 807
810 808 def showparents(**args):
811 809 parents = [[('rev', p.rev()), ('node', p.hex())]
812 810 for p in self._meaningful_parentrevs(ctx)]
813 811 return showlist('parent', parents, **args)
814 812
815 813 def showtags(**args):
816 814 return showlist('tag', ctx.tags(), **args)
817 815
818 816 def showextras(**args):
819 817 for key, value in sorted(ctx.extra().items()):
820 818 args = args.copy()
821 819 args.update(dict(key=key, value=value))
822 820 yield self.t('extra', **args)
823 821
824 822 def showcopies(**args):
825 823 c = [{'name': x[0], 'source': x[1]} for x in copies]
826 824 return showlist('file_copy', c, plural='file_copies', **args)
827 825
828 826 files = []
829 827 def getfiles():
830 828 if not files:
831 829 files[:] = self.repo.status(ctx.parents()[0].node(),
832 830 ctx.node())[:3]
833 831 return files
834 832 def showfiles(**args):
835 833 return showlist('file', ctx.files(), **args)
836 834 def showmods(**args):
837 835 return showlist('file_mod', getfiles()[0], **args)
838 836 def showadds(**args):
839 837 return showlist('file_add', getfiles()[1], **args)
840 838 def showdels(**args):
841 839 return showlist('file_del', getfiles()[2], **args)
842 840 def showmanifest(**args):
843 841 args = args.copy()
844 842 args.update(dict(rev=self.repo.manifest.rev(ctx.changeset()[0]),
845 843 node=hex(ctx.changeset()[0])))
846 844 return self.t('manifest', **args)
847 845
848 846 def showdiffstat(**args):
849 847 diff = patch.diff(self.repo, ctx.parents()[0].node(), ctx.node())
850 848 files, adds, removes = 0, 0, 0
851 849 for i in patch.diffstatdata(util.iterlines(diff)):
852 850 files += 1
853 851 adds += i[1]
854 852 removes += i[2]
855 853 return '%s: +%s/-%s' % (files, adds, removes)
856 854
857 855 defprops = {
858 856 'author': ctx.user(),
859 857 'branches': showbranches,
860 858 'date': ctx.date(),
861 859 'desc': ctx.description().strip(),
862 860 'file_adds': showadds,
863 861 'file_dels': showdels,
864 862 'file_mods': showmods,
865 863 'files': showfiles,
866 864 'file_copies': showcopies,
867 865 'manifest': showmanifest,
868 866 'node': ctx.hex(),
869 867 'parents': showparents,
870 868 'rev': ctx.rev(),
871 869 'tags': showtags,
872 870 'extras': showextras,
873 871 'diffstat': showdiffstat,
874 872 }
875 873 props = props.copy()
876 874 props.update(defprops)
877 875
878 876 # find correct templates for current mode
879 877
880 878 tmplmodes = [
881 879 (True, None),
882 880 (self.ui.verbose, 'verbose'),
883 881 (self.ui.quiet, 'quiet'),
884 882 (self.ui.debugflag, 'debug'),
885 883 ]
886 884
887 885 types = {'header': '', 'changeset': 'changeset'}
888 886 for mode, postfix in tmplmodes:
889 887 for type in types:
890 888 cur = postfix and ('%s_%s' % (type, postfix)) or type
891 889 if mode and cur in self.t:
892 890 types[type] = cur
893 891
894 892 try:
895 893
896 894 # write header
897 895 if types['header']:
898 896 h = templater.stringify(self.t(types['header'], **props))
899 897 if self.buffered:
900 898 self.header[ctx.rev()] = h
901 899 else:
902 900 self.ui.write(h)
903 901
904 902 # write changeset metadata, then patch if requested
905 903 key = types['changeset']
906 904 self.ui.write(templater.stringify(self.t(key, **props)))
907 905 self.showpatch(ctx.node())
908 906
909 907 except KeyError, inst:
910 908 msg = _("%s: no key named '%s'")
911 909 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
912 910 except SyntaxError, inst:
913 911 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
914 912
915 913 def show_changeset(ui, repo, opts, buffered=False, matchfn=False):
916 914 """show one changeset using template or regular display.
917 915
918 916 Display format will be the first non-empty hit of:
919 917 1. option 'template'
920 918 2. option 'style'
921 919 3. [ui] setting 'logtemplate'
922 920 4. [ui] setting 'style'
923 921 If all of these values are either the unset or the empty string,
924 922 regular display via changeset_printer() is done.
925 923 """
926 924 # options
927 925 patch = False
928 926 if opts.get('patch'):
929 927 patch = matchfn or matchall(repo)
930 928
931 929 tmpl = opts.get('template')
932 930 style = None
933 931 if tmpl:
934 932 tmpl = templater.parsestring(tmpl, quoted=False)
935 933 else:
936 934 style = opts.get('style')
937 935
938 936 # ui settings
939 937 if not (tmpl or style):
940 938 tmpl = ui.config('ui', 'logtemplate')
941 939 if tmpl:
942 940 tmpl = templater.parsestring(tmpl)
943 941 else:
944 942 style = ui.config('ui', 'style')
945 943
946 944 if not (tmpl or style):
947 945 return changeset_printer(ui, repo, patch, opts, buffered)
948 946
949 947 mapfile = None
950 948 if style and not tmpl:
951 949 mapfile = style
952 950 if not os.path.split(mapfile)[0]:
953 951 mapname = (templater.templatepath('map-cmdline.' + mapfile)
954 952 or templater.templatepath(mapfile))
955 953 if mapname: mapfile = mapname
956 954
957 955 try:
958 956 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
959 957 except SyntaxError, inst:
960 958 raise util.Abort(inst.args[0])
961 959 if tmpl: t.use_template(tmpl)
962 960 return t
963 961
964 962 def finddate(ui, repo, date):
965 963 """Find the tipmost changeset that matches the given date spec"""
966 964 df = util.matchdate(date)
967 965 get = util.cachefunc(lambda r: repo[r].changeset())
968 966 changeiter, matchfn = walkchangerevs(ui, repo, [], get, {'rev':None})
969 967 results = {}
970 968 for st, rev, fns in changeiter:
971 969 if st == 'add':
972 970 d = get(rev)[2]
973 971 if df(d[0]):
974 972 results[rev] = d
975 973 elif st == 'iter':
976 974 if rev in results:
977 975 ui.status(_("Found revision %s from %s\n") %
978 976 (rev, util.datestr(results[rev])))
979 977 return str(rev)
980 978
981 979 raise util.Abort(_("revision matching date not found"))
982 980
983 981 def walkchangerevs(ui, repo, pats, change, opts):
984 982 '''Iterate over files and the revs in which they changed.
985 983
986 984 Callers most commonly need to iterate backwards over the history
987 985 in which they are interested. Doing so has awful (quadratic-looking)
988 986 performance, so we use iterators in a "windowed" way.
989 987
990 988 We walk a window of revisions in the desired order. Within the
991 989 window, we first walk forwards to gather data, then in the desired
992 990 order (usually backwards) to display it.
993 991
994 992 This function returns an (iterator, matchfn) tuple. The iterator
995 993 yields 3-tuples. They will be of one of the following forms:
996 994
997 995 "window", incrementing, lastrev: stepping through a window,
998 996 positive if walking forwards through revs, last rev in the
999 997 sequence iterated over - use to reset state for the current window
1000 998
1001 999 "add", rev, fns: out-of-order traversal of the given file names
1002 1000 fns, which changed during revision rev - use to gather data for
1003 1001 possible display
1004 1002
1005 1003 "iter", rev, None: in-order traversal of the revs earlier iterated
1006 1004 over with "add" - use to display data'''
1007 1005
1008 1006 def increasing_windows(start, end, windowsize=8, sizelimit=512):
1009 1007 if start < end:
1010 1008 while start < end:
1011 1009 yield start, min(windowsize, end-start)
1012 1010 start += windowsize
1013 1011 if windowsize < sizelimit:
1014 1012 windowsize *= 2
1015 1013 else:
1016 1014 while start > end:
1017 1015 yield start, min(windowsize, start-end-1)
1018 1016 start -= windowsize
1019 1017 if windowsize < sizelimit:
1020 1018 windowsize *= 2
1021 1019
1022 1020 m = match(repo, pats, opts)
1023 1021 follow = opts.get('follow') or opts.get('follow_first')
1024 1022
1025 1023 if not len(repo):
1026 1024 return [], m
1027 1025
1028 1026 if follow:
1029 1027 defrange = '%s:0' % repo['.'].rev()
1030 1028 else:
1031 1029 defrange = '-1:0'
1032 1030 revs = revrange(repo, opts['rev'] or [defrange])
1033 1031 wanted = set()
1034 1032 slowpath = m.anypats() or (m.files() and opts.get('removed'))
1035 1033 fncache = {}
1036 1034
1037 1035 if not slowpath and not m.files():
1038 1036 # No files, no patterns. Display all revs.
1039 1037 wanted = set(revs)
1040 1038 copies = []
1041 1039 if not slowpath:
1042 1040 # Only files, no patterns. Check the history of each file.
1043 1041 def filerevgen(filelog, node):
1044 1042 cl_count = len(repo)
1045 1043 if node is None:
1046 1044 last = len(filelog) - 1
1047 1045 else:
1048 1046 last = filelog.rev(node)
1049 1047 for i, window in increasing_windows(last, nullrev):
1050 1048 revs = []
1051 1049 for j in xrange(i - window, i + 1):
1052 1050 n = filelog.node(j)
1053 1051 revs.append((filelog.linkrev(j),
1054 1052 follow and filelog.renamed(n)))
1055 1053 for rev in reversed(revs):
1056 1054 # only yield rev for which we have the changelog, it can
1057 1055 # happen while doing "hg log" during a pull or commit
1058 1056 if rev[0] < cl_count:
1059 1057 yield rev
1060 1058 def iterfiles():
1061 1059 for filename in m.files():
1062 1060 yield filename, None
1063 1061 for filename_node in copies:
1064 1062 yield filename_node
1065 1063 minrev, maxrev = min(revs), max(revs)
1066 1064 for file_, node in iterfiles():
1067 1065 filelog = repo.file(file_)
1068 1066 if not len(filelog):
1069 1067 if node is None:
1070 1068 # A zero count may be a directory or deleted file, so
1071 1069 # try to find matching entries on the slow path.
1072 1070 if follow:
1073 1071 raise util.Abort(_('cannot follow nonexistent file: "%s"') % file_)
1074 1072 slowpath = True
1075 1073 break
1076 1074 else:
1077 1075 ui.warn(_('%s:%s copy source revision cannot be found!\n')
1078 1076 % (file_, short(node)))
1079 1077 continue
1080 1078 for rev, copied in filerevgen(filelog, node):
1081 1079 if rev <= maxrev:
1082 1080 if rev < minrev:
1083 1081 break
1084 1082 fncache.setdefault(rev, [])
1085 1083 fncache[rev].append(file_)
1086 1084 wanted.add(rev)
1087 1085 if follow and copied:
1088 1086 copies.append(copied)
1089 1087 if slowpath:
1090 1088 if follow:
1091 1089 raise util.Abort(_('can only follow copies/renames for explicit '
1092 1090 'file names'))
1093 1091
1094 1092 # The slow path checks files modified in every changeset.
1095 1093 def changerevgen():
1096 1094 for i, window in increasing_windows(len(repo) - 1, nullrev):
1097 1095 for j in xrange(i - window, i + 1):
1098 1096 yield j, change(j)[3]
1099 1097
1100 1098 for rev, changefiles in changerevgen():
1101 1099 matches = filter(m, changefiles)
1102 1100 if matches:
1103 1101 fncache[rev] = matches
1104 1102 wanted.add(rev)
1105 1103
1106 1104 class followfilter:
1107 1105 def __init__(self, onlyfirst=False):
1108 1106 self.startrev = nullrev
1109 1107 self.roots = []
1110 1108 self.onlyfirst = onlyfirst
1111 1109
1112 1110 def match(self, rev):
1113 1111 def realparents(rev):
1114 1112 if self.onlyfirst:
1115 1113 return repo.changelog.parentrevs(rev)[0:1]
1116 1114 else:
1117 1115 return filter(lambda x: x != nullrev,
1118 1116 repo.changelog.parentrevs(rev))
1119 1117
1120 1118 if self.startrev == nullrev:
1121 1119 self.startrev = rev
1122 1120 return True
1123 1121
1124 1122 if rev > self.startrev:
1125 1123 # forward: all descendants
1126 1124 if not self.roots:
1127 1125 self.roots.append(self.startrev)
1128 1126 for parent in realparents(rev):
1129 1127 if parent in self.roots:
1130 1128 self.roots.append(rev)
1131 1129 return True
1132 1130 else:
1133 1131 # backwards: all parents
1134 1132 if not self.roots:
1135 1133 self.roots.extend(realparents(self.startrev))
1136 1134 if rev in self.roots:
1137 1135 self.roots.remove(rev)
1138 1136 self.roots.extend(realparents(rev))
1139 1137 return True
1140 1138
1141 1139 return False
1142 1140
1143 1141 # it might be worthwhile to do this in the iterator if the rev range
1144 1142 # is descending and the prune args are all within that range
1145 1143 for rev in opts.get('prune', ()):
1146 1144 rev = repo.changelog.rev(repo.lookup(rev))
1147 1145 ff = followfilter()
1148 1146 stop = min(revs[0], revs[-1])
1149 1147 for x in xrange(rev, stop-1, -1):
1150 1148 if ff.match(x):
1151 1149 wanted.discard(x)
1152 1150
1153 1151 def iterate():
1154 1152 if follow and not m.files():
1155 1153 ff = followfilter(onlyfirst=opts.get('follow_first'))
1156 1154 def want(rev):
1157 1155 return ff.match(rev) and rev in wanted
1158 1156 else:
1159 1157 def want(rev):
1160 1158 return rev in wanted
1161 1159
1162 1160 for i, window in increasing_windows(0, len(revs)):
1163 1161 yield 'window', revs[0] < revs[-1], revs[-1]
1164 1162 nrevs = [rev for rev in revs[i:i+window] if want(rev)]
1165 1163 for rev in sorted(nrevs):
1166 1164 fns = fncache.get(rev)
1167 1165 if not fns:
1168 1166 def fns_generator():
1169 1167 for f in change(rev)[3]:
1170 1168 if m(f):
1171 1169 yield f
1172 1170 fns = fns_generator()
1173 1171 yield 'add', rev, fns
1174 1172 for rev in nrevs:
1175 1173 yield 'iter', rev, None
1176 1174 return iterate(), m
1177 1175
1178 1176 def commit(ui, repo, commitfunc, pats, opts):
1179 1177 '''commit the specified files or all outstanding changes'''
1180 1178 date = opts.get('date')
1181 1179 if date:
1182 1180 opts['date'] = util.parsedate(date)
1183 1181 message = logmessage(opts)
1184 1182
1185 1183 # extract addremove carefully -- this function can be called from a command
1186 1184 # that doesn't support addremove
1187 1185 if opts.get('addremove'):
1188 1186 addremove(repo, pats, opts)
1189 1187
1190 1188 m = match(repo, pats, opts)
1191 1189 if pats:
1192 1190 modified, added, removed = repo.status(match=m)[:3]
1193 1191 files = sorted(modified + added + removed)
1194 1192
1195 1193 def is_dir(f):
1196 1194 name = f + '/'
1197 1195 i = bisect.bisect(files, name)
1198 1196 return i < len(files) and files[i].startswith(name)
1199 1197
1200 1198 for f in m.files():
1201 1199 if f == '.':
1202 1200 continue
1203 1201 if f not in files:
1204 1202 rf = repo.wjoin(f)
1205 1203 rel = repo.pathto(f)
1206 1204 try:
1207 1205 mode = os.lstat(rf)[stat.ST_MODE]
1208 1206 except OSError:
1209 1207 if is_dir(f): # deleted directory ?
1210 1208 continue
1211 1209 raise util.Abort(_("file %s not found!") % rel)
1212 1210 if stat.S_ISDIR(mode):
1213 1211 if not is_dir(f):
1214 1212 raise util.Abort(_("no match under directory %s!")
1215 1213 % rel)
1216 1214 elif not (stat.S_ISREG(mode) or stat.S_ISLNK(mode)):
1217 1215 raise util.Abort(_("can't commit %s: "
1218 1216 "unsupported file type!") % rel)
1219 1217 elif f not in repo.dirstate:
1220 1218 raise util.Abort(_("file %s not tracked!") % rel)
1221 1219 m = matchfiles(repo, files)
1222 1220 try:
1223 1221 return commitfunc(ui, repo, message, m, opts)
1224 1222 except ValueError, inst:
1225 1223 raise util.Abort(str(inst))
@@ -1,312 +1,311 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 of the
7 7 # GNU General Public License version 2, incorporated herein by reference.
8 8
9 9 import os
10 from mercurial import ui, hg, util, hook, error, encoding
11 from mercurial import templater, templatefilters
10 from mercurial import ui, hg, util, hook, error, encoding, templater
12 11 from common import get_mtime, ErrorResponse
13 12 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
14 13 from common import HTTP_UNAUTHORIZED, HTTP_METHOD_NOT_ALLOWED
15 14 from request import wsgirequest
16 15 import webcommands, protocol, webutil
17 16
18 17 perms = {
19 18 'changegroup': 'pull',
20 19 'changegroupsubset': 'pull',
21 20 'unbundle': 'push',
22 21 'stream_out': 'pull',
23 22 }
24 23
25 24 class hgweb(object):
26 25 def __init__(self, repo, name=None):
27 26 if isinstance(repo, str):
28 27 u = ui.ui()
29 28 u.setconfig('ui', 'report_untrusted', 'off')
30 29 u.setconfig('ui', 'interactive', 'off')
31 30 self.repo = hg.repository(u, repo)
32 31 else:
33 32 self.repo = repo
34 33
35 34 hook.redirect(True)
36 35 self.mtime = -1
37 36 self.reponame = name
38 37 self.archives = 'zip', 'gz', 'bz2'
39 38 self.stripecount = 1
40 39 # a repo owner may set web.templates in .hg/hgrc to get any file
41 40 # readable by the user running the CGI script
42 41 self.templatepath = self.config('web', 'templates')
43 42
44 43 # The CGI scripts are often run by a user different from the repo owner.
45 44 # Trust the settings from the .hg/hgrc files by default.
46 45 def config(self, section, name, default=None, untrusted=True):
47 46 return self.repo.ui.config(section, name, default,
48 47 untrusted=untrusted)
49 48
50 49 def configbool(self, section, name, default=False, untrusted=True):
51 50 return self.repo.ui.configbool(section, name, default,
52 51 untrusted=untrusted)
53 52
54 53 def configlist(self, section, name, default=None, untrusted=True):
55 54 return self.repo.ui.configlist(section, name, default,
56 55 untrusted=untrusted)
57 56
58 57 def refresh(self):
59 58 mtime = get_mtime(self.repo.root)
60 59 if mtime != self.mtime:
61 60 self.mtime = mtime
62 61 self.repo = hg.repository(self.repo.ui, self.repo.root)
63 62 self.maxchanges = int(self.config("web", "maxchanges", 10))
64 63 self.stripecount = int(self.config("web", "stripes", 1))
65 64 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
66 65 self.maxfiles = int(self.config("web", "maxfiles", 10))
67 66 self.allowpull = self.configbool("web", "allowpull", True)
68 67 self.encoding = self.config("web", "encoding", encoding.encoding)
69 68
70 69 def run(self):
71 70 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
72 71 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
73 72 import mercurial.hgweb.wsgicgi as wsgicgi
74 73 wsgicgi.launch(self)
75 74
76 75 def __call__(self, env, respond):
77 76 req = wsgirequest(env, respond)
78 77 return self.run_wsgi(req)
79 78
80 79 def run_wsgi(self, req):
81 80
82 81 self.refresh()
83 82
84 83 # process this if it's a protocol request
85 84 # protocol bits don't need to create any URLs
86 85 # and the clients always use the old URL structure
87 86
88 87 cmd = req.form.get('cmd', [''])[0]
89 88 if cmd and cmd in protocol.__all__:
90 89 try:
91 90 if cmd in perms:
92 91 try:
93 92 self.check_perm(req, perms[cmd])
94 93 except ErrorResponse, inst:
95 94 if cmd == 'unbundle':
96 95 req.drain()
97 96 raise
98 97 method = getattr(protocol, cmd)
99 98 return method(self.repo, req)
100 99 except ErrorResponse, inst:
101 100 req.respond(inst, protocol.HGTYPE)
102 101 if not inst.message:
103 102 return []
104 103 return '0\n%s\n' % inst.message,
105 104
106 105 # work with CGI variables to create coherent structure
107 106 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
108 107
109 108 req.url = req.env['SCRIPT_NAME']
110 109 if not req.url.endswith('/'):
111 110 req.url += '/'
112 111 if 'REPO_NAME' in req.env:
113 112 req.url += req.env['REPO_NAME'] + '/'
114 113
115 114 if 'PATH_INFO' in req.env:
116 115 parts = req.env['PATH_INFO'].strip('/').split('/')
117 116 repo_parts = req.env.get('REPO_NAME', '').split('/')
118 117 if parts[:len(repo_parts)] == repo_parts:
119 118 parts = parts[len(repo_parts):]
120 119 query = '/'.join(parts)
121 120 else:
122 121 query = req.env['QUERY_STRING'].split('&', 1)[0]
123 122 query = query.split(';', 1)[0]
124 123
125 124 # translate user-visible url structure to internal structure
126 125
127 126 args = query.split('/', 2)
128 127 if 'cmd' not in req.form and args and args[0]:
129 128
130 129 cmd = args.pop(0)
131 130 style = cmd.rfind('-')
132 131 if style != -1:
133 132 req.form['style'] = [cmd[:style]]
134 133 cmd = cmd[style+1:]
135 134
136 135 # avoid accepting e.g. style parameter as command
137 136 if hasattr(webcommands, cmd):
138 137 req.form['cmd'] = [cmd]
139 138 else:
140 139 cmd = ''
141 140
142 141 if cmd == 'static':
143 142 req.form['file'] = ['/'.join(args)]
144 143 else:
145 144 if args and args[0]:
146 145 node = args.pop(0)
147 146 req.form['node'] = [node]
148 147 if args:
149 148 req.form['file'] = args
150 149
151 150 if cmd == 'archive':
152 151 fn = req.form['node'][0]
153 152 for type_, spec in self.archive_specs.iteritems():
154 153 ext = spec[2]
155 154 if fn.endswith(ext):
156 155 req.form['node'] = [fn[:-len(ext)]]
157 156 req.form['type'] = [type_]
158 157
159 158 # process the web interface request
160 159
161 160 try:
162 161 tmpl = self.templater(req)
163 162 ctype = tmpl('mimetype', encoding=self.encoding)
164 163 ctype = templater.stringify(ctype)
165 164
166 165 # check read permissions non-static content
167 166 if cmd != 'static':
168 167 self.check_perm(req, None)
169 168
170 169 if cmd == '':
171 170 req.form['cmd'] = [tmpl.cache['default']]
172 171 cmd = req.form['cmd'][0]
173 172
174 173 if cmd not in webcommands.__all__:
175 174 msg = 'no such method: %s' % cmd
176 175 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
177 176 elif cmd == 'file' and 'raw' in req.form.get('style', []):
178 177 self.ctype = ctype
179 178 content = webcommands.rawfile(self, req, tmpl)
180 179 else:
181 180 content = getattr(webcommands, cmd)(self, req, tmpl)
182 181 req.respond(HTTP_OK, ctype)
183 182
184 183 return content
185 184
186 185 except error.LookupError, err:
187 186 req.respond(HTTP_NOT_FOUND, ctype)
188 187 msg = str(err)
189 188 if 'manifest' not in msg:
190 189 msg = 'revision not found: %s' % err.name
191 190 return tmpl('error', error=msg)
192 191 except (error.RepoError, error.RevlogError), inst:
193 192 req.respond(HTTP_SERVER_ERROR, ctype)
194 193 return tmpl('error', error=str(inst))
195 194 except ErrorResponse, inst:
196 195 req.respond(inst, ctype)
197 196 return tmpl('error', error=inst.message)
198 197
199 198 def templater(self, req):
200 199
201 200 # determine scheme, port and server name
202 201 # this is needed to create absolute urls
203 202
204 203 proto = req.env.get('wsgi.url_scheme')
205 204 if proto == 'https':
206 205 proto = 'https'
207 206 default_port = "443"
208 207 else:
209 208 proto = 'http'
210 209 default_port = "80"
211 210
212 211 port = req.env["SERVER_PORT"]
213 212 port = port != default_port and (":" + port) or ""
214 213 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
215 214 staticurl = self.config("web", "staticurl") or req.url + 'static/'
216 215 if not staticurl.endswith('/'):
217 216 staticurl += '/'
218 217
219 218 # some functions for the templater
220 219
221 220 def header(**map):
222 221 yield tmpl('header', encoding=self.encoding, **map)
223 222
224 223 def footer(**map):
225 224 yield tmpl("footer", **map)
226 225
227 226 def motd(**map):
228 227 yield self.config("web", "motd", "")
229 228
230 229 # figure out which style to use
231 230
232 231 vars = {}
233 232 style = self.config("web", "style", "paper")
234 233 if 'style' in req.form:
235 234 style = req.form['style'][0]
236 235 vars['style'] = style
237 236
238 237 start = req.url[-1] == '?' and '&' or '?'
239 238 sessionvars = webutil.sessionvars(vars, start)
240 239 mapfile = templater.stylemap(style, self.templatepath)
241 240
242 241 if not self.reponame:
243 242 self.reponame = (self.config("web", "name")
244 243 or req.env.get('REPO_NAME')
245 244 or req.url.strip('/') or self.repo.root)
246 245
247 246 # create the templater
248 247
249 tmpl = templater.templater(mapfile, templatefilters.filters,
248 tmpl = templater.templater(mapfile,
250 249 defaults={"url": req.url,
251 250 "staticurl": staticurl,
252 251 "urlbase": urlbase,
253 252 "repo": self.reponame,
254 253 "header": header,
255 254 "footer": footer,
256 255 "motd": motd,
257 256 "sessionvars": sessionvars
258 257 })
259 258 return tmpl
260 259
261 260 def archivelist(self, nodeid):
262 261 allowed = self.configlist("web", "allow_archive")
263 262 for i, spec in self.archive_specs.iteritems():
264 263 if i in allowed or self.configbool("web", "allow" + i):
265 264 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
266 265
267 266 archive_specs = {
268 267 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
269 268 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
270 269 'zip': ('application/zip', 'zip', '.zip', None),
271 270 }
272 271
273 272 def check_perm(self, req, op):
274 273 '''Check permission for operation based on request data (including
275 274 authentication info). Return if op allowed, else raise an ErrorResponse
276 275 exception.'''
277 276
278 277 user = req.env.get('REMOTE_USER')
279 278
280 279 deny_read = self.configlist('web', 'deny_read')
281 280 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
282 281 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
283 282
284 283 allow_read = self.configlist('web', 'allow_read')
285 284 result = (not allow_read) or (allow_read == ['*'])
286 285 if not (result or user in allow_read):
287 286 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
288 287
289 288 if op == 'pull' and not self.allowpull:
290 289 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
291 290 elif op == 'pull' or op is None: # op is None for interface requests
292 291 return
293 292
294 293 # enforce that you can only push using POST requests
295 294 if req.env['REQUEST_METHOD'] != 'POST':
296 295 msg = 'push requires POST request'
297 296 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
298 297
299 298 # require ssl by default for pushing, auth info cannot be sniffed
300 299 # and replayed
301 300 scheme = req.env.get('wsgi.url_scheme')
302 301 if self.configbool('web', 'push_ssl', True) and scheme != 'https':
303 302 raise ErrorResponse(HTTP_OK, 'ssl required')
304 303
305 304 deny = self.configlist('web', 'deny_push')
306 305 if deny and (not user or deny == ['*'] or user in deny):
307 306 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
308 307
309 308 allow = self.configlist('web', 'allow_push')
310 309 result = allow and (allow == ['*'] or user in allow)
311 310 if not result:
312 311 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
@@ -1,314 +1,314 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2, incorporated herein by reference.
8 8
9 9 import os
10 10 from mercurial.i18n import _
11 from mercurial import ui, hg, util, templater, templatefilters
11 from mercurial import ui, hg, util, templater
12 12 from mercurial import error, encoding
13 13 from common import ErrorResponse, get_mtime, staticfile, paritygen,\
14 14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 15 from hgweb_mod import hgweb
16 16 from request import wsgirequest
17 17 import webutil
18 18
19 19 def cleannames(items):
20 20 return [(util.pconvert(name).strip('/'), path) for name, path in items]
21 21
22 22 class hgwebdir(object):
23 23
24 24 def __init__(self, conf, baseui=None):
25 25
26 26 if baseui:
27 27 self.ui = baseui.copy()
28 28 else:
29 29 self.ui = ui.ui()
30 30 self.ui.setconfig('ui', 'report_untrusted', 'off')
31 31 self.ui.setconfig('ui', 'interactive', 'off')
32 32
33 33 if isinstance(conf, (list, tuple)):
34 34 self.repos = cleannames(conf)
35 35 elif isinstance(conf, dict):
36 36 self.repos = sorted(cleannames(conf.items()))
37 37 else:
38 38 self.ui.readconfig(conf, remap={'paths': 'hgweb-paths'}, trust=True)
39 39 self.repos = []
40 40
41 41 self.motd = self.ui.config('web', 'motd')
42 42 self.style = self.ui.config('web', 'style', 'paper')
43 43 self.stripecount = self.ui.config('web', 'stripes', 1)
44 44 if self.stripecount:
45 45 self.stripecount = int(self.stripecount)
46 46 self._baseurl = self.ui.config('web', 'baseurl')
47 47
48 48 if self.repos:
49 49 return
50 50
51 51 for prefix, root in cleannames(self.ui.configitems('hgweb-paths')):
52 52 roothead, roottail = os.path.split(root)
53 53 # "foo = /bar/*" makes every subrepo of /bar/ to be
54 54 # mounted as foo/subrepo
55 55 # and "foo = /bar/**" also recurses into the subdirectories,
56 56 # remember to use it without working dir.
57 57 try:
58 58 recurse = {'*': False, '**': True}[roottail]
59 59 except KeyError:
60 60 self.repos.append((prefix, root))
61 61 continue
62 62 roothead = os.path.normpath(roothead)
63 63 for path in util.walkrepos(roothead, followsym=True,
64 64 recurse=recurse):
65 65 path = os.path.normpath(path)
66 66 name = util.pconvert(path[len(roothead):]).strip('/')
67 67 if prefix:
68 68 name = prefix + '/' + name
69 69 self.repos.append((name, path))
70 70
71 71 for prefix, root in self.ui.configitems('collections'):
72 72 for path in util.walkrepos(root, followsym=True):
73 73 repo = os.path.normpath(path)
74 74 name = repo
75 75 if name.startswith(prefix):
76 76 name = name[len(prefix):]
77 77 self.repos.append((name.lstrip(os.sep), repo))
78 78
79 79 self.repos.sort()
80 80
81 81 def run(self):
82 82 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
83 83 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
84 84 import mercurial.hgweb.wsgicgi as wsgicgi
85 85 wsgicgi.launch(self)
86 86
87 87 def __call__(self, env, respond):
88 88 req = wsgirequest(env, respond)
89 89 return self.run_wsgi(req)
90 90
91 91 def read_allowed(self, ui, req):
92 92 """Check allow_read and deny_read config options of a repo's ui object
93 93 to determine user permissions. By default, with neither option set (or
94 94 both empty), allow all users to read the repo. There are two ways a
95 95 user can be denied read access: (1) deny_read is not empty, and the
96 96 user is unauthenticated or deny_read contains user (or *), and (2)
97 97 allow_read is not empty and the user is not in allow_read. Return True
98 98 if user is allowed to read the repo, else return False."""
99 99
100 100 user = req.env.get('REMOTE_USER')
101 101
102 102 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
103 103 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
104 104 return False
105 105
106 106 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
107 107 # by default, allow reading if no allow_read option has been set
108 108 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
109 109 return True
110 110
111 111 return False
112 112
113 113 def run_wsgi(self, req):
114 114
115 115 try:
116 116 try:
117 117
118 118 virtual = req.env.get("PATH_INFO", "").strip('/')
119 119 tmpl = self.templater(req)
120 120 ctype = tmpl('mimetype', encoding=encoding.encoding)
121 121 ctype = templater.stringify(ctype)
122 122
123 123 # a static file
124 124 if virtual.startswith('static/') or 'static' in req.form:
125 125 if virtual.startswith('static/'):
126 126 fname = virtual[7:]
127 127 else:
128 128 fname = req.form['static'][0]
129 129 static = templater.templatepath('static')
130 130 return (staticfile(static, fname, req),)
131 131
132 132 # top-level index
133 133 elif not virtual:
134 134 req.respond(HTTP_OK, ctype)
135 135 return self.makeindex(req, tmpl)
136 136
137 137 # nested indexes and hgwebs
138 138
139 139 repos = dict(self.repos)
140 140 while virtual:
141 141 real = repos.get(virtual)
142 142 if real:
143 143 req.env['REPO_NAME'] = virtual
144 144 try:
145 145 repo = hg.repository(self.ui, real)
146 146 return hgweb(repo).run_wsgi(req)
147 147 except IOError, inst:
148 148 msg = inst.strerror
149 149 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
150 150 except error.RepoError, inst:
151 151 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
152 152
153 153 # browse subdirectories
154 154 subdir = virtual + '/'
155 155 if [r for r in repos if r.startswith(subdir)]:
156 156 req.respond(HTTP_OK, ctype)
157 157 return self.makeindex(req, tmpl, subdir)
158 158
159 159 up = virtual.rfind('/')
160 160 if up < 0:
161 161 break
162 162 virtual = virtual[:up]
163 163
164 164 # prefixes not found
165 165 req.respond(HTTP_NOT_FOUND, ctype)
166 166 return tmpl("notfound", repo=virtual)
167 167
168 168 except ErrorResponse, err:
169 169 req.respond(err, ctype)
170 170 return tmpl('error', error=err.message or '')
171 171 finally:
172 172 tmpl = None
173 173
174 174 def makeindex(self, req, tmpl, subdir=""):
175 175
176 176 def archivelist(ui, nodeid, url):
177 177 allowed = ui.configlist("web", "allow_archive", untrusted=True)
178 178 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
179 179 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
180 180 untrusted=True):
181 181 yield {"type" : i[0], "extension": i[1],
182 182 "node": nodeid, "url": url}
183 183
184 184 sortdefault = 'name', False
185 185 def entries(sortcolumn="", descending=False, subdir="", **map):
186 186 rows = []
187 187 parity = paritygen(self.stripecount)
188 188 for name, path in self.repos:
189 189 if not name.startswith(subdir):
190 190 continue
191 191 name = name[len(subdir):]
192 192
193 193 u = self.ui.copy()
194 194 try:
195 195 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
196 196 except Exception, e:
197 197 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
198 198 continue
199 199 def get(section, name, default=None):
200 200 return u.config(section, name, default, untrusted=True)
201 201
202 202 if u.configbool("web", "hidden", untrusted=True):
203 203 continue
204 204
205 205 if not self.read_allowed(u, req):
206 206 continue
207 207
208 208 parts = [name]
209 209 if 'PATH_INFO' in req.env:
210 210 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
211 211 if req.env['SCRIPT_NAME']:
212 212 parts.insert(0, req.env['SCRIPT_NAME'])
213 213 url = ('/'.join(parts).replace("//", "/")) + '/'
214 214
215 215 # update time with local timezone
216 216 try:
217 217 d = (get_mtime(path), util.makedate()[1])
218 218 except OSError:
219 219 continue
220 220
221 221 contact = get_contact(get)
222 222 description = get("web", "description", "")
223 223 name = get("web", "name", name)
224 224 row = dict(contact=contact or "unknown",
225 225 contact_sort=contact.upper() or "unknown",
226 226 name=name,
227 227 name_sort=name,
228 228 url=url,
229 229 description=description or "unknown",
230 230 description_sort=description.upper() or "unknown",
231 231 lastchange=d,
232 232 lastchange_sort=d[1]-d[0],
233 233 archives=archivelist(u, "tip", url))
234 234 if (not sortcolumn or (sortcolumn, descending) == sortdefault):
235 235 # fast path for unsorted output
236 236 row['parity'] = parity.next()
237 237 yield row
238 238 else:
239 239 rows.append((row["%s_sort" % sortcolumn], row))
240 240 if rows:
241 241 rows.sort()
242 242 if descending:
243 243 rows.reverse()
244 244 for key, row in rows:
245 245 row['parity'] = parity.next()
246 246 yield row
247 247
248 248 sortable = ["name", "description", "contact", "lastchange"]
249 249 sortcolumn, descending = sortdefault
250 250 if 'sort' in req.form:
251 251 sortcolumn = req.form['sort'][0]
252 252 descending = sortcolumn.startswith('-')
253 253 if descending:
254 254 sortcolumn = sortcolumn[1:]
255 255 if sortcolumn not in sortable:
256 256 sortcolumn = ""
257 257
258 258 sort = [("sort_%s" % column,
259 259 "%s%s" % ((not descending and column == sortcolumn)
260 260 and "-" or "", column))
261 261 for column in sortable]
262 262
263 263 if self._baseurl is not None:
264 264 req.env['SCRIPT_NAME'] = self._baseurl
265 265
266 266 return tmpl("index", entries=entries, subdir=subdir,
267 267 sortcolumn=sortcolumn, descending=descending,
268 268 **dict(sort))
269 269
270 270 def templater(self, req):
271 271
272 272 def header(**map):
273 273 yield tmpl('header', encoding=encoding.encoding, **map)
274 274
275 275 def footer(**map):
276 276 yield tmpl("footer", **map)
277 277
278 278 def motd(**map):
279 279 if self.motd is not None:
280 280 yield self.motd
281 281 else:
282 282 yield config('web', 'motd', '')
283 283
284 284 def config(section, name, default=None, untrusted=True):
285 285 return self.ui.config(section, name, default, untrusted)
286 286
287 287 if self._baseurl is not None:
288 288 req.env['SCRIPT_NAME'] = self._baseurl
289 289
290 290 url = req.env.get('SCRIPT_NAME', '')
291 291 if not url.endswith('/'):
292 292 url += '/'
293 293
294 294 vars = {}
295 295 style = self.style
296 296 if 'style' in req.form:
297 297 vars['style'] = style = req.form['style'][0]
298 298 start = url[-1] == '?' and '&' or '?'
299 299 sessionvars = webutil.sessionvars(vars, start)
300 300
301 301 staticurl = config('web', 'staticurl') or url + 'static/'
302 302 if not staticurl.endswith('/'):
303 303 staticurl += '/'
304 304
305 305 style = 'style' in req.form and req.form['style'][0] or self.style
306 306 mapfile = templater.stylemap(style)
307 tmpl = templater.templater(mapfile, templatefilters.filters,
307 tmpl = templater.templater(mapfile,
308 308 defaults={"header": header,
309 309 "footer": footer,
310 310 "motd": motd,
311 311 "url": url,
312 312 "staticurl": staticurl,
313 313 "sessionvars": sessionvars})
314 314 return tmpl
@@ -1,203 +1,209 b''
1 1 # template-filters.py - common template expansion filters
2 2 #
3 3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2, incorporated herein by reference.
7 7
8 8 import cgi, re, os, time, urllib, textwrap
9 9 import util, templater, encoding
10 10
11 def stringify(thing):
12 '''turn nested template iterator into string.'''
13 if hasattr(thing, '__iter__') and not isinstance(thing, str):
14 return "".join([stringify(t) for t in thing if t is not None])
15 return str(thing)
16
11 17 agescales = [("second", 1),
12 18 ("minute", 60),
13 19 ("hour", 3600),
14 20 ("day", 3600 * 24),
15 21 ("week", 3600 * 24 * 7),
16 22 ("month", 3600 * 24 * 30),
17 23 ("year", 3600 * 24 * 365)]
18 24
19 25 agescales.reverse()
20 26
21 27 def age(date):
22 28 '''turn a (timestamp, tzoff) tuple into an age string.'''
23 29
24 30 def plural(t, c):
25 31 if c == 1:
26 32 return t
27 33 return t + "s"
28 34 def fmt(t, c):
29 35 return "%d %s" % (c, plural(t, c))
30 36
31 37 now = time.time()
32 38 then = date[0]
33 39 if then > now:
34 40 return 'in the future'
35 41
36 42 delta = max(1, int(now - then))
37 43 for t, s in agescales:
38 44 n = delta / s
39 45 if n >= 2 or s == 1:
40 46 return fmt(t, n)
41 47
42 48 para_re = None
43 49 space_re = None
44 50
45 51 def fill(text, width):
46 52 '''fill many paragraphs.'''
47 53 global para_re, space_re
48 54 if para_re is None:
49 55 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
50 56 space_re = re.compile(r' +')
51 57
52 58 def findparas():
53 59 start = 0
54 60 while True:
55 61 m = para_re.search(text, start)
56 62 if not m:
57 63 w = len(text)
58 64 while w > start and text[w-1].isspace(): w -= 1
59 65 yield text[start:w], text[w:]
60 66 break
61 67 yield text[start:m.start(0)], m.group(1)
62 68 start = m.end(1)
63 69
64 70 return "".join([space_re.sub(' ', textwrap.fill(para, width)) + rest
65 71 for para, rest in findparas()])
66 72
67 73 def firstline(text):
68 74 '''return the first line of text'''
69 75 try:
70 76 return text.splitlines(1)[0].rstrip('\r\n')
71 77 except IndexError:
72 78 return ''
73 79
74 80 def nl2br(text):
75 81 '''replace raw newlines with xhtml line breaks.'''
76 82 return text.replace('\n', '<br/>\n')
77 83
78 84 def obfuscate(text):
79 85 text = unicode(text, encoding.encoding, 'replace')
80 86 return ''.join(['&#%d;' % ord(c) for c in text])
81 87
82 88 def domain(author):
83 89 '''get domain of author, or empty string if none.'''
84 90 f = author.find('@')
85 91 if f == -1: return ''
86 92 author = author[f+1:]
87 93 f = author.find('>')
88 94 if f >= 0: author = author[:f]
89 95 return author
90 96
91 97 def person(author):
92 98 '''get name of author, or else username.'''
93 99 f = author.find('<')
94 100 if f == -1: return util.shortuser(author)
95 101 return author[:f].rstrip()
96 102
97 103 def indent(text, prefix):
98 104 '''indent each non-empty line of text after first with prefix.'''
99 105 lines = text.splitlines()
100 106 num_lines = len(lines)
101 107 def indenter():
102 108 for i in xrange(num_lines):
103 109 l = lines[i]
104 110 if i and l.strip():
105 111 yield prefix
106 112 yield l
107 113 if i < num_lines - 1 or text.endswith('\n'):
108 114 yield '\n'
109 115 return "".join(indenter())
110 116
111 117 def permissions(flags):
112 118 if "l" in flags:
113 119 return "lrwxrwxrwx"
114 120 if "x" in flags:
115 121 return "-rwxr-xr-x"
116 122 return "-rw-r--r--"
117 123
118 124 def xmlescape(text):
119 125 text = (text
120 126 .replace('&', '&amp;')
121 127 .replace('<', '&lt;')
122 128 .replace('>', '&gt;')
123 129 .replace('"', '&quot;')
124 130 .replace("'", '&#39;')) # &apos; invalid in HTML
125 131 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
126 132
127 133 _escapes = [
128 134 ('\\', '\\\\'), ('"', '\\"'), ('\t', '\\t'), ('\n', '\\n'),
129 135 ('\r', '\\r'), ('\f', '\\f'), ('\b', '\\b'),
130 136 ]
131 137
132 138 def jsonescape(s):
133 139 for k, v in _escapes:
134 140 s = s.replace(k, v)
135 141 return s
136 142
137 143 def json(obj):
138 144 if obj is None or obj is False or obj is True:
139 145 return {None: 'null', False: 'false', True: 'true'}[obj]
140 146 elif isinstance(obj, int) or isinstance(obj, float):
141 147 return str(obj)
142 148 elif isinstance(obj, str):
143 149 return '"%s"' % jsonescape(obj)
144 150 elif isinstance(obj, unicode):
145 151 return json(obj.encode('utf-8'))
146 152 elif hasattr(obj, 'keys'):
147 153 out = []
148 154 for k, v in obj.iteritems():
149 155 s = '%s: %s' % (json(k), json(v))
150 156 out.append(s)
151 157 return '{' + ', '.join(out) + '}'
152 158 elif hasattr(obj, '__iter__'):
153 159 out = []
154 160 for i in obj:
155 161 out.append(json(i))
156 162 return '[' + ', '.join(out) + ']'
157 163 else:
158 164 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
159 165
160 166 def stripdir(text):
161 167 '''Treat the text as path and strip a directory level, if possible.'''
162 168 dir = os.path.dirname(text)
163 169 if dir == "":
164 170 return os.path.basename(text)
165 171 else:
166 172 return dir
167 173
168 174 def nonempty(str):
169 175 return str or "(none)"
170 176
171 177 filters = {
172 178 "addbreaks": nl2br,
173 179 "basename": os.path.basename,
174 180 "stripdir": stripdir,
175 181 "age": age,
176 182 "date": lambda x: util.datestr(x),
177 183 "domain": domain,
178 184 "email": util.email,
179 185 "escape": lambda x: cgi.escape(x, True),
180 186 "fill68": lambda x: fill(x, width=68),
181 187 "fill76": lambda x: fill(x, width=76),
182 188 "firstline": firstline,
183 189 "tabindent": lambda x: indent(x, '\t'),
184 190 "hgdate": lambda x: "%d %d" % x,
185 191 "isodate": lambda x: util.datestr(x, '%Y-%m-%d %H:%M %1%2'),
186 192 "isodatesec": lambda x: util.datestr(x, '%Y-%m-%d %H:%M:%S %1%2'),
187 193 "json": json,
188 194 "jsonescape": jsonescape,
189 195 "nonempty": nonempty,
190 196 "obfuscate": obfuscate,
191 197 "permissions": permissions,
192 198 "person": person,
193 199 "rfc822date": lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S %1%2"),
194 200 "rfc3339date": lambda x: util.datestr(x, "%Y-%m-%dT%H:%M:%S%1:%2"),
195 201 "short": lambda x: x[:12],
196 202 "shortdate": util.shortdate,
197 "stringify": templater.stringify,
203 "stringify": stringify,
198 204 "strip": lambda x: x.strip(),
199 205 "urlescape": lambda x: urllib.quote(x),
200 206 "user": lambda x: util.shortuser(x),
201 207 "stringescape": lambda x: x.encode('string_escape'),
202 208 "xmlescape": xmlescape,
203 209 }
@@ -1,215 +1,211 b''
1 1 # templater.py - template expansion for output
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2, incorporated herein by reference.
7 7
8 8 from i18n import _
9 9 import re, sys, os
10 import util, config
10 import util, config, templatefilters
11 11
12 12 path = ['templates', '../templates']
13 stringify = templatefilters.stringify
13 14
14 15 def parsestring(s, quoted=True):
15 16 '''parse a string using simple c-like syntax.
16 17 string must be in quotes if quoted is True.'''
17 18 if quoted:
18 19 if len(s) < 2 or s[0] != s[-1]:
19 20 raise SyntaxError(_('unmatched quotes'))
20 21 return s[1:-1].decode('string_escape')
21 22
22 23 return s.decode('string_escape')
23 24
24 25 class engine(object):
25 26 '''template expansion engine.
26 27
27 28 template expansion works like this. a map file contains key=value
28 29 pairs. if value is quoted, it is treated as string. otherwise, it
29 30 is treated as name of template file.
30 31
31 32 templater is asked to expand a key in map. it looks up key, and
32 33 looks for strings like this: {foo}. it expands {foo} by looking up
33 34 foo in map, and substituting it. expansion is recursive: it stops
34 35 when there is no more {foo} to replace.
35 36
36 37 expansion also allows formatting and filtering.
37 38
38 39 format uses key to expand each item in list. syntax is
39 40 {key%format}.
40 41
41 42 filter uses function to transform value. syntax is
42 43 {key|filter1|filter2|...}.'''
43 44
44 45 template_re = re.compile(r"(?:(?:#(?=[\w\|%]+#))|(?:{(?=[\w\|%]+})))"
45 46 r"(\w+)(?:(?:%(\w+))|((?:\|\w+)*))[#}]")
46 47
47 48 def __init__(self, loader, filters={}, defaults={}):
48 49 self.loader = loader
49 50 self.filters = filters
50 51 self.defaults = defaults
51 52
52 53 def process(self, t, map):
53 54 '''Perform expansion. t is name of map element to expand. map contains
54 55 added elements for use during expansion. Is a generator.'''
55 56 tmpl = self.loader(t)
56 57 iters = [self._process(tmpl, map)]
57 58 while iters:
58 59 try:
59 60 item = iters[0].next()
60 61 except StopIteration:
61 62 iters.pop(0)
62 63 continue
63 64 if isinstance(item, str):
64 65 yield item
65 66 elif item is None:
66 67 yield ''
67 68 elif hasattr(item, '__iter__'):
68 69 iters.insert(0, iter(item))
69 70 else:
70 71 yield str(item)
71 72
72 73 def _process(self, tmpl, map):
73 74 '''Render a template. Returns a generator.'''
74 75 while tmpl:
75 76 m = self.template_re.search(tmpl)
76 77 if not m:
77 78 yield tmpl
78 79 break
79 80
80 81 start, end = m.span(0)
81 82 key, format, fl = m.groups()
82 83
83 84 if start:
84 85 yield tmpl[:start]
85 86 tmpl = tmpl[end:]
86 87
87 88 if key in map:
88 89 v = map[key]
89 90 else:
90 91 v = self.defaults.get(key, "")
91 92 if callable(v):
92 93 v = v(**map)
93 94 if format:
94 95 if not hasattr(v, '__iter__'):
95 96 raise SyntaxError(_("Error expanding '%s%%%s'")
96 97 % (key, format))
97 98 lm = map.copy()
98 99 for i in v:
99 100 lm.update(i)
100 101 yield self.process(format, lm)
101 102 else:
102 103 if fl:
103 104 for f in fl.split("|")[1:]:
104 105 v = self.filters[f](v)
105 106 yield v
106 107
107 108 class templater(object):
108 109
109 110 def __init__(self, mapfile, filters={}, defaults={}, cache={},
110 111 minchunk=1024, maxchunk=65536):
111 112 '''set up template engine.
112 113 mapfile is name of file to read map definitions from.
113 114 filters is dict of functions. each transforms a value into another.
114 115 defaults is dict of default map definitions.'''
115 116 self.mapfile = mapfile or 'template'
116 117 self.cache = cache.copy()
117 118 self.map = {}
118 119 self.base = (mapfile and os.path.dirname(mapfile)) or ''
119 self.filters = filters
120 self.filters = templatefilters.filters.copy()
121 self.filters.update(filters)
120 122 self.defaults = defaults
121 123 self.minchunk, self.maxchunk = minchunk, maxchunk
122 124
123 125 if not mapfile:
124 126 return
125 127 if not os.path.exists(mapfile):
126 128 raise util.Abort(_('style not found: %s') % mapfile)
127 129
128 130 conf = config.config()
129 131 conf.read(mapfile)
130 132
131 133 for key, val in conf[''].items():
132 134 if val[0] in "'\"":
133 135 try:
134 136 self.cache[key] = parsestring(val)
135 137 except SyntaxError, inst:
136 138 raise SyntaxError('%s: %s' %
137 139 (conf.source('', key), inst.args[0]))
138 140 else:
139 141 self.map[key] = os.path.join(self.base, val)
140 142
141 143 def __contains__(self, key):
142 144 return key in self.cache or key in self.map
143 145
144 146 def load(self, t):
145 147 '''Get the template for the given template name. Use a local cache.'''
146 148 if not t in self.cache:
147 149 try:
148 150 self.cache[t] = file(self.map[t]).read()
149 151 except IOError, inst:
150 152 raise IOError(inst.args[0], _('template file %s: %s') %
151 153 (self.map[t], inst.args[1]))
152 154 return self.cache[t]
153 155
154 156 def __call__(self, t, **map):
155 157 proc = engine(self.load, self.filters, self.defaults)
156 158 stream = proc.process(t, map)
157 159 if self.minchunk:
158 160 stream = util.increasingchunks(stream, min=self.minchunk,
159 161 max=self.maxchunk)
160 162 return stream
161 163
162 164 def templatepath(name=None):
163 165 '''return location of template file or directory (if no name).
164 166 returns None if not found.'''
165 167 normpaths = []
166 168
167 169 # executable version (py2exe) doesn't support __file__
168 170 if hasattr(sys, 'frozen'):
169 171 module = sys.executable
170 172 else:
171 173 module = __file__
172 174 for f in path:
173 175 if f.startswith('/'):
174 176 p = f
175 177 else:
176 178 fl = f.split('/')
177 179 p = os.path.join(os.path.dirname(module), *fl)
178 180 if name:
179 181 p = os.path.join(p, name)
180 182 if name and os.path.exists(p):
181 183 return os.path.normpath(p)
182 184 elif os.path.isdir(p):
183 185 normpaths.append(os.path.normpath(p))
184 186
185 187 return normpaths
186 188
187 189 def stylemap(style, paths=None):
188 190 """Return path to mapfile for a given style.
189 191
190 192 Searches mapfile in the following locations:
191 193 1. templatepath/style/map
192 194 2. templatepath/map-style
193 195 3. templatepath/map
194 196 """
195 197
196 198 if paths is None:
197 199 paths = templatepath()
198 200 elif isinstance(paths, str):
199 201 paths = [paths]
200 202
201 203 locations = style and [os.path.join(style, "map"), "map-" + style] or []
202 204 locations.append("map")
203 205 for path in paths:
204 206 for location in locations:
205 207 mapfile = os.path.join(path, location)
206 208 if os.path.isfile(mapfile):
207 209 return mapfile
208 210
209 211 raise RuntimeError("No hgweb templates found in %r" % paths)
210
211 def stringify(thing):
212 '''turn nested template iterator into string.'''
213 if hasattr(thing, '__iter__') and not isinstance(thing, str):
214 return "".join([stringify(t) for t in thing if t is not None])
215 return str(thing)
General Comments 0
You need to be logged in to leave comments. Login now