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