##// END OF EJS Templates
color: enabled color support for export command (issue1507)...
Ankur Dahiya -
r17460:a306837f default
parent child Browse files
Show More
@@ -1,1931 +1,1938 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 of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from node import hex, nullid, nullrev, short
9 9 from i18n import _
10 10 import os, sys, errno, re, tempfile
11 11 import util, scmutil, templater, patch, error, templatekw, revlog, copies
12 12 import match as matchmod
13 13 import subrepo, context, repair, bookmarks, graphmod, revset
14 14
15 15 def parsealiases(cmd):
16 16 return cmd.lstrip("^").split("|")
17 17
18 18 def findpossible(cmd, table, strict=False):
19 19 """
20 20 Return cmd -> (aliases, command table entry)
21 21 for each matching command.
22 22 Return debug commands (or their aliases) only if no normal command matches.
23 23 """
24 24 choice = {}
25 25 debugchoice = {}
26 26
27 27 if cmd in table:
28 28 # short-circuit exact matches, "log" alias beats "^log|history"
29 29 keys = [cmd]
30 30 else:
31 31 keys = table.keys()
32 32
33 33 for e in keys:
34 34 aliases = parsealiases(e)
35 35 found = None
36 36 if cmd in aliases:
37 37 found = cmd
38 38 elif not strict:
39 39 for a in aliases:
40 40 if a.startswith(cmd):
41 41 found = a
42 42 break
43 43 if found is not None:
44 44 if aliases[0].startswith("debug") or found.startswith("debug"):
45 45 debugchoice[found] = (aliases, table[e])
46 46 else:
47 47 choice[found] = (aliases, table[e])
48 48
49 49 if not choice and debugchoice:
50 50 choice = debugchoice
51 51
52 52 return choice
53 53
54 54 def findcmd(cmd, table, strict=True):
55 55 """Return (aliases, command table entry) for command string."""
56 56 choice = findpossible(cmd, table, strict)
57 57
58 58 if cmd in choice:
59 59 return choice[cmd]
60 60
61 61 if len(choice) > 1:
62 62 clist = choice.keys()
63 63 clist.sort()
64 64 raise error.AmbiguousCommand(cmd, clist)
65 65
66 66 if choice:
67 67 return choice.values()[0]
68 68
69 69 raise error.UnknownCommand(cmd)
70 70
71 71 def findrepo(p):
72 72 while not os.path.isdir(os.path.join(p, ".hg")):
73 73 oldp, p = p, os.path.dirname(p)
74 74 if p == oldp:
75 75 return None
76 76
77 77 return p
78 78
79 79 def bailifchanged(repo):
80 80 if repo.dirstate.p2() != nullid:
81 81 raise util.Abort(_('outstanding uncommitted merge'))
82 82 modified, added, removed, deleted = repo.status()[:4]
83 83 if modified or added or removed or deleted:
84 84 raise util.Abort(_("outstanding uncommitted changes"))
85 85 ctx = repo[None]
86 86 for s in ctx.substate:
87 87 if ctx.sub(s).dirty():
88 88 raise util.Abort(_("uncommitted changes in subrepo %s") % s)
89 89
90 90 def logmessage(ui, opts):
91 91 """ get the log message according to -m and -l option """
92 92 message = opts.get('message')
93 93 logfile = opts.get('logfile')
94 94
95 95 if message and logfile:
96 96 raise util.Abort(_('options --message and --logfile are mutually '
97 97 'exclusive'))
98 98 if not message and logfile:
99 99 try:
100 100 if logfile == '-':
101 101 message = ui.fin.read()
102 102 else:
103 103 message = '\n'.join(util.readfile(logfile).splitlines())
104 104 except IOError, inst:
105 105 raise util.Abort(_("can't read commit message '%s': %s") %
106 106 (logfile, inst.strerror))
107 107 return message
108 108
109 109 def loglimit(opts):
110 110 """get the log limit according to option -l/--limit"""
111 111 limit = opts.get('limit')
112 112 if limit:
113 113 try:
114 114 limit = int(limit)
115 115 except ValueError:
116 116 raise util.Abort(_('limit must be a positive integer'))
117 117 if limit <= 0:
118 118 raise util.Abort(_('limit must be positive'))
119 119 else:
120 120 limit = None
121 121 return limit
122 122
123 123 def makefilename(repo, pat, node, desc=None,
124 124 total=None, seqno=None, revwidth=None, pathname=None):
125 125 node_expander = {
126 126 'H': lambda: hex(node),
127 127 'R': lambda: str(repo.changelog.rev(node)),
128 128 'h': lambda: short(node),
129 129 'm': lambda: re.sub('[^\w]', '_', str(desc))
130 130 }
131 131 expander = {
132 132 '%': lambda: '%',
133 133 'b': lambda: os.path.basename(repo.root),
134 134 }
135 135
136 136 try:
137 137 if node:
138 138 expander.update(node_expander)
139 139 if node:
140 140 expander['r'] = (lambda:
141 141 str(repo.changelog.rev(node)).zfill(revwidth or 0))
142 142 if total is not None:
143 143 expander['N'] = lambda: str(total)
144 144 if seqno is not None:
145 145 expander['n'] = lambda: str(seqno)
146 146 if total is not None and seqno is not None:
147 147 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
148 148 if pathname is not None:
149 149 expander['s'] = lambda: os.path.basename(pathname)
150 150 expander['d'] = lambda: os.path.dirname(pathname) or '.'
151 151 expander['p'] = lambda: pathname
152 152
153 153 newname = []
154 154 patlen = len(pat)
155 155 i = 0
156 156 while i < patlen:
157 157 c = pat[i]
158 158 if c == '%':
159 159 i += 1
160 160 c = pat[i]
161 161 c = expander[c]()
162 162 newname.append(c)
163 163 i += 1
164 164 return ''.join(newname)
165 165 except KeyError, inst:
166 166 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
167 167 inst.args[0])
168 168
169 169 def makefileobj(repo, pat, node=None, desc=None, total=None,
170 170 seqno=None, revwidth=None, mode='wb', pathname=None):
171 171
172 172 writable = mode not in ('r', 'rb')
173 173
174 174 if not pat or pat == '-':
175 175 fp = writable and repo.ui.fout or repo.ui.fin
176 176 if util.safehasattr(fp, 'fileno'):
177 177 return os.fdopen(os.dup(fp.fileno()), mode)
178 178 else:
179 179 # if this fp can't be duped properly, return
180 180 # a dummy object that can be closed
181 181 class wrappedfileobj(object):
182 182 noop = lambda x: None
183 183 def __init__(self, f):
184 184 self.f = f
185 185 def __getattr__(self, attr):
186 186 if attr == 'close':
187 187 return self.noop
188 188 else:
189 189 return getattr(self.f, attr)
190 190
191 191 return wrappedfileobj(fp)
192 192 if util.safehasattr(pat, 'write') and writable:
193 193 return pat
194 194 if util.safehasattr(pat, 'read') and 'r' in mode:
195 195 return pat
196 196 return open(makefilename(repo, pat, node, desc, total, seqno, revwidth,
197 197 pathname),
198 198 mode)
199 199
200 200 def openrevlog(repo, cmd, file_, opts):
201 201 """opens the changelog, manifest, a filelog or a given revlog"""
202 202 cl = opts['changelog']
203 203 mf = opts['manifest']
204 204 msg = None
205 205 if cl and mf:
206 206 msg = _('cannot specify --changelog and --manifest at the same time')
207 207 elif cl or mf:
208 208 if file_:
209 209 msg = _('cannot specify filename with --changelog or --manifest')
210 210 elif not repo:
211 211 msg = _('cannot specify --changelog or --manifest '
212 212 'without a repository')
213 213 if msg:
214 214 raise util.Abort(msg)
215 215
216 216 r = None
217 217 if repo:
218 218 if cl:
219 219 r = repo.changelog
220 220 elif mf:
221 221 r = repo.manifest
222 222 elif file_:
223 223 filelog = repo.file(file_)
224 224 if len(filelog):
225 225 r = filelog
226 226 if not r:
227 227 if not file_:
228 228 raise error.CommandError(cmd, _('invalid arguments'))
229 229 if not os.path.isfile(file_):
230 230 raise util.Abort(_("revlog '%s' not found") % file_)
231 231 r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
232 232 file_[:-2] + ".i")
233 233 return r
234 234
235 235 def copy(ui, repo, pats, opts, rename=False):
236 236 # called with the repo lock held
237 237 #
238 238 # hgsep => pathname that uses "/" to separate directories
239 239 # ossep => pathname that uses os.sep to separate directories
240 240 cwd = repo.getcwd()
241 241 targets = {}
242 242 after = opts.get("after")
243 243 dryrun = opts.get("dry_run")
244 244 wctx = repo[None]
245 245
246 246 def walkpat(pat):
247 247 srcs = []
248 248 badstates = after and '?' or '?r'
249 249 m = scmutil.match(repo[None], [pat], opts, globbed=True)
250 250 for abs in repo.walk(m):
251 251 state = repo.dirstate[abs]
252 252 rel = m.rel(abs)
253 253 exact = m.exact(abs)
254 254 if state in badstates:
255 255 if exact and state == '?':
256 256 ui.warn(_('%s: not copying - file is not managed\n') % rel)
257 257 if exact and state == 'r':
258 258 ui.warn(_('%s: not copying - file has been marked for'
259 259 ' remove\n') % rel)
260 260 continue
261 261 # abs: hgsep
262 262 # rel: ossep
263 263 srcs.append((abs, rel, exact))
264 264 return srcs
265 265
266 266 # abssrc: hgsep
267 267 # relsrc: ossep
268 268 # otarget: ossep
269 269 def copyfile(abssrc, relsrc, otarget, exact):
270 270 abstarget = scmutil.canonpath(repo.root, cwd, otarget)
271 271 if '/' in abstarget:
272 272 # We cannot normalize abstarget itself, this would prevent
273 273 # case only renames, like a => A.
274 274 abspath, absname = abstarget.rsplit('/', 1)
275 275 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
276 276 reltarget = repo.pathto(abstarget, cwd)
277 277 target = repo.wjoin(abstarget)
278 278 src = repo.wjoin(abssrc)
279 279 state = repo.dirstate[abstarget]
280 280
281 281 scmutil.checkportable(ui, abstarget)
282 282
283 283 # check for collisions
284 284 prevsrc = targets.get(abstarget)
285 285 if prevsrc is not None:
286 286 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
287 287 (reltarget, repo.pathto(abssrc, cwd),
288 288 repo.pathto(prevsrc, cwd)))
289 289 return
290 290
291 291 # check for overwrites
292 292 exists = os.path.lexists(target)
293 293 samefile = False
294 294 if exists and abssrc != abstarget:
295 295 if (repo.dirstate.normalize(abssrc) ==
296 296 repo.dirstate.normalize(abstarget)):
297 297 if not rename:
298 298 ui.warn(_("%s: can't copy - same file\n") % reltarget)
299 299 return
300 300 exists = False
301 301 samefile = True
302 302
303 303 if not after and exists or after and state in 'mn':
304 304 if not opts['force']:
305 305 ui.warn(_('%s: not overwriting - file exists\n') %
306 306 reltarget)
307 307 return
308 308
309 309 if after:
310 310 if not exists:
311 311 if rename:
312 312 ui.warn(_('%s: not recording move - %s does not exist\n') %
313 313 (relsrc, reltarget))
314 314 else:
315 315 ui.warn(_('%s: not recording copy - %s does not exist\n') %
316 316 (relsrc, reltarget))
317 317 return
318 318 elif not dryrun:
319 319 try:
320 320 if exists:
321 321 os.unlink(target)
322 322 targetdir = os.path.dirname(target) or '.'
323 323 if not os.path.isdir(targetdir):
324 324 os.makedirs(targetdir)
325 325 if samefile:
326 326 tmp = target + "~hgrename"
327 327 os.rename(src, tmp)
328 328 os.rename(tmp, target)
329 329 else:
330 330 util.copyfile(src, target)
331 331 srcexists = True
332 332 except IOError, inst:
333 333 if inst.errno == errno.ENOENT:
334 334 ui.warn(_('%s: deleted in working copy\n') % relsrc)
335 335 srcexists = False
336 336 else:
337 337 ui.warn(_('%s: cannot copy - %s\n') %
338 338 (relsrc, inst.strerror))
339 339 return True # report a failure
340 340
341 341 if ui.verbose or not exact:
342 342 if rename:
343 343 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
344 344 else:
345 345 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
346 346
347 347 targets[abstarget] = abssrc
348 348
349 349 # fix up dirstate
350 350 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
351 351 dryrun=dryrun, cwd=cwd)
352 352 if rename and not dryrun:
353 353 if not after and srcexists and not samefile:
354 354 util.unlinkpath(repo.wjoin(abssrc))
355 355 wctx.forget([abssrc])
356 356
357 357 # pat: ossep
358 358 # dest ossep
359 359 # srcs: list of (hgsep, hgsep, ossep, bool)
360 360 # return: function that takes hgsep and returns ossep
361 361 def targetpathfn(pat, dest, srcs):
362 362 if os.path.isdir(pat):
363 363 abspfx = scmutil.canonpath(repo.root, cwd, pat)
364 364 abspfx = util.localpath(abspfx)
365 365 if destdirexists:
366 366 striplen = len(os.path.split(abspfx)[0])
367 367 else:
368 368 striplen = len(abspfx)
369 369 if striplen:
370 370 striplen += len(os.sep)
371 371 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
372 372 elif destdirexists:
373 373 res = lambda p: os.path.join(dest,
374 374 os.path.basename(util.localpath(p)))
375 375 else:
376 376 res = lambda p: dest
377 377 return res
378 378
379 379 # pat: ossep
380 380 # dest ossep
381 381 # srcs: list of (hgsep, hgsep, ossep, bool)
382 382 # return: function that takes hgsep and returns ossep
383 383 def targetpathafterfn(pat, dest, srcs):
384 384 if matchmod.patkind(pat):
385 385 # a mercurial pattern
386 386 res = lambda p: os.path.join(dest,
387 387 os.path.basename(util.localpath(p)))
388 388 else:
389 389 abspfx = scmutil.canonpath(repo.root, cwd, pat)
390 390 if len(abspfx) < len(srcs[0][0]):
391 391 # A directory. Either the target path contains the last
392 392 # component of the source path or it does not.
393 393 def evalpath(striplen):
394 394 score = 0
395 395 for s in srcs:
396 396 t = os.path.join(dest, util.localpath(s[0])[striplen:])
397 397 if os.path.lexists(t):
398 398 score += 1
399 399 return score
400 400
401 401 abspfx = util.localpath(abspfx)
402 402 striplen = len(abspfx)
403 403 if striplen:
404 404 striplen += len(os.sep)
405 405 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
406 406 score = evalpath(striplen)
407 407 striplen1 = len(os.path.split(abspfx)[0])
408 408 if striplen1:
409 409 striplen1 += len(os.sep)
410 410 if evalpath(striplen1) > score:
411 411 striplen = striplen1
412 412 res = lambda p: os.path.join(dest,
413 413 util.localpath(p)[striplen:])
414 414 else:
415 415 # a file
416 416 if destdirexists:
417 417 res = lambda p: os.path.join(dest,
418 418 os.path.basename(util.localpath(p)))
419 419 else:
420 420 res = lambda p: dest
421 421 return res
422 422
423 423
424 424 pats = scmutil.expandpats(pats)
425 425 if not pats:
426 426 raise util.Abort(_('no source or destination specified'))
427 427 if len(pats) == 1:
428 428 raise util.Abort(_('no destination specified'))
429 429 dest = pats.pop()
430 430 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
431 431 if not destdirexists:
432 432 if len(pats) > 1 or matchmod.patkind(pats[0]):
433 433 raise util.Abort(_('with multiple sources, destination must be an '
434 434 'existing directory'))
435 435 if util.endswithsep(dest):
436 436 raise util.Abort(_('destination %s is not a directory') % dest)
437 437
438 438 tfn = targetpathfn
439 439 if after:
440 440 tfn = targetpathafterfn
441 441 copylist = []
442 442 for pat in pats:
443 443 srcs = walkpat(pat)
444 444 if not srcs:
445 445 continue
446 446 copylist.append((tfn(pat, dest, srcs), srcs))
447 447 if not copylist:
448 448 raise util.Abort(_('no files to copy'))
449 449
450 450 errors = 0
451 451 for targetpath, srcs in copylist:
452 452 for abssrc, relsrc, exact in srcs:
453 453 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
454 454 errors += 1
455 455
456 456 if errors:
457 457 ui.warn(_('(consider using --after)\n'))
458 458
459 459 return errors != 0
460 460
461 461 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
462 462 runargs=None, appendpid=False):
463 463 '''Run a command as a service.'''
464 464
465 465 if opts['daemon'] and not opts['daemon_pipefds']:
466 466 # Signal child process startup with file removal
467 467 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
468 468 os.close(lockfd)
469 469 try:
470 470 if not runargs:
471 471 runargs = util.hgcmd() + sys.argv[1:]
472 472 runargs.append('--daemon-pipefds=%s' % lockpath)
473 473 # Don't pass --cwd to the child process, because we've already
474 474 # changed directory.
475 475 for i in xrange(1, len(runargs)):
476 476 if runargs[i].startswith('--cwd='):
477 477 del runargs[i]
478 478 break
479 479 elif runargs[i].startswith('--cwd'):
480 480 del runargs[i:i + 2]
481 481 break
482 482 def condfn():
483 483 return not os.path.exists(lockpath)
484 484 pid = util.rundetached(runargs, condfn)
485 485 if pid < 0:
486 486 raise util.Abort(_('child process failed to start'))
487 487 finally:
488 488 try:
489 489 os.unlink(lockpath)
490 490 except OSError, e:
491 491 if e.errno != errno.ENOENT:
492 492 raise
493 493 if parentfn:
494 494 return parentfn(pid)
495 495 else:
496 496 return
497 497
498 498 if initfn:
499 499 initfn()
500 500
501 501 if opts['pid_file']:
502 502 mode = appendpid and 'a' or 'w'
503 503 fp = open(opts['pid_file'], mode)
504 504 fp.write(str(os.getpid()) + '\n')
505 505 fp.close()
506 506
507 507 if opts['daemon_pipefds']:
508 508 lockpath = opts['daemon_pipefds']
509 509 try:
510 510 os.setsid()
511 511 except AttributeError:
512 512 pass
513 513 os.unlink(lockpath)
514 514 util.hidewindow()
515 515 sys.stdout.flush()
516 516 sys.stderr.flush()
517 517
518 518 nullfd = os.open(os.devnull, os.O_RDWR)
519 519 logfilefd = nullfd
520 520 if logfile:
521 521 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
522 522 os.dup2(nullfd, 0)
523 523 os.dup2(logfilefd, 1)
524 524 os.dup2(logfilefd, 2)
525 525 if nullfd not in (0, 1, 2):
526 526 os.close(nullfd)
527 527 if logfile and logfilefd not in (0, 1, 2):
528 528 os.close(logfilefd)
529 529
530 530 if runfn:
531 531 return runfn()
532 532
533 533 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
534 534 opts=None):
535 535 '''export changesets as hg patches.'''
536 536
537 537 total = len(revs)
538 538 revwidth = max([len(str(rev)) for rev in revs])
539 539
540 540 def single(rev, seqno, fp):
541 541 ctx = repo[rev]
542 542 node = ctx.node()
543 543 parents = [p.node() for p in ctx.parents() if p]
544 544 branch = ctx.branch()
545 545 if switch_parent:
546 546 parents.reverse()
547 547 prev = (parents and parents[0]) or nullid
548 548
549 549 shouldclose = False
550 if not fp:
550 if not fp and len(template) > 0:
551 551 desc_lines = ctx.description().rstrip().split('\n')
552 552 desc = desc_lines[0] #Commit always has a first line.
553 553 fp = makefileobj(repo, template, node, desc=desc, total=total,
554 554 seqno=seqno, revwidth=revwidth, mode='ab')
555 555 if fp != template:
556 556 shouldclose = True
557 if fp != sys.stdout and util.safehasattr(fp, 'name'):
557 if fp and fp != sys.stdout and util.safehasattr(fp, 'name'):
558 558 repo.ui.note("%s\n" % fp.name)
559 559
560 fp.write("# HG changeset patch\n")
561 fp.write("# User %s\n" % ctx.user())
562 fp.write("# Date %d %d\n" % ctx.date())
560 if not fp:
561 write = repo.ui.write
562 else:
563 def write(s, **kw):
564 fp.write(s)
565
566
567 write("# HG changeset patch\n")
568 write("# User %s\n" % ctx.user())
569 write("# Date %d %d\n" % ctx.date())
563 570 if branch and branch != 'default':
564 fp.write("# Branch %s\n" % branch)
565 fp.write("# Node ID %s\n" % hex(node))
566 fp.write("# Parent %s\n" % hex(prev))
571 write("# Branch %s\n" % branch)
572 write("# Node ID %s\n" % hex(node))
573 write("# Parent %s\n" % hex(prev))
567 574 if len(parents) > 1:
568 fp.write("# Parent %s\n" % hex(parents[1]))
569 fp.write(ctx.description().rstrip())
570 fp.write("\n\n")
575 write("# Parent %s\n" % hex(parents[1]))
576 write(ctx.description().rstrip())
577 write("\n\n")
571 578
572 for chunk in patch.diff(repo, prev, node, opts=opts):
573 fp.write(chunk)
579 for chunk, label in patch.diffui(repo, prev, node, opts=opts):
580 write(chunk, label=label)
574 581
575 582 if shouldclose:
576 583 fp.close()
577 584
578 585 for seqno, rev in enumerate(revs):
579 586 single(rev, seqno + 1, fp)
580 587
581 588 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
582 589 changes=None, stat=False, fp=None, prefix='',
583 590 listsubrepos=False):
584 591 '''show diff or diffstat.'''
585 592 if fp is None:
586 593 write = ui.write
587 594 else:
588 595 def write(s, **kw):
589 596 fp.write(s)
590 597
591 598 if stat:
592 599 diffopts = diffopts.copy(context=0)
593 600 width = 80
594 601 if not ui.plain():
595 602 width = ui.termwidth()
596 603 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
597 604 prefix=prefix)
598 605 for chunk, label in patch.diffstatui(util.iterlines(chunks),
599 606 width=width,
600 607 git=diffopts.git):
601 608 write(chunk, label=label)
602 609 else:
603 610 for chunk, label in patch.diffui(repo, node1, node2, match,
604 611 changes, diffopts, prefix=prefix):
605 612 write(chunk, label=label)
606 613
607 614 if listsubrepos:
608 615 ctx1 = repo[node1]
609 616 ctx2 = repo[node2]
610 617 for subpath, sub in subrepo.itersubrepos(ctx1, ctx2):
611 618 tempnode2 = node2
612 619 try:
613 620 if node2 is not None:
614 621 tempnode2 = ctx2.substate[subpath][1]
615 622 except KeyError:
616 623 # A subrepo that existed in node1 was deleted between node1 and
617 624 # node2 (inclusive). Thus, ctx2's substate won't contain that
618 625 # subpath. The best we can do is to ignore it.
619 626 tempnode2 = None
620 627 submatch = matchmod.narrowmatcher(subpath, match)
621 628 sub.diff(diffopts, tempnode2, submatch, changes=changes,
622 629 stat=stat, fp=fp, prefix=prefix)
623 630
624 631 class changeset_printer(object):
625 632 '''show changeset information when templating not requested.'''
626 633
627 634 def __init__(self, ui, repo, patch, diffopts, buffered):
628 635 self.ui = ui
629 636 self.repo = repo
630 637 self.buffered = buffered
631 638 self.patch = patch
632 639 self.diffopts = diffopts
633 640 self.header = {}
634 641 self.hunk = {}
635 642 self.lastheader = None
636 643 self.footer = None
637 644
638 645 def flush(self, rev):
639 646 if rev in self.header:
640 647 h = self.header[rev]
641 648 if h != self.lastheader:
642 649 self.lastheader = h
643 650 self.ui.write(h)
644 651 del self.header[rev]
645 652 if rev in self.hunk:
646 653 self.ui.write(self.hunk[rev])
647 654 del self.hunk[rev]
648 655 return 1
649 656 return 0
650 657
651 658 def close(self):
652 659 if self.footer:
653 660 self.ui.write(self.footer)
654 661
655 662 def show(self, ctx, copies=None, matchfn=None, **props):
656 663 if self.buffered:
657 664 self.ui.pushbuffer()
658 665 self._show(ctx, copies, matchfn, props)
659 666 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
660 667 else:
661 668 self._show(ctx, copies, matchfn, props)
662 669
663 670 def _show(self, ctx, copies, matchfn, props):
664 671 '''show a single changeset or file revision'''
665 672 changenode = ctx.node()
666 673 rev = ctx.rev()
667 674
668 675 if self.ui.quiet:
669 676 self.ui.write("%d:%s\n" % (rev, short(changenode)),
670 677 label='log.node')
671 678 return
672 679
673 680 log = self.repo.changelog
674 681 date = util.datestr(ctx.date())
675 682
676 683 hexfunc = self.ui.debugflag and hex or short
677 684
678 685 parents = [(p, hexfunc(log.node(p)))
679 686 for p in self._meaningful_parentrevs(log, rev)]
680 687
681 688 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
682 689 label='log.changeset')
683 690
684 691 branch = ctx.branch()
685 692 # don't show the default branch name
686 693 if branch != 'default':
687 694 self.ui.write(_("branch: %s\n") % branch,
688 695 label='log.branch')
689 696 for bookmark in self.repo.nodebookmarks(changenode):
690 697 self.ui.write(_("bookmark: %s\n") % bookmark,
691 698 label='log.bookmark')
692 699 for tag in self.repo.nodetags(changenode):
693 700 self.ui.write(_("tag: %s\n") % tag,
694 701 label='log.tag')
695 702 if self.ui.debugflag and ctx.phase():
696 703 self.ui.write(_("phase: %s\n") % _(ctx.phasestr()),
697 704 label='log.phase')
698 705 for parent in parents:
699 706 self.ui.write(_("parent: %d:%s\n") % parent,
700 707 label='log.parent')
701 708
702 709 if self.ui.debugflag:
703 710 mnode = ctx.manifestnode()
704 711 self.ui.write(_("manifest: %d:%s\n") %
705 712 (self.repo.manifest.rev(mnode), hex(mnode)),
706 713 label='ui.debug log.manifest')
707 714 self.ui.write(_("user: %s\n") % ctx.user(),
708 715 label='log.user')
709 716 self.ui.write(_("date: %s\n") % date,
710 717 label='log.date')
711 718
712 719 if self.ui.debugflag:
713 720 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
714 721 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
715 722 files):
716 723 if value:
717 724 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
718 725 label='ui.debug log.files')
719 726 elif ctx.files() and self.ui.verbose:
720 727 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
721 728 label='ui.note log.files')
722 729 if copies and self.ui.verbose:
723 730 copies = ['%s (%s)' % c for c in copies]
724 731 self.ui.write(_("copies: %s\n") % ' '.join(copies),
725 732 label='ui.note log.copies')
726 733
727 734 extra = ctx.extra()
728 735 if extra and self.ui.debugflag:
729 736 for key, value in sorted(extra.items()):
730 737 self.ui.write(_("extra: %s=%s\n")
731 738 % (key, value.encode('string_escape')),
732 739 label='ui.debug log.extra')
733 740
734 741 description = ctx.description().strip()
735 742 if description:
736 743 if self.ui.verbose:
737 744 self.ui.write(_("description:\n"),
738 745 label='ui.note log.description')
739 746 self.ui.write(description,
740 747 label='ui.note log.description')
741 748 self.ui.write("\n\n")
742 749 else:
743 750 self.ui.write(_("summary: %s\n") %
744 751 description.splitlines()[0],
745 752 label='log.summary')
746 753 self.ui.write("\n")
747 754
748 755 self.showpatch(changenode, matchfn)
749 756
750 757 def showpatch(self, node, matchfn):
751 758 if not matchfn:
752 759 matchfn = self.patch
753 760 if matchfn:
754 761 stat = self.diffopts.get('stat')
755 762 diff = self.diffopts.get('patch')
756 763 diffopts = patch.diffopts(self.ui, self.diffopts)
757 764 prev = self.repo.changelog.parents(node)[0]
758 765 if stat:
759 766 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
760 767 match=matchfn, stat=True)
761 768 if diff:
762 769 if stat:
763 770 self.ui.write("\n")
764 771 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
765 772 match=matchfn, stat=False)
766 773 self.ui.write("\n")
767 774
768 775 def _meaningful_parentrevs(self, log, rev):
769 776 """Return list of meaningful (or all if debug) parentrevs for rev.
770 777
771 778 For merges (two non-nullrev revisions) both parents are meaningful.
772 779 Otherwise the first parent revision is considered meaningful if it
773 780 is not the preceding revision.
774 781 """
775 782 parents = log.parentrevs(rev)
776 783 if not self.ui.debugflag and parents[1] == nullrev:
777 784 if parents[0] >= rev - 1:
778 785 parents = []
779 786 else:
780 787 parents = [parents[0]]
781 788 return parents
782 789
783 790
784 791 class changeset_templater(changeset_printer):
785 792 '''format changeset information.'''
786 793
787 794 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
788 795 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
789 796 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
790 797 defaulttempl = {
791 798 'parent': '{rev}:{node|formatnode} ',
792 799 'manifest': '{rev}:{node|formatnode}',
793 800 'file_copy': '{name} ({source})',
794 801 'extra': '{key}={value|stringescape}'
795 802 }
796 803 # filecopy is preserved for compatibility reasons
797 804 defaulttempl['filecopy'] = defaulttempl['file_copy']
798 805 self.t = templater.templater(mapfile, {'formatnode': formatnode},
799 806 cache=defaulttempl)
800 807 self.cache = {}
801 808
802 809 def use_template(self, t):
803 810 '''set template string to use'''
804 811 self.t.cache['changeset'] = t
805 812
806 813 def _meaningful_parentrevs(self, ctx):
807 814 """Return list of meaningful (or all if debug) parentrevs for rev.
808 815 """
809 816 parents = ctx.parents()
810 817 if len(parents) > 1:
811 818 return parents
812 819 if self.ui.debugflag:
813 820 return [parents[0], self.repo['null']]
814 821 if parents[0].rev() >= ctx.rev() - 1:
815 822 return []
816 823 return parents
817 824
818 825 def _show(self, ctx, copies, matchfn, props):
819 826 '''show a single changeset or file revision'''
820 827
821 828 showlist = templatekw.showlist
822 829
823 830 # showparents() behaviour depends on ui trace level which
824 831 # causes unexpected behaviours at templating level and makes
825 832 # it harder to extract it in a standalone function. Its
826 833 # behaviour cannot be changed so leave it here for now.
827 834 def showparents(**args):
828 835 ctx = args['ctx']
829 836 parents = [[('rev', p.rev()), ('node', p.hex())]
830 837 for p in self._meaningful_parentrevs(ctx)]
831 838 return showlist('parent', parents, **args)
832 839
833 840 props = props.copy()
834 841 props.update(templatekw.keywords)
835 842 props['parents'] = showparents
836 843 props['templ'] = self.t
837 844 props['ctx'] = ctx
838 845 props['repo'] = self.repo
839 846 props['revcache'] = {'copies': copies}
840 847 props['cache'] = self.cache
841 848
842 849 # find correct templates for current mode
843 850
844 851 tmplmodes = [
845 852 (True, None),
846 853 (self.ui.verbose, 'verbose'),
847 854 (self.ui.quiet, 'quiet'),
848 855 (self.ui.debugflag, 'debug'),
849 856 ]
850 857
851 858 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
852 859 for mode, postfix in tmplmodes:
853 860 for type in types:
854 861 cur = postfix and ('%s_%s' % (type, postfix)) or type
855 862 if mode and cur in self.t:
856 863 types[type] = cur
857 864
858 865 try:
859 866
860 867 # write header
861 868 if types['header']:
862 869 h = templater.stringify(self.t(types['header'], **props))
863 870 if self.buffered:
864 871 self.header[ctx.rev()] = h
865 872 else:
866 873 if self.lastheader != h:
867 874 self.lastheader = h
868 875 self.ui.write(h)
869 876
870 877 # write changeset metadata, then patch if requested
871 878 key = types['changeset']
872 879 self.ui.write(templater.stringify(self.t(key, **props)))
873 880 self.showpatch(ctx.node(), matchfn)
874 881
875 882 if types['footer']:
876 883 if not self.footer:
877 884 self.footer = templater.stringify(self.t(types['footer'],
878 885 **props))
879 886
880 887 except KeyError, inst:
881 888 msg = _("%s: no key named '%s'")
882 889 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
883 890 except SyntaxError, inst:
884 891 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
885 892
886 893 def show_changeset(ui, repo, opts, buffered=False):
887 894 """show one changeset using template or regular display.
888 895
889 896 Display format will be the first non-empty hit of:
890 897 1. option 'template'
891 898 2. option 'style'
892 899 3. [ui] setting 'logtemplate'
893 900 4. [ui] setting 'style'
894 901 If all of these values are either the unset or the empty string,
895 902 regular display via changeset_printer() is done.
896 903 """
897 904 # options
898 905 patch = False
899 906 if opts.get('patch') or opts.get('stat'):
900 907 patch = scmutil.matchall(repo)
901 908
902 909 tmpl = opts.get('template')
903 910 style = None
904 911 if tmpl:
905 912 tmpl = templater.parsestring(tmpl, quoted=False)
906 913 else:
907 914 style = opts.get('style')
908 915
909 916 # ui settings
910 917 if not (tmpl or style):
911 918 tmpl = ui.config('ui', 'logtemplate')
912 919 if tmpl:
913 920 try:
914 921 tmpl = templater.parsestring(tmpl)
915 922 except SyntaxError:
916 923 tmpl = templater.parsestring(tmpl, quoted=False)
917 924 else:
918 925 style = util.expandpath(ui.config('ui', 'style', ''))
919 926
920 927 if not (tmpl or style):
921 928 return changeset_printer(ui, repo, patch, opts, buffered)
922 929
923 930 mapfile = None
924 931 if style and not tmpl:
925 932 mapfile = style
926 933 if not os.path.split(mapfile)[0]:
927 934 mapname = (templater.templatepath('map-cmdline.' + mapfile)
928 935 or templater.templatepath(mapfile))
929 936 if mapname:
930 937 mapfile = mapname
931 938
932 939 try:
933 940 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
934 941 except SyntaxError, inst:
935 942 raise util.Abort(inst.args[0])
936 943 if tmpl:
937 944 t.use_template(tmpl)
938 945 return t
939 946
940 947 def finddate(ui, repo, date):
941 948 """Find the tipmost changeset that matches the given date spec"""
942 949
943 950 df = util.matchdate(date)
944 951 m = scmutil.matchall(repo)
945 952 results = {}
946 953
947 954 def prep(ctx, fns):
948 955 d = ctx.date()
949 956 if df(d[0]):
950 957 results[ctx.rev()] = d
951 958
952 959 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
953 960 rev = ctx.rev()
954 961 if rev in results:
955 962 ui.status(_("found revision %s from %s\n") %
956 963 (rev, util.datestr(results[rev])))
957 964 return str(rev)
958 965
959 966 raise util.Abort(_("revision matching date not found"))
960 967
961 968 def increasingwindows(start, end, windowsize=8, sizelimit=512):
962 969 if start < end:
963 970 while start < end:
964 971 yield start, min(windowsize, end - start)
965 972 start += windowsize
966 973 if windowsize < sizelimit:
967 974 windowsize *= 2
968 975 else:
969 976 while start > end:
970 977 yield start, min(windowsize, start - end - 1)
971 978 start -= windowsize
972 979 if windowsize < sizelimit:
973 980 windowsize *= 2
974 981
975 982 def walkchangerevs(repo, match, opts, prepare):
976 983 '''Iterate over files and the revs in which they changed.
977 984
978 985 Callers most commonly need to iterate backwards over the history
979 986 in which they are interested. Doing so has awful (quadratic-looking)
980 987 performance, so we use iterators in a "windowed" way.
981 988
982 989 We walk a window of revisions in the desired order. Within the
983 990 window, we first walk forwards to gather data, then in the desired
984 991 order (usually backwards) to display it.
985 992
986 993 This function returns an iterator yielding contexts. Before
987 994 yielding each context, the iterator will first call the prepare
988 995 function on each context in the window in forward order.'''
989 996
990 997 follow = opts.get('follow') or opts.get('follow_first')
991 998
992 999 if not len(repo):
993 1000 return []
994 1001
995 1002 if follow:
996 1003 defrange = '%s:0' % repo['.'].rev()
997 1004 else:
998 1005 defrange = '-1:0'
999 1006 revs = scmutil.revrange(repo, opts.get('rev') or [defrange])
1000 1007 if not revs:
1001 1008 return []
1002 1009 wanted = set()
1003 1010 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1004 1011 fncache = {}
1005 1012 change = repo.changectx
1006 1013
1007 1014 # First step is to fill wanted, the set of revisions that we want to yield.
1008 1015 # When it does not induce extra cost, we also fill fncache for revisions in
1009 1016 # wanted: a cache of filenames that were changed (ctx.files()) and that
1010 1017 # match the file filtering conditions.
1011 1018
1012 1019 if not slowpath and not match.files():
1013 1020 # No files, no patterns. Display all revs.
1014 1021 wanted = set(revs)
1015 1022 copies = []
1016 1023
1017 1024 if not slowpath and match.files():
1018 1025 # We only have to read through the filelog to find wanted revisions
1019 1026
1020 1027 minrev, maxrev = min(revs), max(revs)
1021 1028 def filerevgen(filelog, last):
1022 1029 """
1023 1030 Only files, no patterns. Check the history of each file.
1024 1031
1025 1032 Examines filelog entries within minrev, maxrev linkrev range
1026 1033 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1027 1034 tuples in backwards order
1028 1035 """
1029 1036 cl_count = len(repo)
1030 1037 revs = []
1031 1038 for j in xrange(0, last + 1):
1032 1039 linkrev = filelog.linkrev(j)
1033 1040 if linkrev < minrev:
1034 1041 continue
1035 1042 # only yield rev for which we have the changelog, it can
1036 1043 # happen while doing "hg log" during a pull or commit
1037 1044 if linkrev >= cl_count:
1038 1045 break
1039 1046
1040 1047 parentlinkrevs = []
1041 1048 for p in filelog.parentrevs(j):
1042 1049 if p != nullrev:
1043 1050 parentlinkrevs.append(filelog.linkrev(p))
1044 1051 n = filelog.node(j)
1045 1052 revs.append((linkrev, parentlinkrevs,
1046 1053 follow and filelog.renamed(n)))
1047 1054
1048 1055 return reversed(revs)
1049 1056 def iterfiles():
1050 1057 pctx = repo['.']
1051 1058 for filename in match.files():
1052 1059 if follow:
1053 1060 if filename not in pctx:
1054 1061 raise util.Abort(_('cannot follow file not in parent '
1055 1062 'revision: "%s"') % filename)
1056 1063 yield filename, pctx[filename].filenode()
1057 1064 else:
1058 1065 yield filename, None
1059 1066 for filename_node in copies:
1060 1067 yield filename_node
1061 1068 for file_, node in iterfiles():
1062 1069 filelog = repo.file(file_)
1063 1070 if not len(filelog):
1064 1071 if node is None:
1065 1072 # A zero count may be a directory or deleted file, so
1066 1073 # try to find matching entries on the slow path.
1067 1074 if follow:
1068 1075 raise util.Abort(
1069 1076 _('cannot follow nonexistent file: "%s"') % file_)
1070 1077 slowpath = True
1071 1078 break
1072 1079 else:
1073 1080 continue
1074 1081
1075 1082 if node is None:
1076 1083 last = len(filelog) - 1
1077 1084 else:
1078 1085 last = filelog.rev(node)
1079 1086
1080 1087
1081 1088 # keep track of all ancestors of the file
1082 1089 ancestors = set([filelog.linkrev(last)])
1083 1090
1084 1091 # iterate from latest to oldest revision
1085 1092 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1086 1093 if not follow:
1087 1094 if rev > maxrev:
1088 1095 continue
1089 1096 else:
1090 1097 # Note that last might not be the first interesting
1091 1098 # rev to us:
1092 1099 # if the file has been changed after maxrev, we'll
1093 1100 # have linkrev(last) > maxrev, and we still need
1094 1101 # to explore the file graph
1095 1102 if rev not in ancestors:
1096 1103 continue
1097 1104 # XXX insert 1327 fix here
1098 1105 if flparentlinkrevs:
1099 1106 ancestors.update(flparentlinkrevs)
1100 1107
1101 1108 fncache.setdefault(rev, []).append(file_)
1102 1109 wanted.add(rev)
1103 1110 if copied:
1104 1111 copies.append(copied)
1105 1112 if slowpath:
1106 1113 # We have to read the changelog to match filenames against
1107 1114 # changed files
1108 1115
1109 1116 if follow:
1110 1117 raise util.Abort(_('can only follow copies/renames for explicit '
1111 1118 'filenames'))
1112 1119
1113 1120 # The slow path checks files modified in every changeset.
1114 1121 for i in sorted(revs):
1115 1122 ctx = change(i)
1116 1123 matches = filter(match, ctx.files())
1117 1124 if matches:
1118 1125 fncache[i] = matches
1119 1126 wanted.add(i)
1120 1127
1121 1128 class followfilter(object):
1122 1129 def __init__(self, onlyfirst=False):
1123 1130 self.startrev = nullrev
1124 1131 self.roots = set()
1125 1132 self.onlyfirst = onlyfirst
1126 1133
1127 1134 def match(self, rev):
1128 1135 def realparents(rev):
1129 1136 if self.onlyfirst:
1130 1137 return repo.changelog.parentrevs(rev)[0:1]
1131 1138 else:
1132 1139 return filter(lambda x: x != nullrev,
1133 1140 repo.changelog.parentrevs(rev))
1134 1141
1135 1142 if self.startrev == nullrev:
1136 1143 self.startrev = rev
1137 1144 return True
1138 1145
1139 1146 if rev > self.startrev:
1140 1147 # forward: all descendants
1141 1148 if not self.roots:
1142 1149 self.roots.add(self.startrev)
1143 1150 for parent in realparents(rev):
1144 1151 if parent in self.roots:
1145 1152 self.roots.add(rev)
1146 1153 return True
1147 1154 else:
1148 1155 # backwards: all parents
1149 1156 if not self.roots:
1150 1157 self.roots.update(realparents(self.startrev))
1151 1158 if rev in self.roots:
1152 1159 self.roots.remove(rev)
1153 1160 self.roots.update(realparents(rev))
1154 1161 return True
1155 1162
1156 1163 return False
1157 1164
1158 1165 # it might be worthwhile to do this in the iterator if the rev range
1159 1166 # is descending and the prune args are all within that range
1160 1167 for rev in opts.get('prune', ()):
1161 1168 rev = repo[rev].rev()
1162 1169 ff = followfilter()
1163 1170 stop = min(revs[0], revs[-1])
1164 1171 for x in xrange(rev, stop - 1, -1):
1165 1172 if ff.match(x):
1166 1173 wanted.discard(x)
1167 1174
1168 1175 # Now that wanted is correctly initialized, we can iterate over the
1169 1176 # revision range, yielding only revisions in wanted.
1170 1177 def iterate():
1171 1178 if follow and not match.files():
1172 1179 ff = followfilter(onlyfirst=opts.get('follow_first'))
1173 1180 def want(rev):
1174 1181 return ff.match(rev) and rev in wanted
1175 1182 else:
1176 1183 def want(rev):
1177 1184 return rev in wanted
1178 1185
1179 1186 for i, window in increasingwindows(0, len(revs)):
1180 1187 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1181 1188 for rev in sorted(nrevs):
1182 1189 fns = fncache.get(rev)
1183 1190 ctx = change(rev)
1184 1191 if not fns:
1185 1192 def fns_generator():
1186 1193 for f in ctx.files():
1187 1194 if match(f):
1188 1195 yield f
1189 1196 fns = fns_generator()
1190 1197 prepare(ctx, fns)
1191 1198 for rev in nrevs:
1192 1199 yield change(rev)
1193 1200 return iterate()
1194 1201
1195 1202 def _makegraphfilematcher(repo, pats, followfirst):
1196 1203 # When displaying a revision with --patch --follow FILE, we have
1197 1204 # to know which file of the revision must be diffed. With
1198 1205 # --follow, we want the names of the ancestors of FILE in the
1199 1206 # revision, stored in "fcache". "fcache" is populated by
1200 1207 # reproducing the graph traversal already done by --follow revset
1201 1208 # and relating linkrevs to file names (which is not "correct" but
1202 1209 # good enough).
1203 1210 fcache = {}
1204 1211 fcacheready = [False]
1205 1212 pctx = repo['.']
1206 1213 wctx = repo[None]
1207 1214
1208 1215 def populate():
1209 1216 for fn in pats:
1210 1217 for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
1211 1218 for c in i:
1212 1219 fcache.setdefault(c.linkrev(), set()).add(c.path())
1213 1220
1214 1221 def filematcher(rev):
1215 1222 if not fcacheready[0]:
1216 1223 # Lazy initialization
1217 1224 fcacheready[0] = True
1218 1225 populate()
1219 1226 return scmutil.match(wctx, fcache.get(rev, []), default='path')
1220 1227
1221 1228 return filematcher
1222 1229
1223 1230 def _makegraphlogrevset(repo, pats, opts, revs):
1224 1231 """Return (expr, filematcher) where expr is a revset string built
1225 1232 from log options and file patterns or None. If --stat or --patch
1226 1233 are not passed filematcher is None. Otherwise it is a callable
1227 1234 taking a revision number and returning a match objects filtering
1228 1235 the files to be detailed when displaying the revision.
1229 1236 """
1230 1237 opt2revset = {
1231 1238 'no_merges': ('not merge()', None),
1232 1239 'only_merges': ('merge()', None),
1233 1240 '_ancestors': ('ancestors(%(val)s)', None),
1234 1241 '_fancestors': ('_firstancestors(%(val)s)', None),
1235 1242 '_descendants': ('descendants(%(val)s)', None),
1236 1243 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1237 1244 '_matchfiles': ('_matchfiles(%(val)s)', None),
1238 1245 'date': ('date(%(val)r)', None),
1239 1246 'branch': ('branch(%(val)r)', ' or '),
1240 1247 '_patslog': ('filelog(%(val)r)', ' or '),
1241 1248 '_patsfollow': ('follow(%(val)r)', ' or '),
1242 1249 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1243 1250 'keyword': ('keyword(%(val)r)', ' or '),
1244 1251 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1245 1252 'user': ('user(%(val)r)', ' or '),
1246 1253 }
1247 1254
1248 1255 opts = dict(opts)
1249 1256 # follow or not follow?
1250 1257 follow = opts.get('follow') or opts.get('follow_first')
1251 1258 followfirst = opts.get('follow_first') and 1 or 0
1252 1259 # --follow with FILE behaviour depends on revs...
1253 1260 startrev = revs[0]
1254 1261 followdescendants = (len(revs) > 1 and revs[0] < revs[1]) and 1 or 0
1255 1262
1256 1263 # branch and only_branch are really aliases and must be handled at
1257 1264 # the same time
1258 1265 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1259 1266 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1260 1267 # pats/include/exclude are passed to match.match() directly in
1261 1268 # _matchfiles() revset but walkchangerevs() builds its matcher with
1262 1269 # scmutil.match(). The difference is input pats are globbed on
1263 1270 # platforms without shell expansion (windows).
1264 1271 pctx = repo[None]
1265 1272 match, pats = scmutil.matchandpats(pctx, pats, opts)
1266 1273 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1267 1274 if not slowpath:
1268 1275 for f in match.files():
1269 1276 if follow and f not in pctx:
1270 1277 raise util.Abort(_('cannot follow file not in parent '
1271 1278 'revision: "%s"') % f)
1272 1279 filelog = repo.file(f)
1273 1280 if not len(filelog):
1274 1281 # A zero count may be a directory or deleted file, so
1275 1282 # try to find matching entries on the slow path.
1276 1283 if follow:
1277 1284 raise util.Abort(
1278 1285 _('cannot follow nonexistent file: "%s"') % f)
1279 1286 slowpath = True
1280 1287 if slowpath:
1281 1288 # See walkchangerevs() slow path.
1282 1289 #
1283 1290 if follow:
1284 1291 raise util.Abort(_('can only follow copies/renames for explicit '
1285 1292 'filenames'))
1286 1293 # pats/include/exclude cannot be represented as separate
1287 1294 # revset expressions as their filtering logic applies at file
1288 1295 # level. For instance "-I a -X a" matches a revision touching
1289 1296 # "a" and "b" while "file(a) and not file(b)" does
1290 1297 # not. Besides, filesets are evaluated against the working
1291 1298 # directory.
1292 1299 matchargs = ['r:', 'd:relpath']
1293 1300 for p in pats:
1294 1301 matchargs.append('p:' + p)
1295 1302 for p in opts.get('include', []):
1296 1303 matchargs.append('i:' + p)
1297 1304 for p in opts.get('exclude', []):
1298 1305 matchargs.append('x:' + p)
1299 1306 matchargs = ','.join(('%r' % p) for p in matchargs)
1300 1307 opts['_matchfiles'] = matchargs
1301 1308 else:
1302 1309 if follow:
1303 1310 fpats = ('_patsfollow', '_patsfollowfirst')
1304 1311 fnopats = (('_ancestors', '_fancestors'),
1305 1312 ('_descendants', '_fdescendants'))
1306 1313 if pats:
1307 1314 # follow() revset interprets its file argument as a
1308 1315 # manifest entry, so use match.files(), not pats.
1309 1316 opts[fpats[followfirst]] = list(match.files())
1310 1317 else:
1311 1318 opts[fnopats[followdescendants][followfirst]] = str(startrev)
1312 1319 else:
1313 1320 opts['_patslog'] = list(pats)
1314 1321
1315 1322 filematcher = None
1316 1323 if opts.get('patch') or opts.get('stat'):
1317 1324 if follow:
1318 1325 filematcher = _makegraphfilematcher(repo, pats, followfirst)
1319 1326 else:
1320 1327 filematcher = lambda rev: match
1321 1328
1322 1329 expr = []
1323 1330 for op, val in opts.iteritems():
1324 1331 if not val:
1325 1332 continue
1326 1333 if op not in opt2revset:
1327 1334 continue
1328 1335 revop, andor = opt2revset[op]
1329 1336 if '%(val)' not in revop:
1330 1337 expr.append(revop)
1331 1338 else:
1332 1339 if not isinstance(val, list):
1333 1340 e = revop % {'val': val}
1334 1341 else:
1335 1342 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
1336 1343 expr.append(e)
1337 1344
1338 1345 if expr:
1339 1346 expr = '(' + ' and '.join(expr) + ')'
1340 1347 else:
1341 1348 expr = None
1342 1349 return expr, filematcher
1343 1350
1344 1351 def getgraphlogrevs(repo, pats, opts):
1345 1352 """Return (revs, expr, filematcher) where revs is an iterable of
1346 1353 revision numbers, expr is a revset string built from log options
1347 1354 and file patterns or None, and used to filter 'revs'. If --stat or
1348 1355 --patch are not passed filematcher is None. Otherwise it is a
1349 1356 callable taking a revision number and returning a match objects
1350 1357 filtering the files to be detailed when displaying the revision.
1351 1358 """
1352 1359 def increasingrevs(repo, revs, matcher):
1353 1360 # The sorted input rev sequence is chopped in sub-sequences
1354 1361 # which are sorted in ascending order and passed to the
1355 1362 # matcher. The filtered revs are sorted again as they were in
1356 1363 # the original sub-sequence. This achieve several things:
1357 1364 #
1358 1365 # - getlogrevs() now returns a generator which behaviour is
1359 1366 # adapted to log need. First results come fast, last ones
1360 1367 # are batched for performances.
1361 1368 #
1362 1369 # - revset matchers often operate faster on revision in
1363 1370 # changelog order, because most filters deal with the
1364 1371 # changelog.
1365 1372 #
1366 1373 # - revset matchers can reorder revisions. "A or B" typically
1367 1374 # returns returns the revision matching A then the revision
1368 1375 # matching B. We want to hide this internal implementation
1369 1376 # detail from the caller, and sorting the filtered revision
1370 1377 # again achieves this.
1371 1378 for i, window in increasingwindows(0, len(revs), windowsize=1):
1372 1379 orevs = revs[i:i + window]
1373 1380 nrevs = set(matcher(repo, sorted(orevs)))
1374 1381 for rev in orevs:
1375 1382 if rev in nrevs:
1376 1383 yield rev
1377 1384
1378 1385 if not len(repo):
1379 1386 return iter([]), None, None
1380 1387 # Default --rev value depends on --follow but --follow behaviour
1381 1388 # depends on revisions resolved from --rev...
1382 1389 follow = opts.get('follow') or opts.get('follow_first')
1383 1390 if opts.get('rev'):
1384 1391 revs = scmutil.revrange(repo, opts['rev'])
1385 1392 else:
1386 1393 if follow and len(repo) > 0:
1387 1394 revs = scmutil.revrange(repo, ['.:0'])
1388 1395 else:
1389 1396 revs = range(len(repo) - 1, -1, -1)
1390 1397 if not revs:
1391 1398 return iter([]), None, None
1392 1399 expr, filematcher = _makegraphlogrevset(repo, pats, opts, revs)
1393 1400 if expr:
1394 1401 matcher = revset.match(repo.ui, expr)
1395 1402 revs = increasingrevs(repo, revs, matcher)
1396 1403 if not opts.get('hidden'):
1397 1404 # --hidden is still experimental and not worth a dedicated revset
1398 1405 # yet. Fortunately, filtering revision number is fast.
1399 1406 revs = (r for r in revs if r not in repo.hiddenrevs)
1400 1407 else:
1401 1408 revs = iter(revs)
1402 1409 return revs, expr, filematcher
1403 1410
1404 1411 def displaygraph(ui, dag, displayer, showparents, edgefn, getrenamed=None,
1405 1412 filematcher=None):
1406 1413 seen, state = [], graphmod.asciistate()
1407 1414 for rev, type, ctx, parents in dag:
1408 1415 char = 'o'
1409 1416 if ctx.node() in showparents:
1410 1417 char = '@'
1411 1418 elif ctx.obsolete():
1412 1419 char = 'x'
1413 1420 copies = None
1414 1421 if getrenamed and ctx.rev():
1415 1422 copies = []
1416 1423 for fn in ctx.files():
1417 1424 rename = getrenamed(fn, ctx.rev())
1418 1425 if rename:
1419 1426 copies.append((fn, rename[0]))
1420 1427 revmatchfn = None
1421 1428 if filematcher is not None:
1422 1429 revmatchfn = filematcher(ctx.rev())
1423 1430 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
1424 1431 lines = displayer.hunk.pop(rev).split('\n')
1425 1432 if not lines[-1]:
1426 1433 del lines[-1]
1427 1434 displayer.flush(rev)
1428 1435 edges = edgefn(type, char, lines, seen, rev, parents)
1429 1436 for type, char, lines, coldata in edges:
1430 1437 graphmod.ascii(ui, state, type, char, lines, coldata)
1431 1438 displayer.close()
1432 1439
1433 1440 def graphlog(ui, repo, *pats, **opts):
1434 1441 # Parameters are identical to log command ones
1435 1442 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
1436 1443 revs = sorted(revs, reverse=1)
1437 1444 limit = loglimit(opts)
1438 1445 if limit is not None:
1439 1446 revs = revs[:limit]
1440 1447 revdag = graphmod.dagwalker(repo, revs)
1441 1448
1442 1449 getrenamed = None
1443 1450 if opts.get('copies'):
1444 1451 endrev = None
1445 1452 if opts.get('rev'):
1446 1453 endrev = max(scmutil.revrange(repo, opts.get('rev'))) + 1
1447 1454 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
1448 1455 displayer = show_changeset(ui, repo, opts, buffered=True)
1449 1456 showparents = [ctx.node() for ctx in repo[None].parents()]
1450 1457 displaygraph(ui, revdag, displayer, showparents,
1451 1458 graphmod.asciiedges, getrenamed, filematcher)
1452 1459
1453 1460 def checkunsupportedgraphflags(pats, opts):
1454 1461 for op in ["newest_first"]:
1455 1462 if op in opts and opts[op]:
1456 1463 raise util.Abort(_("-G/--graph option is incompatible with --%s")
1457 1464 % op.replace("_", "-"))
1458 1465
1459 1466 def graphrevs(repo, nodes, opts):
1460 1467 limit = loglimit(opts)
1461 1468 nodes.reverse()
1462 1469 if limit is not None:
1463 1470 nodes = nodes[:limit]
1464 1471 return graphmod.nodes(repo, nodes)
1465 1472
1466 1473 def add(ui, repo, match, dryrun, listsubrepos, prefix, explicitonly):
1467 1474 join = lambda f: os.path.join(prefix, f)
1468 1475 bad = []
1469 1476 oldbad = match.bad
1470 1477 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1471 1478 names = []
1472 1479 wctx = repo[None]
1473 1480 cca = None
1474 1481 abort, warn = scmutil.checkportabilityalert(ui)
1475 1482 if abort or warn:
1476 1483 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
1477 1484 for f in repo.walk(match):
1478 1485 exact = match.exact(f)
1479 1486 if exact or not explicitonly and f not in repo.dirstate:
1480 1487 if cca:
1481 1488 cca(f)
1482 1489 names.append(f)
1483 1490 if ui.verbose or not exact:
1484 1491 ui.status(_('adding %s\n') % match.rel(join(f)))
1485 1492
1486 1493 for subpath in wctx.substate:
1487 1494 sub = wctx.sub(subpath)
1488 1495 try:
1489 1496 submatch = matchmod.narrowmatcher(subpath, match)
1490 1497 if listsubrepos:
1491 1498 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1492 1499 False))
1493 1500 else:
1494 1501 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1495 1502 True))
1496 1503 except error.LookupError:
1497 1504 ui.status(_("skipping missing subrepository: %s\n")
1498 1505 % join(subpath))
1499 1506
1500 1507 if not dryrun:
1501 1508 rejected = wctx.add(names, prefix)
1502 1509 bad.extend(f for f in rejected if f in match.files())
1503 1510 return bad
1504 1511
1505 1512 def forget(ui, repo, match, prefix, explicitonly):
1506 1513 join = lambda f: os.path.join(prefix, f)
1507 1514 bad = []
1508 1515 oldbad = match.bad
1509 1516 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1510 1517 wctx = repo[None]
1511 1518 forgot = []
1512 1519 s = repo.status(match=match, clean=True)
1513 1520 forget = sorted(s[0] + s[1] + s[3] + s[6])
1514 1521 if explicitonly:
1515 1522 forget = [f for f in forget if match.exact(f)]
1516 1523
1517 1524 for subpath in wctx.substate:
1518 1525 sub = wctx.sub(subpath)
1519 1526 try:
1520 1527 submatch = matchmod.narrowmatcher(subpath, match)
1521 1528 subbad, subforgot = sub.forget(ui, submatch, prefix)
1522 1529 bad.extend([subpath + '/' + f for f in subbad])
1523 1530 forgot.extend([subpath + '/' + f for f in subforgot])
1524 1531 except error.LookupError:
1525 1532 ui.status(_("skipping missing subrepository: %s\n")
1526 1533 % join(subpath))
1527 1534
1528 1535 if not explicitonly:
1529 1536 for f in match.files():
1530 1537 if f not in repo.dirstate and not os.path.isdir(match.rel(join(f))):
1531 1538 if f not in forgot:
1532 1539 if os.path.exists(match.rel(join(f))):
1533 1540 ui.warn(_('not removing %s: '
1534 1541 'file is already untracked\n')
1535 1542 % match.rel(join(f)))
1536 1543 bad.append(f)
1537 1544
1538 1545 for f in forget:
1539 1546 if ui.verbose or not match.exact(f):
1540 1547 ui.status(_('removing %s\n') % match.rel(join(f)))
1541 1548
1542 1549 rejected = wctx.forget(forget, prefix)
1543 1550 bad.extend(f for f in rejected if f in match.files())
1544 1551 forgot.extend(forget)
1545 1552 return bad, forgot
1546 1553
1547 1554 def duplicatecopies(repo, rev, p1):
1548 1555 "Reproduce copies found in the source revision in the dirstate for grafts"
1549 1556 for dst, src in copies.pathcopies(repo[p1], repo[rev]).iteritems():
1550 1557 repo.dirstate.copy(src, dst)
1551 1558
1552 1559 def commit(ui, repo, commitfunc, pats, opts):
1553 1560 '''commit the specified files or all outstanding changes'''
1554 1561 date = opts.get('date')
1555 1562 if date:
1556 1563 opts['date'] = util.parsedate(date)
1557 1564 message = logmessage(ui, opts)
1558 1565
1559 1566 # extract addremove carefully -- this function can be called from a command
1560 1567 # that doesn't support addremove
1561 1568 if opts.get('addremove'):
1562 1569 scmutil.addremove(repo, pats, opts)
1563 1570
1564 1571 return commitfunc(ui, repo, message,
1565 1572 scmutil.match(repo[None], pats, opts), opts)
1566 1573
1567 1574 def amend(ui, repo, commitfunc, old, extra, pats, opts):
1568 1575 ui.note(_('amending changeset %s\n') % old)
1569 1576 base = old.p1()
1570 1577
1571 1578 wlock = repo.wlock()
1572 1579 try:
1573 1580 # First, do a regular commit to record all changes in the working
1574 1581 # directory (if there are any)
1575 1582 ui.callhooks = False
1576 1583 try:
1577 1584 node = commit(ui, repo, commitfunc, pats, opts)
1578 1585 finally:
1579 1586 ui.callhooks = True
1580 1587 ctx = repo[node]
1581 1588
1582 1589 # Participating changesets:
1583 1590 #
1584 1591 # node/ctx o - new (intermediate) commit that contains changes from
1585 1592 # | working dir to go into amending commit (or a workingctx
1586 1593 # | if there were no changes)
1587 1594 # |
1588 1595 # old o - changeset to amend
1589 1596 # |
1590 1597 # base o - parent of amending changeset
1591 1598
1592 1599 # Update extra dict from amended commit (e.g. to preserve graft source)
1593 1600 extra.update(old.extra())
1594 1601
1595 1602 # Also update it from the intermediate commit or from the wctx
1596 1603 extra.update(ctx.extra())
1597 1604
1598 1605 files = set(old.files())
1599 1606
1600 1607 # Second, we use either the commit we just did, or if there were no
1601 1608 # changes the parent of the working directory as the version of the
1602 1609 # files in the final amend commit
1603 1610 if node:
1604 1611 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
1605 1612
1606 1613 user = ctx.user()
1607 1614 date = ctx.date()
1608 1615 message = ctx.description()
1609 1616 # Recompute copies (avoid recording a -> b -> a)
1610 1617 copied = copies.pathcopies(base, ctx)
1611 1618
1612 1619 # Prune files which were reverted by the updates: if old introduced
1613 1620 # file X and our intermediate commit, node, renamed that file, then
1614 1621 # those two files are the same and we can discard X from our list
1615 1622 # of files. Likewise if X was deleted, it's no longer relevant
1616 1623 files.update(ctx.files())
1617 1624
1618 1625 def samefile(f):
1619 1626 if f in ctx.manifest():
1620 1627 a = ctx.filectx(f)
1621 1628 if f in base.manifest():
1622 1629 b = base.filectx(f)
1623 1630 return (not a.cmp(b)
1624 1631 and a.flags() == b.flags())
1625 1632 else:
1626 1633 return False
1627 1634 else:
1628 1635 return f not in base.manifest()
1629 1636 files = [f for f in files if not samefile(f)]
1630 1637
1631 1638 def filectxfn(repo, ctx_, path):
1632 1639 try:
1633 1640 fctx = ctx[path]
1634 1641 flags = fctx.flags()
1635 1642 mctx = context.memfilectx(fctx.path(), fctx.data(),
1636 1643 islink='l' in flags,
1637 1644 isexec='x' in flags,
1638 1645 copied=copied.get(path))
1639 1646 return mctx
1640 1647 except KeyError:
1641 1648 raise IOError
1642 1649 else:
1643 1650 ui.note(_('copying changeset %s to %s\n') % (old, base))
1644 1651
1645 1652 # Use version of files as in the old cset
1646 1653 def filectxfn(repo, ctx_, path):
1647 1654 try:
1648 1655 return old.filectx(path)
1649 1656 except KeyError:
1650 1657 raise IOError
1651 1658
1652 1659 # See if we got a message from -m or -l, if not, open the editor
1653 1660 # with the message of the changeset to amend
1654 1661 user = opts.get('user') or old.user()
1655 1662 date = opts.get('date') or old.date()
1656 1663 message = logmessage(ui, opts)
1657 1664 if not message:
1658 1665 cctx = context.workingctx(repo, old.description(), user, date,
1659 1666 extra,
1660 1667 repo.status(base.node(), old.node()))
1661 1668 message = commitforceeditor(repo, cctx, [])
1662 1669
1663 1670 new = context.memctx(repo,
1664 1671 parents=[base.node(), nullid],
1665 1672 text=message,
1666 1673 files=files,
1667 1674 filectxfn=filectxfn,
1668 1675 user=user,
1669 1676 date=date,
1670 1677 extra=extra)
1671 1678 newid = repo.commitctx(new)
1672 1679 if newid != old.node():
1673 1680 # Reroute the working copy parent to the new changeset
1674 1681 repo.setparents(newid, nullid)
1675 1682
1676 1683 # Move bookmarks from old parent to amend commit
1677 1684 bms = repo.nodebookmarks(old.node())
1678 1685 if bms:
1679 1686 for bm in bms:
1680 1687 repo._bookmarks[bm] = newid
1681 1688 bookmarks.write(repo)
1682 1689
1683 1690 # Strip the intermediate commit (if there was one) and the amended
1684 1691 # commit
1685 1692 lock = repo.lock()
1686 1693 try:
1687 1694 if node:
1688 1695 ui.note(_('stripping intermediate changeset %s\n') % ctx)
1689 1696 ui.note(_('stripping amended changeset %s\n') % old)
1690 1697 repair.strip(ui, repo, old.node(), topic='amend-backup')
1691 1698 finally:
1692 1699 lock.release()
1693 1700 finally:
1694 1701 wlock.release()
1695 1702 return newid
1696 1703
1697 1704 def commiteditor(repo, ctx, subs):
1698 1705 if ctx.description():
1699 1706 return ctx.description()
1700 1707 return commitforceeditor(repo, ctx, subs)
1701 1708
1702 1709 def commitforceeditor(repo, ctx, subs):
1703 1710 edittext = []
1704 1711 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1705 1712 if ctx.description():
1706 1713 edittext.append(ctx.description())
1707 1714 edittext.append("")
1708 1715 edittext.append("") # Empty line between message and comments.
1709 1716 edittext.append(_("HG: Enter commit message."
1710 1717 " Lines beginning with 'HG:' are removed."))
1711 1718 edittext.append(_("HG: Leave message empty to abort commit."))
1712 1719 edittext.append("HG: --")
1713 1720 edittext.append(_("HG: user: %s") % ctx.user())
1714 1721 if ctx.p2():
1715 1722 edittext.append(_("HG: branch merge"))
1716 1723 if ctx.branch():
1717 1724 edittext.append(_("HG: branch '%s'") % ctx.branch())
1718 1725 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1719 1726 edittext.extend([_("HG: added %s") % f for f in added])
1720 1727 edittext.extend([_("HG: changed %s") % f for f in modified])
1721 1728 edittext.extend([_("HG: removed %s") % f for f in removed])
1722 1729 if not added and not modified and not removed:
1723 1730 edittext.append(_("HG: no files changed"))
1724 1731 edittext.append("")
1725 1732 # run editor in the repository root
1726 1733 olddir = os.getcwd()
1727 1734 os.chdir(repo.root)
1728 1735 text = repo.ui.edit("\n".join(edittext), ctx.user())
1729 1736 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
1730 1737 os.chdir(olddir)
1731 1738
1732 1739 if not text.strip():
1733 1740 raise util.Abort(_("empty commit message"))
1734 1741
1735 1742 return text
1736 1743
1737 1744 def revert(ui, repo, ctx, parents, *pats, **opts):
1738 1745 parent, p2 = parents
1739 1746 node = ctx.node()
1740 1747
1741 1748 mf = ctx.manifest()
1742 1749 if node == parent:
1743 1750 pmf = mf
1744 1751 else:
1745 1752 pmf = None
1746 1753
1747 1754 # need all matching names in dirstate and manifest of target rev,
1748 1755 # so have to walk both. do not print errors if files exist in one
1749 1756 # but not other.
1750 1757
1751 1758 names = {}
1752 1759
1753 1760 wlock = repo.wlock()
1754 1761 try:
1755 1762 # walk dirstate.
1756 1763
1757 1764 m = scmutil.match(repo[None], pats, opts)
1758 1765 m.bad = lambda x, y: False
1759 1766 for abs in repo.walk(m):
1760 1767 names[abs] = m.rel(abs), m.exact(abs)
1761 1768
1762 1769 # walk target manifest.
1763 1770
1764 1771 def badfn(path, msg):
1765 1772 if path in names:
1766 1773 return
1767 1774 if path in ctx.substate:
1768 1775 return
1769 1776 path_ = path + '/'
1770 1777 for f in names:
1771 1778 if f.startswith(path_):
1772 1779 return
1773 1780 ui.warn("%s: %s\n" % (m.rel(path), msg))
1774 1781
1775 1782 m = scmutil.match(ctx, pats, opts)
1776 1783 m.bad = badfn
1777 1784 for abs in ctx.walk(m):
1778 1785 if abs not in names:
1779 1786 names[abs] = m.rel(abs), m.exact(abs)
1780 1787
1781 1788 # get the list of subrepos that must be reverted
1782 1789 targetsubs = [s for s in ctx.substate if m(s)]
1783 1790 m = scmutil.matchfiles(repo, names)
1784 1791 changes = repo.status(match=m)[:4]
1785 1792 modified, added, removed, deleted = map(set, changes)
1786 1793
1787 1794 # if f is a rename, also revert the source
1788 1795 cwd = repo.getcwd()
1789 1796 for f in added:
1790 1797 src = repo.dirstate.copied(f)
1791 1798 if src and src not in names and repo.dirstate[src] == 'r':
1792 1799 removed.add(src)
1793 1800 names[src] = (repo.pathto(src, cwd), True)
1794 1801
1795 1802 def removeforget(abs):
1796 1803 if repo.dirstate[abs] == 'a':
1797 1804 return _('forgetting %s\n')
1798 1805 return _('removing %s\n')
1799 1806
1800 1807 revert = ([], _('reverting %s\n'))
1801 1808 add = ([], _('adding %s\n'))
1802 1809 remove = ([], removeforget)
1803 1810 undelete = ([], _('undeleting %s\n'))
1804 1811
1805 1812 disptable = (
1806 1813 # dispatch table:
1807 1814 # file state
1808 1815 # action if in target manifest
1809 1816 # action if not in target manifest
1810 1817 # make backup if in target manifest
1811 1818 # make backup if not in target manifest
1812 1819 (modified, revert, remove, True, True),
1813 1820 (added, revert, remove, True, False),
1814 1821 (removed, undelete, None, False, False),
1815 1822 (deleted, revert, remove, False, False),
1816 1823 )
1817 1824
1818 1825 for abs, (rel, exact) in sorted(names.items()):
1819 1826 mfentry = mf.get(abs)
1820 1827 target = repo.wjoin(abs)
1821 1828 def handle(xlist, dobackup):
1822 1829 xlist[0].append(abs)
1823 1830 if (dobackup and not opts.get('no_backup') and
1824 1831 os.path.lexists(target)):
1825 1832 bakname = "%s.orig" % rel
1826 1833 ui.note(_('saving current version of %s as %s\n') %
1827 1834 (rel, bakname))
1828 1835 if not opts.get('dry_run'):
1829 1836 util.rename(target, bakname)
1830 1837 if ui.verbose or not exact:
1831 1838 msg = xlist[1]
1832 1839 if not isinstance(msg, basestring):
1833 1840 msg = msg(abs)
1834 1841 ui.status(msg % rel)
1835 1842 for table, hitlist, misslist, backuphit, backupmiss in disptable:
1836 1843 if abs not in table:
1837 1844 continue
1838 1845 # file has changed in dirstate
1839 1846 if mfentry:
1840 1847 handle(hitlist, backuphit)
1841 1848 elif misslist is not None:
1842 1849 handle(misslist, backupmiss)
1843 1850 break
1844 1851 else:
1845 1852 if abs not in repo.dirstate:
1846 1853 if mfentry:
1847 1854 handle(add, True)
1848 1855 elif exact:
1849 1856 ui.warn(_('file not managed: %s\n') % rel)
1850 1857 continue
1851 1858 # file has not changed in dirstate
1852 1859 if node == parent:
1853 1860 if exact:
1854 1861 ui.warn(_('no changes needed to %s\n') % rel)
1855 1862 continue
1856 1863 if pmf is None:
1857 1864 # only need parent manifest in this unlikely case,
1858 1865 # so do not read by default
1859 1866 pmf = repo[parent].manifest()
1860 1867 if abs in pmf and mfentry:
1861 1868 # if version of file is same in parent and target
1862 1869 # manifests, do nothing
1863 1870 if (pmf[abs] != mfentry or
1864 1871 pmf.flags(abs) != mf.flags(abs)):
1865 1872 handle(revert, False)
1866 1873 else:
1867 1874 handle(remove, False)
1868 1875
1869 1876 if not opts.get('dry_run'):
1870 1877 def checkout(f):
1871 1878 fc = ctx[f]
1872 1879 repo.wwrite(f, fc.data(), fc.flags())
1873 1880
1874 1881 audit_path = scmutil.pathauditor(repo.root)
1875 1882 for f in remove[0]:
1876 1883 if repo.dirstate[f] == 'a':
1877 1884 repo.dirstate.drop(f)
1878 1885 continue
1879 1886 audit_path(f)
1880 1887 try:
1881 1888 util.unlinkpath(repo.wjoin(f))
1882 1889 except OSError:
1883 1890 pass
1884 1891 repo.dirstate.remove(f)
1885 1892
1886 1893 normal = None
1887 1894 if node == parent:
1888 1895 # We're reverting to our parent. If possible, we'd like status
1889 1896 # to report the file as clean. We have to use normallookup for
1890 1897 # merges to avoid losing information about merged/dirty files.
1891 1898 if p2 != nullid:
1892 1899 normal = repo.dirstate.normallookup
1893 1900 else:
1894 1901 normal = repo.dirstate.normal
1895 1902 for f in revert[0]:
1896 1903 checkout(f)
1897 1904 if normal:
1898 1905 normal(f)
1899 1906
1900 1907 for f in add[0]:
1901 1908 checkout(f)
1902 1909 repo.dirstate.add(f)
1903 1910
1904 1911 normal = repo.dirstate.normallookup
1905 1912 if node == parent and p2 == nullid:
1906 1913 normal = repo.dirstate.normal
1907 1914 for f in undelete[0]:
1908 1915 checkout(f)
1909 1916 normal(f)
1910 1917
1911 1918 if targetsubs:
1912 1919 # Revert the subrepos on the revert list
1913 1920 for sub in targetsubs:
1914 1921 ctx.sub(sub).revert(ui, ctx.substate[sub], *pats, **opts)
1915 1922 finally:
1916 1923 wlock.release()
1917 1924
1918 1925 def command(table):
1919 1926 '''returns a function object bound to table which can be used as
1920 1927 a decorator for populating table as a command table'''
1921 1928
1922 1929 def cmd(name, options, synopsis=None):
1923 1930 def decorator(func):
1924 1931 if synopsis:
1925 1932 table[name] = func, options[:], synopsis
1926 1933 else:
1927 1934 table[name] = func, options[:]
1928 1935 return func
1929 1936 return decorator
1930 1937
1931 1938 return cmd
@@ -1,147 +1,171 b''
1 1 $ hg init repo
2 2 $ cd repo
3 3 $ touch foo
4 4 $ hg add foo
5 5 $ for i in 0 1 2 3 4 5 6 7 8 9 10 11; do
6 6 > echo "foo-$i" >> foo
7 7 > hg ci -m "foo-$i"
8 8 > done
9 9
10 10 $ for out in "%nof%N" "%%%H" "%b-%R" "%h" "%r" "%m"; do
11 11 > echo
12 12 > echo "# foo-$out.patch"
13 13 > hg export -v -o "foo-$out.patch" 2:tip
14 14 > done
15 15
16 16 # foo-%nof%N.patch
17 17 exporting patches:
18 18 foo-01of10.patch
19 19 foo-02of10.patch
20 20 foo-03of10.patch
21 21 foo-04of10.patch
22 22 foo-05of10.patch
23 23 foo-06of10.patch
24 24 foo-07of10.patch
25 25 foo-08of10.patch
26 26 foo-09of10.patch
27 27 foo-10of10.patch
28 28
29 29 # foo-%%%H.patch
30 30 exporting patches:
31 31 foo-%617188a1c80f869a7b66c85134da88a6fb145f67.patch
32 32 foo-%dd41a5ff707a5225204105611ba49cc5c229d55f.patch
33 33 foo-%f95a5410f8664b6e1490a4af654e4b7d41a7b321.patch
34 34 foo-%4346bcfde53b4d9042489078bcfa9c3e28201db2.patch
35 35 foo-%afda8c3a009cc99449a05ad8aa4655648c4ecd34.patch
36 36 foo-%35284ce2b6b99c9d2ac66268fe99e68e1974e1aa.patch
37 37 foo-%9688c41894e6931305fa7165a37f6568050b4e9b.patch
38 38 foo-%747d3c68f8ec44bb35816bfcd59aeb50b9654c2f.patch
39 39 foo-%5f17a83f5fbd9414006a5e563eab4c8a00729efd.patch
40 40 foo-%f3acbafac161ec68f1598af38f794f28847ca5d3.patch
41 41
42 42 # foo-%b-%R.patch
43 43 exporting patches:
44 44 foo-repo-2.patch
45 45 foo-repo-3.patch
46 46 foo-repo-4.patch
47 47 foo-repo-5.patch
48 48 foo-repo-6.patch
49 49 foo-repo-7.patch
50 50 foo-repo-8.patch
51 51 foo-repo-9.patch
52 52 foo-repo-10.patch
53 53 foo-repo-11.patch
54 54
55 55 # foo-%h.patch
56 56 exporting patches:
57 57 foo-617188a1c80f.patch
58 58 foo-dd41a5ff707a.patch
59 59 foo-f95a5410f866.patch
60 60 foo-4346bcfde53b.patch
61 61 foo-afda8c3a009c.patch
62 62 foo-35284ce2b6b9.patch
63 63 foo-9688c41894e6.patch
64 64 foo-747d3c68f8ec.patch
65 65 foo-5f17a83f5fbd.patch
66 66 foo-f3acbafac161.patch
67 67
68 68 # foo-%r.patch
69 69 exporting patches:
70 70 foo-02.patch
71 71 foo-03.patch
72 72 foo-04.patch
73 73 foo-05.patch
74 74 foo-06.patch
75 75 foo-07.patch
76 76 foo-08.patch
77 77 foo-09.patch
78 78 foo-10.patch
79 79 foo-11.patch
80 80
81 81 # foo-%m.patch
82 82 exporting patches:
83 83 foo-foo_2.patch
84 84 foo-foo_3.patch
85 85 foo-foo_4.patch
86 86 foo-foo_5.patch
87 87 foo-foo_6.patch
88 88 foo-foo_7.patch
89 89 foo-foo_8.patch
90 90 foo-foo_9.patch
91 91 foo-foo_10.patch
92 92 foo-foo_11.patch
93 93
94 94 Exporting 4 changesets to a file:
95 95
96 96 $ hg export -o export_internal 1 2 3 4
97 97 $ grep HG export_internal | wc -l
98 98 \s*4 (re)
99 99
100 100 Exporting 4 changesets to a file:
101 101
102 102 $ hg export 1 2 3 4 | grep HG | wc -l
103 103 \s*4 (re)
104 104
105 105 Exporting revision -2 to a file:
106 106
107 107 $ hg export -- -2
108 108 # HG changeset patch
109 109 # User test
110 110 # Date 0 0
111 111 # Node ID 5f17a83f5fbd9414006a5e563eab4c8a00729efd
112 112 # Parent 747d3c68f8ec44bb35816bfcd59aeb50b9654c2f
113 113 foo-10
114 114
115 115 diff -r 747d3c68f8ec -r 5f17a83f5fbd foo
116 116 --- a/foo Thu Jan 01 00:00:00 1970 +0000
117 117 +++ b/foo Thu Jan 01 00:00:00 1970 +0000
118 118 @@ -8,3 +8,4 @@
119 119 foo-7
120 120 foo-8
121 121 foo-9
122 122 +foo-10
123 123
124 124 Checking if only alphanumeric characters are used in the file name (%m option):
125 125
126 126 $ echo "line" >> foo
127 127 $ hg commit -m " !\"#$%&(,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~"
128 128 $ hg export -v -o %m.patch tip
129 129 exporting patch:
130 130 ____________0123456789_______ABCDEFGHIJKLMNOPQRSTUVWXYZ______abcdefghijklmnopqrstuvwxyz____.patch
131 131
132 132 Catch exporting unknown revisions (especially empty revsets, see issue3353)
133 133
134 134 $ hg export
135 135 abort: export requires at least one changeset
136 136 [255]
137 137 $ hg export ""
138 138 hg: parse error: empty query
139 139 [255]
140 140 $ hg export 999
141 141 abort: unknown revision '999'!
142 142 [255]
143 143 $ hg export "not all()"
144 144 abort: export requires at least one changeset
145 145 [255]
146 146
147 Check for color output
148 $ echo "[color]" >> $HGRCPATH
149 $ echo "mode = ansi" >> $HGRCPATH
150 $ echo "[extensions]" >> $HGRCPATH
151 $ echo "color=" >> $HGRCPATH
152
153 $ hg export --color always --nodates tip
154 # HG changeset patch
155 # User test
156 # Date 0 0
157 # Node ID * (glob)
158 # Parent * (glob)
159 !"#$%&(,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
160
161 \x1b[0;1mdiff -r f3acbafac161 -r 197ecd81a57f foo\x1b[0m (esc)
162 \x1b[0;31;1m--- a/foo\x1b[0m (esc)
163 \x1b[0;32;1m+++ b/foo\x1b[0m (esc)
164 \x1b[0;35m@@ -10,3 +10,4 @@\x1b[0m (esc)
165 foo-9
166 foo-10
167 foo-11
168 \x1b[0;32m+line\x1b[0m (esc)
169
170
147 171 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now