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