##// END OF EJS Templates
record: change return value of recording code...
Laurent Charignon -
r24476:1378f20c default
parent child Browse files
Show More
@@ -1,3263 +1,3261
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, cStringIO, shutil
11 11 import util, scmutil, templater, patch, error, templatekw, revlog, copies
12 12 import match as matchmod
13 13 import context, repair, graphmod, revset, phases, obsolete, pathutil
14 14 import changelog
15 15 import bookmarks
16 16 import encoding
17 17 import crecord as crecordmod
18 18 import lock as lockmod
19 19
20 20 def parsealiases(cmd):
21 21 return cmd.lstrip("^").split("|")
22 22
23 23 def setupwrapcolorwrite(ui):
24 24 # wrap ui.write so diff output can be labeled/colorized
25 25 def wrapwrite(orig, *args, **kw):
26 26 label = kw.pop('label', '')
27 27 for chunk, l in patch.difflabel(lambda: args):
28 28 orig(chunk, label=label + l)
29 29
30 30 oldwrite = ui.write
31 31 def wrap(*args, **kwargs):
32 32 return wrapwrite(oldwrite, *args, **kwargs)
33 33 setattr(ui, 'write', wrap)
34 34 return oldwrite
35 35
36 36 def filterchunks(ui, originalhunks, usecurses, testfile):
37 37 if usecurses:
38 38 if testfile:
39 39 recordfn = crecordmod.testdecorator(testfile,
40 40 crecordmod.testchunkselector)
41 41 else:
42 42 recordfn = crecordmod.chunkselector
43 43
44 44 return crecordmod.filterpatch(ui, originalhunks, recordfn)
45 45
46 46 else:
47 47 return patch.filterpatch(ui, originalhunks)
48 48
49 49 def recordfilter(ui, originalhunks):
50 50 usecurses = ui.configbool('experimental', 'crecord', False)
51 51 testfile = ui.config('experimental', 'crecordtest', None)
52 52 oldwrite = setupwrapcolorwrite(ui)
53 53 try:
54 54 newchunks = filterchunks(ui, originalhunks, usecurses, testfile)
55 55 finally:
56 56 ui.write = oldwrite
57 57 return newchunks
58 58
59 59 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
60 60 filterfn, *pats, **opts):
61 61 import merge as mergemod
62 62 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
63 63 ishunk = lambda x: isinstance(x, hunkclasses)
64 64
65 65 if not ui.interactive():
66 66 raise util.Abort(_('running non-interactively, use %s instead') %
67 67 cmdsuggest)
68 68
69 69 # make sure username is set before going interactive
70 70 if not opts.get('user'):
71 71 ui.username() # raise exception, username not provided
72 72
73 73 def recordfunc(ui, repo, message, match, opts):
74 74 """This is generic record driver.
75 75
76 76 Its job is to interactively filter local changes, and
77 77 accordingly prepare working directory into a state in which the
78 78 job can be delegated to a non-interactive commit command such as
79 79 'commit' or 'qrefresh'.
80 80
81 81 After the actual job is done by non-interactive command, the
82 82 working directory is restored to its original state.
83 83
84 84 In the end we'll record interesting changes, and everything else
85 85 will be left in place, so the user can continue working.
86 86 """
87 87
88 88 checkunfinished(repo, commit=True)
89 89 merge = len(repo[None].parents()) > 1
90 90 if merge:
91 91 raise util.Abort(_('cannot partially commit a merge '
92 92 '(use "hg commit" instead)'))
93 93
94 94 status = repo.status(match=match)
95 95 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
96 96 diffopts.nodates = True
97 97 diffopts.git = True
98 98 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
99 99 originalchunks = patch.parsepatch(originaldiff)
100 100
101 101 # 1. filter patch, so we have intending-to apply subset of it
102 102 try:
103 103 chunks = filterfn(ui, originalchunks)
104 104 except patch.PatchError, err:
105 105 raise util.Abort(_('error parsing patch: %s') % err)
106 106
107 107 contenders = set()
108 108 for h in chunks:
109 109 try:
110 110 contenders.update(set(h.files()))
111 111 except AttributeError:
112 112 pass
113 113
114 114 changed = status.modified + status.added + status.removed
115 115 newfiles = [f for f in changed if f in contenders]
116 116 if not newfiles:
117 117 ui.status(_('no changes to record\n'))
118 118 return 0
119 119
120 120 newandmodifiedfiles = set()
121 121 for h in chunks:
122 122 isnew = h.filename() in status.added
123 123 if ishunk(h) and isnew and not h in originalchunks:
124 124 newandmodifiedfiles.add(h.filename())
125 125
126 126 modified = set(status.modified)
127 127
128 128 # 2. backup changed files, so we can restore them in the end
129 129
130 130 if backupall:
131 131 tobackup = changed
132 132 else:
133 133 tobackup = [f for f in newfiles
134 134 if f in modified or f in newandmodifiedfiles]
135 135
136 136 backups = {}
137 137 if tobackup:
138 138 backupdir = repo.join('record-backups')
139 139 try:
140 140 os.mkdir(backupdir)
141 141 except OSError, err:
142 142 if err.errno != errno.EEXIST:
143 143 raise
144 144 try:
145 145 # backup continues
146 146 for f in tobackup:
147 147 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
148 148 dir=backupdir)
149 149 os.close(fd)
150 150 ui.debug('backup %r as %r\n' % (f, tmpname))
151 151 util.copyfile(repo.wjoin(f), tmpname)
152 152 shutil.copystat(repo.wjoin(f), tmpname)
153 153 backups[f] = tmpname
154 154
155 155 fp = cStringIO.StringIO()
156 156 for c in chunks:
157 157 fname = c.filename()
158 158 if fname in backups or fname in newandmodifiedfiles:
159 159 c.write(fp)
160 160 dopatch = fp.tell()
161 161 fp.seek(0)
162 162
163 163 [os.unlink(c) for c in newandmodifiedfiles]
164 164
165 165 # 3a. apply filtered patch to clean repo (clean)
166 166 if backups:
167 167 # Equivalent to hg.revert
168 168 choices = lambda key: key in backups
169 169 mergemod.update(repo, repo.dirstate.p1(),
170 170 False, True, choices)
171 171
172 172
173 173 # 3b. (apply)
174 174 if dopatch:
175 175 try:
176 176 ui.debug('applying patch\n')
177 177 ui.debug(fp.getvalue())
178 178 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
179 179 except patch.PatchError, err:
180 180 raise util.Abort(str(err))
181 181 del fp
182 182
183 183 # 4. We prepared working directory according to filtered
184 184 # patch. Now is the time to delegate the job to
185 185 # commit/qrefresh or the like!
186 186
187 187 # Make all of the pathnames absolute.
188 188 newfiles = [repo.wjoin(nf) for nf in newfiles]
189 commitfunc(ui, repo, *newfiles, **opts)
190
191 return 0
189 return commitfunc(ui, repo, *newfiles, **opts)
192 190 finally:
193 191 # 5. finally restore backed-up files
194 192 try:
195 193 for realname, tmpname in backups.iteritems():
196 194 ui.debug('restoring %r to %r\n' % (tmpname, realname))
197 195 util.copyfile(tmpname, repo.wjoin(realname))
198 196 # Our calls to copystat() here and above are a
199 197 # hack to trick any editors that have f open that
200 198 # we haven't modified them.
201 199 #
202 200 # Also note that this racy as an editor could
203 201 # notice the file's mtime before we've finished
204 202 # writing it.
205 203 shutil.copystat(tmpname, repo.wjoin(realname))
206 204 os.unlink(tmpname)
207 205 if tobackup:
208 206 os.rmdir(backupdir)
209 207 except OSError:
210 208 pass
211 209
212 210 return commit(ui, repo, recordfunc, pats, opts)
213 211
214 212 def findpossible(cmd, table, strict=False):
215 213 """
216 214 Return cmd -> (aliases, command table entry)
217 215 for each matching command.
218 216 Return debug commands (or their aliases) only if no normal command matches.
219 217 """
220 218 choice = {}
221 219 debugchoice = {}
222 220
223 221 if cmd in table:
224 222 # short-circuit exact matches, "log" alias beats "^log|history"
225 223 keys = [cmd]
226 224 else:
227 225 keys = table.keys()
228 226
229 227 allcmds = []
230 228 for e in keys:
231 229 aliases = parsealiases(e)
232 230 allcmds.extend(aliases)
233 231 found = None
234 232 if cmd in aliases:
235 233 found = cmd
236 234 elif not strict:
237 235 for a in aliases:
238 236 if a.startswith(cmd):
239 237 found = a
240 238 break
241 239 if found is not None:
242 240 if aliases[0].startswith("debug") or found.startswith("debug"):
243 241 debugchoice[found] = (aliases, table[e])
244 242 else:
245 243 choice[found] = (aliases, table[e])
246 244
247 245 if not choice and debugchoice:
248 246 choice = debugchoice
249 247
250 248 return choice, allcmds
251 249
252 250 def findcmd(cmd, table, strict=True):
253 251 """Return (aliases, command table entry) for command string."""
254 252 choice, allcmds = findpossible(cmd, table, strict)
255 253
256 254 if cmd in choice:
257 255 return choice[cmd]
258 256
259 257 if len(choice) > 1:
260 258 clist = choice.keys()
261 259 clist.sort()
262 260 raise error.AmbiguousCommand(cmd, clist)
263 261
264 262 if choice:
265 263 return choice.values()[0]
266 264
267 265 raise error.UnknownCommand(cmd, allcmds)
268 266
269 267 def findrepo(p):
270 268 while not os.path.isdir(os.path.join(p, ".hg")):
271 269 oldp, p = p, os.path.dirname(p)
272 270 if p == oldp:
273 271 return None
274 272
275 273 return p
276 274
277 275 def bailifchanged(repo, merge=True):
278 276 if merge and repo.dirstate.p2() != nullid:
279 277 raise util.Abort(_('outstanding uncommitted merge'))
280 278 modified, added, removed, deleted = repo.status()[:4]
281 279 if modified or added or removed or deleted:
282 280 raise util.Abort(_('uncommitted changes'))
283 281 ctx = repo[None]
284 282 for s in sorted(ctx.substate):
285 283 ctx.sub(s).bailifchanged()
286 284
287 285 def logmessage(ui, opts):
288 286 """ get the log message according to -m and -l option """
289 287 message = opts.get('message')
290 288 logfile = opts.get('logfile')
291 289
292 290 if message and logfile:
293 291 raise util.Abort(_('options --message and --logfile are mutually '
294 292 'exclusive'))
295 293 if not message and logfile:
296 294 try:
297 295 if logfile == '-':
298 296 message = ui.fin.read()
299 297 else:
300 298 message = '\n'.join(util.readfile(logfile).splitlines())
301 299 except IOError, inst:
302 300 raise util.Abort(_("can't read commit message '%s': %s") %
303 301 (logfile, inst.strerror))
304 302 return message
305 303
306 304 def mergeeditform(ctxorbool, baseformname):
307 305 """return appropriate editform name (referencing a committemplate)
308 306
309 307 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
310 308 merging is committed.
311 309
312 310 This returns baseformname with '.merge' appended if it is a merge,
313 311 otherwise '.normal' is appended.
314 312 """
315 313 if isinstance(ctxorbool, bool):
316 314 if ctxorbool:
317 315 return baseformname + ".merge"
318 316 elif 1 < len(ctxorbool.parents()):
319 317 return baseformname + ".merge"
320 318
321 319 return baseformname + ".normal"
322 320
323 321 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
324 322 editform='', **opts):
325 323 """get appropriate commit message editor according to '--edit' option
326 324
327 325 'finishdesc' is a function to be called with edited commit message
328 326 (= 'description' of the new changeset) just after editing, but
329 327 before checking empty-ness. It should return actual text to be
330 328 stored into history. This allows to change description before
331 329 storing.
332 330
333 331 'extramsg' is a extra message to be shown in the editor instead of
334 332 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
335 333 is automatically added.
336 334
337 335 'editform' is a dot-separated list of names, to distinguish
338 336 the purpose of commit text editing.
339 337
340 338 'getcommiteditor' returns 'commitforceeditor' regardless of
341 339 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
342 340 they are specific for usage in MQ.
343 341 """
344 342 if edit or finishdesc or extramsg:
345 343 return lambda r, c, s: commitforceeditor(r, c, s,
346 344 finishdesc=finishdesc,
347 345 extramsg=extramsg,
348 346 editform=editform)
349 347 elif editform:
350 348 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
351 349 else:
352 350 return commiteditor
353 351
354 352 def loglimit(opts):
355 353 """get the log limit according to option -l/--limit"""
356 354 limit = opts.get('limit')
357 355 if limit:
358 356 try:
359 357 limit = int(limit)
360 358 except ValueError:
361 359 raise util.Abort(_('limit must be a positive integer'))
362 360 if limit <= 0:
363 361 raise util.Abort(_('limit must be positive'))
364 362 else:
365 363 limit = None
366 364 return limit
367 365
368 366 def makefilename(repo, pat, node, desc=None,
369 367 total=None, seqno=None, revwidth=None, pathname=None):
370 368 node_expander = {
371 369 'H': lambda: hex(node),
372 370 'R': lambda: str(repo.changelog.rev(node)),
373 371 'h': lambda: short(node),
374 372 'm': lambda: re.sub('[^\w]', '_', str(desc))
375 373 }
376 374 expander = {
377 375 '%': lambda: '%',
378 376 'b': lambda: os.path.basename(repo.root),
379 377 }
380 378
381 379 try:
382 380 if node:
383 381 expander.update(node_expander)
384 382 if node:
385 383 expander['r'] = (lambda:
386 384 str(repo.changelog.rev(node)).zfill(revwidth or 0))
387 385 if total is not None:
388 386 expander['N'] = lambda: str(total)
389 387 if seqno is not None:
390 388 expander['n'] = lambda: str(seqno)
391 389 if total is not None and seqno is not None:
392 390 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
393 391 if pathname is not None:
394 392 expander['s'] = lambda: os.path.basename(pathname)
395 393 expander['d'] = lambda: os.path.dirname(pathname) or '.'
396 394 expander['p'] = lambda: pathname
397 395
398 396 newname = []
399 397 patlen = len(pat)
400 398 i = 0
401 399 while i < patlen:
402 400 c = pat[i]
403 401 if c == '%':
404 402 i += 1
405 403 c = pat[i]
406 404 c = expander[c]()
407 405 newname.append(c)
408 406 i += 1
409 407 return ''.join(newname)
410 408 except KeyError, inst:
411 409 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
412 410 inst.args[0])
413 411
414 412 def makefileobj(repo, pat, node=None, desc=None, total=None,
415 413 seqno=None, revwidth=None, mode='wb', modemap=None,
416 414 pathname=None):
417 415
418 416 writable = mode not in ('r', 'rb')
419 417
420 418 if not pat or pat == '-':
421 419 if writable:
422 420 fp = repo.ui.fout
423 421 else:
424 422 fp = repo.ui.fin
425 423 if util.safehasattr(fp, 'fileno'):
426 424 return os.fdopen(os.dup(fp.fileno()), mode)
427 425 else:
428 426 # if this fp can't be duped properly, return
429 427 # a dummy object that can be closed
430 428 class wrappedfileobj(object):
431 429 noop = lambda x: None
432 430 def __init__(self, f):
433 431 self.f = f
434 432 def __getattr__(self, attr):
435 433 if attr == 'close':
436 434 return self.noop
437 435 else:
438 436 return getattr(self.f, attr)
439 437
440 438 return wrappedfileobj(fp)
441 439 if util.safehasattr(pat, 'write') and writable:
442 440 return pat
443 441 if util.safehasattr(pat, 'read') and 'r' in mode:
444 442 return pat
445 443 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
446 444 if modemap is not None:
447 445 mode = modemap.get(fn, mode)
448 446 if mode == 'wb':
449 447 modemap[fn] = 'ab'
450 448 return open(fn, mode)
451 449
452 450 def openrevlog(repo, cmd, file_, opts):
453 451 """opens the changelog, manifest, a filelog or a given revlog"""
454 452 cl = opts['changelog']
455 453 mf = opts['manifest']
456 454 msg = None
457 455 if cl and mf:
458 456 msg = _('cannot specify --changelog and --manifest at the same time')
459 457 elif cl or mf:
460 458 if file_:
461 459 msg = _('cannot specify filename with --changelog or --manifest')
462 460 elif not repo:
463 461 msg = _('cannot specify --changelog or --manifest '
464 462 'without a repository')
465 463 if msg:
466 464 raise util.Abort(msg)
467 465
468 466 r = None
469 467 if repo:
470 468 if cl:
471 469 r = repo.unfiltered().changelog
472 470 elif mf:
473 471 r = repo.manifest
474 472 elif file_:
475 473 filelog = repo.file(file_)
476 474 if len(filelog):
477 475 r = filelog
478 476 if not r:
479 477 if not file_:
480 478 raise error.CommandError(cmd, _('invalid arguments'))
481 479 if not os.path.isfile(file_):
482 480 raise util.Abort(_("revlog '%s' not found") % file_)
483 481 r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
484 482 file_[:-2] + ".i")
485 483 return r
486 484
487 485 def copy(ui, repo, pats, opts, rename=False):
488 486 # called with the repo lock held
489 487 #
490 488 # hgsep => pathname that uses "/" to separate directories
491 489 # ossep => pathname that uses os.sep to separate directories
492 490 cwd = repo.getcwd()
493 491 targets = {}
494 492 after = opts.get("after")
495 493 dryrun = opts.get("dry_run")
496 494 wctx = repo[None]
497 495
498 496 def walkpat(pat):
499 497 srcs = []
500 498 if after:
501 499 badstates = '?'
502 500 else:
503 501 badstates = '?r'
504 502 m = scmutil.match(repo[None], [pat], opts, globbed=True)
505 503 for abs in repo.walk(m):
506 504 state = repo.dirstate[abs]
507 505 rel = m.rel(abs)
508 506 exact = m.exact(abs)
509 507 if state in badstates:
510 508 if exact and state == '?':
511 509 ui.warn(_('%s: not copying - file is not managed\n') % rel)
512 510 if exact and state == 'r':
513 511 ui.warn(_('%s: not copying - file has been marked for'
514 512 ' remove\n') % rel)
515 513 continue
516 514 # abs: hgsep
517 515 # rel: ossep
518 516 srcs.append((abs, rel, exact))
519 517 return srcs
520 518
521 519 # abssrc: hgsep
522 520 # relsrc: ossep
523 521 # otarget: ossep
524 522 def copyfile(abssrc, relsrc, otarget, exact):
525 523 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
526 524 if '/' in abstarget:
527 525 # We cannot normalize abstarget itself, this would prevent
528 526 # case only renames, like a => A.
529 527 abspath, absname = abstarget.rsplit('/', 1)
530 528 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
531 529 reltarget = repo.pathto(abstarget, cwd)
532 530 target = repo.wjoin(abstarget)
533 531 src = repo.wjoin(abssrc)
534 532 state = repo.dirstate[abstarget]
535 533
536 534 scmutil.checkportable(ui, abstarget)
537 535
538 536 # check for collisions
539 537 prevsrc = targets.get(abstarget)
540 538 if prevsrc is not None:
541 539 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
542 540 (reltarget, repo.pathto(abssrc, cwd),
543 541 repo.pathto(prevsrc, cwd)))
544 542 return
545 543
546 544 # check for overwrites
547 545 exists = os.path.lexists(target)
548 546 samefile = False
549 547 if exists and abssrc != abstarget:
550 548 if (repo.dirstate.normalize(abssrc) ==
551 549 repo.dirstate.normalize(abstarget)):
552 550 if not rename:
553 551 ui.warn(_("%s: can't copy - same file\n") % reltarget)
554 552 return
555 553 exists = False
556 554 samefile = True
557 555
558 556 if not after and exists or after and state in 'mn':
559 557 if not opts['force']:
560 558 ui.warn(_('%s: not overwriting - file exists\n') %
561 559 reltarget)
562 560 return
563 561
564 562 if after:
565 563 if not exists:
566 564 if rename:
567 565 ui.warn(_('%s: not recording move - %s does not exist\n') %
568 566 (relsrc, reltarget))
569 567 else:
570 568 ui.warn(_('%s: not recording copy - %s does not exist\n') %
571 569 (relsrc, reltarget))
572 570 return
573 571 elif not dryrun:
574 572 try:
575 573 if exists:
576 574 os.unlink(target)
577 575 targetdir = os.path.dirname(target) or '.'
578 576 if not os.path.isdir(targetdir):
579 577 os.makedirs(targetdir)
580 578 if samefile:
581 579 tmp = target + "~hgrename"
582 580 os.rename(src, tmp)
583 581 os.rename(tmp, target)
584 582 else:
585 583 util.copyfile(src, target)
586 584 srcexists = True
587 585 except IOError, inst:
588 586 if inst.errno == errno.ENOENT:
589 587 ui.warn(_('%s: deleted in working directory\n') % relsrc)
590 588 srcexists = False
591 589 else:
592 590 ui.warn(_('%s: cannot copy - %s\n') %
593 591 (relsrc, inst.strerror))
594 592 return True # report a failure
595 593
596 594 if ui.verbose or not exact:
597 595 if rename:
598 596 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
599 597 else:
600 598 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
601 599
602 600 targets[abstarget] = abssrc
603 601
604 602 # fix up dirstate
605 603 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
606 604 dryrun=dryrun, cwd=cwd)
607 605 if rename and not dryrun:
608 606 if not after and srcexists and not samefile:
609 607 util.unlinkpath(repo.wjoin(abssrc))
610 608 wctx.forget([abssrc])
611 609
612 610 # pat: ossep
613 611 # dest ossep
614 612 # srcs: list of (hgsep, hgsep, ossep, bool)
615 613 # return: function that takes hgsep and returns ossep
616 614 def targetpathfn(pat, dest, srcs):
617 615 if os.path.isdir(pat):
618 616 abspfx = pathutil.canonpath(repo.root, cwd, pat)
619 617 abspfx = util.localpath(abspfx)
620 618 if destdirexists:
621 619 striplen = len(os.path.split(abspfx)[0])
622 620 else:
623 621 striplen = len(abspfx)
624 622 if striplen:
625 623 striplen += len(os.sep)
626 624 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
627 625 elif destdirexists:
628 626 res = lambda p: os.path.join(dest,
629 627 os.path.basename(util.localpath(p)))
630 628 else:
631 629 res = lambda p: dest
632 630 return res
633 631
634 632 # pat: ossep
635 633 # dest ossep
636 634 # srcs: list of (hgsep, hgsep, ossep, bool)
637 635 # return: function that takes hgsep and returns ossep
638 636 def targetpathafterfn(pat, dest, srcs):
639 637 if matchmod.patkind(pat):
640 638 # a mercurial pattern
641 639 res = lambda p: os.path.join(dest,
642 640 os.path.basename(util.localpath(p)))
643 641 else:
644 642 abspfx = pathutil.canonpath(repo.root, cwd, pat)
645 643 if len(abspfx) < len(srcs[0][0]):
646 644 # A directory. Either the target path contains the last
647 645 # component of the source path or it does not.
648 646 def evalpath(striplen):
649 647 score = 0
650 648 for s in srcs:
651 649 t = os.path.join(dest, util.localpath(s[0])[striplen:])
652 650 if os.path.lexists(t):
653 651 score += 1
654 652 return score
655 653
656 654 abspfx = util.localpath(abspfx)
657 655 striplen = len(abspfx)
658 656 if striplen:
659 657 striplen += len(os.sep)
660 658 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
661 659 score = evalpath(striplen)
662 660 striplen1 = len(os.path.split(abspfx)[0])
663 661 if striplen1:
664 662 striplen1 += len(os.sep)
665 663 if evalpath(striplen1) > score:
666 664 striplen = striplen1
667 665 res = lambda p: os.path.join(dest,
668 666 util.localpath(p)[striplen:])
669 667 else:
670 668 # a file
671 669 if destdirexists:
672 670 res = lambda p: os.path.join(dest,
673 671 os.path.basename(util.localpath(p)))
674 672 else:
675 673 res = lambda p: dest
676 674 return res
677 675
678 676
679 677 pats = scmutil.expandpats(pats)
680 678 if not pats:
681 679 raise util.Abort(_('no source or destination specified'))
682 680 if len(pats) == 1:
683 681 raise util.Abort(_('no destination specified'))
684 682 dest = pats.pop()
685 683 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
686 684 if not destdirexists:
687 685 if len(pats) > 1 or matchmod.patkind(pats[0]):
688 686 raise util.Abort(_('with multiple sources, destination must be an '
689 687 'existing directory'))
690 688 if util.endswithsep(dest):
691 689 raise util.Abort(_('destination %s is not a directory') % dest)
692 690
693 691 tfn = targetpathfn
694 692 if after:
695 693 tfn = targetpathafterfn
696 694 copylist = []
697 695 for pat in pats:
698 696 srcs = walkpat(pat)
699 697 if not srcs:
700 698 continue
701 699 copylist.append((tfn(pat, dest, srcs), srcs))
702 700 if not copylist:
703 701 raise util.Abort(_('no files to copy'))
704 702
705 703 errors = 0
706 704 for targetpath, srcs in copylist:
707 705 for abssrc, relsrc, exact in srcs:
708 706 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
709 707 errors += 1
710 708
711 709 if errors:
712 710 ui.warn(_('(consider using --after)\n'))
713 711
714 712 return errors != 0
715 713
716 714 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
717 715 runargs=None, appendpid=False):
718 716 '''Run a command as a service.'''
719 717
720 718 def writepid(pid):
721 719 if opts['pid_file']:
722 720 if appendpid:
723 721 mode = 'a'
724 722 else:
725 723 mode = 'w'
726 724 fp = open(opts['pid_file'], mode)
727 725 fp.write(str(pid) + '\n')
728 726 fp.close()
729 727
730 728 if opts['daemon'] and not opts['daemon_pipefds']:
731 729 # Signal child process startup with file removal
732 730 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
733 731 os.close(lockfd)
734 732 try:
735 733 if not runargs:
736 734 runargs = util.hgcmd() + sys.argv[1:]
737 735 runargs.append('--daemon-pipefds=%s' % lockpath)
738 736 # Don't pass --cwd to the child process, because we've already
739 737 # changed directory.
740 738 for i in xrange(1, len(runargs)):
741 739 if runargs[i].startswith('--cwd='):
742 740 del runargs[i]
743 741 break
744 742 elif runargs[i].startswith('--cwd'):
745 743 del runargs[i:i + 2]
746 744 break
747 745 def condfn():
748 746 return not os.path.exists(lockpath)
749 747 pid = util.rundetached(runargs, condfn)
750 748 if pid < 0:
751 749 raise util.Abort(_('child process failed to start'))
752 750 writepid(pid)
753 751 finally:
754 752 try:
755 753 os.unlink(lockpath)
756 754 except OSError, e:
757 755 if e.errno != errno.ENOENT:
758 756 raise
759 757 if parentfn:
760 758 return parentfn(pid)
761 759 else:
762 760 return
763 761
764 762 if initfn:
765 763 initfn()
766 764
767 765 if not opts['daemon']:
768 766 writepid(os.getpid())
769 767
770 768 if opts['daemon_pipefds']:
771 769 lockpath = opts['daemon_pipefds']
772 770 try:
773 771 os.setsid()
774 772 except AttributeError:
775 773 pass
776 774 os.unlink(lockpath)
777 775 util.hidewindow()
778 776 sys.stdout.flush()
779 777 sys.stderr.flush()
780 778
781 779 nullfd = os.open(os.devnull, os.O_RDWR)
782 780 logfilefd = nullfd
783 781 if logfile:
784 782 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
785 783 os.dup2(nullfd, 0)
786 784 os.dup2(logfilefd, 1)
787 785 os.dup2(logfilefd, 2)
788 786 if nullfd not in (0, 1, 2):
789 787 os.close(nullfd)
790 788 if logfile and logfilefd not in (0, 1, 2):
791 789 os.close(logfilefd)
792 790
793 791 if runfn:
794 792 return runfn()
795 793
796 794 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
797 795 """Utility function used by commands.import to import a single patch
798 796
799 797 This function is explicitly defined here to help the evolve extension to
800 798 wrap this part of the import logic.
801 799
802 800 The API is currently a bit ugly because it a simple code translation from
803 801 the import command. Feel free to make it better.
804 802
805 803 :hunk: a patch (as a binary string)
806 804 :parents: nodes that will be parent of the created commit
807 805 :opts: the full dict of option passed to the import command
808 806 :msgs: list to save commit message to.
809 807 (used in case we need to save it when failing)
810 808 :updatefunc: a function that update a repo to a given node
811 809 updatefunc(<repo>, <node>)
812 810 """
813 811 tmpname, message, user, date, branch, nodeid, p1, p2 = \
814 812 patch.extract(ui, hunk)
815 813
816 814 update = not opts.get('bypass')
817 815 strip = opts["strip"]
818 816 prefix = opts["prefix"]
819 817 sim = float(opts.get('similarity') or 0)
820 818 if not tmpname:
821 819 return (None, None, False)
822 820 msg = _('applied to working directory')
823 821
824 822 rejects = False
825 823
826 824 try:
827 825 cmdline_message = logmessage(ui, opts)
828 826 if cmdline_message:
829 827 # pickup the cmdline msg
830 828 message = cmdline_message
831 829 elif message:
832 830 # pickup the patch msg
833 831 message = message.strip()
834 832 else:
835 833 # launch the editor
836 834 message = None
837 835 ui.debug('message:\n%s\n' % message)
838 836
839 837 if len(parents) == 1:
840 838 parents.append(repo[nullid])
841 839 if opts.get('exact'):
842 840 if not nodeid or not p1:
843 841 raise util.Abort(_('not a Mercurial patch'))
844 842 p1 = repo[p1]
845 843 p2 = repo[p2 or nullid]
846 844 elif p2:
847 845 try:
848 846 p1 = repo[p1]
849 847 p2 = repo[p2]
850 848 # Without any options, consider p2 only if the
851 849 # patch is being applied on top of the recorded
852 850 # first parent.
853 851 if p1 != parents[0]:
854 852 p1 = parents[0]
855 853 p2 = repo[nullid]
856 854 except error.RepoError:
857 855 p1, p2 = parents
858 856 if p2.node() == nullid:
859 857 ui.warn(_("warning: import the patch as a normal revision\n"
860 858 "(use --exact to import the patch as a merge)\n"))
861 859 else:
862 860 p1, p2 = parents
863 861
864 862 n = None
865 863 if update:
866 864 repo.dirstate.beginparentchange()
867 865 if p1 != parents[0]:
868 866 updatefunc(repo, p1.node())
869 867 if p2 != parents[1]:
870 868 repo.setparents(p1.node(), p2.node())
871 869
872 870 if opts.get('exact') or opts.get('import_branch'):
873 871 repo.dirstate.setbranch(branch or 'default')
874 872
875 873 partial = opts.get('partial', False)
876 874 files = set()
877 875 try:
878 876 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
879 877 files=files, eolmode=None, similarity=sim / 100.0)
880 878 except patch.PatchError, e:
881 879 if not partial:
882 880 raise util.Abort(str(e))
883 881 if partial:
884 882 rejects = True
885 883
886 884 files = list(files)
887 885 if opts.get('no_commit'):
888 886 if message:
889 887 msgs.append(message)
890 888 else:
891 889 if opts.get('exact') or p2:
892 890 # If you got here, you either use --force and know what
893 891 # you are doing or used --exact or a merge patch while
894 892 # being updated to its first parent.
895 893 m = None
896 894 else:
897 895 m = scmutil.matchfiles(repo, files or [])
898 896 editform = mergeeditform(repo[None], 'import.normal')
899 897 if opts.get('exact'):
900 898 editor = None
901 899 else:
902 900 editor = getcommiteditor(editform=editform, **opts)
903 901 n = repo.commit(message, opts.get('user') or user,
904 902 opts.get('date') or date, match=m,
905 903 editor=editor, force=partial)
906 904 repo.dirstate.endparentchange()
907 905 else:
908 906 if opts.get('exact') or opts.get('import_branch'):
909 907 branch = branch or 'default'
910 908 else:
911 909 branch = p1.branch()
912 910 store = patch.filestore()
913 911 try:
914 912 files = set()
915 913 try:
916 914 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
917 915 files, eolmode=None)
918 916 except patch.PatchError, e:
919 917 raise util.Abort(str(e))
920 918 if opts.get('exact'):
921 919 editor = None
922 920 else:
923 921 editor = getcommiteditor(editform='import.bypass')
924 922 memctx = context.makememctx(repo, (p1.node(), p2.node()),
925 923 message,
926 924 opts.get('user') or user,
927 925 opts.get('date') or date,
928 926 branch, files, store,
929 927 editor=editor)
930 928 n = memctx.commit()
931 929 finally:
932 930 store.close()
933 931 if opts.get('exact') and opts.get('no_commit'):
934 932 # --exact with --no-commit is still useful in that it does merge
935 933 # and branch bits
936 934 ui.warn(_("warning: can't check exact import with --no-commit\n"))
937 935 elif opts.get('exact') and hex(n) != nodeid:
938 936 raise util.Abort(_('patch is damaged or loses information'))
939 937 if n:
940 938 # i18n: refers to a short changeset id
941 939 msg = _('created %s') % short(n)
942 940 return (msg, n, rejects)
943 941 finally:
944 942 os.unlink(tmpname)
945 943
946 944 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
947 945 opts=None):
948 946 '''export changesets as hg patches.'''
949 947
950 948 total = len(revs)
951 949 revwidth = max([len(str(rev)) for rev in revs])
952 950 filemode = {}
953 951
954 952 def single(rev, seqno, fp):
955 953 ctx = repo[rev]
956 954 node = ctx.node()
957 955 parents = [p.node() for p in ctx.parents() if p]
958 956 branch = ctx.branch()
959 957 if switch_parent:
960 958 parents.reverse()
961 959
962 960 if parents:
963 961 prev = parents[0]
964 962 else:
965 963 prev = nullid
966 964
967 965 shouldclose = False
968 966 if not fp and len(template) > 0:
969 967 desc_lines = ctx.description().rstrip().split('\n')
970 968 desc = desc_lines[0] #Commit always has a first line.
971 969 fp = makefileobj(repo, template, node, desc=desc, total=total,
972 970 seqno=seqno, revwidth=revwidth, mode='wb',
973 971 modemap=filemode)
974 972 if fp != template:
975 973 shouldclose = True
976 974 if fp and fp != sys.stdout and util.safehasattr(fp, 'name'):
977 975 repo.ui.note("%s\n" % fp.name)
978 976
979 977 if not fp:
980 978 write = repo.ui.write
981 979 else:
982 980 def write(s, **kw):
983 981 fp.write(s)
984 982
985 983
986 984 write("# HG changeset patch\n")
987 985 write("# User %s\n" % ctx.user())
988 986 write("# Date %d %d\n" % ctx.date())
989 987 write("# %s\n" % util.datestr(ctx.date()))
990 988 if branch and branch != 'default':
991 989 write("# Branch %s\n" % branch)
992 990 write("# Node ID %s\n" % hex(node))
993 991 write("# Parent %s\n" % hex(prev))
994 992 if len(parents) > 1:
995 993 write("# Parent %s\n" % hex(parents[1]))
996 994 write(ctx.description().rstrip())
997 995 write("\n\n")
998 996
999 997 for chunk, label in patch.diffui(repo, prev, node, opts=opts):
1000 998 write(chunk, label=label)
1001 999
1002 1000 if shouldclose:
1003 1001 fp.close()
1004 1002
1005 1003 for seqno, rev in enumerate(revs):
1006 1004 single(rev, seqno + 1, fp)
1007 1005
1008 1006 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
1009 1007 changes=None, stat=False, fp=None, prefix='',
1010 1008 root='', listsubrepos=False):
1011 1009 '''show diff or diffstat.'''
1012 1010 if fp is None:
1013 1011 write = ui.write
1014 1012 else:
1015 1013 def write(s, **kw):
1016 1014 fp.write(s)
1017 1015
1018 1016 if root:
1019 1017 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
1020 1018 else:
1021 1019 relroot = ''
1022 1020 if relroot != '':
1023 1021 # XXX relative roots currently don't work if the root is within a
1024 1022 # subrepo
1025 1023 uirelroot = match.uipath(relroot)
1026 1024 relroot += '/'
1027 1025 for matchroot in match.files():
1028 1026 if not matchroot.startswith(relroot):
1029 1027 ui.warn(_('warning: %s not inside relative root %s\n') % (
1030 1028 match.uipath(matchroot), uirelroot))
1031 1029
1032 1030 if stat:
1033 1031 diffopts = diffopts.copy(context=0)
1034 1032 width = 80
1035 1033 if not ui.plain():
1036 1034 width = ui.termwidth()
1037 1035 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
1038 1036 prefix=prefix, relroot=relroot)
1039 1037 for chunk, label in patch.diffstatui(util.iterlines(chunks),
1040 1038 width=width,
1041 1039 git=diffopts.git):
1042 1040 write(chunk, label=label)
1043 1041 else:
1044 1042 for chunk, label in patch.diffui(repo, node1, node2, match,
1045 1043 changes, diffopts, prefix=prefix,
1046 1044 relroot=relroot):
1047 1045 write(chunk, label=label)
1048 1046
1049 1047 if listsubrepos:
1050 1048 ctx1 = repo[node1]
1051 1049 ctx2 = repo[node2]
1052 1050 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
1053 1051 tempnode2 = node2
1054 1052 try:
1055 1053 if node2 is not None:
1056 1054 tempnode2 = ctx2.substate[subpath][1]
1057 1055 except KeyError:
1058 1056 # A subrepo that existed in node1 was deleted between node1 and
1059 1057 # node2 (inclusive). Thus, ctx2's substate won't contain that
1060 1058 # subpath. The best we can do is to ignore it.
1061 1059 tempnode2 = None
1062 1060 submatch = matchmod.narrowmatcher(subpath, match)
1063 1061 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
1064 1062 stat=stat, fp=fp, prefix=prefix)
1065 1063
1066 1064 class changeset_printer(object):
1067 1065 '''show changeset information when templating not requested.'''
1068 1066
1069 1067 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1070 1068 self.ui = ui
1071 1069 self.repo = repo
1072 1070 self.buffered = buffered
1073 1071 self.matchfn = matchfn
1074 1072 self.diffopts = diffopts
1075 1073 self.header = {}
1076 1074 self.hunk = {}
1077 1075 self.lastheader = None
1078 1076 self.footer = None
1079 1077
1080 1078 def flush(self, rev):
1081 1079 if rev in self.header:
1082 1080 h = self.header[rev]
1083 1081 if h != self.lastheader:
1084 1082 self.lastheader = h
1085 1083 self.ui.write(h)
1086 1084 del self.header[rev]
1087 1085 if rev in self.hunk:
1088 1086 self.ui.write(self.hunk[rev])
1089 1087 del self.hunk[rev]
1090 1088 return 1
1091 1089 return 0
1092 1090
1093 1091 def close(self):
1094 1092 if self.footer:
1095 1093 self.ui.write(self.footer)
1096 1094
1097 1095 def show(self, ctx, copies=None, matchfn=None, **props):
1098 1096 if self.buffered:
1099 1097 self.ui.pushbuffer()
1100 1098 self._show(ctx, copies, matchfn, props)
1101 1099 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
1102 1100 else:
1103 1101 self._show(ctx, copies, matchfn, props)
1104 1102
1105 1103 def _show(self, ctx, copies, matchfn, props):
1106 1104 '''show a single changeset or file revision'''
1107 1105 changenode = ctx.node()
1108 1106 rev = ctx.rev()
1109 1107
1110 1108 if self.ui.quiet:
1111 1109 self.ui.write("%d:%s\n" % (rev, short(changenode)),
1112 1110 label='log.node')
1113 1111 return
1114 1112
1115 1113 log = self.repo.changelog
1116 1114 date = util.datestr(ctx.date())
1117 1115
1118 1116 if self.ui.debugflag:
1119 1117 hexfunc = hex
1120 1118 else:
1121 1119 hexfunc = short
1122 1120
1123 1121 parents = [(p, hexfunc(log.node(p)))
1124 1122 for p in self._meaningful_parentrevs(log, rev)]
1125 1123
1126 1124 # i18n: column positioning for "hg log"
1127 1125 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
1128 1126 label='log.changeset changeset.%s' % ctx.phasestr())
1129 1127
1130 1128 # branches are shown first before any other names due to backwards
1131 1129 # compatibility
1132 1130 branch = ctx.branch()
1133 1131 # don't show the default branch name
1134 1132 if branch != 'default':
1135 1133 # i18n: column positioning for "hg log"
1136 1134 self.ui.write(_("branch: %s\n") % branch,
1137 1135 label='log.branch')
1138 1136
1139 1137 for name, ns in self.repo.names.iteritems():
1140 1138 # branches has special logic already handled above, so here we just
1141 1139 # skip it
1142 1140 if name == 'branches':
1143 1141 continue
1144 1142 # we will use the templatename as the color name since those two
1145 1143 # should be the same
1146 1144 for name in ns.names(self.repo, changenode):
1147 1145 self.ui.write(ns.logfmt % name,
1148 1146 label='log.%s' % ns.colorname)
1149 1147 if self.ui.debugflag:
1150 1148 # i18n: column positioning for "hg log"
1151 1149 self.ui.write(_("phase: %s\n") % _(ctx.phasestr()),
1152 1150 label='log.phase')
1153 1151 for parent in parents:
1154 1152 label = 'log.parent changeset.%s' % self.repo[parent[0]].phasestr()
1155 1153 # i18n: column positioning for "hg log"
1156 1154 self.ui.write(_("parent: %d:%s\n") % parent,
1157 1155 label=label)
1158 1156
1159 1157 if self.ui.debugflag:
1160 1158 mnode = ctx.manifestnode()
1161 1159 # i18n: column positioning for "hg log"
1162 1160 self.ui.write(_("manifest: %d:%s\n") %
1163 1161 (self.repo.manifest.rev(mnode), hex(mnode)),
1164 1162 label='ui.debug log.manifest')
1165 1163 # i18n: column positioning for "hg log"
1166 1164 self.ui.write(_("user: %s\n") % ctx.user(),
1167 1165 label='log.user')
1168 1166 # i18n: column positioning for "hg log"
1169 1167 self.ui.write(_("date: %s\n") % date,
1170 1168 label='log.date')
1171 1169
1172 1170 if self.ui.debugflag:
1173 1171 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
1174 1172 for key, value in zip([# i18n: column positioning for "hg log"
1175 1173 _("files:"),
1176 1174 # i18n: column positioning for "hg log"
1177 1175 _("files+:"),
1178 1176 # i18n: column positioning for "hg log"
1179 1177 _("files-:")], files):
1180 1178 if value:
1181 1179 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
1182 1180 label='ui.debug log.files')
1183 1181 elif ctx.files() and self.ui.verbose:
1184 1182 # i18n: column positioning for "hg log"
1185 1183 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
1186 1184 label='ui.note log.files')
1187 1185 if copies and self.ui.verbose:
1188 1186 copies = ['%s (%s)' % c for c in copies]
1189 1187 # i18n: column positioning for "hg log"
1190 1188 self.ui.write(_("copies: %s\n") % ' '.join(copies),
1191 1189 label='ui.note log.copies')
1192 1190
1193 1191 extra = ctx.extra()
1194 1192 if extra and self.ui.debugflag:
1195 1193 for key, value in sorted(extra.items()):
1196 1194 # i18n: column positioning for "hg log"
1197 1195 self.ui.write(_("extra: %s=%s\n")
1198 1196 % (key, value.encode('string_escape')),
1199 1197 label='ui.debug log.extra')
1200 1198
1201 1199 description = ctx.description().strip()
1202 1200 if description:
1203 1201 if self.ui.verbose:
1204 1202 self.ui.write(_("description:\n"),
1205 1203 label='ui.note log.description')
1206 1204 self.ui.write(description,
1207 1205 label='ui.note log.description')
1208 1206 self.ui.write("\n\n")
1209 1207 else:
1210 1208 # i18n: column positioning for "hg log"
1211 1209 self.ui.write(_("summary: %s\n") %
1212 1210 description.splitlines()[0],
1213 1211 label='log.summary')
1214 1212 self.ui.write("\n")
1215 1213
1216 1214 self.showpatch(changenode, matchfn)
1217 1215
1218 1216 def showpatch(self, node, matchfn):
1219 1217 if not matchfn:
1220 1218 matchfn = self.matchfn
1221 1219 if matchfn:
1222 1220 stat = self.diffopts.get('stat')
1223 1221 diff = self.diffopts.get('patch')
1224 1222 diffopts = patch.diffallopts(self.ui, self.diffopts)
1225 1223 prev = self.repo.changelog.parents(node)[0]
1226 1224 if stat:
1227 1225 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1228 1226 match=matchfn, stat=True)
1229 1227 if diff:
1230 1228 if stat:
1231 1229 self.ui.write("\n")
1232 1230 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1233 1231 match=matchfn, stat=False)
1234 1232 self.ui.write("\n")
1235 1233
1236 1234 def _meaningful_parentrevs(self, log, rev):
1237 1235 """Return list of meaningful (or all if debug) parentrevs for rev.
1238 1236
1239 1237 For merges (two non-nullrev revisions) both parents are meaningful.
1240 1238 Otherwise the first parent revision is considered meaningful if it
1241 1239 is not the preceding revision.
1242 1240 """
1243 1241 parents = log.parentrevs(rev)
1244 1242 if not self.ui.debugflag and parents[1] == nullrev:
1245 1243 if parents[0] >= rev - 1:
1246 1244 parents = []
1247 1245 else:
1248 1246 parents = [parents[0]]
1249 1247 return parents
1250 1248
1251 1249 class jsonchangeset(changeset_printer):
1252 1250 '''format changeset information.'''
1253 1251
1254 1252 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1255 1253 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1256 1254 self.cache = {}
1257 1255 self._first = True
1258 1256
1259 1257 def close(self):
1260 1258 if not self._first:
1261 1259 self.ui.write("\n]\n")
1262 1260 else:
1263 1261 self.ui.write("[]\n")
1264 1262
1265 1263 def _show(self, ctx, copies, matchfn, props):
1266 1264 '''show a single changeset or file revision'''
1267 1265 hexnode = hex(ctx.node())
1268 1266 rev = ctx.rev()
1269 1267 j = encoding.jsonescape
1270 1268
1271 1269 if self._first:
1272 1270 self.ui.write("[\n {")
1273 1271 self._first = False
1274 1272 else:
1275 1273 self.ui.write(",\n {")
1276 1274
1277 1275 if self.ui.quiet:
1278 1276 self.ui.write('\n "rev": %d' % rev)
1279 1277 self.ui.write(',\n "node": "%s"' % hexnode)
1280 1278 self.ui.write('\n }')
1281 1279 return
1282 1280
1283 1281 self.ui.write('\n "rev": %d' % rev)
1284 1282 self.ui.write(',\n "node": "%s"' % hexnode)
1285 1283 self.ui.write(',\n "branch": "%s"' % j(ctx.branch()))
1286 1284 self.ui.write(',\n "phase": "%s"' % ctx.phasestr())
1287 1285 self.ui.write(',\n "user": "%s"' % j(ctx.user()))
1288 1286 self.ui.write(',\n "date": [%d, %d]' % ctx.date())
1289 1287 self.ui.write(',\n "desc": "%s"' % j(ctx.description()))
1290 1288
1291 1289 self.ui.write(',\n "bookmarks": [%s]' %
1292 1290 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
1293 1291 self.ui.write(',\n "tags": [%s]' %
1294 1292 ", ".join('"%s"' % j(t) for t in ctx.tags()))
1295 1293 self.ui.write(',\n "parents": [%s]' %
1296 1294 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
1297 1295
1298 1296 if self.ui.debugflag:
1299 1297 self.ui.write(',\n "manifest": "%s"' % hex(ctx.manifestnode()))
1300 1298
1301 1299 self.ui.write(',\n "extra": {%s}' %
1302 1300 ", ".join('"%s": "%s"' % (j(k), j(v))
1303 1301 for k, v in ctx.extra().items()))
1304 1302
1305 1303 files = ctx.p1().status(ctx)
1306 1304 self.ui.write(',\n "modified": [%s]' %
1307 1305 ", ".join('"%s"' % j(f) for f in files[0]))
1308 1306 self.ui.write(',\n "added": [%s]' %
1309 1307 ", ".join('"%s"' % j(f) for f in files[1]))
1310 1308 self.ui.write(',\n "removed": [%s]' %
1311 1309 ", ".join('"%s"' % j(f) for f in files[2]))
1312 1310
1313 1311 elif self.ui.verbose:
1314 1312 self.ui.write(',\n "files": [%s]' %
1315 1313 ", ".join('"%s"' % j(f) for f in ctx.files()))
1316 1314
1317 1315 if copies:
1318 1316 self.ui.write(',\n "copies": {%s}' %
1319 1317 ", ".join('"%s": "%s"' % (j(k), j(v))
1320 1318 for k, v in copies))
1321 1319
1322 1320 matchfn = self.matchfn
1323 1321 if matchfn:
1324 1322 stat = self.diffopts.get('stat')
1325 1323 diff = self.diffopts.get('patch')
1326 1324 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
1327 1325 node, prev = ctx.node(), ctx.p1().node()
1328 1326 if stat:
1329 1327 self.ui.pushbuffer()
1330 1328 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1331 1329 match=matchfn, stat=True)
1332 1330 self.ui.write(',\n "diffstat": "%s"' % j(self.ui.popbuffer()))
1333 1331 if diff:
1334 1332 self.ui.pushbuffer()
1335 1333 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1336 1334 match=matchfn, stat=False)
1337 1335 self.ui.write(',\n "diff": "%s"' % j(self.ui.popbuffer()))
1338 1336
1339 1337 self.ui.write("\n }")
1340 1338
1341 1339 class changeset_templater(changeset_printer):
1342 1340 '''format changeset information.'''
1343 1341
1344 1342 def __init__(self, ui, repo, matchfn, diffopts, tmpl, mapfile, buffered):
1345 1343 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1346 1344 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
1347 1345 defaulttempl = {
1348 1346 'parent': '{rev}:{node|formatnode} ',
1349 1347 'manifest': '{rev}:{node|formatnode}',
1350 1348 'file_copy': '{name} ({source})',
1351 1349 'extra': '{key}={value|stringescape}'
1352 1350 }
1353 1351 # filecopy is preserved for compatibility reasons
1354 1352 defaulttempl['filecopy'] = defaulttempl['file_copy']
1355 1353 self.t = templater.templater(mapfile, {'formatnode': formatnode},
1356 1354 cache=defaulttempl)
1357 1355 if tmpl:
1358 1356 self.t.cache['changeset'] = tmpl
1359 1357
1360 1358 self.cache = {}
1361 1359
1362 1360 def _meaningful_parentrevs(self, ctx):
1363 1361 """Return list of meaningful (or all if debug) parentrevs for rev.
1364 1362 """
1365 1363 parents = ctx.parents()
1366 1364 if len(parents) > 1:
1367 1365 return parents
1368 1366 if self.ui.debugflag:
1369 1367 return [parents[0], self.repo['null']]
1370 1368 if parents[0].rev() >= ctx.rev() - 1:
1371 1369 return []
1372 1370 return parents
1373 1371
1374 1372 def _show(self, ctx, copies, matchfn, props):
1375 1373 '''show a single changeset or file revision'''
1376 1374
1377 1375 showlist = templatekw.showlist
1378 1376
1379 1377 # showparents() behaviour depends on ui trace level which
1380 1378 # causes unexpected behaviours at templating level and makes
1381 1379 # it harder to extract it in a standalone function. Its
1382 1380 # behaviour cannot be changed so leave it here for now.
1383 1381 def showparents(**args):
1384 1382 ctx = args['ctx']
1385 1383 parents = [[('rev', p.rev()),
1386 1384 ('node', p.hex()),
1387 1385 ('phase', p.phasestr())]
1388 1386 for p in self._meaningful_parentrevs(ctx)]
1389 1387 return showlist('parent', parents, **args)
1390 1388
1391 1389 props = props.copy()
1392 1390 props.update(templatekw.keywords)
1393 1391 props['parents'] = showparents
1394 1392 props['templ'] = self.t
1395 1393 props['ctx'] = ctx
1396 1394 props['repo'] = self.repo
1397 1395 props['revcache'] = {'copies': copies}
1398 1396 props['cache'] = self.cache
1399 1397
1400 1398 # find correct templates for current mode
1401 1399
1402 1400 tmplmodes = [
1403 1401 (True, None),
1404 1402 (self.ui.verbose, 'verbose'),
1405 1403 (self.ui.quiet, 'quiet'),
1406 1404 (self.ui.debugflag, 'debug'),
1407 1405 ]
1408 1406
1409 1407 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
1410 1408 for mode, postfix in tmplmodes:
1411 1409 for type in types:
1412 1410 cur = postfix and ('%s_%s' % (type, postfix)) or type
1413 1411 if mode and cur in self.t:
1414 1412 types[type] = cur
1415 1413
1416 1414 try:
1417 1415
1418 1416 # write header
1419 1417 if types['header']:
1420 1418 h = templater.stringify(self.t(types['header'], **props))
1421 1419 if self.buffered:
1422 1420 self.header[ctx.rev()] = h
1423 1421 else:
1424 1422 if self.lastheader != h:
1425 1423 self.lastheader = h
1426 1424 self.ui.write(h)
1427 1425
1428 1426 # write changeset metadata, then patch if requested
1429 1427 key = types['changeset']
1430 1428 self.ui.write(templater.stringify(self.t(key, **props)))
1431 1429 self.showpatch(ctx.node(), matchfn)
1432 1430
1433 1431 if types['footer']:
1434 1432 if not self.footer:
1435 1433 self.footer = templater.stringify(self.t(types['footer'],
1436 1434 **props))
1437 1435
1438 1436 except KeyError, inst:
1439 1437 msg = _("%s: no key named '%s'")
1440 1438 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
1441 1439 except SyntaxError, inst:
1442 1440 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
1443 1441
1444 1442 def gettemplate(ui, tmpl, style):
1445 1443 """
1446 1444 Find the template matching the given template spec or style.
1447 1445 """
1448 1446
1449 1447 # ui settings
1450 1448 if not tmpl and not style: # template are stronger than style
1451 1449 tmpl = ui.config('ui', 'logtemplate')
1452 1450 if tmpl:
1453 1451 try:
1454 1452 tmpl = templater.parsestring(tmpl)
1455 1453 except SyntaxError:
1456 1454 tmpl = templater.parsestring(tmpl, quoted=False)
1457 1455 return tmpl, None
1458 1456 else:
1459 1457 style = util.expandpath(ui.config('ui', 'style', ''))
1460 1458
1461 1459 if not tmpl and style:
1462 1460 mapfile = style
1463 1461 if not os.path.split(mapfile)[0]:
1464 1462 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1465 1463 or templater.templatepath(mapfile))
1466 1464 if mapname:
1467 1465 mapfile = mapname
1468 1466 return None, mapfile
1469 1467
1470 1468 if not tmpl:
1471 1469 return None, None
1472 1470
1473 1471 # looks like a literal template?
1474 1472 if '{' in tmpl:
1475 1473 return tmpl, None
1476 1474
1477 1475 # perhaps a stock style?
1478 1476 if not os.path.split(tmpl)[0]:
1479 1477 mapname = (templater.templatepath('map-cmdline.' + tmpl)
1480 1478 or templater.templatepath(tmpl))
1481 1479 if mapname and os.path.isfile(mapname):
1482 1480 return None, mapname
1483 1481
1484 1482 # perhaps it's a reference to [templates]
1485 1483 t = ui.config('templates', tmpl)
1486 1484 if t:
1487 1485 try:
1488 1486 tmpl = templater.parsestring(t)
1489 1487 except SyntaxError:
1490 1488 tmpl = templater.parsestring(t, quoted=False)
1491 1489 return tmpl, None
1492 1490
1493 1491 if tmpl == 'list':
1494 1492 ui.write(_("available styles: %s\n") % templater.stylelist())
1495 1493 raise util.Abort(_("specify a template"))
1496 1494
1497 1495 # perhaps it's a path to a map or a template
1498 1496 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
1499 1497 # is it a mapfile for a style?
1500 1498 if os.path.basename(tmpl).startswith("map-"):
1501 1499 return None, os.path.realpath(tmpl)
1502 1500 tmpl = open(tmpl).read()
1503 1501 return tmpl, None
1504 1502
1505 1503 # constant string?
1506 1504 return tmpl, None
1507 1505
1508 1506 def show_changeset(ui, repo, opts, buffered=False):
1509 1507 """show one changeset using template or regular display.
1510 1508
1511 1509 Display format will be the first non-empty hit of:
1512 1510 1. option 'template'
1513 1511 2. option 'style'
1514 1512 3. [ui] setting 'logtemplate'
1515 1513 4. [ui] setting 'style'
1516 1514 If all of these values are either the unset or the empty string,
1517 1515 regular display via changeset_printer() is done.
1518 1516 """
1519 1517 # options
1520 1518 matchfn = None
1521 1519 if opts.get('patch') or opts.get('stat'):
1522 1520 matchfn = scmutil.matchall(repo)
1523 1521
1524 1522 if opts.get('template') == 'json':
1525 1523 return jsonchangeset(ui, repo, matchfn, opts, buffered)
1526 1524
1527 1525 tmpl, mapfile = gettemplate(ui, opts.get('template'), opts.get('style'))
1528 1526
1529 1527 if not tmpl and not mapfile:
1530 1528 return changeset_printer(ui, repo, matchfn, opts, buffered)
1531 1529
1532 1530 try:
1533 1531 t = changeset_templater(ui, repo, matchfn, opts, tmpl, mapfile,
1534 1532 buffered)
1535 1533 except SyntaxError, inst:
1536 1534 raise util.Abort(inst.args[0])
1537 1535 return t
1538 1536
1539 1537 def showmarker(ui, marker):
1540 1538 """utility function to display obsolescence marker in a readable way
1541 1539
1542 1540 To be used by debug function."""
1543 1541 ui.write(hex(marker.precnode()))
1544 1542 for repl in marker.succnodes():
1545 1543 ui.write(' ')
1546 1544 ui.write(hex(repl))
1547 1545 ui.write(' %X ' % marker.flags())
1548 1546 parents = marker.parentnodes()
1549 1547 if parents is not None:
1550 1548 ui.write('{%s} ' % ', '.join(hex(p) for p in parents))
1551 1549 ui.write('(%s) ' % util.datestr(marker.date()))
1552 1550 ui.write('{%s}' % (', '.join('%r: %r' % t for t in
1553 1551 sorted(marker.metadata().items())
1554 1552 if t[0] != 'date')))
1555 1553 ui.write('\n')
1556 1554
1557 1555 def finddate(ui, repo, date):
1558 1556 """Find the tipmost changeset that matches the given date spec"""
1559 1557
1560 1558 df = util.matchdate(date)
1561 1559 m = scmutil.matchall(repo)
1562 1560 results = {}
1563 1561
1564 1562 def prep(ctx, fns):
1565 1563 d = ctx.date()
1566 1564 if df(d[0]):
1567 1565 results[ctx.rev()] = d
1568 1566
1569 1567 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1570 1568 rev = ctx.rev()
1571 1569 if rev in results:
1572 1570 ui.status(_("found revision %s from %s\n") %
1573 1571 (rev, util.datestr(results[rev])))
1574 1572 return str(rev)
1575 1573
1576 1574 raise util.Abort(_("revision matching date not found"))
1577 1575
1578 1576 def increasingwindows(windowsize=8, sizelimit=512):
1579 1577 while True:
1580 1578 yield windowsize
1581 1579 if windowsize < sizelimit:
1582 1580 windowsize *= 2
1583 1581
1584 1582 class FileWalkError(Exception):
1585 1583 pass
1586 1584
1587 1585 def walkfilerevs(repo, match, follow, revs, fncache):
1588 1586 '''Walks the file history for the matched files.
1589 1587
1590 1588 Returns the changeset revs that are involved in the file history.
1591 1589
1592 1590 Throws FileWalkError if the file history can't be walked using
1593 1591 filelogs alone.
1594 1592 '''
1595 1593 wanted = set()
1596 1594 copies = []
1597 1595 minrev, maxrev = min(revs), max(revs)
1598 1596 def filerevgen(filelog, last):
1599 1597 """
1600 1598 Only files, no patterns. Check the history of each file.
1601 1599
1602 1600 Examines filelog entries within minrev, maxrev linkrev range
1603 1601 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1604 1602 tuples in backwards order
1605 1603 """
1606 1604 cl_count = len(repo)
1607 1605 revs = []
1608 1606 for j in xrange(0, last + 1):
1609 1607 linkrev = filelog.linkrev(j)
1610 1608 if linkrev < minrev:
1611 1609 continue
1612 1610 # only yield rev for which we have the changelog, it can
1613 1611 # happen while doing "hg log" during a pull or commit
1614 1612 if linkrev >= cl_count:
1615 1613 break
1616 1614
1617 1615 parentlinkrevs = []
1618 1616 for p in filelog.parentrevs(j):
1619 1617 if p != nullrev:
1620 1618 parentlinkrevs.append(filelog.linkrev(p))
1621 1619 n = filelog.node(j)
1622 1620 revs.append((linkrev, parentlinkrevs,
1623 1621 follow and filelog.renamed(n)))
1624 1622
1625 1623 return reversed(revs)
1626 1624 def iterfiles():
1627 1625 pctx = repo['.']
1628 1626 for filename in match.files():
1629 1627 if follow:
1630 1628 if filename not in pctx:
1631 1629 raise util.Abort(_('cannot follow file not in parent '
1632 1630 'revision: "%s"') % filename)
1633 1631 yield filename, pctx[filename].filenode()
1634 1632 else:
1635 1633 yield filename, None
1636 1634 for filename_node in copies:
1637 1635 yield filename_node
1638 1636
1639 1637 for file_, node in iterfiles():
1640 1638 filelog = repo.file(file_)
1641 1639 if not len(filelog):
1642 1640 if node is None:
1643 1641 # A zero count may be a directory or deleted file, so
1644 1642 # try to find matching entries on the slow path.
1645 1643 if follow:
1646 1644 raise util.Abort(
1647 1645 _('cannot follow nonexistent file: "%s"') % file_)
1648 1646 raise FileWalkError("Cannot walk via filelog")
1649 1647 else:
1650 1648 continue
1651 1649
1652 1650 if node is None:
1653 1651 last = len(filelog) - 1
1654 1652 else:
1655 1653 last = filelog.rev(node)
1656 1654
1657 1655
1658 1656 # keep track of all ancestors of the file
1659 1657 ancestors = set([filelog.linkrev(last)])
1660 1658
1661 1659 # iterate from latest to oldest revision
1662 1660 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1663 1661 if not follow:
1664 1662 if rev > maxrev:
1665 1663 continue
1666 1664 else:
1667 1665 # Note that last might not be the first interesting
1668 1666 # rev to us:
1669 1667 # if the file has been changed after maxrev, we'll
1670 1668 # have linkrev(last) > maxrev, and we still need
1671 1669 # to explore the file graph
1672 1670 if rev not in ancestors:
1673 1671 continue
1674 1672 # XXX insert 1327 fix here
1675 1673 if flparentlinkrevs:
1676 1674 ancestors.update(flparentlinkrevs)
1677 1675
1678 1676 fncache.setdefault(rev, []).append(file_)
1679 1677 wanted.add(rev)
1680 1678 if copied:
1681 1679 copies.append(copied)
1682 1680
1683 1681 return wanted
1684 1682
1685 1683 class _followfilter(object):
1686 1684 def __init__(self, repo, onlyfirst=False):
1687 1685 self.repo = repo
1688 1686 self.startrev = nullrev
1689 1687 self.roots = set()
1690 1688 self.onlyfirst = onlyfirst
1691 1689
1692 1690 def match(self, rev):
1693 1691 def realparents(rev):
1694 1692 if self.onlyfirst:
1695 1693 return self.repo.changelog.parentrevs(rev)[0:1]
1696 1694 else:
1697 1695 return filter(lambda x: x != nullrev,
1698 1696 self.repo.changelog.parentrevs(rev))
1699 1697
1700 1698 if self.startrev == nullrev:
1701 1699 self.startrev = rev
1702 1700 return True
1703 1701
1704 1702 if rev > self.startrev:
1705 1703 # forward: all descendants
1706 1704 if not self.roots:
1707 1705 self.roots.add(self.startrev)
1708 1706 for parent in realparents(rev):
1709 1707 if parent in self.roots:
1710 1708 self.roots.add(rev)
1711 1709 return True
1712 1710 else:
1713 1711 # backwards: all parents
1714 1712 if not self.roots:
1715 1713 self.roots.update(realparents(self.startrev))
1716 1714 if rev in self.roots:
1717 1715 self.roots.remove(rev)
1718 1716 self.roots.update(realparents(rev))
1719 1717 return True
1720 1718
1721 1719 return False
1722 1720
1723 1721 def walkchangerevs(repo, match, opts, prepare):
1724 1722 '''Iterate over files and the revs in which they changed.
1725 1723
1726 1724 Callers most commonly need to iterate backwards over the history
1727 1725 in which they are interested. Doing so has awful (quadratic-looking)
1728 1726 performance, so we use iterators in a "windowed" way.
1729 1727
1730 1728 We walk a window of revisions in the desired order. Within the
1731 1729 window, we first walk forwards to gather data, then in the desired
1732 1730 order (usually backwards) to display it.
1733 1731
1734 1732 This function returns an iterator yielding contexts. Before
1735 1733 yielding each context, the iterator will first call the prepare
1736 1734 function on each context in the window in forward order.'''
1737 1735
1738 1736 follow = opts.get('follow') or opts.get('follow_first')
1739 1737 revs = _logrevs(repo, opts)
1740 1738 if not revs:
1741 1739 return []
1742 1740 wanted = set()
1743 1741 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1744 1742 fncache = {}
1745 1743 change = repo.changectx
1746 1744
1747 1745 # First step is to fill wanted, the set of revisions that we want to yield.
1748 1746 # When it does not induce extra cost, we also fill fncache for revisions in
1749 1747 # wanted: a cache of filenames that were changed (ctx.files()) and that
1750 1748 # match the file filtering conditions.
1751 1749
1752 1750 if match.always():
1753 1751 # No files, no patterns. Display all revs.
1754 1752 wanted = revs
1755 1753
1756 1754 if not slowpath and match.files():
1757 1755 # We only have to read through the filelog to find wanted revisions
1758 1756
1759 1757 try:
1760 1758 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1761 1759 except FileWalkError:
1762 1760 slowpath = True
1763 1761
1764 1762 # We decided to fall back to the slowpath because at least one
1765 1763 # of the paths was not a file. Check to see if at least one of them
1766 1764 # existed in history, otherwise simply return
1767 1765 for path in match.files():
1768 1766 if path == '.' or path in repo.store:
1769 1767 break
1770 1768 else:
1771 1769 return []
1772 1770
1773 1771 if slowpath:
1774 1772 # We have to read the changelog to match filenames against
1775 1773 # changed files
1776 1774
1777 1775 if follow:
1778 1776 raise util.Abort(_('can only follow copies/renames for explicit '
1779 1777 'filenames'))
1780 1778
1781 1779 # The slow path checks files modified in every changeset.
1782 1780 # This is really slow on large repos, so compute the set lazily.
1783 1781 class lazywantedset(object):
1784 1782 def __init__(self):
1785 1783 self.set = set()
1786 1784 self.revs = set(revs)
1787 1785
1788 1786 # No need to worry about locality here because it will be accessed
1789 1787 # in the same order as the increasing window below.
1790 1788 def __contains__(self, value):
1791 1789 if value in self.set:
1792 1790 return True
1793 1791 elif not value in self.revs:
1794 1792 return False
1795 1793 else:
1796 1794 self.revs.discard(value)
1797 1795 ctx = change(value)
1798 1796 matches = filter(match, ctx.files())
1799 1797 if matches:
1800 1798 fncache[value] = matches
1801 1799 self.set.add(value)
1802 1800 return True
1803 1801 return False
1804 1802
1805 1803 def discard(self, value):
1806 1804 self.revs.discard(value)
1807 1805 self.set.discard(value)
1808 1806
1809 1807 wanted = lazywantedset()
1810 1808
1811 1809 # it might be worthwhile to do this in the iterator if the rev range
1812 1810 # is descending and the prune args are all within that range
1813 1811 for rev in opts.get('prune', ()):
1814 1812 rev = repo[rev].rev()
1815 1813 ff = _followfilter(repo)
1816 1814 stop = min(revs[0], revs[-1])
1817 1815 for x in xrange(rev, stop - 1, -1):
1818 1816 if ff.match(x):
1819 1817 wanted = wanted - [x]
1820 1818
1821 1819 # Now that wanted is correctly initialized, we can iterate over the
1822 1820 # revision range, yielding only revisions in wanted.
1823 1821 def iterate():
1824 1822 if follow and not match.files():
1825 1823 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
1826 1824 def want(rev):
1827 1825 return ff.match(rev) and rev in wanted
1828 1826 else:
1829 1827 def want(rev):
1830 1828 return rev in wanted
1831 1829
1832 1830 it = iter(revs)
1833 1831 stopiteration = False
1834 1832 for windowsize in increasingwindows():
1835 1833 nrevs = []
1836 1834 for i in xrange(windowsize):
1837 1835 try:
1838 1836 rev = it.next()
1839 1837 if want(rev):
1840 1838 nrevs.append(rev)
1841 1839 except (StopIteration):
1842 1840 stopiteration = True
1843 1841 break
1844 1842 for rev in sorted(nrevs):
1845 1843 fns = fncache.get(rev)
1846 1844 ctx = change(rev)
1847 1845 if not fns:
1848 1846 def fns_generator():
1849 1847 for f in ctx.files():
1850 1848 if match(f):
1851 1849 yield f
1852 1850 fns = fns_generator()
1853 1851 prepare(ctx, fns)
1854 1852 for rev in nrevs:
1855 1853 yield change(rev)
1856 1854
1857 1855 if stopiteration:
1858 1856 break
1859 1857
1860 1858 return iterate()
1861 1859
1862 1860 def _makefollowlogfilematcher(repo, files, followfirst):
1863 1861 # When displaying a revision with --patch --follow FILE, we have
1864 1862 # to know which file of the revision must be diffed. With
1865 1863 # --follow, we want the names of the ancestors of FILE in the
1866 1864 # revision, stored in "fcache". "fcache" is populated by
1867 1865 # reproducing the graph traversal already done by --follow revset
1868 1866 # and relating linkrevs to file names (which is not "correct" but
1869 1867 # good enough).
1870 1868 fcache = {}
1871 1869 fcacheready = [False]
1872 1870 pctx = repo['.']
1873 1871
1874 1872 def populate():
1875 1873 for fn in files:
1876 1874 for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
1877 1875 for c in i:
1878 1876 fcache.setdefault(c.linkrev(), set()).add(c.path())
1879 1877
1880 1878 def filematcher(rev):
1881 1879 if not fcacheready[0]:
1882 1880 # Lazy initialization
1883 1881 fcacheready[0] = True
1884 1882 populate()
1885 1883 return scmutil.matchfiles(repo, fcache.get(rev, []))
1886 1884
1887 1885 return filematcher
1888 1886
1889 1887 def _makenofollowlogfilematcher(repo, pats, opts):
1890 1888 '''hook for extensions to override the filematcher for non-follow cases'''
1891 1889 return None
1892 1890
1893 1891 def _makelogrevset(repo, pats, opts, revs):
1894 1892 """Return (expr, filematcher) where expr is a revset string built
1895 1893 from log options and file patterns or None. If --stat or --patch
1896 1894 are not passed filematcher is None. Otherwise it is a callable
1897 1895 taking a revision number and returning a match objects filtering
1898 1896 the files to be detailed when displaying the revision.
1899 1897 """
1900 1898 opt2revset = {
1901 1899 'no_merges': ('not merge()', None),
1902 1900 'only_merges': ('merge()', None),
1903 1901 '_ancestors': ('ancestors(%(val)s)', None),
1904 1902 '_fancestors': ('_firstancestors(%(val)s)', None),
1905 1903 '_descendants': ('descendants(%(val)s)', None),
1906 1904 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1907 1905 '_matchfiles': ('_matchfiles(%(val)s)', None),
1908 1906 'date': ('date(%(val)r)', None),
1909 1907 'branch': ('branch(%(val)r)', ' or '),
1910 1908 '_patslog': ('filelog(%(val)r)', ' or '),
1911 1909 '_patsfollow': ('follow(%(val)r)', ' or '),
1912 1910 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1913 1911 'keyword': ('keyword(%(val)r)', ' or '),
1914 1912 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1915 1913 'user': ('user(%(val)r)', ' or '),
1916 1914 }
1917 1915
1918 1916 opts = dict(opts)
1919 1917 # follow or not follow?
1920 1918 follow = opts.get('follow') or opts.get('follow_first')
1921 1919 if opts.get('follow_first'):
1922 1920 followfirst = 1
1923 1921 else:
1924 1922 followfirst = 0
1925 1923 # --follow with FILE behaviour depends on revs...
1926 1924 it = iter(revs)
1927 1925 startrev = it.next()
1928 1926 try:
1929 1927 followdescendants = startrev < it.next()
1930 1928 except (StopIteration):
1931 1929 followdescendants = False
1932 1930
1933 1931 # branch and only_branch are really aliases and must be handled at
1934 1932 # the same time
1935 1933 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1936 1934 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1937 1935 # pats/include/exclude are passed to match.match() directly in
1938 1936 # _matchfiles() revset but walkchangerevs() builds its matcher with
1939 1937 # scmutil.match(). The difference is input pats are globbed on
1940 1938 # platforms without shell expansion (windows).
1941 1939 pctx = repo[None]
1942 1940 match, pats = scmutil.matchandpats(pctx, pats, opts)
1943 1941 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1944 1942 if not slowpath:
1945 1943 for f in match.files():
1946 1944 if follow and f not in pctx:
1947 1945 # If the file exists, it may be a directory, so let it
1948 1946 # take the slow path.
1949 1947 if os.path.exists(repo.wjoin(f)):
1950 1948 slowpath = True
1951 1949 continue
1952 1950 else:
1953 1951 raise util.Abort(_('cannot follow file not in parent '
1954 1952 'revision: "%s"') % f)
1955 1953 filelog = repo.file(f)
1956 1954 if not filelog:
1957 1955 # A zero count may be a directory or deleted file, so
1958 1956 # try to find matching entries on the slow path.
1959 1957 if follow:
1960 1958 raise util.Abort(
1961 1959 _('cannot follow nonexistent file: "%s"') % f)
1962 1960 slowpath = True
1963 1961
1964 1962 # We decided to fall back to the slowpath because at least one
1965 1963 # of the paths was not a file. Check to see if at least one of them
1966 1964 # existed in history - in that case, we'll continue down the
1967 1965 # slowpath; otherwise, we can turn off the slowpath
1968 1966 if slowpath:
1969 1967 for path in match.files():
1970 1968 if path == '.' or path in repo.store:
1971 1969 break
1972 1970 else:
1973 1971 slowpath = False
1974 1972
1975 1973 fpats = ('_patsfollow', '_patsfollowfirst')
1976 1974 fnopats = (('_ancestors', '_fancestors'),
1977 1975 ('_descendants', '_fdescendants'))
1978 1976 if slowpath:
1979 1977 # See walkchangerevs() slow path.
1980 1978 #
1981 1979 # pats/include/exclude cannot be represented as separate
1982 1980 # revset expressions as their filtering logic applies at file
1983 1981 # level. For instance "-I a -X a" matches a revision touching
1984 1982 # "a" and "b" while "file(a) and not file(b)" does
1985 1983 # not. Besides, filesets are evaluated against the working
1986 1984 # directory.
1987 1985 matchargs = ['r:', 'd:relpath']
1988 1986 for p in pats:
1989 1987 matchargs.append('p:' + p)
1990 1988 for p in opts.get('include', []):
1991 1989 matchargs.append('i:' + p)
1992 1990 for p in opts.get('exclude', []):
1993 1991 matchargs.append('x:' + p)
1994 1992 matchargs = ','.join(('%r' % p) for p in matchargs)
1995 1993 opts['_matchfiles'] = matchargs
1996 1994 if follow:
1997 1995 opts[fnopats[0][followfirst]] = '.'
1998 1996 else:
1999 1997 if follow:
2000 1998 if pats:
2001 1999 # follow() revset interprets its file argument as a
2002 2000 # manifest entry, so use match.files(), not pats.
2003 2001 opts[fpats[followfirst]] = list(match.files())
2004 2002 else:
2005 2003 op = fnopats[followdescendants][followfirst]
2006 2004 opts[op] = 'rev(%d)' % startrev
2007 2005 else:
2008 2006 opts['_patslog'] = list(pats)
2009 2007
2010 2008 filematcher = None
2011 2009 if opts.get('patch') or opts.get('stat'):
2012 2010 # When following files, track renames via a special matcher.
2013 2011 # If we're forced to take the slowpath it means we're following
2014 2012 # at least one pattern/directory, so don't bother with rename tracking.
2015 2013 if follow and not match.always() and not slowpath:
2016 2014 # _makefollowlogfilematcher expects its files argument to be
2017 2015 # relative to the repo root, so use match.files(), not pats.
2018 2016 filematcher = _makefollowlogfilematcher(repo, match.files(),
2019 2017 followfirst)
2020 2018 else:
2021 2019 filematcher = _makenofollowlogfilematcher(repo, pats, opts)
2022 2020 if filematcher is None:
2023 2021 filematcher = lambda rev: match
2024 2022
2025 2023 expr = []
2026 2024 for op, val in sorted(opts.iteritems()):
2027 2025 if not val:
2028 2026 continue
2029 2027 if op not in opt2revset:
2030 2028 continue
2031 2029 revop, andor = opt2revset[op]
2032 2030 if '%(val)' not in revop:
2033 2031 expr.append(revop)
2034 2032 else:
2035 2033 if not isinstance(val, list):
2036 2034 e = revop % {'val': val}
2037 2035 else:
2038 2036 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
2039 2037 expr.append(e)
2040 2038
2041 2039 if expr:
2042 2040 expr = '(' + ' and '.join(expr) + ')'
2043 2041 else:
2044 2042 expr = None
2045 2043 return expr, filematcher
2046 2044
2047 2045 def _logrevs(repo, opts):
2048 2046 # Default --rev value depends on --follow but --follow behaviour
2049 2047 # depends on revisions resolved from --rev...
2050 2048 follow = opts.get('follow') or opts.get('follow_first')
2051 2049 if opts.get('rev'):
2052 2050 revs = scmutil.revrange(repo, opts['rev'])
2053 2051 elif follow and repo.dirstate.p1() == nullid:
2054 2052 revs = revset.baseset()
2055 2053 elif follow:
2056 2054 revs = repo.revs('reverse(:.)')
2057 2055 else:
2058 2056 revs = revset.spanset(repo)
2059 2057 revs.reverse()
2060 2058 return revs
2061 2059
2062 2060 def getgraphlogrevs(repo, pats, opts):
2063 2061 """Return (revs, expr, filematcher) where revs is an iterable of
2064 2062 revision numbers, expr is a revset string built from log options
2065 2063 and file patterns or None, and used to filter 'revs'. If --stat or
2066 2064 --patch are not passed filematcher is None. Otherwise it is a
2067 2065 callable taking a revision number and returning a match objects
2068 2066 filtering the files to be detailed when displaying the revision.
2069 2067 """
2070 2068 limit = loglimit(opts)
2071 2069 revs = _logrevs(repo, opts)
2072 2070 if not revs:
2073 2071 return revset.baseset(), None, None
2074 2072 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2075 2073 if opts.get('rev'):
2076 2074 # User-specified revs might be unsorted, but don't sort before
2077 2075 # _makelogrevset because it might depend on the order of revs
2078 2076 revs.sort(reverse=True)
2079 2077 if expr:
2080 2078 # Revset matchers often operate faster on revisions in changelog
2081 2079 # order, because most filters deal with the changelog.
2082 2080 revs.reverse()
2083 2081 matcher = revset.match(repo.ui, expr)
2084 2082 # Revset matches can reorder revisions. "A or B" typically returns
2085 2083 # returns the revision matching A then the revision matching B. Sort
2086 2084 # again to fix that.
2087 2085 revs = matcher(repo, revs)
2088 2086 revs.sort(reverse=True)
2089 2087 if limit is not None:
2090 2088 limitedrevs = []
2091 2089 for idx, rev in enumerate(revs):
2092 2090 if idx >= limit:
2093 2091 break
2094 2092 limitedrevs.append(rev)
2095 2093 revs = revset.baseset(limitedrevs)
2096 2094
2097 2095 return revs, expr, filematcher
2098 2096
2099 2097 def getlogrevs(repo, pats, opts):
2100 2098 """Return (revs, expr, filematcher) where revs is an iterable of
2101 2099 revision numbers, expr is a revset string built from log options
2102 2100 and file patterns or None, and used to filter 'revs'. If --stat or
2103 2101 --patch are not passed filematcher is None. Otherwise it is a
2104 2102 callable taking a revision number and returning a match objects
2105 2103 filtering the files to be detailed when displaying the revision.
2106 2104 """
2107 2105 limit = loglimit(opts)
2108 2106 revs = _logrevs(repo, opts)
2109 2107 if not revs:
2110 2108 return revset.baseset([]), None, None
2111 2109 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2112 2110 if expr:
2113 2111 # Revset matchers often operate faster on revisions in changelog
2114 2112 # order, because most filters deal with the changelog.
2115 2113 if not opts.get('rev'):
2116 2114 revs.reverse()
2117 2115 matcher = revset.match(repo.ui, expr)
2118 2116 # Revset matches can reorder revisions. "A or B" typically returns
2119 2117 # returns the revision matching A then the revision matching B. Sort
2120 2118 # again to fix that.
2121 2119 revs = matcher(repo, revs)
2122 2120 if not opts.get('rev'):
2123 2121 revs.sort(reverse=True)
2124 2122 if limit is not None:
2125 2123 count = 0
2126 2124 limitedrevs = []
2127 2125 it = iter(revs)
2128 2126 while count < limit:
2129 2127 try:
2130 2128 limitedrevs.append(it.next())
2131 2129 except (StopIteration):
2132 2130 break
2133 2131 count += 1
2134 2132 revs = revset.baseset(limitedrevs)
2135 2133
2136 2134 return revs, expr, filematcher
2137 2135
2138 2136 def displaygraph(ui, dag, displayer, showparents, edgefn, getrenamed=None,
2139 2137 filematcher=None):
2140 2138 seen, state = [], graphmod.asciistate()
2141 2139 for rev, type, ctx, parents in dag:
2142 2140 char = 'o'
2143 2141 if ctx.node() in showparents:
2144 2142 char = '@'
2145 2143 elif ctx.obsolete():
2146 2144 char = 'x'
2147 2145 elif ctx.closesbranch():
2148 2146 char = '_'
2149 2147 copies = None
2150 2148 if getrenamed and ctx.rev():
2151 2149 copies = []
2152 2150 for fn in ctx.files():
2153 2151 rename = getrenamed(fn, ctx.rev())
2154 2152 if rename:
2155 2153 copies.append((fn, rename[0]))
2156 2154 revmatchfn = None
2157 2155 if filematcher is not None:
2158 2156 revmatchfn = filematcher(ctx.rev())
2159 2157 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
2160 2158 lines = displayer.hunk.pop(rev).split('\n')
2161 2159 if not lines[-1]:
2162 2160 del lines[-1]
2163 2161 displayer.flush(rev)
2164 2162 edges = edgefn(type, char, lines, seen, rev, parents)
2165 2163 for type, char, lines, coldata in edges:
2166 2164 graphmod.ascii(ui, state, type, char, lines, coldata)
2167 2165 displayer.close()
2168 2166
2169 2167 def graphlog(ui, repo, *pats, **opts):
2170 2168 # Parameters are identical to log command ones
2171 2169 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
2172 2170 revdag = graphmod.dagwalker(repo, revs)
2173 2171
2174 2172 getrenamed = None
2175 2173 if opts.get('copies'):
2176 2174 endrev = None
2177 2175 if opts.get('rev'):
2178 2176 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
2179 2177 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
2180 2178 displayer = show_changeset(ui, repo, opts, buffered=True)
2181 2179 showparents = [ctx.node() for ctx in repo[None].parents()]
2182 2180 displaygraph(ui, revdag, displayer, showparents,
2183 2181 graphmod.asciiedges, getrenamed, filematcher)
2184 2182
2185 2183 def checkunsupportedgraphflags(pats, opts):
2186 2184 for op in ["newest_first"]:
2187 2185 if op in opts and opts[op]:
2188 2186 raise util.Abort(_("-G/--graph option is incompatible with --%s")
2189 2187 % op.replace("_", "-"))
2190 2188
2191 2189 def graphrevs(repo, nodes, opts):
2192 2190 limit = loglimit(opts)
2193 2191 nodes.reverse()
2194 2192 if limit is not None:
2195 2193 nodes = nodes[:limit]
2196 2194 return graphmod.nodes(repo, nodes)
2197 2195
2198 2196 def add(ui, repo, match, prefix, explicitonly, **opts):
2199 2197 join = lambda f: os.path.join(prefix, f)
2200 2198 bad = []
2201 2199 oldbad = match.bad
2202 2200 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
2203 2201 names = []
2204 2202 wctx = repo[None]
2205 2203 cca = None
2206 2204 abort, warn = scmutil.checkportabilityalert(ui)
2207 2205 if abort or warn:
2208 2206 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2209 2207 for f in wctx.walk(match):
2210 2208 exact = match.exact(f)
2211 2209 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2212 2210 if cca:
2213 2211 cca(f)
2214 2212 names.append(f)
2215 2213 if ui.verbose or not exact:
2216 2214 ui.status(_('adding %s\n') % match.rel(f))
2217 2215
2218 2216 for subpath in sorted(wctx.substate):
2219 2217 sub = wctx.sub(subpath)
2220 2218 try:
2221 2219 submatch = matchmod.narrowmatcher(subpath, match)
2222 2220 if opts.get('subrepos'):
2223 2221 bad.extend(sub.add(ui, submatch, prefix, False, **opts))
2224 2222 else:
2225 2223 bad.extend(sub.add(ui, submatch, prefix, True, **opts))
2226 2224 except error.LookupError:
2227 2225 ui.status(_("skipping missing subrepository: %s\n")
2228 2226 % join(subpath))
2229 2227
2230 2228 if not opts.get('dry_run'):
2231 2229 rejected = wctx.add(names, prefix)
2232 2230 bad.extend(f for f in rejected if f in match.files())
2233 2231 return bad
2234 2232
2235 2233 def forget(ui, repo, match, prefix, explicitonly):
2236 2234 join = lambda f: os.path.join(prefix, f)
2237 2235 bad = []
2238 2236 oldbad = match.bad
2239 2237 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
2240 2238 wctx = repo[None]
2241 2239 forgot = []
2242 2240 s = repo.status(match=match, clean=True)
2243 2241 forget = sorted(s[0] + s[1] + s[3] + s[6])
2244 2242 if explicitonly:
2245 2243 forget = [f for f in forget if match.exact(f)]
2246 2244
2247 2245 for subpath in sorted(wctx.substate):
2248 2246 sub = wctx.sub(subpath)
2249 2247 try:
2250 2248 submatch = matchmod.narrowmatcher(subpath, match)
2251 2249 subbad, subforgot = sub.forget(submatch, prefix)
2252 2250 bad.extend([subpath + '/' + f for f in subbad])
2253 2251 forgot.extend([subpath + '/' + f for f in subforgot])
2254 2252 except error.LookupError:
2255 2253 ui.status(_("skipping missing subrepository: %s\n")
2256 2254 % join(subpath))
2257 2255
2258 2256 if not explicitonly:
2259 2257 for f in match.files():
2260 2258 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2261 2259 if f not in forgot:
2262 2260 if repo.wvfs.exists(f):
2263 2261 ui.warn(_('not removing %s: '
2264 2262 'file is already untracked\n')
2265 2263 % match.rel(f))
2266 2264 bad.append(f)
2267 2265
2268 2266 for f in forget:
2269 2267 if ui.verbose or not match.exact(f):
2270 2268 ui.status(_('removing %s\n') % match.rel(f))
2271 2269
2272 2270 rejected = wctx.forget(forget, prefix)
2273 2271 bad.extend(f for f in rejected if f in match.files())
2274 2272 forgot.extend(f for f in forget if f not in rejected)
2275 2273 return bad, forgot
2276 2274
2277 2275 def files(ui, ctx, m, fm, fmt, subrepos):
2278 2276 rev = ctx.rev()
2279 2277 ret = 1
2280 2278 ds = ctx.repo().dirstate
2281 2279
2282 2280 for f in ctx.matches(m):
2283 2281 if rev is None and ds[f] == 'r':
2284 2282 continue
2285 2283 fm.startitem()
2286 2284 if ui.verbose:
2287 2285 fc = ctx[f]
2288 2286 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2289 2287 fm.data(abspath=f)
2290 2288 fm.write('path', fmt, m.rel(f))
2291 2289 ret = 0
2292 2290
2293 2291 if subrepos:
2294 2292 for subpath in sorted(ctx.substate):
2295 2293 sub = ctx.sub(subpath)
2296 2294 try:
2297 2295 submatch = matchmod.narrowmatcher(subpath, m)
2298 2296 if sub.printfiles(ui, submatch, fm, fmt) == 0:
2299 2297 ret = 0
2300 2298 except error.LookupError:
2301 2299 ui.status(_("skipping missing subrepository: %s\n")
2302 2300 % m.abs(subpath))
2303 2301
2304 2302 return ret
2305 2303
2306 2304 def remove(ui, repo, m, prefix, after, force, subrepos):
2307 2305 join = lambda f: os.path.join(prefix, f)
2308 2306 ret = 0
2309 2307 s = repo.status(match=m, clean=True)
2310 2308 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2311 2309
2312 2310 wctx = repo[None]
2313 2311
2314 2312 for subpath in sorted(wctx.substate):
2315 2313 def matchessubrepo(matcher, subpath):
2316 2314 if matcher.exact(subpath):
2317 2315 return True
2318 2316 for f in matcher.files():
2319 2317 if f.startswith(subpath):
2320 2318 return True
2321 2319 return False
2322 2320
2323 2321 if subrepos or matchessubrepo(m, subpath):
2324 2322 sub = wctx.sub(subpath)
2325 2323 try:
2326 2324 submatch = matchmod.narrowmatcher(subpath, m)
2327 2325 if sub.removefiles(submatch, prefix, after, force, subrepos):
2328 2326 ret = 1
2329 2327 except error.LookupError:
2330 2328 ui.status(_("skipping missing subrepository: %s\n")
2331 2329 % join(subpath))
2332 2330
2333 2331 # warn about failure to delete explicit files/dirs
2334 2332 deleteddirs = scmutil.dirs(deleted)
2335 2333 for f in m.files():
2336 2334 def insubrepo():
2337 2335 for subpath in wctx.substate:
2338 2336 if f.startswith(subpath):
2339 2337 return True
2340 2338 return False
2341 2339
2342 2340 isdir = f in deleteddirs or f in wctx.dirs()
2343 2341 if f in repo.dirstate or isdir or f == '.' or insubrepo():
2344 2342 continue
2345 2343
2346 2344 if repo.wvfs.exists(f):
2347 2345 if repo.wvfs.isdir(f):
2348 2346 ui.warn(_('not removing %s: no tracked files\n')
2349 2347 % m.rel(f))
2350 2348 else:
2351 2349 ui.warn(_('not removing %s: file is untracked\n')
2352 2350 % m.rel(f))
2353 2351 # missing files will generate a warning elsewhere
2354 2352 ret = 1
2355 2353
2356 2354 if force:
2357 2355 list = modified + deleted + clean + added
2358 2356 elif after:
2359 2357 list = deleted
2360 2358 for f in modified + added + clean:
2361 2359 ui.warn(_('not removing %s: file still exists\n') % m.rel(f))
2362 2360 ret = 1
2363 2361 else:
2364 2362 list = deleted + clean
2365 2363 for f in modified:
2366 2364 ui.warn(_('not removing %s: file is modified (use -f'
2367 2365 ' to force removal)\n') % m.rel(f))
2368 2366 ret = 1
2369 2367 for f in added:
2370 2368 ui.warn(_('not removing %s: file has been marked for add'
2371 2369 ' (use forget to undo)\n') % m.rel(f))
2372 2370 ret = 1
2373 2371
2374 2372 for f in sorted(list):
2375 2373 if ui.verbose or not m.exact(f):
2376 2374 ui.status(_('removing %s\n') % m.rel(f))
2377 2375
2378 2376 wlock = repo.wlock()
2379 2377 try:
2380 2378 if not after:
2381 2379 for f in list:
2382 2380 if f in added:
2383 2381 continue # we never unlink added files on remove
2384 2382 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
2385 2383 repo[None].forget(list)
2386 2384 finally:
2387 2385 wlock.release()
2388 2386
2389 2387 return ret
2390 2388
2391 2389 def cat(ui, repo, ctx, matcher, prefix, **opts):
2392 2390 err = 1
2393 2391
2394 2392 def write(path):
2395 2393 fp = makefileobj(repo, opts.get('output'), ctx.node(),
2396 2394 pathname=os.path.join(prefix, path))
2397 2395 data = ctx[path].data()
2398 2396 if opts.get('decode'):
2399 2397 data = repo.wwritedata(path, data)
2400 2398 fp.write(data)
2401 2399 fp.close()
2402 2400
2403 2401 # Automation often uses hg cat on single files, so special case it
2404 2402 # for performance to avoid the cost of parsing the manifest.
2405 2403 if len(matcher.files()) == 1 and not matcher.anypats():
2406 2404 file = matcher.files()[0]
2407 2405 mf = repo.manifest
2408 2406 mfnode = ctx._changeset[0]
2409 2407 if mf.find(mfnode, file)[0]:
2410 2408 write(file)
2411 2409 return 0
2412 2410
2413 2411 # Don't warn about "missing" files that are really in subrepos
2414 2412 bad = matcher.bad
2415 2413
2416 2414 def badfn(path, msg):
2417 2415 for subpath in ctx.substate:
2418 2416 if path.startswith(subpath):
2419 2417 return
2420 2418 bad(path, msg)
2421 2419
2422 2420 matcher.bad = badfn
2423 2421
2424 2422 for abs in ctx.walk(matcher):
2425 2423 write(abs)
2426 2424 err = 0
2427 2425
2428 2426 matcher.bad = bad
2429 2427
2430 2428 for subpath in sorted(ctx.substate):
2431 2429 sub = ctx.sub(subpath)
2432 2430 try:
2433 2431 submatch = matchmod.narrowmatcher(subpath, matcher)
2434 2432
2435 2433 if not sub.cat(submatch, os.path.join(prefix, sub._path),
2436 2434 **opts):
2437 2435 err = 0
2438 2436 except error.RepoLookupError:
2439 2437 ui.status(_("skipping missing subrepository: %s\n")
2440 2438 % os.path.join(prefix, subpath))
2441 2439
2442 2440 return err
2443 2441
2444 2442 def commit(ui, repo, commitfunc, pats, opts):
2445 2443 '''commit the specified files or all outstanding changes'''
2446 2444 date = opts.get('date')
2447 2445 if date:
2448 2446 opts['date'] = util.parsedate(date)
2449 2447 message = logmessage(ui, opts)
2450 2448 matcher = scmutil.match(repo[None], pats, opts)
2451 2449
2452 2450 # extract addremove carefully -- this function can be called from a command
2453 2451 # that doesn't support addremove
2454 2452 if opts.get('addremove'):
2455 2453 if scmutil.addremove(repo, matcher, "", opts) != 0:
2456 2454 raise util.Abort(
2457 2455 _("failed to mark all new/missing files as added/removed"))
2458 2456
2459 2457 return commitfunc(ui, repo, message, matcher, opts)
2460 2458
2461 2459 def amend(ui, repo, commitfunc, old, extra, pats, opts):
2462 2460 # amend will reuse the existing user if not specified, but the obsolete
2463 2461 # marker creation requires that the current user's name is specified.
2464 2462 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2465 2463 ui.username() # raise exception if username not set
2466 2464
2467 2465 ui.note(_('amending changeset %s\n') % old)
2468 2466 base = old.p1()
2469 2467
2470 2468 wlock = lock = newid = None
2471 2469 try:
2472 2470 wlock = repo.wlock()
2473 2471 lock = repo.lock()
2474 2472 tr = repo.transaction('amend')
2475 2473 try:
2476 2474 # See if we got a message from -m or -l, if not, open the editor
2477 2475 # with the message of the changeset to amend
2478 2476 message = logmessage(ui, opts)
2479 2477 # ensure logfile does not conflict with later enforcement of the
2480 2478 # message. potential logfile content has been processed by
2481 2479 # `logmessage` anyway.
2482 2480 opts.pop('logfile')
2483 2481 # First, do a regular commit to record all changes in the working
2484 2482 # directory (if there are any)
2485 2483 ui.callhooks = False
2486 2484 currentbookmark = repo._bookmarkcurrent
2487 2485 try:
2488 2486 repo._bookmarkcurrent = None
2489 2487 opts['message'] = 'temporary amend commit for %s' % old
2490 2488 node = commit(ui, repo, commitfunc, pats, opts)
2491 2489 finally:
2492 2490 repo._bookmarkcurrent = currentbookmark
2493 2491 ui.callhooks = True
2494 2492 ctx = repo[node]
2495 2493
2496 2494 # Participating changesets:
2497 2495 #
2498 2496 # node/ctx o - new (intermediate) commit that contains changes
2499 2497 # | from working dir to go into amending commit
2500 2498 # | (or a workingctx if there were no changes)
2501 2499 # |
2502 2500 # old o - changeset to amend
2503 2501 # |
2504 2502 # base o - parent of amending changeset
2505 2503
2506 2504 # Update extra dict from amended commit (e.g. to preserve graft
2507 2505 # source)
2508 2506 extra.update(old.extra())
2509 2507
2510 2508 # Also update it from the intermediate commit or from the wctx
2511 2509 extra.update(ctx.extra())
2512 2510
2513 2511 if len(old.parents()) > 1:
2514 2512 # ctx.files() isn't reliable for merges, so fall back to the
2515 2513 # slower repo.status() method
2516 2514 files = set([fn for st in repo.status(base, old)[:3]
2517 2515 for fn in st])
2518 2516 else:
2519 2517 files = set(old.files())
2520 2518
2521 2519 # Second, we use either the commit we just did, or if there were no
2522 2520 # changes the parent of the working directory as the version of the
2523 2521 # files in the final amend commit
2524 2522 if node:
2525 2523 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
2526 2524
2527 2525 user = ctx.user()
2528 2526 date = ctx.date()
2529 2527 # Recompute copies (avoid recording a -> b -> a)
2530 2528 copied = copies.pathcopies(base, ctx)
2531 2529 if old.p2:
2532 2530 copied.update(copies.pathcopies(old.p2(), ctx))
2533 2531
2534 2532 # Prune files which were reverted by the updates: if old
2535 2533 # introduced file X and our intermediate commit, node,
2536 2534 # renamed that file, then those two files are the same and
2537 2535 # we can discard X from our list of files. Likewise if X
2538 2536 # was deleted, it's no longer relevant
2539 2537 files.update(ctx.files())
2540 2538
2541 2539 def samefile(f):
2542 2540 if f in ctx.manifest():
2543 2541 a = ctx.filectx(f)
2544 2542 if f in base.manifest():
2545 2543 b = base.filectx(f)
2546 2544 return (not a.cmp(b)
2547 2545 and a.flags() == b.flags())
2548 2546 else:
2549 2547 return False
2550 2548 else:
2551 2549 return f not in base.manifest()
2552 2550 files = [f for f in files if not samefile(f)]
2553 2551
2554 2552 def filectxfn(repo, ctx_, path):
2555 2553 try:
2556 2554 fctx = ctx[path]
2557 2555 flags = fctx.flags()
2558 2556 mctx = context.memfilectx(repo,
2559 2557 fctx.path(), fctx.data(),
2560 2558 islink='l' in flags,
2561 2559 isexec='x' in flags,
2562 2560 copied=copied.get(path))
2563 2561 return mctx
2564 2562 except KeyError:
2565 2563 return None
2566 2564 else:
2567 2565 ui.note(_('copying changeset %s to %s\n') % (old, base))
2568 2566
2569 2567 # Use version of files as in the old cset
2570 2568 def filectxfn(repo, ctx_, path):
2571 2569 try:
2572 2570 return old.filectx(path)
2573 2571 except KeyError:
2574 2572 return None
2575 2573
2576 2574 user = opts.get('user') or old.user()
2577 2575 date = opts.get('date') or old.date()
2578 2576 editform = mergeeditform(old, 'commit.amend')
2579 2577 editor = getcommiteditor(editform=editform, **opts)
2580 2578 if not message:
2581 2579 editor = getcommiteditor(edit=True, editform=editform)
2582 2580 message = old.description()
2583 2581
2584 2582 pureextra = extra.copy()
2585 2583 extra['amend_source'] = old.hex()
2586 2584
2587 2585 new = context.memctx(repo,
2588 2586 parents=[base.node(), old.p2().node()],
2589 2587 text=message,
2590 2588 files=files,
2591 2589 filectxfn=filectxfn,
2592 2590 user=user,
2593 2591 date=date,
2594 2592 extra=extra,
2595 2593 editor=editor)
2596 2594
2597 2595 newdesc = changelog.stripdesc(new.description())
2598 2596 if ((not node)
2599 2597 and newdesc == old.description()
2600 2598 and user == old.user()
2601 2599 and date == old.date()
2602 2600 and pureextra == old.extra()):
2603 2601 # nothing changed. continuing here would create a new node
2604 2602 # anyway because of the amend_source noise.
2605 2603 #
2606 2604 # This not what we expect from amend.
2607 2605 return old.node()
2608 2606
2609 2607 ph = repo.ui.config('phases', 'new-commit', phases.draft)
2610 2608 try:
2611 2609 if opts.get('secret'):
2612 2610 commitphase = 'secret'
2613 2611 else:
2614 2612 commitphase = old.phase()
2615 2613 repo.ui.setconfig('phases', 'new-commit', commitphase, 'amend')
2616 2614 newid = repo.commitctx(new)
2617 2615 finally:
2618 2616 repo.ui.setconfig('phases', 'new-commit', ph, 'amend')
2619 2617 if newid != old.node():
2620 2618 # Reroute the working copy parent to the new changeset
2621 2619 repo.setparents(newid, nullid)
2622 2620
2623 2621 # Move bookmarks from old parent to amend commit
2624 2622 bms = repo.nodebookmarks(old.node())
2625 2623 if bms:
2626 2624 marks = repo._bookmarks
2627 2625 for bm in bms:
2628 2626 marks[bm] = newid
2629 2627 marks.write()
2630 2628 #commit the whole amend process
2631 2629 createmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
2632 2630 if createmarkers and newid != old.node():
2633 2631 # mark the new changeset as successor of the rewritten one
2634 2632 new = repo[newid]
2635 2633 obs = [(old, (new,))]
2636 2634 if node:
2637 2635 obs.append((ctx, ()))
2638 2636
2639 2637 obsolete.createmarkers(repo, obs)
2640 2638 tr.close()
2641 2639 finally:
2642 2640 tr.release()
2643 2641 if not createmarkers and newid != old.node():
2644 2642 # Strip the intermediate commit (if there was one) and the amended
2645 2643 # commit
2646 2644 if node:
2647 2645 ui.note(_('stripping intermediate changeset %s\n') % ctx)
2648 2646 ui.note(_('stripping amended changeset %s\n') % old)
2649 2647 repair.strip(ui, repo, old.node(), topic='amend-backup')
2650 2648 finally:
2651 2649 if newid is None:
2652 2650 repo.dirstate.invalidate()
2653 2651 lockmod.release(lock, wlock)
2654 2652 return newid
2655 2653
2656 2654 def commiteditor(repo, ctx, subs, editform=''):
2657 2655 if ctx.description():
2658 2656 return ctx.description()
2659 2657 return commitforceeditor(repo, ctx, subs, editform=editform)
2660 2658
2661 2659 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2662 2660 editform=''):
2663 2661 if not extramsg:
2664 2662 extramsg = _("Leave message empty to abort commit.")
2665 2663
2666 2664 forms = [e for e in editform.split('.') if e]
2667 2665 forms.insert(0, 'changeset')
2668 2666 while forms:
2669 2667 tmpl = repo.ui.config('committemplate', '.'.join(forms))
2670 2668 if tmpl:
2671 2669 committext = buildcommittemplate(repo, ctx, subs, extramsg, tmpl)
2672 2670 break
2673 2671 forms.pop()
2674 2672 else:
2675 2673 committext = buildcommittext(repo, ctx, subs, extramsg)
2676 2674
2677 2675 # run editor in the repository root
2678 2676 olddir = os.getcwd()
2679 2677 os.chdir(repo.root)
2680 2678 text = repo.ui.edit(committext, ctx.user(), ctx.extra(), editform=editform)
2681 2679 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2682 2680 os.chdir(olddir)
2683 2681
2684 2682 if finishdesc:
2685 2683 text = finishdesc(text)
2686 2684 if not text.strip():
2687 2685 raise util.Abort(_("empty commit message"))
2688 2686
2689 2687 return text
2690 2688
2691 2689 def buildcommittemplate(repo, ctx, subs, extramsg, tmpl):
2692 2690 ui = repo.ui
2693 2691 tmpl, mapfile = gettemplate(ui, tmpl, None)
2694 2692
2695 2693 try:
2696 2694 t = changeset_templater(ui, repo, None, {}, tmpl, mapfile, False)
2697 2695 except SyntaxError, inst:
2698 2696 raise util.Abort(inst.args[0])
2699 2697
2700 2698 for k, v in repo.ui.configitems('committemplate'):
2701 2699 if k != 'changeset':
2702 2700 t.t.cache[k] = v
2703 2701
2704 2702 if not extramsg:
2705 2703 extramsg = '' # ensure that extramsg is string
2706 2704
2707 2705 ui.pushbuffer()
2708 2706 t.show(ctx, extramsg=extramsg)
2709 2707 return ui.popbuffer()
2710 2708
2711 2709 def buildcommittext(repo, ctx, subs, extramsg):
2712 2710 edittext = []
2713 2711 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2714 2712 if ctx.description():
2715 2713 edittext.append(ctx.description())
2716 2714 edittext.append("")
2717 2715 edittext.append("") # Empty line between message and comments.
2718 2716 edittext.append(_("HG: Enter commit message."
2719 2717 " Lines beginning with 'HG:' are removed."))
2720 2718 edittext.append("HG: %s" % extramsg)
2721 2719 edittext.append("HG: --")
2722 2720 edittext.append(_("HG: user: %s") % ctx.user())
2723 2721 if ctx.p2():
2724 2722 edittext.append(_("HG: branch merge"))
2725 2723 if ctx.branch():
2726 2724 edittext.append(_("HG: branch '%s'") % ctx.branch())
2727 2725 if bookmarks.iscurrent(repo):
2728 2726 edittext.append(_("HG: bookmark '%s'") % repo._bookmarkcurrent)
2729 2727 edittext.extend([_("HG: subrepo %s") % s for s in subs])
2730 2728 edittext.extend([_("HG: added %s") % f for f in added])
2731 2729 edittext.extend([_("HG: changed %s") % f for f in modified])
2732 2730 edittext.extend([_("HG: removed %s") % f for f in removed])
2733 2731 if not added and not modified and not removed:
2734 2732 edittext.append(_("HG: no files changed"))
2735 2733 edittext.append("")
2736 2734
2737 2735 return "\n".join(edittext)
2738 2736
2739 2737 def commitstatus(repo, node, branch, bheads=None, opts={}):
2740 2738 ctx = repo[node]
2741 2739 parents = ctx.parents()
2742 2740
2743 2741 if (not opts.get('amend') and bheads and node not in bheads and not
2744 2742 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2745 2743 repo.ui.status(_('created new head\n'))
2746 2744 # The message is not printed for initial roots. For the other
2747 2745 # changesets, it is printed in the following situations:
2748 2746 #
2749 2747 # Par column: for the 2 parents with ...
2750 2748 # N: null or no parent
2751 2749 # B: parent is on another named branch
2752 2750 # C: parent is a regular non head changeset
2753 2751 # H: parent was a branch head of the current branch
2754 2752 # Msg column: whether we print "created new head" message
2755 2753 # In the following, it is assumed that there already exists some
2756 2754 # initial branch heads of the current branch, otherwise nothing is
2757 2755 # printed anyway.
2758 2756 #
2759 2757 # Par Msg Comment
2760 2758 # N N y additional topo root
2761 2759 #
2762 2760 # B N y additional branch root
2763 2761 # C N y additional topo head
2764 2762 # H N n usual case
2765 2763 #
2766 2764 # B B y weird additional branch root
2767 2765 # C B y branch merge
2768 2766 # H B n merge with named branch
2769 2767 #
2770 2768 # C C y additional head from merge
2771 2769 # C H n merge with a head
2772 2770 #
2773 2771 # H H n head merge: head count decreases
2774 2772
2775 2773 if not opts.get('close_branch'):
2776 2774 for r in parents:
2777 2775 if r.closesbranch() and r.branch() == branch:
2778 2776 repo.ui.status(_('reopening closed branch head %d\n') % r)
2779 2777
2780 2778 if repo.ui.debugflag:
2781 2779 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
2782 2780 elif repo.ui.verbose:
2783 2781 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
2784 2782
2785 2783 def revert(ui, repo, ctx, parents, *pats, **opts):
2786 2784 parent, p2 = parents
2787 2785 node = ctx.node()
2788 2786
2789 2787 mf = ctx.manifest()
2790 2788 if node == p2:
2791 2789 parent = p2
2792 2790 if node == parent:
2793 2791 pmf = mf
2794 2792 else:
2795 2793 pmf = None
2796 2794
2797 2795 # need all matching names in dirstate and manifest of target rev,
2798 2796 # so have to walk both. do not print errors if files exist in one
2799 2797 # but not other. in both cases, filesets should be evaluated against
2800 2798 # workingctx to get consistent result (issue4497). this means 'set:**'
2801 2799 # cannot be used to select missing files from target rev.
2802 2800
2803 2801 # `names` is a mapping for all elements in working copy and target revision
2804 2802 # The mapping is in the form:
2805 2803 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2806 2804 names = {}
2807 2805
2808 2806 wlock = repo.wlock()
2809 2807 try:
2810 2808 ## filling of the `names` mapping
2811 2809 # walk dirstate to fill `names`
2812 2810
2813 2811 interactive = opts.get('interactive', False)
2814 2812 wctx = repo[None]
2815 2813 m = scmutil.match(wctx, pats, opts)
2816 2814 if not m.always():
2817 2815 m.bad = lambda x, y: False
2818 2816 for abs in repo.walk(m):
2819 2817 names[abs] = m.rel(abs), m.exact(abs)
2820 2818
2821 2819 # walk target manifest to fill `names`
2822 2820
2823 2821 def badfn(path, msg):
2824 2822 if path in names:
2825 2823 return
2826 2824 if path in ctx.substate:
2827 2825 return
2828 2826 path_ = path + '/'
2829 2827 for f in names:
2830 2828 if f.startswith(path_):
2831 2829 return
2832 2830 ui.warn("%s: %s\n" % (m.rel(path), msg))
2833 2831
2834 2832 m.bad = badfn
2835 2833 for abs in ctx.walk(m):
2836 2834 if abs not in names:
2837 2835 names[abs] = m.rel(abs), m.exact(abs)
2838 2836
2839 2837 # Find status of all file in `names`.
2840 2838 m = scmutil.matchfiles(repo, names)
2841 2839
2842 2840 changes = repo.status(node1=node, match=m,
2843 2841 unknown=True, ignored=True, clean=True)
2844 2842 else:
2845 2843 changes = repo.status(node1=node, match=m)
2846 2844 for kind in changes:
2847 2845 for abs in kind:
2848 2846 names[abs] = m.rel(abs), m.exact(abs)
2849 2847
2850 2848 m = scmutil.matchfiles(repo, names)
2851 2849
2852 2850 modified = set(changes.modified)
2853 2851 added = set(changes.added)
2854 2852 removed = set(changes.removed)
2855 2853 _deleted = set(changes.deleted)
2856 2854 unknown = set(changes.unknown)
2857 2855 unknown.update(changes.ignored)
2858 2856 clean = set(changes.clean)
2859 2857 modadded = set()
2860 2858
2861 2859 # split between files known in target manifest and the others
2862 2860 smf = set(mf)
2863 2861
2864 2862 # determine the exact nature of the deleted changesets
2865 2863 deladded = _deleted - smf
2866 2864 deleted = _deleted - deladded
2867 2865
2868 2866 # We need to account for the state of the file in the dirstate,
2869 2867 # even when we revert against something else than parent. This will
2870 2868 # slightly alter the behavior of revert (doing back up or not, delete
2871 2869 # or just forget etc).
2872 2870 if parent == node:
2873 2871 dsmodified = modified
2874 2872 dsadded = added
2875 2873 dsremoved = removed
2876 2874 # store all local modifications, useful later for rename detection
2877 2875 localchanges = dsmodified | dsadded
2878 2876 modified, added, removed = set(), set(), set()
2879 2877 else:
2880 2878 changes = repo.status(node1=parent, match=m)
2881 2879 dsmodified = set(changes.modified)
2882 2880 dsadded = set(changes.added)
2883 2881 dsremoved = set(changes.removed)
2884 2882 # store all local modifications, useful later for rename detection
2885 2883 localchanges = dsmodified | dsadded
2886 2884
2887 2885 # only take into account for removes between wc and target
2888 2886 clean |= dsremoved - removed
2889 2887 dsremoved &= removed
2890 2888 # distinct between dirstate remove and other
2891 2889 removed -= dsremoved
2892 2890
2893 2891 modadded = added & dsmodified
2894 2892 added -= modadded
2895 2893
2896 2894 # tell newly modified apart.
2897 2895 dsmodified &= modified
2898 2896 dsmodified |= modified & dsadded # dirstate added may needs backup
2899 2897 modified -= dsmodified
2900 2898
2901 2899 # We need to wait for some post-processing to update this set
2902 2900 # before making the distinction. The dirstate will be used for
2903 2901 # that purpose.
2904 2902 dsadded = added
2905 2903
2906 2904 # in case of merge, files that are actually added can be reported as
2907 2905 # modified, we need to post process the result
2908 2906 if p2 != nullid:
2909 2907 if pmf is None:
2910 2908 # only need parent manifest in the merge case,
2911 2909 # so do not read by default
2912 2910 pmf = repo[parent].manifest()
2913 2911 mergeadd = dsmodified - set(pmf)
2914 2912 dsadded |= mergeadd
2915 2913 dsmodified -= mergeadd
2916 2914
2917 2915 # if f is a rename, update `names` to also revert the source
2918 2916 cwd = repo.getcwd()
2919 2917 for f in localchanges:
2920 2918 src = repo.dirstate.copied(f)
2921 2919 # XXX should we check for rename down to target node?
2922 2920 if src and src not in names and repo.dirstate[src] == 'r':
2923 2921 dsremoved.add(src)
2924 2922 names[src] = (repo.pathto(src, cwd), True)
2925 2923
2926 2924 # distinguish between file to forget and the other
2927 2925 added = set()
2928 2926 for abs in dsadded:
2929 2927 if repo.dirstate[abs] != 'a':
2930 2928 added.add(abs)
2931 2929 dsadded -= added
2932 2930
2933 2931 for abs in deladded:
2934 2932 if repo.dirstate[abs] == 'a':
2935 2933 dsadded.add(abs)
2936 2934 deladded -= dsadded
2937 2935
2938 2936 # For files marked as removed, we check if an unknown file is present at
2939 2937 # the same path. If a such file exists it may need to be backed up.
2940 2938 # Making the distinction at this stage helps have simpler backup
2941 2939 # logic.
2942 2940 removunk = set()
2943 2941 for abs in removed:
2944 2942 target = repo.wjoin(abs)
2945 2943 if os.path.lexists(target):
2946 2944 removunk.add(abs)
2947 2945 removed -= removunk
2948 2946
2949 2947 dsremovunk = set()
2950 2948 for abs in dsremoved:
2951 2949 target = repo.wjoin(abs)
2952 2950 if os.path.lexists(target):
2953 2951 dsremovunk.add(abs)
2954 2952 dsremoved -= dsremovunk
2955 2953
2956 2954 # action to be actually performed by revert
2957 2955 # (<list of file>, message>) tuple
2958 2956 actions = {'revert': ([], _('reverting %s\n')),
2959 2957 'add': ([], _('adding %s\n')),
2960 2958 'remove': ([], _('removing %s\n')),
2961 2959 'drop': ([], _('removing %s\n')),
2962 2960 'forget': ([], _('forgetting %s\n')),
2963 2961 'undelete': ([], _('undeleting %s\n')),
2964 2962 'noop': (None, _('no changes needed to %s\n')),
2965 2963 'unknown': (None, _('file not managed: %s\n')),
2966 2964 }
2967 2965
2968 2966 # "constant" that convey the backup strategy.
2969 2967 # All set to `discard` if `no-backup` is set do avoid checking
2970 2968 # no_backup lower in the code.
2971 2969 # These values are ordered for comparison purposes
2972 2970 backup = 2 # unconditionally do backup
2973 2971 check = 1 # check if the existing file differs from target
2974 2972 discard = 0 # never do backup
2975 2973 if opts.get('no_backup'):
2976 2974 backup = check = discard
2977 2975
2978 2976 backupanddel = actions['remove']
2979 2977 if not opts.get('no_backup'):
2980 2978 backupanddel = actions['drop']
2981 2979
2982 2980 disptable = (
2983 2981 # dispatch table:
2984 2982 # file state
2985 2983 # action
2986 2984 # make backup
2987 2985
2988 2986 ## Sets that results that will change file on disk
2989 2987 # Modified compared to target, no local change
2990 2988 (modified, actions['revert'], discard),
2991 2989 # Modified compared to target, but local file is deleted
2992 2990 (deleted, actions['revert'], discard),
2993 2991 # Modified compared to target, local change
2994 2992 (dsmodified, actions['revert'], backup),
2995 2993 # Added since target
2996 2994 (added, actions['remove'], discard),
2997 2995 # Added in working directory
2998 2996 (dsadded, actions['forget'], discard),
2999 2997 # Added since target, have local modification
3000 2998 (modadded, backupanddel, backup),
3001 2999 # Added since target but file is missing in working directory
3002 3000 (deladded, actions['drop'], discard),
3003 3001 # Removed since target, before working copy parent
3004 3002 (removed, actions['add'], discard),
3005 3003 # Same as `removed` but an unknown file exists at the same path
3006 3004 (removunk, actions['add'], check),
3007 3005 # Removed since targe, marked as such in working copy parent
3008 3006 (dsremoved, actions['undelete'], discard),
3009 3007 # Same as `dsremoved` but an unknown file exists at the same path
3010 3008 (dsremovunk, actions['undelete'], check),
3011 3009 ## the following sets does not result in any file changes
3012 3010 # File with no modification
3013 3011 (clean, actions['noop'], discard),
3014 3012 # Existing file, not tracked anywhere
3015 3013 (unknown, actions['unknown'], discard),
3016 3014 )
3017 3015
3018 3016 for abs, (rel, exact) in sorted(names.items()):
3019 3017 # target file to be touch on disk (relative to cwd)
3020 3018 target = repo.wjoin(abs)
3021 3019 # search the entry in the dispatch table.
3022 3020 # if the file is in any of these sets, it was touched in the working
3023 3021 # directory parent and we are sure it needs to be reverted.
3024 3022 for table, (xlist, msg), dobackup in disptable:
3025 3023 if abs not in table:
3026 3024 continue
3027 3025 if xlist is not None:
3028 3026 xlist.append(abs)
3029 3027 if dobackup and (backup <= dobackup
3030 3028 or wctx[abs].cmp(ctx[abs])):
3031 3029 bakname = "%s.orig" % rel
3032 3030 ui.note(_('saving current version of %s as %s\n') %
3033 3031 (rel, bakname))
3034 3032 if not opts.get('dry_run'):
3035 3033 if interactive:
3036 3034 util.copyfile(target, bakname)
3037 3035 else:
3038 3036 util.rename(target, bakname)
3039 3037 if ui.verbose or not exact:
3040 3038 if not isinstance(msg, basestring):
3041 3039 msg = msg(abs)
3042 3040 ui.status(msg % rel)
3043 3041 elif exact:
3044 3042 ui.warn(msg % rel)
3045 3043 break
3046 3044
3047 3045
3048 3046 if not opts.get('dry_run'):
3049 3047 needdata = ('revert', 'add', 'undelete')
3050 3048 _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
3051 3049 _performrevert(repo, parents, ctx, actions, interactive)
3052 3050
3053 3051 # get the list of subrepos that must be reverted
3054 3052 subrepomatch = scmutil.match(wctx, pats, opts)
3055 3053 targetsubs = sorted(s for s in wctx.substate if subrepomatch(s))
3056 3054
3057 3055 if targetsubs:
3058 3056 # Revert the subrepos on the revert list
3059 3057 for sub in targetsubs:
3060 3058 try:
3061 3059 wctx.sub(sub).revert(ctx.substate[sub], *pats, **opts)
3062 3060 except KeyError:
3063 3061 raise util.Abort("subrepository '%s' does not exist in %s!"
3064 3062 % (sub, short(ctx.node())))
3065 3063 finally:
3066 3064 wlock.release()
3067 3065
3068 3066 def _revertprefetch(repo, ctx, *files):
3069 3067 """Let extension changing the storage layer prefetch content"""
3070 3068 pass
3071 3069
3072 3070 def _performrevert(repo, parents, ctx, actions, interactive=False):
3073 3071 """function that actually perform all the actions computed for revert
3074 3072
3075 3073 This is an independent function to let extension to plug in and react to
3076 3074 the imminent revert.
3077 3075
3078 3076 Make sure you have the working directory locked when calling this function.
3079 3077 """
3080 3078 parent, p2 = parents
3081 3079 node = ctx.node()
3082 3080 def checkout(f):
3083 3081 fc = ctx[f]
3084 3082 repo.wwrite(f, fc.data(), fc.flags())
3085 3083
3086 3084 audit_path = pathutil.pathauditor(repo.root)
3087 3085 for f in actions['forget'][0]:
3088 3086 repo.dirstate.drop(f)
3089 3087 for f in actions['remove'][0]:
3090 3088 audit_path(f)
3091 3089 util.unlinkpath(repo.wjoin(f))
3092 3090 repo.dirstate.remove(f)
3093 3091 for f in actions['drop'][0]:
3094 3092 audit_path(f)
3095 3093 repo.dirstate.remove(f)
3096 3094
3097 3095 normal = None
3098 3096 if node == parent:
3099 3097 # We're reverting to our parent. If possible, we'd like status
3100 3098 # to report the file as clean. We have to use normallookup for
3101 3099 # merges to avoid losing information about merged/dirty files.
3102 3100 if p2 != nullid:
3103 3101 normal = repo.dirstate.normallookup
3104 3102 else:
3105 3103 normal = repo.dirstate.normal
3106 3104
3107 3105 if interactive:
3108 3106 # Prompt the user for changes to revert
3109 3107 torevert = [repo.wjoin(f) for f in actions['revert'][0]]
3110 3108 m = scmutil.match(ctx, torevert, {})
3111 3109 diff = patch.diff(repo, None, ctx.node(), m)
3112 3110 originalchunks = patch.parsepatch(diff)
3113 3111 try:
3114 3112 chunks = recordfilter(repo.ui, originalchunks)
3115 3113 except patch.PatchError, err:
3116 3114 raise util.Abort(_('error parsing patch: %s') % err)
3117 3115
3118 3116 # Apply changes
3119 3117 fp = cStringIO.StringIO()
3120 3118 for c in chunks:
3121 3119 c.write(fp)
3122 3120 dopatch = fp.tell()
3123 3121 fp.seek(0)
3124 3122 if dopatch:
3125 3123 try:
3126 3124 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3127 3125 except patch.PatchError, err:
3128 3126 raise util.Abort(str(err))
3129 3127 del fp
3130 3128
3131 3129 for f in actions['revert'][0]:
3132 3130 if normal:
3133 3131 normal(f)
3134 3132
3135 3133 else:
3136 3134 for f in actions['revert'][0]:
3137 3135 checkout(f)
3138 3136 if normal:
3139 3137 normal(f)
3140 3138
3141 3139 for f in actions['add'][0]:
3142 3140 checkout(f)
3143 3141 repo.dirstate.add(f)
3144 3142
3145 3143 normal = repo.dirstate.normallookup
3146 3144 if node == parent and p2 == nullid:
3147 3145 normal = repo.dirstate.normal
3148 3146 for f in actions['undelete'][0]:
3149 3147 checkout(f)
3150 3148 normal(f)
3151 3149
3152 3150 copied = copies.pathcopies(repo[parent], ctx)
3153 3151
3154 3152 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3155 3153 if f in copied:
3156 3154 repo.dirstate.copy(copied[f], f)
3157 3155
3158 3156 def command(table):
3159 3157 """Returns a function object to be used as a decorator for making commands.
3160 3158
3161 3159 This function receives a command table as its argument. The table should
3162 3160 be a dict.
3163 3161
3164 3162 The returned function can be used as a decorator for adding commands
3165 3163 to that command table. This function accepts multiple arguments to define
3166 3164 a command.
3167 3165
3168 3166 The first argument is the command name.
3169 3167
3170 3168 The options argument is an iterable of tuples defining command arguments.
3171 3169 See ``mercurial.fancyopts.fancyopts()`` for the format of each tuple.
3172 3170
3173 3171 The synopsis argument defines a short, one line summary of how to use the
3174 3172 command. This shows up in the help output.
3175 3173
3176 3174 The norepo argument defines whether the command does not require a
3177 3175 local repository. Most commands operate against a repository, thus the
3178 3176 default is False.
3179 3177
3180 3178 The optionalrepo argument defines whether the command optionally requires
3181 3179 a local repository.
3182 3180
3183 3181 The inferrepo argument defines whether to try to find a repository from the
3184 3182 command line arguments. If True, arguments will be examined for potential
3185 3183 repository locations. See ``findrepo()``. If a repository is found, it
3186 3184 will be used.
3187 3185 """
3188 3186 def cmd(name, options=(), synopsis=None, norepo=False, optionalrepo=False,
3189 3187 inferrepo=False):
3190 3188 def decorator(func):
3191 3189 if synopsis:
3192 3190 table[name] = func, list(options), synopsis
3193 3191 else:
3194 3192 table[name] = func, list(options)
3195 3193
3196 3194 if norepo:
3197 3195 # Avoid import cycle.
3198 3196 import commands
3199 3197 commands.norepo += ' %s' % ' '.join(parsealiases(name))
3200 3198
3201 3199 if optionalrepo:
3202 3200 import commands
3203 3201 commands.optionalrepo += ' %s' % ' '.join(parsealiases(name))
3204 3202
3205 3203 if inferrepo:
3206 3204 import commands
3207 3205 commands.inferrepo += ' %s' % ' '.join(parsealiases(name))
3208 3206
3209 3207 return func
3210 3208 return decorator
3211 3209
3212 3210 return cmd
3213 3211
3214 3212 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3215 3213 # commands.outgoing. "missing" is "missing" of the result of
3216 3214 # "findcommonoutgoing()"
3217 3215 outgoinghooks = util.hooks()
3218 3216
3219 3217 # a list of (ui, repo) functions called by commands.summary
3220 3218 summaryhooks = util.hooks()
3221 3219
3222 3220 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3223 3221 #
3224 3222 # functions should return tuple of booleans below, if 'changes' is None:
3225 3223 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3226 3224 #
3227 3225 # otherwise, 'changes' is a tuple of tuples below:
3228 3226 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3229 3227 # - (desturl, destbranch, destpeer, outgoing)
3230 3228 summaryremotehooks = util.hooks()
3231 3229
3232 3230 # A list of state files kept by multistep operations like graft.
3233 3231 # Since graft cannot be aborted, it is considered 'clearable' by update.
3234 3232 # note: bisect is intentionally excluded
3235 3233 # (state file, clearable, allowcommit, error, hint)
3236 3234 unfinishedstates = [
3237 3235 ('graftstate', True, False, _('graft in progress'),
3238 3236 _("use 'hg graft --continue' or 'hg update' to abort")),
3239 3237 ('updatestate', True, False, _('last update was interrupted'),
3240 3238 _("use 'hg update' to get a consistent checkout"))
3241 3239 ]
3242 3240
3243 3241 def checkunfinished(repo, commit=False):
3244 3242 '''Look for an unfinished multistep operation, like graft, and abort
3245 3243 if found. It's probably good to check this right before
3246 3244 bailifchanged().
3247 3245 '''
3248 3246 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3249 3247 if commit and allowcommit:
3250 3248 continue
3251 3249 if repo.vfs.exists(f):
3252 3250 raise util.Abort(msg, hint=hint)
3253 3251
3254 3252 def clearunfinished(repo):
3255 3253 '''Check for unfinished operations (as above), and clear the ones
3256 3254 that are clearable.
3257 3255 '''
3258 3256 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3259 3257 if not clearable and repo.vfs.exists(f):
3260 3258 raise util.Abort(msg, hint=hint)
3261 3259 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3262 3260 if clearable and repo.vfs.exists(f):
3263 3261 util.unlink(repo.join(f))
General Comments 0
You need to be logged in to leave comments. Login now