##// END OF EJS Templates
cmdutil: handle and warn about missing copy revisions
Patrick Mezard -
r6536:dfdef3d5 default
parent child Browse files
Show More
@@ -1,1176 +1,1181 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
6 6 # of the GNU General Public License, 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
11 11 import mdiff, bdiff, util, templater, templatefilters, patch, errno
12 12
13 13 revrangesep = ':'
14 14
15 15 class UnknownCommand(Exception):
16 16 """Exception raised if command is not in the command table."""
17 17 class AmbiguousCommand(Exception):
18 18 """Exception raised if command shortcut matches more than one command."""
19 19
20 20 def findpossible(ui, cmd, table):
21 21 """
22 22 Return cmd -> (aliases, command table entry)
23 23 for each matching command.
24 24 Return debug commands (or their aliases) only if no normal command matches.
25 25 """
26 26 choice = {}
27 27 debugchoice = {}
28 28 for e in table.keys():
29 29 aliases = e.lstrip("^").split("|")
30 30 found = None
31 31 if cmd in aliases:
32 32 found = cmd
33 33 elif not ui.config("ui", "strict"):
34 34 for a in aliases:
35 35 if a.startswith(cmd):
36 36 found = a
37 37 break
38 38 if found is not None:
39 39 if aliases[0].startswith("debug") or found.startswith("debug"):
40 40 debugchoice[found] = (aliases, table[e])
41 41 else:
42 42 choice[found] = (aliases, table[e])
43 43
44 44 if not choice and debugchoice:
45 45 choice = debugchoice
46 46
47 47 return choice
48 48
49 49 def findcmd(ui, cmd, table):
50 50 """Return (aliases, command table entry) for command string."""
51 51 choice = findpossible(ui, cmd, table)
52 52
53 53 if cmd in choice:
54 54 return choice[cmd]
55 55
56 56 if len(choice) > 1:
57 57 clist = choice.keys()
58 58 clist.sort()
59 59 raise AmbiguousCommand(cmd, clist)
60 60
61 61 if choice:
62 62 return choice.values()[0]
63 63
64 64 raise UnknownCommand(cmd)
65 65
66 66 def bail_if_changed(repo):
67 67 if repo.dirstate.parents()[1] != nullid:
68 68 raise util.Abort(_('outstanding uncommitted merge'))
69 69 modified, added, removed, deleted = repo.status()[:4]
70 70 if modified or added or removed or deleted:
71 71 raise util.Abort(_("outstanding uncommitted changes"))
72 72
73 73 def logmessage(opts):
74 74 """ get the log message according to -m and -l option """
75 75 message = opts['message']
76 76 logfile = opts['logfile']
77 77
78 78 if message and logfile:
79 79 raise util.Abort(_('options --message and --logfile are mutually '
80 80 'exclusive'))
81 81 if not message and logfile:
82 82 try:
83 83 if logfile == '-':
84 84 message = sys.stdin.read()
85 85 else:
86 86 message = open(logfile).read()
87 87 except IOError, inst:
88 88 raise util.Abort(_("can't read commit message '%s': %s") %
89 89 (logfile, inst.strerror))
90 90 return message
91 91
92 92 def loglimit(opts):
93 93 """get the log limit according to option -l/--limit"""
94 94 limit = opts.get('limit')
95 95 if limit:
96 96 try:
97 97 limit = int(limit)
98 98 except ValueError:
99 99 raise util.Abort(_('limit must be a positive integer'))
100 100 if limit <= 0: raise util.Abort(_('limit must be positive'))
101 101 else:
102 102 limit = sys.maxint
103 103 return limit
104 104
105 105 def setremoteconfig(ui, opts):
106 106 "copy remote options to ui tree"
107 107 if opts.get('ssh'):
108 108 ui.setconfig("ui", "ssh", opts['ssh'])
109 109 if opts.get('remotecmd'):
110 110 ui.setconfig("ui", "remotecmd", opts['remotecmd'])
111 111
112 112 def revpair(repo, revs):
113 113 '''return pair of nodes, given list of revisions. second item can
114 114 be None, meaning use working dir.'''
115 115
116 116 def revfix(repo, val, defval):
117 117 if not val and val != 0 and defval is not None:
118 118 val = defval
119 119 return repo.lookup(val)
120 120
121 121 if not revs:
122 122 return repo.dirstate.parents()[0], None
123 123 end = None
124 124 if len(revs) == 1:
125 125 if revrangesep in revs[0]:
126 126 start, end = revs[0].split(revrangesep, 1)
127 127 start = revfix(repo, start, 0)
128 128 end = revfix(repo, end, repo.changelog.count() - 1)
129 129 else:
130 130 start = revfix(repo, revs[0], None)
131 131 elif len(revs) == 2:
132 132 if revrangesep in revs[0] or revrangesep in revs[1]:
133 133 raise util.Abort(_('too many revisions specified'))
134 134 start = revfix(repo, revs[0], None)
135 135 end = revfix(repo, revs[1], None)
136 136 else:
137 137 raise util.Abort(_('too many revisions specified'))
138 138 return start, end
139 139
140 140 def revrange(repo, revs):
141 141 """Yield revision as strings from a list of revision specifications."""
142 142
143 143 def revfix(repo, val, defval):
144 144 if not val and val != 0 and defval is not None:
145 145 return defval
146 146 return repo.changelog.rev(repo.lookup(val))
147 147
148 148 seen, l = {}, []
149 149 for spec in revs:
150 150 if revrangesep in spec:
151 151 start, end = spec.split(revrangesep, 1)
152 152 start = revfix(repo, start, 0)
153 153 end = revfix(repo, end, repo.changelog.count() - 1)
154 154 step = start > end and -1 or 1
155 155 for rev in xrange(start, end+step, step):
156 156 if rev in seen:
157 157 continue
158 158 seen[rev] = 1
159 159 l.append(rev)
160 160 else:
161 161 rev = revfix(repo, spec, None)
162 162 if rev in seen:
163 163 continue
164 164 seen[rev] = 1
165 165 l.append(rev)
166 166
167 167 return l
168 168
169 169 def make_filename(repo, pat, node,
170 170 total=None, seqno=None, revwidth=None, pathname=None):
171 171 node_expander = {
172 172 'H': lambda: hex(node),
173 173 'R': lambda: str(repo.changelog.rev(node)),
174 174 'h': lambda: short(node),
175 175 }
176 176 expander = {
177 177 '%': lambda: '%',
178 178 'b': lambda: os.path.basename(repo.root),
179 179 }
180 180
181 181 try:
182 182 if node:
183 183 expander.update(node_expander)
184 184 if node:
185 185 expander['r'] = (lambda:
186 186 str(repo.changelog.rev(node)).zfill(revwidth or 0))
187 187 if total is not None:
188 188 expander['N'] = lambda: str(total)
189 189 if seqno is not None:
190 190 expander['n'] = lambda: str(seqno)
191 191 if total is not None and seqno is not None:
192 192 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
193 193 if pathname is not None:
194 194 expander['s'] = lambda: os.path.basename(pathname)
195 195 expander['d'] = lambda: os.path.dirname(pathname) or '.'
196 196 expander['p'] = lambda: pathname
197 197
198 198 newname = []
199 199 patlen = len(pat)
200 200 i = 0
201 201 while i < patlen:
202 202 c = pat[i]
203 203 if c == '%':
204 204 i += 1
205 205 c = pat[i]
206 206 c = expander[c]()
207 207 newname.append(c)
208 208 i += 1
209 209 return ''.join(newname)
210 210 except KeyError, inst:
211 211 raise util.Abort(_("invalid format spec '%%%s' in output file name") %
212 212 inst.args[0])
213 213
214 214 def make_file(repo, pat, node=None,
215 215 total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
216 216 if not pat or pat == '-':
217 217 return 'w' in mode and sys.stdout or sys.stdin
218 218 if hasattr(pat, 'write') and 'w' in mode:
219 219 return pat
220 220 if hasattr(pat, 'read') and 'r' in mode:
221 221 return pat
222 222 return open(make_filename(repo, pat, node, total, seqno, revwidth,
223 223 pathname),
224 224 mode)
225 225
226 226 def matchpats(repo, pats=[], opts={}, globbed=False, default=None):
227 227 cwd = repo.getcwd()
228 228 return util.cmdmatcher(repo.root, cwd, pats or [], opts.get('include'),
229 229 opts.get('exclude'), globbed=globbed,
230 230 default=default)
231 231
232 232 def walk(repo, pats=[], opts={}, node=None, badmatch=None, globbed=False,
233 233 default=None):
234 234 files, matchfn, anypats = matchpats(repo, pats, opts, globbed=globbed,
235 235 default=default)
236 236 exact = dict.fromkeys(files)
237 237 cwd = repo.getcwd()
238 238 for src, fn in repo.walk(node=node, files=files, match=matchfn,
239 239 badmatch=badmatch):
240 240 yield src, fn, repo.pathto(fn, cwd), fn in exact
241 241
242 242 def findrenames(repo, added=None, removed=None, threshold=0.5):
243 243 '''find renamed files -- yields (before, after, score) tuples'''
244 244 if added is None or removed is None:
245 245 added, removed = repo.status()[1:3]
246 246 ctx = repo.changectx()
247 247 for a in added:
248 248 aa = repo.wread(a)
249 249 bestname, bestscore = None, threshold
250 250 for r in removed:
251 251 rr = ctx.filectx(r).data()
252 252
253 253 # bdiff.blocks() returns blocks of matching lines
254 254 # count the number of bytes in each
255 255 equal = 0
256 256 alines = mdiff.splitnewlines(aa)
257 257 matches = bdiff.blocks(aa, rr)
258 258 for x1,x2,y1,y2 in matches:
259 259 for line in alines[x1:x2]:
260 260 equal += len(line)
261 261
262 262 lengths = len(aa) + len(rr)
263 263 if lengths:
264 264 myscore = equal*2.0 / lengths
265 265 if myscore >= bestscore:
266 266 bestname, bestscore = r, myscore
267 267 if bestname:
268 268 yield bestname, a, bestscore
269 269
270 270 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
271 271 if dry_run is None:
272 272 dry_run = opts.get('dry_run')
273 273 if similarity is None:
274 274 similarity = float(opts.get('similarity') or 0)
275 275 add, remove = [], []
276 276 mapping = {}
277 277 for src, abs, rel, exact in walk(repo, pats, opts):
278 278 target = repo.wjoin(abs)
279 279 if src == 'f' and abs not in repo.dirstate:
280 280 add.append(abs)
281 281 mapping[abs] = rel, exact
282 282 if repo.ui.verbose or not exact:
283 283 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
284 284 if repo.dirstate[abs] != 'r' and (not util.lexists(target)
285 285 or (os.path.isdir(target) and not os.path.islink(target))):
286 286 remove.append(abs)
287 287 mapping[abs] = rel, exact
288 288 if repo.ui.verbose or not exact:
289 289 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
290 290 if not dry_run:
291 291 repo.remove(remove)
292 292 repo.add(add)
293 293 if similarity > 0:
294 294 for old, new, score in findrenames(repo, add, remove, similarity):
295 295 oldrel, oldexact = mapping[old]
296 296 newrel, newexact = mapping[new]
297 297 if repo.ui.verbose or not oldexact or not newexact:
298 298 repo.ui.status(_('recording removal of %s as rename to %s '
299 299 '(%d%% similar)\n') %
300 300 (oldrel, newrel, score * 100))
301 301 if not dry_run:
302 302 repo.copy(old, new)
303 303
304 304 def copy(ui, repo, pats, opts, rename=False):
305 305 # called with the repo lock held
306 306 #
307 307 # hgsep => pathname that uses "/" to separate directories
308 308 # ossep => pathname that uses os.sep to separate directories
309 309 cwd = repo.getcwd()
310 310 targets = {}
311 311 after = opts.get("after")
312 312 dryrun = opts.get("dry_run")
313 313
314 314 def walkpat(pat):
315 315 srcs = []
316 316 for tag, abs, rel, exact in walk(repo, [pat], opts, globbed=True):
317 317 state = repo.dirstate[abs]
318 318 if state in '?r':
319 319 if exact and state == '?':
320 320 ui.warn(_('%s: not copying - file is not managed\n') % rel)
321 321 if exact and state == 'r':
322 322 ui.warn(_('%s: not copying - file has been marked for'
323 323 ' remove\n') % rel)
324 324 continue
325 325 # abs: hgsep
326 326 # rel: ossep
327 327 srcs.append((abs, rel, exact))
328 328 return srcs
329 329
330 330 # abssrc: hgsep
331 331 # relsrc: ossep
332 332 # otarget: ossep
333 333 def copyfile(abssrc, relsrc, otarget, exact):
334 334 abstarget = util.canonpath(repo.root, cwd, otarget)
335 335 reltarget = repo.pathto(abstarget, cwd)
336 336 target = repo.wjoin(abstarget)
337 337 src = repo.wjoin(abssrc)
338 338 state = repo.dirstate[abstarget]
339 339
340 340 # check for collisions
341 341 prevsrc = targets.get(abstarget)
342 342 if prevsrc is not None:
343 343 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
344 344 (reltarget, repo.pathto(abssrc, cwd),
345 345 repo.pathto(prevsrc, cwd)))
346 346 return
347 347
348 348 # check for overwrites
349 349 exists = os.path.exists(target)
350 350 if (not after and exists or after and state in 'mn'):
351 351 if not opts['force']:
352 352 ui.warn(_('%s: not overwriting - file exists\n') %
353 353 reltarget)
354 354 return
355 355
356 356 if after:
357 357 if not exists:
358 358 return
359 359 elif not dryrun:
360 360 try:
361 361 if exists:
362 362 os.unlink(target)
363 363 targetdir = os.path.dirname(target) or '.'
364 364 if not os.path.isdir(targetdir):
365 365 os.makedirs(targetdir)
366 366 util.copyfile(src, target)
367 367 except IOError, inst:
368 368 if inst.errno == errno.ENOENT:
369 369 ui.warn(_('%s: deleted in working copy\n') % relsrc)
370 370 else:
371 371 ui.warn(_('%s: cannot copy - %s\n') %
372 372 (relsrc, inst.strerror))
373 373 return True # report a failure
374 374
375 375 if ui.verbose or not exact:
376 376 action = rename and "moving" or "copying"
377 377 ui.status(_('%s %s to %s\n') % (action, relsrc, reltarget))
378 378
379 379 targets[abstarget] = abssrc
380 380
381 381 # fix up dirstate
382 382 origsrc = repo.dirstate.copied(abssrc) or abssrc
383 383 if abstarget == origsrc: # copying back a copy?
384 384 if state not in 'mn' and not dryrun:
385 385 repo.dirstate.normallookup(abstarget)
386 386 else:
387 387 if repo.dirstate[origsrc] == 'a':
388 388 if not ui.quiet:
389 389 ui.warn(_("%s has not been committed yet, so no copy "
390 390 "data will be stored for %s.\n")
391 391 % (repo.pathto(origsrc, cwd), reltarget))
392 392 if abstarget not in repo.dirstate and not dryrun:
393 393 repo.add([abstarget])
394 394 elif not dryrun:
395 395 repo.copy(origsrc, abstarget)
396 396
397 397 if rename and not dryrun:
398 398 repo.remove([abssrc], True)
399 399
400 400 # pat: ossep
401 401 # dest ossep
402 402 # srcs: list of (hgsep, hgsep, ossep, bool)
403 403 # return: function that takes hgsep and returns ossep
404 404 def targetpathfn(pat, dest, srcs):
405 405 if os.path.isdir(pat):
406 406 abspfx = util.canonpath(repo.root, cwd, pat)
407 407 abspfx = util.localpath(abspfx)
408 408 if destdirexists:
409 409 striplen = len(os.path.split(abspfx)[0])
410 410 else:
411 411 striplen = len(abspfx)
412 412 if striplen:
413 413 striplen += len(os.sep)
414 414 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
415 415 elif destdirexists:
416 416 res = lambda p: os.path.join(dest,
417 417 os.path.basename(util.localpath(p)))
418 418 else:
419 419 res = lambda p: dest
420 420 return res
421 421
422 422 # pat: ossep
423 423 # dest ossep
424 424 # srcs: list of (hgsep, hgsep, ossep, bool)
425 425 # return: function that takes hgsep and returns ossep
426 426 def targetpathafterfn(pat, dest, srcs):
427 427 if util.patkind(pat, None)[0]:
428 428 # a mercurial pattern
429 429 res = lambda p: os.path.join(dest,
430 430 os.path.basename(util.localpath(p)))
431 431 else:
432 432 abspfx = util.canonpath(repo.root, cwd, pat)
433 433 if len(abspfx) < len(srcs[0][0]):
434 434 # A directory. Either the target path contains the last
435 435 # component of the source path or it does not.
436 436 def evalpath(striplen):
437 437 score = 0
438 438 for s in srcs:
439 439 t = os.path.join(dest, util.localpath(s[0])[striplen:])
440 440 if os.path.exists(t):
441 441 score += 1
442 442 return score
443 443
444 444 abspfx = util.localpath(abspfx)
445 445 striplen = len(abspfx)
446 446 if striplen:
447 447 striplen += len(os.sep)
448 448 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
449 449 score = evalpath(striplen)
450 450 striplen1 = len(os.path.split(abspfx)[0])
451 451 if striplen1:
452 452 striplen1 += len(os.sep)
453 453 if evalpath(striplen1) > score:
454 454 striplen = striplen1
455 455 res = lambda p: os.path.join(dest,
456 456 util.localpath(p)[striplen:])
457 457 else:
458 458 # a file
459 459 if destdirexists:
460 460 res = lambda p: os.path.join(dest,
461 461 os.path.basename(util.localpath(p)))
462 462 else:
463 463 res = lambda p: dest
464 464 return res
465 465
466 466
467 467 pats = util.expand_glob(pats)
468 468 if not pats:
469 469 raise util.Abort(_('no source or destination specified'))
470 470 if len(pats) == 1:
471 471 raise util.Abort(_('no destination specified'))
472 472 dest = pats.pop()
473 473 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
474 474 if not destdirexists:
475 475 if len(pats) > 1 or util.patkind(pats[0], None)[0]:
476 476 raise util.Abort(_('with multiple sources, destination must be an '
477 477 'existing directory'))
478 478 if util.endswithsep(dest):
479 479 raise util.Abort(_('destination %s is not a directory') % dest)
480 480
481 481 tfn = targetpathfn
482 482 if after:
483 483 tfn = targetpathafterfn
484 484 copylist = []
485 485 for pat in pats:
486 486 srcs = walkpat(pat)
487 487 if not srcs:
488 488 continue
489 489 copylist.append((tfn(pat, dest, srcs), srcs))
490 490 if not copylist:
491 491 raise util.Abort(_('no files to copy'))
492 492
493 493 errors = 0
494 494 for targetpath, srcs in copylist:
495 495 for abssrc, relsrc, exact in srcs:
496 496 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
497 497 errors += 1
498 498
499 499 if errors:
500 500 ui.warn(_('(consider using --after)\n'))
501 501
502 502 return errors
503 503
504 504 def service(opts, parentfn=None, initfn=None, runfn=None):
505 505 '''Run a command as a service.'''
506 506
507 507 if opts['daemon'] and not opts['daemon_pipefds']:
508 508 rfd, wfd = os.pipe()
509 509 args = sys.argv[:]
510 510 args.append('--daemon-pipefds=%d,%d' % (rfd, wfd))
511 511 # Don't pass --cwd to the child process, because we've already
512 512 # changed directory.
513 513 for i in xrange(1,len(args)):
514 514 if args[i].startswith('--cwd='):
515 515 del args[i]
516 516 break
517 517 elif args[i].startswith('--cwd'):
518 518 del args[i:i+2]
519 519 break
520 520 pid = os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
521 521 args[0], args)
522 522 os.close(wfd)
523 523 os.read(rfd, 1)
524 524 if parentfn:
525 525 return parentfn(pid)
526 526 else:
527 527 os._exit(0)
528 528
529 529 if initfn:
530 530 initfn()
531 531
532 532 if opts['pid_file']:
533 533 fp = open(opts['pid_file'], 'w')
534 534 fp.write(str(os.getpid()) + '\n')
535 535 fp.close()
536 536
537 537 if opts['daemon_pipefds']:
538 538 rfd, wfd = [int(x) for x in opts['daemon_pipefds'].split(',')]
539 539 os.close(rfd)
540 540 try:
541 541 os.setsid()
542 542 except AttributeError:
543 543 pass
544 544 os.write(wfd, 'y')
545 545 os.close(wfd)
546 546 sys.stdout.flush()
547 547 sys.stderr.flush()
548 548 fd = os.open(util.nulldev, os.O_RDWR)
549 549 if fd != 0: os.dup2(fd, 0)
550 550 if fd != 1: os.dup2(fd, 1)
551 551 if fd != 2: os.dup2(fd, 2)
552 552 if fd not in (0, 1, 2): os.close(fd)
553 553
554 554 if runfn:
555 555 return runfn()
556 556
557 557 class changeset_printer(object):
558 558 '''show changeset information when templating not requested.'''
559 559
560 560 def __init__(self, ui, repo, patch, buffered):
561 561 self.ui = ui
562 562 self.repo = repo
563 563 self.buffered = buffered
564 564 self.patch = patch
565 565 self.header = {}
566 566 self.hunk = {}
567 567 self.lastheader = None
568 568
569 569 def flush(self, rev):
570 570 if rev in self.header:
571 571 h = self.header[rev]
572 572 if h != self.lastheader:
573 573 self.lastheader = h
574 574 self.ui.write(h)
575 575 del self.header[rev]
576 576 if rev in self.hunk:
577 577 self.ui.write(self.hunk[rev])
578 578 del self.hunk[rev]
579 579 return 1
580 580 return 0
581 581
582 582 def show(self, rev=0, changenode=None, copies=(), **props):
583 583 if self.buffered:
584 584 self.ui.pushbuffer()
585 585 self._show(rev, changenode, copies, props)
586 586 self.hunk[rev] = self.ui.popbuffer()
587 587 else:
588 588 self._show(rev, changenode, copies, props)
589 589
590 590 def _show(self, rev, changenode, copies, props):
591 591 '''show a single changeset or file revision'''
592 592 log = self.repo.changelog
593 593 if changenode is None:
594 594 changenode = log.node(rev)
595 595 elif not rev:
596 596 rev = log.rev(changenode)
597 597
598 598 if self.ui.quiet:
599 599 self.ui.write("%d:%s\n" % (rev, short(changenode)))
600 600 return
601 601
602 602 changes = log.read(changenode)
603 603 date = util.datestr(changes[2])
604 604 extra = changes[5]
605 605 branch = extra.get("branch")
606 606
607 607 hexfunc = self.ui.debugflag and hex or short
608 608
609 609 parents = [(p, hexfunc(log.node(p)))
610 610 for p in self._meaningful_parentrevs(log, rev)]
611 611
612 612 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)))
613 613
614 614 # don't show the default branch name
615 615 if branch != 'default':
616 616 branch = util.tolocal(branch)
617 617 self.ui.write(_("branch: %s\n") % branch)
618 618 for tag in self.repo.nodetags(changenode):
619 619 self.ui.write(_("tag: %s\n") % tag)
620 620 for parent in parents:
621 621 self.ui.write(_("parent: %d:%s\n") % parent)
622 622
623 623 if self.ui.debugflag:
624 624 self.ui.write(_("manifest: %d:%s\n") %
625 625 (self.repo.manifest.rev(changes[0]), hex(changes[0])))
626 626 self.ui.write(_("user: %s\n") % changes[1])
627 627 self.ui.write(_("date: %s\n") % date)
628 628
629 629 if self.ui.debugflag:
630 630 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
631 631 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
632 632 files):
633 633 if value:
634 634 self.ui.write("%-12s %s\n" % (key, " ".join(value)))
635 635 elif changes[3] and self.ui.verbose:
636 636 self.ui.write(_("files: %s\n") % " ".join(changes[3]))
637 637 if copies and self.ui.verbose:
638 638 copies = ['%s (%s)' % c for c in copies]
639 639 self.ui.write(_("copies: %s\n") % ' '.join(copies))
640 640
641 641 if extra and self.ui.debugflag:
642 642 extraitems = extra.items()
643 643 extraitems.sort()
644 644 for key, value in extraitems:
645 645 self.ui.write(_("extra: %s=%s\n")
646 646 % (key, value.encode('string_escape')))
647 647
648 648 description = changes[4].strip()
649 649 if description:
650 650 if self.ui.verbose:
651 651 self.ui.write(_("description:\n"))
652 652 self.ui.write(description)
653 653 self.ui.write("\n\n")
654 654 else:
655 655 self.ui.write(_("summary: %s\n") %
656 656 description.splitlines()[0])
657 657 self.ui.write("\n")
658 658
659 659 self.showpatch(changenode)
660 660
661 661 def showpatch(self, node):
662 662 if self.patch:
663 663 prev = self.repo.changelog.parents(node)[0]
664 664 patch.diff(self.repo, prev, node, match=self.patch, fp=self.ui,
665 665 opts=patch.diffopts(self.ui))
666 666 self.ui.write("\n")
667 667
668 668 def _meaningful_parentrevs(self, log, rev):
669 669 """Return list of meaningful (or all if debug) parentrevs for rev.
670 670
671 671 For merges (two non-nullrev revisions) both parents are meaningful.
672 672 Otherwise the first parent revision is considered meaningful if it
673 673 is not the preceding revision.
674 674 """
675 675 parents = log.parentrevs(rev)
676 676 if not self.ui.debugflag and parents[1] == nullrev:
677 677 if parents[0] >= rev - 1:
678 678 parents = []
679 679 else:
680 680 parents = [parents[0]]
681 681 return parents
682 682
683 683
684 684 class changeset_templater(changeset_printer):
685 685 '''format changeset information.'''
686 686
687 687 def __init__(self, ui, repo, patch, mapfile, buffered):
688 688 changeset_printer.__init__(self, ui, repo, patch, buffered)
689 689 filters = templatefilters.filters.copy()
690 690 filters['formatnode'] = (ui.debugflag and (lambda x: x)
691 691 or (lambda x: x[:12]))
692 692 self.t = templater.templater(mapfile, filters,
693 693 cache={
694 694 'parent': '{rev}:{node|formatnode} ',
695 695 'manifest': '{rev}:{node|formatnode}',
696 696 'filecopy': '{name} ({source})'})
697 697
698 698 def use_template(self, t):
699 699 '''set template string to use'''
700 700 self.t.cache['changeset'] = t
701 701
702 702 def _show(self, rev, changenode, copies, props):
703 703 '''show a single changeset or file revision'''
704 704 log = self.repo.changelog
705 705 if changenode is None:
706 706 changenode = log.node(rev)
707 707 elif not rev:
708 708 rev = log.rev(changenode)
709 709
710 710 changes = log.read(changenode)
711 711
712 712 def showlist(name, values, plural=None, **args):
713 713 '''expand set of values.
714 714 name is name of key in template map.
715 715 values is list of strings or dicts.
716 716 plural is plural of name, if not simply name + 's'.
717 717
718 718 expansion works like this, given name 'foo'.
719 719
720 720 if values is empty, expand 'no_foos'.
721 721
722 722 if 'foo' not in template map, return values as a string,
723 723 joined by space.
724 724
725 725 expand 'start_foos'.
726 726
727 727 for each value, expand 'foo'. if 'last_foo' in template
728 728 map, expand it instead of 'foo' for last key.
729 729
730 730 expand 'end_foos'.
731 731 '''
732 732 if plural: names = plural
733 733 else: names = name + 's'
734 734 if not values:
735 735 noname = 'no_' + names
736 736 if noname in self.t:
737 737 yield self.t(noname, **args)
738 738 return
739 739 if name not in self.t:
740 740 if isinstance(values[0], str):
741 741 yield ' '.join(values)
742 742 else:
743 743 for v in values:
744 744 yield dict(v, **args)
745 745 return
746 746 startname = 'start_' + names
747 747 if startname in self.t:
748 748 yield self.t(startname, **args)
749 749 vargs = args.copy()
750 750 def one(v, tag=name):
751 751 try:
752 752 vargs.update(v)
753 753 except (AttributeError, ValueError):
754 754 try:
755 755 for a, b in v:
756 756 vargs[a] = b
757 757 except ValueError:
758 758 vargs[name] = v
759 759 return self.t(tag, **vargs)
760 760 lastname = 'last_' + name
761 761 if lastname in self.t:
762 762 last = values.pop()
763 763 else:
764 764 last = None
765 765 for v in values:
766 766 yield one(v)
767 767 if last is not None:
768 768 yield one(last, tag=lastname)
769 769 endname = 'end_' + names
770 770 if endname in self.t:
771 771 yield self.t(endname, **args)
772 772
773 773 def showbranches(**args):
774 774 branch = changes[5].get("branch")
775 775 if branch != 'default':
776 776 branch = util.tolocal(branch)
777 777 return showlist('branch', [branch], plural='branches', **args)
778 778
779 779 def showparents(**args):
780 780 parents = [[('rev', p), ('node', hex(log.node(p)))]
781 781 for p in self._meaningful_parentrevs(log, rev)]
782 782 return showlist('parent', parents, **args)
783 783
784 784 def showtags(**args):
785 785 return showlist('tag', self.repo.nodetags(changenode), **args)
786 786
787 787 def showextras(**args):
788 788 extras = changes[5].items()
789 789 extras.sort()
790 790 for key, value in extras:
791 791 args = args.copy()
792 792 args.update(dict(key=key, value=value))
793 793 yield self.t('extra', **args)
794 794
795 795 def showcopies(**args):
796 796 c = [{'name': x[0], 'source': x[1]} for x in copies]
797 797 return showlist('file_copy', c, plural='file_copies', **args)
798 798
799 799 files = []
800 800 def getfiles():
801 801 if not files:
802 802 files[:] = self.repo.status(
803 803 log.parents(changenode)[0], changenode)[:3]
804 804 return files
805 805 def showfiles(**args):
806 806 return showlist('file', changes[3], **args)
807 807 def showmods(**args):
808 808 return showlist('file_mod', getfiles()[0], **args)
809 809 def showadds(**args):
810 810 return showlist('file_add', getfiles()[1], **args)
811 811 def showdels(**args):
812 812 return showlist('file_del', getfiles()[2], **args)
813 813 def showmanifest(**args):
814 814 args = args.copy()
815 815 args.update(dict(rev=self.repo.manifest.rev(changes[0]),
816 816 node=hex(changes[0])))
817 817 return self.t('manifest', **args)
818 818
819 819 defprops = {
820 820 'author': changes[1],
821 821 'branches': showbranches,
822 822 'date': changes[2],
823 823 'desc': changes[4].strip(),
824 824 'file_adds': showadds,
825 825 'file_dels': showdels,
826 826 'file_mods': showmods,
827 827 'files': showfiles,
828 828 'file_copies': showcopies,
829 829 'manifest': showmanifest,
830 830 'node': hex(changenode),
831 831 'parents': showparents,
832 832 'rev': rev,
833 833 'tags': showtags,
834 834 'extras': showextras,
835 835 }
836 836 props = props.copy()
837 837 props.update(defprops)
838 838
839 839 try:
840 840 if self.ui.debugflag and 'header_debug' in self.t:
841 841 key = 'header_debug'
842 842 elif self.ui.quiet and 'header_quiet' in self.t:
843 843 key = 'header_quiet'
844 844 elif self.ui.verbose and 'header_verbose' in self.t:
845 845 key = 'header_verbose'
846 846 elif 'header' in self.t:
847 847 key = 'header'
848 848 else:
849 849 key = ''
850 850 if key:
851 851 h = templater.stringify(self.t(key, **props))
852 852 if self.buffered:
853 853 self.header[rev] = h
854 854 else:
855 855 self.ui.write(h)
856 856 if self.ui.debugflag and 'changeset_debug' in self.t:
857 857 key = 'changeset_debug'
858 858 elif self.ui.quiet and 'changeset_quiet' in self.t:
859 859 key = 'changeset_quiet'
860 860 elif self.ui.verbose and 'changeset_verbose' in self.t:
861 861 key = 'changeset_verbose'
862 862 else:
863 863 key = 'changeset'
864 864 self.ui.write(templater.stringify(self.t(key, **props)))
865 865 self.showpatch(changenode)
866 866 except KeyError, inst:
867 867 raise util.Abort(_("%s: no key named '%s'") % (self.t.mapfile,
868 868 inst.args[0]))
869 869 except SyntaxError, inst:
870 870 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
871 871
872 872 def show_changeset(ui, repo, opts, buffered=False, matchfn=False):
873 873 """show one changeset using template or regular display.
874 874
875 875 Display format will be the first non-empty hit of:
876 876 1. option 'template'
877 877 2. option 'style'
878 878 3. [ui] setting 'logtemplate'
879 879 4. [ui] setting 'style'
880 880 If all of these values are either the unset or the empty string,
881 881 regular display via changeset_printer() is done.
882 882 """
883 883 # options
884 884 patch = False
885 885 if opts.get('patch'):
886 886 patch = matchfn or util.always
887 887
888 888 tmpl = opts.get('template')
889 889 mapfile = None
890 890 if tmpl:
891 891 tmpl = templater.parsestring(tmpl, quoted=False)
892 892 else:
893 893 mapfile = opts.get('style')
894 894 # ui settings
895 895 if not mapfile:
896 896 tmpl = ui.config('ui', 'logtemplate')
897 897 if tmpl:
898 898 tmpl = templater.parsestring(tmpl)
899 899 else:
900 900 mapfile = ui.config('ui', 'style')
901 901
902 902 if tmpl or mapfile:
903 903 if mapfile:
904 904 if not os.path.split(mapfile)[0]:
905 905 mapname = (templater.templatepath('map-cmdline.' + mapfile)
906 906 or templater.templatepath(mapfile))
907 907 if mapname: mapfile = mapname
908 908 try:
909 909 t = changeset_templater(ui, repo, patch, mapfile, buffered)
910 910 except SyntaxError, inst:
911 911 raise util.Abort(inst.args[0])
912 912 if tmpl: t.use_template(tmpl)
913 913 return t
914 914 return changeset_printer(ui, repo, patch, buffered)
915 915
916 916 def finddate(ui, repo, date):
917 917 """Find the tipmost changeset that matches the given date spec"""
918 918 df = util.matchdate(date)
919 919 get = util.cachefunc(lambda r: repo.changectx(r).changeset())
920 920 changeiter, matchfn = walkchangerevs(ui, repo, [], get, {'rev':None})
921 921 results = {}
922 922 for st, rev, fns in changeiter:
923 923 if st == 'add':
924 924 d = get(rev)[2]
925 925 if df(d[0]):
926 926 results[rev] = d
927 927 elif st == 'iter':
928 928 if rev in results:
929 929 ui.status("Found revision %s from %s\n" %
930 930 (rev, util.datestr(results[rev])))
931 931 return str(rev)
932 932
933 933 raise util.Abort(_("revision matching date not found"))
934 934
935 935 def walkchangerevs(ui, repo, pats, change, opts):
936 936 '''Iterate over files and the revs they changed in.
937 937
938 938 Callers most commonly need to iterate backwards over the history
939 939 it is interested in. Doing so has awful (quadratic-looking)
940 940 performance, so we use iterators in a "windowed" way.
941 941
942 942 We walk a window of revisions in the desired order. Within the
943 943 window, we first walk forwards to gather data, then in the desired
944 944 order (usually backwards) to display it.
945 945
946 946 This function returns an (iterator, matchfn) tuple. The iterator
947 947 yields 3-tuples. They will be of one of the following forms:
948 948
949 949 "window", incrementing, lastrev: stepping through a window,
950 950 positive if walking forwards through revs, last rev in the
951 951 sequence iterated over - use to reset state for the current window
952 952
953 953 "add", rev, fns: out-of-order traversal of the given file names
954 954 fns, which changed during revision rev - use to gather data for
955 955 possible display
956 956
957 957 "iter", rev, None: in-order traversal of the revs earlier iterated
958 958 over with "add" - use to display data'''
959 959
960 960 def increasing_windows(start, end, windowsize=8, sizelimit=512):
961 961 if start < end:
962 962 while start < end:
963 963 yield start, min(windowsize, end-start)
964 964 start += windowsize
965 965 if windowsize < sizelimit:
966 966 windowsize *= 2
967 967 else:
968 968 while start > end:
969 969 yield start, min(windowsize, start-end-1)
970 970 start -= windowsize
971 971 if windowsize < sizelimit:
972 972 windowsize *= 2
973 973
974 974 files, matchfn, anypats = matchpats(repo, pats, opts)
975 975 follow = opts.get('follow') or opts.get('follow_first')
976 976
977 977 if repo.changelog.count() == 0:
978 978 return [], matchfn
979 979
980 980 if follow:
981 981 defrange = '%s:0' % repo.changectx().rev()
982 982 else:
983 983 defrange = '-1:0'
984 984 revs = revrange(repo, opts['rev'] or [defrange])
985 985 wanted = {}
986 986 slowpath = anypats or opts.get('removed')
987 987 fncache = {}
988 988
989 989 if not slowpath and not files:
990 990 # No files, no patterns. Display all revs.
991 991 wanted = dict.fromkeys(revs)
992 992 copies = []
993 993 if not slowpath:
994 994 # Only files, no patterns. Check the history of each file.
995 995 def filerevgen(filelog, node):
996 996 cl_count = repo.changelog.count()
997 997 if node is None:
998 998 last = filelog.count() - 1
999 999 else:
1000 1000 last = filelog.rev(node)
1001 1001 for i, window in increasing_windows(last, nullrev):
1002 1002 revs = []
1003 1003 for j in xrange(i - window, i + 1):
1004 1004 n = filelog.node(j)
1005 1005 revs.append((filelog.linkrev(n),
1006 1006 follow and filelog.renamed(n)))
1007 1007 revs.reverse()
1008 1008 for rev in revs:
1009 1009 # only yield rev for which we have the changelog, it can
1010 1010 # happen while doing "hg log" during a pull or commit
1011 1011 if rev[0] < cl_count:
1012 1012 yield rev
1013 1013 def iterfiles():
1014 1014 for filename in files:
1015 1015 yield filename, None
1016 1016 for filename_node in copies:
1017 1017 yield filename_node
1018 1018 minrev, maxrev = min(revs), max(revs)
1019 1019 for file_, node in iterfiles():
1020 1020 filelog = repo.file(file_)
1021 if filelog.count() == 0:
1022 if node is None:
1021 1023 # A zero count may be a directory or deleted file, so
1022 1024 # try to find matching entries on the slow path.
1023 if filelog.count() == 0:
1024 1025 slowpath = True
1025 1026 break
1027 else:
1028 ui.warn(_('%s:%s copy source revision cannot be found!\n')
1029 % (file_, short(node)))
1030 continue
1026 1031 for rev, copied in filerevgen(filelog, node):
1027 1032 if rev <= maxrev:
1028 1033 if rev < minrev:
1029 1034 break
1030 1035 fncache.setdefault(rev, [])
1031 1036 fncache[rev].append(file_)
1032 1037 wanted[rev] = 1
1033 1038 if follow and copied:
1034 1039 copies.append(copied)
1035 1040 if slowpath:
1036 1041 if follow:
1037 1042 raise util.Abort(_('can only follow copies/renames for explicit '
1038 1043 'file names'))
1039 1044
1040 1045 # The slow path checks files modified in every changeset.
1041 1046 def changerevgen():
1042 1047 for i, window in increasing_windows(repo.changelog.count()-1,
1043 1048 nullrev):
1044 1049 for j in xrange(i - window, i + 1):
1045 1050 yield j, change(j)[3]
1046 1051
1047 1052 for rev, changefiles in changerevgen():
1048 1053 matches = filter(matchfn, changefiles)
1049 1054 if matches:
1050 1055 fncache[rev] = matches
1051 1056 wanted[rev] = 1
1052 1057
1053 1058 class followfilter:
1054 1059 def __init__(self, onlyfirst=False):
1055 1060 self.startrev = nullrev
1056 1061 self.roots = []
1057 1062 self.onlyfirst = onlyfirst
1058 1063
1059 1064 def match(self, rev):
1060 1065 def realparents(rev):
1061 1066 if self.onlyfirst:
1062 1067 return repo.changelog.parentrevs(rev)[0:1]
1063 1068 else:
1064 1069 return filter(lambda x: x != nullrev,
1065 1070 repo.changelog.parentrevs(rev))
1066 1071
1067 1072 if self.startrev == nullrev:
1068 1073 self.startrev = rev
1069 1074 return True
1070 1075
1071 1076 if rev > self.startrev:
1072 1077 # forward: all descendants
1073 1078 if not self.roots:
1074 1079 self.roots.append(self.startrev)
1075 1080 for parent in realparents(rev):
1076 1081 if parent in self.roots:
1077 1082 self.roots.append(rev)
1078 1083 return True
1079 1084 else:
1080 1085 # backwards: all parents
1081 1086 if not self.roots:
1082 1087 self.roots.extend(realparents(self.startrev))
1083 1088 if rev in self.roots:
1084 1089 self.roots.remove(rev)
1085 1090 self.roots.extend(realparents(rev))
1086 1091 return True
1087 1092
1088 1093 return False
1089 1094
1090 1095 # it might be worthwhile to do this in the iterator if the rev range
1091 1096 # is descending and the prune args are all within that range
1092 1097 for rev in opts.get('prune', ()):
1093 1098 rev = repo.changelog.rev(repo.lookup(rev))
1094 1099 ff = followfilter()
1095 1100 stop = min(revs[0], revs[-1])
1096 1101 for x in xrange(rev, stop-1, -1):
1097 1102 if ff.match(x) and x in wanted:
1098 1103 del wanted[x]
1099 1104
1100 1105 def iterate():
1101 1106 if follow and not files:
1102 1107 ff = followfilter(onlyfirst=opts.get('follow_first'))
1103 1108 def want(rev):
1104 1109 if ff.match(rev) and rev in wanted:
1105 1110 return True
1106 1111 return False
1107 1112 else:
1108 1113 def want(rev):
1109 1114 return rev in wanted
1110 1115
1111 1116 for i, window in increasing_windows(0, len(revs)):
1112 1117 yield 'window', revs[0] < revs[-1], revs[-1]
1113 1118 nrevs = [rev for rev in revs[i:i+window] if want(rev)]
1114 1119 srevs = list(nrevs)
1115 1120 srevs.sort()
1116 1121 for rev in srevs:
1117 1122 fns = fncache.get(rev)
1118 1123 if not fns:
1119 1124 def fns_generator():
1120 1125 for f in change(rev)[3]:
1121 1126 if matchfn(f):
1122 1127 yield f
1123 1128 fns = fns_generator()
1124 1129 yield 'add', rev, fns
1125 1130 for rev in nrevs:
1126 1131 yield 'iter', rev, None
1127 1132 return iterate(), matchfn
1128 1133
1129 1134 def commit(ui, repo, commitfunc, pats, opts):
1130 1135 '''commit the specified files or all outstanding changes'''
1131 1136 date = opts.get('date')
1132 1137 if date:
1133 1138 opts['date'] = util.parsedate(date)
1134 1139 message = logmessage(opts)
1135 1140
1136 1141 # extract addremove carefully -- this function can be called from a command
1137 1142 # that doesn't support addremove
1138 1143 if opts.get('addremove'):
1139 1144 addremove(repo, pats, opts)
1140 1145
1141 1146 fns, match, anypats = matchpats(repo, pats, opts)
1142 1147 if pats:
1143 1148 status = repo.status(files=fns, match=match)
1144 1149 modified, added, removed, deleted, unknown = status[:5]
1145 1150 files = modified + added + removed
1146 1151 slist = None
1147 1152 for f in fns:
1148 1153 if f == '.':
1149 1154 continue
1150 1155 if f not in files:
1151 1156 rf = repo.wjoin(f)
1152 1157 rel = repo.pathto(f)
1153 1158 try:
1154 1159 mode = os.lstat(rf)[stat.ST_MODE]
1155 1160 except OSError:
1156 1161 raise util.Abort(_("file %s not found!") % rel)
1157 1162 if stat.S_ISDIR(mode):
1158 1163 name = f + '/'
1159 1164 if slist is None:
1160 1165 slist = list(files)
1161 1166 slist.sort()
1162 1167 i = bisect.bisect(slist, name)
1163 1168 if i >= len(slist) or not slist[i].startswith(name):
1164 1169 raise util.Abort(_("no match under directory %s!")
1165 1170 % rel)
1166 1171 elif not (stat.S_ISREG(mode) or stat.S_ISLNK(mode)):
1167 1172 raise util.Abort(_("can't commit %s: "
1168 1173 "unsupported file type!") % rel)
1169 1174 elif f not in repo.dirstate:
1170 1175 raise util.Abort(_("file %s not tracked!") % rel)
1171 1176 else:
1172 1177 files = []
1173 1178 try:
1174 1179 return commitfunc(ui, repo, files, message, match, opts)
1175 1180 except ValueError, inst:
1176 1181 raise util.Abort(str(inst))
General Comments 0
You need to be logged in to leave comments. Login now