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