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