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