##// END OF EJS Templates
cmdutil: service: add an optional runargs argument to pass the command to run...
Nicolas Dumazet -
r9513:ae88c721 default
parent child Browse files
Show More
@@ -1,1254 +1,1256
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, errno, re, glob
11 11 import mdiff, bdiff, util, templater, 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.copy() # 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.copy() # 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 = set(), []
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.add(rev)
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.add(rev)
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 filename") %
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 expandpats(pats):
239 239 if not util.expandglobs:
240 240 return list(pats)
241 241 ret = []
242 242 for p in pats:
243 243 kind, name = _match._patsplit(p, None)
244 244 if kind is None:
245 245 try:
246 246 globbed = glob.glob(name)
247 247 except re.error:
248 248 globbed = [name]
249 249 if globbed:
250 250 ret.extend(globbed)
251 251 continue
252 252 ret.append(p)
253 253 return ret
254 254
255 255 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
256 256 if not globbed and default == 'relpath':
257 257 pats = expandpats(pats or [])
258 258 m = _match.match(repo.root, repo.getcwd(), pats,
259 259 opts.get('include'), opts.get('exclude'), default)
260 260 def badfn(f, msg):
261 261 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
262 262 m.bad = badfn
263 263 return m
264 264
265 265 def matchall(repo):
266 266 return _match.always(repo.root, repo.getcwd())
267 267
268 268 def matchfiles(repo, files):
269 269 return _match.exact(repo.root, repo.getcwd(), files)
270 270
271 271 def findrenames(repo, added, removed, threshold):
272 272 '''find renamed files -- yields (before, after, score) tuples'''
273 273 ctx = repo['.']
274 274 for a in added:
275 275 aa = repo.wread(a)
276 276 bestname, bestscore = None, threshold
277 277 for r in removed:
278 278 if r not in ctx:
279 279 continue
280 280 rr = ctx.filectx(r).data()
281 281
282 282 # bdiff.blocks() returns blocks of matching lines
283 283 # count the number of bytes in each
284 284 equal = 0
285 285 alines = mdiff.splitnewlines(aa)
286 286 matches = bdiff.blocks(aa, rr)
287 287 for x1,x2,y1,y2 in matches:
288 288 for line in alines[x1:x2]:
289 289 equal += len(line)
290 290
291 291 lengths = len(aa) + len(rr)
292 292 if lengths:
293 293 myscore = equal*2.0 / lengths
294 294 if myscore >= bestscore:
295 295 bestname, bestscore = r, myscore
296 296 if bestname:
297 297 yield bestname, a, bestscore
298 298
299 299 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
300 300 if dry_run is None:
301 301 dry_run = opts.get('dry_run')
302 302 if similarity is None:
303 303 similarity = float(opts.get('similarity') or 0)
304 304 # we'd use status here, except handling of symlinks and ignore is tricky
305 305 added, unknown, deleted, removed = [], [], [], []
306 306 audit_path = util.path_auditor(repo.root)
307 307 m = match(repo, pats, opts)
308 308 for abs in repo.walk(m):
309 309 target = repo.wjoin(abs)
310 310 good = True
311 311 try:
312 312 audit_path(abs)
313 313 except:
314 314 good = False
315 315 rel = m.rel(abs)
316 316 exact = m.exact(abs)
317 317 if good and abs not in repo.dirstate:
318 318 unknown.append(abs)
319 319 if repo.ui.verbose or not exact:
320 320 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
321 321 elif repo.dirstate[abs] != 'r' and (not good or not util.lexists(target)
322 322 or (os.path.isdir(target) and not os.path.islink(target))):
323 323 deleted.append(abs)
324 324 if repo.ui.verbose or not exact:
325 325 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
326 326 # for finding renames
327 327 elif repo.dirstate[abs] == 'r':
328 328 removed.append(abs)
329 329 elif repo.dirstate[abs] == 'a':
330 330 added.append(abs)
331 331 if not dry_run:
332 332 repo.remove(deleted)
333 333 repo.add(unknown)
334 334 if similarity > 0:
335 335 for old, new, score in findrenames(repo, added + unknown,
336 336 removed + deleted, similarity):
337 337 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
338 338 repo.ui.status(_('recording removal of %s as rename to %s '
339 339 '(%d%% similar)\n') %
340 340 (m.rel(old), m.rel(new), score * 100))
341 341 if not dry_run:
342 342 repo.copy(old, new)
343 343
344 344 def copy(ui, repo, pats, opts, rename=False):
345 345 # called with the repo lock held
346 346 #
347 347 # hgsep => pathname that uses "/" to separate directories
348 348 # ossep => pathname that uses os.sep to separate directories
349 349 cwd = repo.getcwd()
350 350 targets = {}
351 351 after = opts.get("after")
352 352 dryrun = opts.get("dry_run")
353 353
354 354 def walkpat(pat):
355 355 srcs = []
356 356 m = match(repo, [pat], opts, globbed=True)
357 357 for abs in repo.walk(m):
358 358 state = repo.dirstate[abs]
359 359 rel = m.rel(abs)
360 360 exact = m.exact(abs)
361 361 if state in '?r':
362 362 if exact and state == '?':
363 363 ui.warn(_('%s: not copying - file is not managed\n') % rel)
364 364 if exact and state == 'r':
365 365 ui.warn(_('%s: not copying - file has been marked for'
366 366 ' remove\n') % rel)
367 367 continue
368 368 # abs: hgsep
369 369 # rel: ossep
370 370 srcs.append((abs, rel, exact))
371 371 return srcs
372 372
373 373 # abssrc: hgsep
374 374 # relsrc: ossep
375 375 # otarget: ossep
376 376 def copyfile(abssrc, relsrc, otarget, exact):
377 377 abstarget = util.canonpath(repo.root, cwd, otarget)
378 378 reltarget = repo.pathto(abstarget, cwd)
379 379 target = repo.wjoin(abstarget)
380 380 src = repo.wjoin(abssrc)
381 381 state = repo.dirstate[abstarget]
382 382
383 383 # check for collisions
384 384 prevsrc = targets.get(abstarget)
385 385 if prevsrc is not None:
386 386 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
387 387 (reltarget, repo.pathto(abssrc, cwd),
388 388 repo.pathto(prevsrc, cwd)))
389 389 return
390 390
391 391 # check for overwrites
392 392 exists = os.path.exists(target)
393 393 if not after and exists or after and state in 'mn':
394 394 if not opts['force']:
395 395 ui.warn(_('%s: not overwriting - file exists\n') %
396 396 reltarget)
397 397 return
398 398
399 399 if after:
400 400 if not exists:
401 401 return
402 402 elif not dryrun:
403 403 try:
404 404 if exists:
405 405 os.unlink(target)
406 406 targetdir = os.path.dirname(target) or '.'
407 407 if not os.path.isdir(targetdir):
408 408 os.makedirs(targetdir)
409 409 util.copyfile(src, target)
410 410 except IOError, inst:
411 411 if inst.errno == errno.ENOENT:
412 412 ui.warn(_('%s: deleted in working copy\n') % relsrc)
413 413 else:
414 414 ui.warn(_('%s: cannot copy - %s\n') %
415 415 (relsrc, inst.strerror))
416 416 return True # report a failure
417 417
418 418 if ui.verbose or not exact:
419 419 if rename:
420 420 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
421 421 else:
422 422 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
423 423
424 424 targets[abstarget] = abssrc
425 425
426 426 # fix up dirstate
427 427 origsrc = repo.dirstate.copied(abssrc) or abssrc
428 428 if abstarget == origsrc: # copying back a copy?
429 429 if state not in 'mn' and not dryrun:
430 430 repo.dirstate.normallookup(abstarget)
431 431 else:
432 432 if repo.dirstate[origsrc] == 'a' and origsrc == abssrc:
433 433 if not ui.quiet:
434 434 ui.warn(_("%s has not been committed yet, so no copy "
435 435 "data will be stored for %s.\n")
436 436 % (repo.pathto(origsrc, cwd), reltarget))
437 437 if repo.dirstate[abstarget] in '?r' and not dryrun:
438 438 repo.add([abstarget])
439 439 elif not dryrun:
440 440 repo.copy(origsrc, abstarget)
441 441
442 442 if rename and not dryrun:
443 443 repo.remove([abssrc], not after)
444 444
445 445 # pat: ossep
446 446 # dest ossep
447 447 # srcs: list of (hgsep, hgsep, ossep, bool)
448 448 # return: function that takes hgsep and returns ossep
449 449 def targetpathfn(pat, dest, srcs):
450 450 if os.path.isdir(pat):
451 451 abspfx = util.canonpath(repo.root, cwd, pat)
452 452 abspfx = util.localpath(abspfx)
453 453 if destdirexists:
454 454 striplen = len(os.path.split(abspfx)[0])
455 455 else:
456 456 striplen = len(abspfx)
457 457 if striplen:
458 458 striplen += len(os.sep)
459 459 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
460 460 elif destdirexists:
461 461 res = lambda p: os.path.join(dest,
462 462 os.path.basename(util.localpath(p)))
463 463 else:
464 464 res = lambda p: dest
465 465 return res
466 466
467 467 # pat: ossep
468 468 # dest ossep
469 469 # srcs: list of (hgsep, hgsep, ossep, bool)
470 470 # return: function that takes hgsep and returns ossep
471 471 def targetpathafterfn(pat, dest, srcs):
472 472 if _match.patkind(pat):
473 473 # a mercurial pattern
474 474 res = lambda p: os.path.join(dest,
475 475 os.path.basename(util.localpath(p)))
476 476 else:
477 477 abspfx = util.canonpath(repo.root, cwd, pat)
478 478 if len(abspfx) < len(srcs[0][0]):
479 479 # A directory. Either the target path contains the last
480 480 # component of the source path or it does not.
481 481 def evalpath(striplen):
482 482 score = 0
483 483 for s in srcs:
484 484 t = os.path.join(dest, util.localpath(s[0])[striplen:])
485 485 if os.path.exists(t):
486 486 score += 1
487 487 return score
488 488
489 489 abspfx = util.localpath(abspfx)
490 490 striplen = len(abspfx)
491 491 if striplen:
492 492 striplen += len(os.sep)
493 493 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
494 494 score = evalpath(striplen)
495 495 striplen1 = len(os.path.split(abspfx)[0])
496 496 if striplen1:
497 497 striplen1 += len(os.sep)
498 498 if evalpath(striplen1) > score:
499 499 striplen = striplen1
500 500 res = lambda p: os.path.join(dest,
501 501 util.localpath(p)[striplen:])
502 502 else:
503 503 # a file
504 504 if destdirexists:
505 505 res = lambda p: os.path.join(dest,
506 506 os.path.basename(util.localpath(p)))
507 507 else:
508 508 res = lambda p: dest
509 509 return res
510 510
511 511
512 512 pats = expandpats(pats)
513 513 if not pats:
514 514 raise util.Abort(_('no source or destination specified'))
515 515 if len(pats) == 1:
516 516 raise util.Abort(_('no destination specified'))
517 517 dest = pats.pop()
518 518 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
519 519 if not destdirexists:
520 520 if len(pats) > 1 or _match.patkind(pats[0]):
521 521 raise util.Abort(_('with multiple sources, destination must be an '
522 522 'existing directory'))
523 523 if util.endswithsep(dest):
524 524 raise util.Abort(_('destination %s is not a directory') % dest)
525 525
526 526 tfn = targetpathfn
527 527 if after:
528 528 tfn = targetpathafterfn
529 529 copylist = []
530 530 for pat in pats:
531 531 srcs = walkpat(pat)
532 532 if not srcs:
533 533 continue
534 534 copylist.append((tfn(pat, dest, srcs), srcs))
535 535 if not copylist:
536 536 raise util.Abort(_('no files to copy'))
537 537
538 538 errors = 0
539 539 for targetpath, srcs in copylist:
540 540 for abssrc, relsrc, exact in srcs:
541 541 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
542 542 errors += 1
543 543
544 544 if errors:
545 545 ui.warn(_('(consider using --after)\n'))
546 546
547 547 return errors
548 548
549 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None):
549 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
550 runargs=None):
550 551 '''Run a command as a service.'''
551 552
552 553 if opts['daemon'] and not opts['daemon_pipefds']:
553 554 rfd, wfd = os.pipe()
554 args = sys.argv[:]
555 args.append('--daemon-pipefds=%d,%d' % (rfd, wfd))
555 if not runargs:
556 runargs = sys.argv[:]
557 runargs.append('--daemon-pipefds=%d,%d' % (rfd, wfd))
556 558 # Don't pass --cwd to the child process, because we've already
557 559 # changed directory.
558 for i in xrange(1,len(args)):
559 if args[i].startswith('--cwd='):
560 del args[i]
560 for i in xrange(1,len(runargs)):
561 if runargs[i].startswith('--cwd='):
562 del runargs[i]
561 563 break
562 elif args[i].startswith('--cwd'):
563 del args[i:i+2]
564 elif runargs[i].startswith('--cwd'):
565 del runargs[i:i+2]
564 566 break
565 567 pid = os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
566 args[0], args)
568 runargs[0], runargs)
567 569 os.close(wfd)
568 570 os.read(rfd, 1)
569 571 if parentfn:
570 572 return parentfn(pid)
571 573 else:
572 574 os._exit(0)
573 575
574 576 if initfn:
575 577 initfn()
576 578
577 579 if opts['pid_file']:
578 580 fp = open(opts['pid_file'], 'w')
579 581 fp.write(str(os.getpid()) + '\n')
580 582 fp.close()
581 583
582 584 if opts['daemon_pipefds']:
583 585 rfd, wfd = [int(x) for x in opts['daemon_pipefds'].split(',')]
584 586 os.close(rfd)
585 587 try:
586 588 os.setsid()
587 589 except AttributeError:
588 590 pass
589 591 os.write(wfd, 'y')
590 592 os.close(wfd)
591 593 sys.stdout.flush()
592 594 sys.stderr.flush()
593 595
594 596 nullfd = os.open(util.nulldev, os.O_RDWR)
595 597 logfilefd = nullfd
596 598 if logfile:
597 599 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
598 600 os.dup2(nullfd, 0)
599 601 os.dup2(logfilefd, 1)
600 602 os.dup2(logfilefd, 2)
601 603 if nullfd not in (0, 1, 2):
602 604 os.close(nullfd)
603 605 if logfile and logfilefd not in (0, 1, 2):
604 606 os.close(logfilefd)
605 607
606 608 if runfn:
607 609 return runfn()
608 610
609 611 class changeset_printer(object):
610 612 '''show changeset information when templating not requested.'''
611 613
612 614 def __init__(self, ui, repo, patch, diffopts, buffered):
613 615 self.ui = ui
614 616 self.repo = repo
615 617 self.buffered = buffered
616 618 self.patch = patch
617 619 self.diffopts = diffopts
618 620 self.header = {}
619 621 self.hunk = {}
620 622 self.lastheader = None
621 623
622 624 def flush(self, rev):
623 625 if rev in self.header:
624 626 h = self.header[rev]
625 627 if h != self.lastheader:
626 628 self.lastheader = h
627 629 self.ui.write(h)
628 630 del self.header[rev]
629 631 if rev in self.hunk:
630 632 self.ui.write(self.hunk[rev])
631 633 del self.hunk[rev]
632 634 return 1
633 635 return 0
634 636
635 637 def show(self, ctx, copies=(), **props):
636 638 if self.buffered:
637 639 self.ui.pushbuffer()
638 640 self._show(ctx, copies, props)
639 641 self.hunk[ctx.rev()] = self.ui.popbuffer()
640 642 else:
641 643 self._show(ctx, copies, props)
642 644
643 645 def _show(self, ctx, copies, props):
644 646 '''show a single changeset or file revision'''
645 647 changenode = ctx.node()
646 648 rev = ctx.rev()
647 649
648 650 if self.ui.quiet:
649 651 self.ui.write("%d:%s\n" % (rev, short(changenode)))
650 652 return
651 653
652 654 log = self.repo.changelog
653 655 changes = log.read(changenode)
654 656 date = util.datestr(changes[2])
655 657 extra = changes[5]
656 658 branch = extra.get("branch")
657 659
658 660 hexfunc = self.ui.debugflag and hex or short
659 661
660 662 parents = [(p, hexfunc(log.node(p)))
661 663 for p in self._meaningful_parentrevs(log, rev)]
662 664
663 665 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)))
664 666
665 667 # don't show the default branch name
666 668 if branch != 'default':
667 669 branch = encoding.tolocal(branch)
668 670 self.ui.write(_("branch: %s\n") % branch)
669 671 for tag in self.repo.nodetags(changenode):
670 672 self.ui.write(_("tag: %s\n") % tag)
671 673 for parent in parents:
672 674 self.ui.write(_("parent: %d:%s\n") % parent)
673 675
674 676 if self.ui.debugflag:
675 677 self.ui.write(_("manifest: %d:%s\n") %
676 678 (self.repo.manifest.rev(changes[0]), hex(changes[0])))
677 679 self.ui.write(_("user: %s\n") % changes[1])
678 680 self.ui.write(_("date: %s\n") % date)
679 681
680 682 if self.ui.debugflag:
681 683 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
682 684 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
683 685 files):
684 686 if value:
685 687 self.ui.write("%-12s %s\n" % (key, " ".join(value)))
686 688 elif changes[3] and self.ui.verbose:
687 689 self.ui.write(_("files: %s\n") % " ".join(changes[3]))
688 690 if copies and self.ui.verbose:
689 691 copies = ['%s (%s)' % c for c in copies]
690 692 self.ui.write(_("copies: %s\n") % ' '.join(copies))
691 693
692 694 if extra and self.ui.debugflag:
693 695 for key, value in sorted(extra.items()):
694 696 self.ui.write(_("extra: %s=%s\n")
695 697 % (key, value.encode('string_escape')))
696 698
697 699 description = changes[4].strip()
698 700 if description:
699 701 if self.ui.verbose:
700 702 self.ui.write(_("description:\n"))
701 703 self.ui.write(description)
702 704 self.ui.write("\n\n")
703 705 else:
704 706 self.ui.write(_("summary: %s\n") %
705 707 description.splitlines()[0])
706 708 self.ui.write("\n")
707 709
708 710 self.showpatch(changenode)
709 711
710 712 def showpatch(self, node):
711 713 if self.patch:
712 714 prev = self.repo.changelog.parents(node)[0]
713 715 chunks = patch.diff(self.repo, prev, node, match=self.patch,
714 716 opts=patch.diffopts(self.ui, self.diffopts))
715 717 for chunk in chunks:
716 718 self.ui.write(chunk)
717 719 self.ui.write("\n")
718 720
719 721 def _meaningful_parentrevs(self, log, rev):
720 722 """Return list of meaningful (or all if debug) parentrevs for rev.
721 723
722 724 For merges (two non-nullrev revisions) both parents are meaningful.
723 725 Otherwise the first parent revision is considered meaningful if it
724 726 is not the preceding revision.
725 727 """
726 728 parents = log.parentrevs(rev)
727 729 if not self.ui.debugflag and parents[1] == nullrev:
728 730 if parents[0] >= rev - 1:
729 731 parents = []
730 732 else:
731 733 parents = [parents[0]]
732 734 return parents
733 735
734 736
735 737 class changeset_templater(changeset_printer):
736 738 '''format changeset information.'''
737 739
738 740 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
739 741 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
740 742 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
741 743 self.t = templater.templater(mapfile, {'formatnode': formatnode},
742 744 cache={
743 745 'parent': '{rev}:{node|formatnode} ',
744 746 'manifest': '{rev}:{node|formatnode}',
745 747 'filecopy': '{name} ({source})'})
746 748
747 749 def use_template(self, t):
748 750 '''set template string to use'''
749 751 self.t.cache['changeset'] = t
750 752
751 753 def _meaningful_parentrevs(self, ctx):
752 754 """Return list of meaningful (or all if debug) parentrevs for rev.
753 755 """
754 756 parents = ctx.parents()
755 757 if len(parents) > 1:
756 758 return parents
757 759 if self.ui.debugflag:
758 760 return [parents[0], self.repo['null']]
759 761 if parents[0].rev() >= ctx.rev() - 1:
760 762 return []
761 763 return parents
762 764
763 765 def _show(self, ctx, copies, props):
764 766 '''show a single changeset or file revision'''
765 767
766 768 def showlist(name, values, plural=None, **args):
767 769 '''expand set of values.
768 770 name is name of key in template map.
769 771 values is list of strings or dicts.
770 772 plural is plural of name, if not simply name + 's'.
771 773
772 774 expansion works like this, given name 'foo'.
773 775
774 776 if values is empty, expand 'no_foos'.
775 777
776 778 if 'foo' not in template map, return values as a string,
777 779 joined by space.
778 780
779 781 expand 'start_foos'.
780 782
781 783 for each value, expand 'foo'. if 'last_foo' in template
782 784 map, expand it instead of 'foo' for last key.
783 785
784 786 expand 'end_foos'.
785 787 '''
786 788 if plural: names = plural
787 789 else: names = name + 's'
788 790 if not values:
789 791 noname = 'no_' + names
790 792 if noname in self.t:
791 793 yield self.t(noname, **args)
792 794 return
793 795 if name not in self.t:
794 796 if isinstance(values[0], str):
795 797 yield ' '.join(values)
796 798 else:
797 799 for v in values:
798 800 yield dict(v, **args)
799 801 return
800 802 startname = 'start_' + names
801 803 if startname in self.t:
802 804 yield self.t(startname, **args)
803 805 vargs = args.copy()
804 806 def one(v, tag=name):
805 807 try:
806 808 vargs.update(v)
807 809 except (AttributeError, ValueError):
808 810 try:
809 811 for a, b in v:
810 812 vargs[a] = b
811 813 except ValueError:
812 814 vargs[name] = v
813 815 return self.t(tag, **vargs)
814 816 lastname = 'last_' + name
815 817 if lastname in self.t:
816 818 last = values.pop()
817 819 else:
818 820 last = None
819 821 for v in values:
820 822 yield one(v)
821 823 if last is not None:
822 824 yield one(last, tag=lastname)
823 825 endname = 'end_' + names
824 826 if endname in self.t:
825 827 yield self.t(endname, **args)
826 828
827 829 def showbranches(**args):
828 830 branch = ctx.branch()
829 831 if branch != 'default':
830 832 branch = encoding.tolocal(branch)
831 833 return showlist('branch', [branch], plural='branches', **args)
832 834
833 835 def showparents(**args):
834 836 parents = [[('rev', p.rev()), ('node', p.hex())]
835 837 for p in self._meaningful_parentrevs(ctx)]
836 838 return showlist('parent', parents, **args)
837 839
838 840 def showtags(**args):
839 841 return showlist('tag', ctx.tags(), **args)
840 842
841 843 def showextras(**args):
842 844 for key, value in sorted(ctx.extra().items()):
843 845 args = args.copy()
844 846 args.update(dict(key=key, value=value))
845 847 yield self.t('extra', **args)
846 848
847 849 def showcopies(**args):
848 850 c = [{'name': x[0], 'source': x[1]} for x in copies]
849 851 return showlist('file_copy', c, plural='file_copies', **args)
850 852
851 853 files = []
852 854 def getfiles():
853 855 if not files:
854 856 files[:] = self.repo.status(ctx.parents()[0].node(),
855 857 ctx.node())[:3]
856 858 return files
857 859 def showfiles(**args):
858 860 return showlist('file', ctx.files(), **args)
859 861 def showmods(**args):
860 862 return showlist('file_mod', getfiles()[0], **args)
861 863 def showadds(**args):
862 864 return showlist('file_add', getfiles()[1], **args)
863 865 def showdels(**args):
864 866 return showlist('file_del', getfiles()[2], **args)
865 867 def showmanifest(**args):
866 868 args = args.copy()
867 869 args.update(dict(rev=self.repo.manifest.rev(ctx.changeset()[0]),
868 870 node=hex(ctx.changeset()[0])))
869 871 return self.t('manifest', **args)
870 872
871 873 def showdiffstat(**args):
872 874 diff = patch.diff(self.repo, ctx.parents()[0].node(), ctx.node())
873 875 files, adds, removes = 0, 0, 0
874 876 for i in patch.diffstatdata(util.iterlines(diff)):
875 877 files += 1
876 878 adds += i[1]
877 879 removes += i[2]
878 880 return '%s: +%s/-%s' % (files, adds, removes)
879 881
880 882 defprops = {
881 883 'author': ctx.user(),
882 884 'branches': showbranches,
883 885 'date': ctx.date(),
884 886 'desc': ctx.description().strip(),
885 887 'file_adds': showadds,
886 888 'file_dels': showdels,
887 889 'file_mods': showmods,
888 890 'files': showfiles,
889 891 'file_copies': showcopies,
890 892 'manifest': showmanifest,
891 893 'node': ctx.hex(),
892 894 'parents': showparents,
893 895 'rev': ctx.rev(),
894 896 'tags': showtags,
895 897 'extras': showextras,
896 898 'diffstat': showdiffstat,
897 899 }
898 900 props = props.copy()
899 901 props.update(defprops)
900 902
901 903 # find correct templates for current mode
902 904
903 905 tmplmodes = [
904 906 (True, None),
905 907 (self.ui.verbose, 'verbose'),
906 908 (self.ui.quiet, 'quiet'),
907 909 (self.ui.debugflag, 'debug'),
908 910 ]
909 911
910 912 types = {'header': '', 'changeset': 'changeset'}
911 913 for mode, postfix in tmplmodes:
912 914 for type in types:
913 915 cur = postfix and ('%s_%s' % (type, postfix)) or type
914 916 if mode and cur in self.t:
915 917 types[type] = cur
916 918
917 919 try:
918 920
919 921 # write header
920 922 if types['header']:
921 923 h = templater.stringify(self.t(types['header'], **props))
922 924 if self.buffered:
923 925 self.header[ctx.rev()] = h
924 926 else:
925 927 self.ui.write(h)
926 928
927 929 # write changeset metadata, then patch if requested
928 930 key = types['changeset']
929 931 self.ui.write(templater.stringify(self.t(key, **props)))
930 932 self.showpatch(ctx.node())
931 933
932 934 except KeyError, inst:
933 935 msg = _("%s: no key named '%s'")
934 936 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
935 937 except SyntaxError, inst:
936 938 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
937 939
938 940 def show_changeset(ui, repo, opts, buffered=False, matchfn=False):
939 941 """show one changeset using template or regular display.
940 942
941 943 Display format will be the first non-empty hit of:
942 944 1. option 'template'
943 945 2. option 'style'
944 946 3. [ui] setting 'logtemplate'
945 947 4. [ui] setting 'style'
946 948 If all of these values are either the unset or the empty string,
947 949 regular display via changeset_printer() is done.
948 950 """
949 951 # options
950 952 patch = False
951 953 if opts.get('patch'):
952 954 patch = matchfn or matchall(repo)
953 955
954 956 tmpl = opts.get('template')
955 957 style = None
956 958 if tmpl:
957 959 tmpl = templater.parsestring(tmpl, quoted=False)
958 960 else:
959 961 style = opts.get('style')
960 962
961 963 # ui settings
962 964 if not (tmpl or style):
963 965 tmpl = ui.config('ui', 'logtemplate')
964 966 if tmpl:
965 967 tmpl = templater.parsestring(tmpl)
966 968 else:
967 969 style = ui.config('ui', 'style')
968 970
969 971 if not (tmpl or style):
970 972 return changeset_printer(ui, repo, patch, opts, buffered)
971 973
972 974 mapfile = None
973 975 if style and not tmpl:
974 976 mapfile = style
975 977 if not os.path.split(mapfile)[0]:
976 978 mapname = (templater.templatepath('map-cmdline.' + mapfile)
977 979 or templater.templatepath(mapfile))
978 980 if mapname: mapfile = mapname
979 981
980 982 try:
981 983 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
982 984 except SyntaxError, inst:
983 985 raise util.Abort(inst.args[0])
984 986 if tmpl: t.use_template(tmpl)
985 987 return t
986 988
987 989 def finddate(ui, repo, date):
988 990 """Find the tipmost changeset that matches the given date spec"""
989 991 df = util.matchdate(date)
990 992 get = util.cachefunc(lambda r: repo[r])
991 993 changeiter, matchfn = walkchangerevs(ui, repo, [], get, {'rev':None})
992 994 results = {}
993 995 for st, rev, fns in changeiter:
994 996 if st == 'add':
995 997 d = get(rev).date()
996 998 if df(d[0]):
997 999 results[rev] = d
998 1000 elif st == 'iter':
999 1001 if rev in results:
1000 1002 ui.status(_("Found revision %s from %s\n") %
1001 1003 (rev, util.datestr(results[rev])))
1002 1004 return str(rev)
1003 1005
1004 1006 raise util.Abort(_("revision matching date not found"))
1005 1007
1006 1008 def walkchangerevs(ui, repo, pats, change, opts):
1007 1009 '''Iterate over files and the revs in which they changed.
1008 1010
1009 1011 Callers most commonly need to iterate backwards over the history
1010 1012 in which they are interested. Doing so has awful (quadratic-looking)
1011 1013 performance, so we use iterators in a "windowed" way.
1012 1014
1013 1015 We walk a window of revisions in the desired order. Within the
1014 1016 window, we first walk forwards to gather data, then in the desired
1015 1017 order (usually backwards) to display it.
1016 1018
1017 1019 This function returns an (iterator, matchfn) tuple. The iterator
1018 1020 yields 3-tuples. They will be of one of the following forms:
1019 1021
1020 1022 "window", incrementing, lastrev: stepping through a window,
1021 1023 positive if walking forwards through revs, last rev in the
1022 1024 sequence iterated over - use to reset state for the current window
1023 1025
1024 1026 "add", rev, fns: out-of-order traversal of the given filenames
1025 1027 fns, which changed during revision rev - use to gather data for
1026 1028 possible display
1027 1029
1028 1030 "iter", rev, None: in-order traversal of the revs earlier iterated
1029 1031 over with "add" - use to display data'''
1030 1032
1031 1033 def increasing_windows(start, end, windowsize=8, sizelimit=512):
1032 1034 if start < end:
1033 1035 while start < end:
1034 1036 yield start, min(windowsize, end-start)
1035 1037 start += windowsize
1036 1038 if windowsize < sizelimit:
1037 1039 windowsize *= 2
1038 1040 else:
1039 1041 while start > end:
1040 1042 yield start, min(windowsize, start-end-1)
1041 1043 start -= windowsize
1042 1044 if windowsize < sizelimit:
1043 1045 windowsize *= 2
1044 1046
1045 1047 m = match(repo, pats, opts)
1046 1048 follow = opts.get('follow') or opts.get('follow_first')
1047 1049
1048 1050 if not len(repo):
1049 1051 return [], m
1050 1052
1051 1053 if follow:
1052 1054 defrange = '%s:0' % repo['.'].rev()
1053 1055 else:
1054 1056 defrange = '-1:0'
1055 1057 revs = revrange(repo, opts['rev'] or [defrange])
1056 1058 wanted = set()
1057 1059 slowpath = m.anypats() or (m.files() and opts.get('removed'))
1058 1060 fncache = {}
1059 1061
1060 1062 if not slowpath and not m.files():
1061 1063 # No files, no patterns. Display all revs.
1062 1064 wanted = set(revs)
1063 1065 copies = []
1064 1066 if not slowpath:
1065 1067 # Only files, no patterns. Check the history of each file.
1066 1068 def filerevgen(filelog, node):
1067 1069 cl_count = len(repo)
1068 1070 if node is None:
1069 1071 last = len(filelog) - 1
1070 1072 else:
1071 1073 last = filelog.rev(node)
1072 1074 for i, window in increasing_windows(last, nullrev):
1073 1075 revs = []
1074 1076 for j in xrange(i - window, i + 1):
1075 1077 n = filelog.node(j)
1076 1078 revs.append((filelog.linkrev(j),
1077 1079 follow and filelog.renamed(n)))
1078 1080 for rev in reversed(revs):
1079 1081 # only yield rev for which we have the changelog, it can
1080 1082 # happen while doing "hg log" during a pull or commit
1081 1083 if rev[0] < cl_count:
1082 1084 yield rev
1083 1085 def iterfiles():
1084 1086 for filename in m.files():
1085 1087 yield filename, None
1086 1088 for filename_node in copies:
1087 1089 yield filename_node
1088 1090 minrev, maxrev = min(revs), max(revs)
1089 1091 for file_, node in iterfiles():
1090 1092 filelog = repo.file(file_)
1091 1093 if not len(filelog):
1092 1094 if node is None:
1093 1095 # A zero count may be a directory or deleted file, so
1094 1096 # try to find matching entries on the slow path.
1095 1097 if follow:
1096 1098 raise util.Abort(_('cannot follow nonexistent file: "%s"') % file_)
1097 1099 slowpath = True
1098 1100 break
1099 1101 else:
1100 1102 ui.warn(_('%s:%s copy source revision cannot be found!\n')
1101 1103 % (file_, short(node)))
1102 1104 continue
1103 1105 for rev, copied in filerevgen(filelog, node):
1104 1106 if rev <= maxrev:
1105 1107 if rev < minrev:
1106 1108 break
1107 1109 fncache.setdefault(rev, [])
1108 1110 fncache[rev].append(file_)
1109 1111 wanted.add(rev)
1110 1112 if follow and copied:
1111 1113 copies.append(copied)
1112 1114 if slowpath:
1113 1115 if follow:
1114 1116 raise util.Abort(_('can only follow copies/renames for explicit '
1115 1117 'filenames'))
1116 1118
1117 1119 # The slow path checks files modified in every changeset.
1118 1120 def changerevgen():
1119 1121 for i, window in increasing_windows(len(repo) - 1, nullrev):
1120 1122 for j in xrange(i - window, i + 1):
1121 1123 yield change(j)
1122 1124
1123 1125 for ctx in changerevgen():
1124 1126 matches = filter(m, ctx.files())
1125 1127 if matches:
1126 1128 fncache[ctx.rev()] = matches
1127 1129 wanted.add(ctx.rev())
1128 1130
1129 1131 class followfilter(object):
1130 1132 def __init__(self, onlyfirst=False):
1131 1133 self.startrev = nullrev
1132 1134 self.roots = []
1133 1135 self.onlyfirst = onlyfirst
1134 1136
1135 1137 def match(self, rev):
1136 1138 def realparents(rev):
1137 1139 if self.onlyfirst:
1138 1140 return repo.changelog.parentrevs(rev)[0:1]
1139 1141 else:
1140 1142 return filter(lambda x: x != nullrev,
1141 1143 repo.changelog.parentrevs(rev))
1142 1144
1143 1145 if self.startrev == nullrev:
1144 1146 self.startrev = rev
1145 1147 return True
1146 1148
1147 1149 if rev > self.startrev:
1148 1150 # forward: all descendants
1149 1151 if not self.roots:
1150 1152 self.roots.append(self.startrev)
1151 1153 for parent in realparents(rev):
1152 1154 if parent in self.roots:
1153 1155 self.roots.append(rev)
1154 1156 return True
1155 1157 else:
1156 1158 # backwards: all parents
1157 1159 if not self.roots:
1158 1160 self.roots.extend(realparents(self.startrev))
1159 1161 if rev in self.roots:
1160 1162 self.roots.remove(rev)
1161 1163 self.roots.extend(realparents(rev))
1162 1164 return True
1163 1165
1164 1166 return False
1165 1167
1166 1168 # it might be worthwhile to do this in the iterator if the rev range
1167 1169 # is descending and the prune args are all within that range
1168 1170 for rev in opts.get('prune', ()):
1169 1171 rev = repo.changelog.rev(repo.lookup(rev))
1170 1172 ff = followfilter()
1171 1173 stop = min(revs[0], revs[-1])
1172 1174 for x in xrange(rev, stop-1, -1):
1173 1175 if ff.match(x):
1174 1176 wanted.discard(x)
1175 1177
1176 1178 def iterate():
1177 1179 if follow and not m.files():
1178 1180 ff = followfilter(onlyfirst=opts.get('follow_first'))
1179 1181 def want(rev):
1180 1182 return ff.match(rev) and rev in wanted
1181 1183 else:
1182 1184 def want(rev):
1183 1185 return rev in wanted
1184 1186
1185 1187 for i, window in increasing_windows(0, len(revs)):
1186 1188 yield 'window', revs[0] < revs[-1], revs[-1]
1187 1189 nrevs = [rev for rev in revs[i:i+window] if want(rev)]
1188 1190 for rev in sorted(nrevs):
1189 1191 fns = fncache.get(rev)
1190 1192 if not fns:
1191 1193 def fns_generator():
1192 1194 for f in change(rev).files():
1193 1195 if m(f):
1194 1196 yield f
1195 1197 fns = fns_generator()
1196 1198 yield 'add', rev, fns
1197 1199 for rev in nrevs:
1198 1200 yield 'iter', rev, None
1199 1201 return iterate(), m
1200 1202
1201 1203 def commit(ui, repo, commitfunc, pats, opts):
1202 1204 '''commit the specified files or all outstanding changes'''
1203 1205 date = opts.get('date')
1204 1206 if date:
1205 1207 opts['date'] = util.parsedate(date)
1206 1208 message = logmessage(opts)
1207 1209
1208 1210 # extract addremove carefully -- this function can be called from a command
1209 1211 # that doesn't support addremove
1210 1212 if opts.get('addremove'):
1211 1213 addremove(repo, pats, opts)
1212 1214
1213 1215 return commitfunc(ui, repo, message, match(repo, pats, opts), opts)
1214 1216
1215 1217 def commiteditor(repo, ctx, subs):
1216 1218 if ctx.description():
1217 1219 return ctx.description()
1218 1220 return commitforceeditor(repo, ctx, subs)
1219 1221
1220 1222 def commitforceeditor(repo, ctx, subs):
1221 1223 edittext = []
1222 1224 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1223 1225 if ctx.description():
1224 1226 edittext.append(ctx.description())
1225 1227 edittext.append("")
1226 1228 edittext.append("") # Empty line between message and comments.
1227 1229 edittext.append(_("HG: Enter commit message."
1228 1230 " Lines beginning with 'HG:' are removed."))
1229 1231 edittext.append(_("HG: Leave message empty to abort commit."))
1230 1232 edittext.append("HG: --")
1231 1233 edittext.append(_("HG: user: %s") % ctx.user())
1232 1234 if ctx.p2():
1233 1235 edittext.append(_("HG: branch merge"))
1234 1236 if ctx.branch():
1235 1237 edittext.append(_("HG: branch '%s'")
1236 1238 % encoding.tolocal(ctx.branch()))
1237 1239 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1238 1240 edittext.extend([_("HG: added %s") % f for f in added])
1239 1241 edittext.extend([_("HG: changed %s") % f for f in modified])
1240 1242 edittext.extend([_("HG: removed %s") % f for f in removed])
1241 1243 if not added and not modified and not removed:
1242 1244 edittext.append(_("HG: no files changed"))
1243 1245 edittext.append("")
1244 1246 # run editor in the repository root
1245 1247 olddir = os.getcwd()
1246 1248 os.chdir(repo.root)
1247 1249 text = repo.ui.edit("\n".join(edittext), ctx.user())
1248 1250 text = re.sub("(?m)^HG:.*\n", "", text)
1249 1251 os.chdir(olddir)
1250 1252
1251 1253 if not text.strip():
1252 1254 raise util.Abort(_("empty commit message"))
1253 1255
1254 1256 return text
General Comments 0
You need to be logged in to leave comments. Login now