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