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