##// END OF EJS Templates
cmdutil: extract a _changesetlabels function out of changeset_printer._show()...
Denis Laxalde -
r30694:5289fd78 default
parent child Browse files
Show More
@@ -1,3450 +1,3454 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 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(pycompat.ossep)
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(pycompat.ossep)
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(pycompat.ossep)
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 def _changesetlabels(ctx):
1146 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
1147 return ' '.join(labels)
1148
1145 1149 class changeset_printer(object):
1146 1150 '''show changeset information when templating not requested.'''
1147 1151
1148 1152 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1149 1153 self.ui = ui
1150 1154 self.repo = repo
1151 1155 self.buffered = buffered
1152 1156 self.matchfn = matchfn
1153 1157 self.diffopts = diffopts
1154 1158 self.header = {}
1155 1159 self.hunk = {}
1156 1160 self.lastheader = None
1157 1161 self.footer = None
1158 1162
1159 1163 def flush(self, ctx):
1160 1164 rev = ctx.rev()
1161 1165 if rev in self.header:
1162 1166 h = self.header[rev]
1163 1167 if h != self.lastheader:
1164 1168 self.lastheader = h
1165 1169 self.ui.write(h)
1166 1170 del self.header[rev]
1167 1171 if rev in self.hunk:
1168 1172 self.ui.write(self.hunk[rev])
1169 1173 del self.hunk[rev]
1170 1174 return 1
1171 1175 return 0
1172 1176
1173 1177 def close(self):
1174 1178 if self.footer:
1175 1179 self.ui.write(self.footer)
1176 1180
1177 1181 def show(self, ctx, copies=None, matchfn=None, **props):
1178 1182 if self.buffered:
1179 1183 self.ui.pushbuffer(labeled=True)
1180 1184 self._show(ctx, copies, matchfn, props)
1181 1185 self.hunk[ctx.rev()] = self.ui.popbuffer()
1182 1186 else:
1183 1187 self._show(ctx, copies, matchfn, props)
1184 1188
1185 1189 def _show(self, ctx, copies, matchfn, props):
1186 1190 '''show a single changeset or file revision'''
1187 1191 changenode = ctx.node()
1188 1192 rev = ctx.rev()
1189 1193 if self.ui.debugflag:
1190 1194 hexfunc = hex
1191 1195 else:
1192 1196 hexfunc = short
1193 1197 # as of now, wctx.node() and wctx.rev() return None, but we want to
1194 1198 # show the same values as {node} and {rev} templatekw
1195 1199 revnode = (scmutil.intrev(rev), hexfunc(bin(ctx.hex())))
1196 1200
1197 1201 if self.ui.quiet:
1198 1202 self.ui.write("%d:%s\n" % revnode, label='log.node')
1199 1203 return
1200 1204
1201 1205 date = util.datestr(ctx.date())
1202 1206
1203 1207 # i18n: column positioning for "hg log"
1204 1208 self.ui.write(_("changeset: %d:%s\n") % revnode,
1205 label='log.changeset changeset.%s' % ctx.phasestr())
1209 label=_changesetlabels(ctx))
1206 1210
1207 1211 # branches are shown first before any other names due to backwards
1208 1212 # compatibility
1209 1213 branch = ctx.branch()
1210 1214 # don't show the default branch name
1211 1215 if branch != 'default':
1212 1216 # i18n: column positioning for "hg log"
1213 1217 self.ui.write(_("branch: %s\n") % branch,
1214 1218 label='log.branch')
1215 1219
1216 1220 for nsname, ns in self.repo.names.iteritems():
1217 1221 # branches has special logic already handled above, so here we just
1218 1222 # skip it
1219 1223 if nsname == 'branches':
1220 1224 continue
1221 1225 # we will use the templatename as the color name since those two
1222 1226 # should be the same
1223 1227 for name in ns.names(self.repo, changenode):
1224 1228 self.ui.write(ns.logfmt % name,
1225 1229 label='log.%s' % ns.colorname)
1226 1230 if self.ui.debugflag:
1227 1231 # i18n: column positioning for "hg log"
1228 1232 self.ui.write(_("phase: %s\n") % ctx.phasestr(),
1229 1233 label='log.phase')
1230 1234 for pctx in scmutil.meaningfulparents(self.repo, ctx):
1231 1235 label = 'log.parent changeset.%s' % pctx.phasestr()
1232 1236 # i18n: column positioning for "hg log"
1233 1237 self.ui.write(_("parent: %d:%s\n")
1234 1238 % (pctx.rev(), hexfunc(pctx.node())),
1235 1239 label=label)
1236 1240
1237 1241 if self.ui.debugflag and rev is not None:
1238 1242 mnode = ctx.manifestnode()
1239 1243 # i18n: column positioning for "hg log"
1240 1244 self.ui.write(_("manifest: %d:%s\n") %
1241 1245 (self.repo.manifestlog._revlog.rev(mnode),
1242 1246 hex(mnode)),
1243 1247 label='ui.debug log.manifest')
1244 1248 # i18n: column positioning for "hg log"
1245 1249 self.ui.write(_("user: %s\n") % ctx.user(),
1246 1250 label='log.user')
1247 1251 # i18n: column positioning for "hg log"
1248 1252 self.ui.write(_("date: %s\n") % date,
1249 1253 label='log.date')
1250 1254
1251 1255 if self.ui.debugflag:
1252 1256 files = ctx.p1().status(ctx)[:3]
1253 1257 for key, value in zip([# i18n: column positioning for "hg log"
1254 1258 _("files:"),
1255 1259 # i18n: column positioning for "hg log"
1256 1260 _("files+:"),
1257 1261 # i18n: column positioning for "hg log"
1258 1262 _("files-:")], files):
1259 1263 if value:
1260 1264 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
1261 1265 label='ui.debug log.files')
1262 1266 elif ctx.files() and self.ui.verbose:
1263 1267 # i18n: column positioning for "hg log"
1264 1268 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
1265 1269 label='ui.note log.files')
1266 1270 if copies and self.ui.verbose:
1267 1271 copies = ['%s (%s)' % c for c in copies]
1268 1272 # i18n: column positioning for "hg log"
1269 1273 self.ui.write(_("copies: %s\n") % ' '.join(copies),
1270 1274 label='ui.note log.copies')
1271 1275
1272 1276 extra = ctx.extra()
1273 1277 if extra and self.ui.debugflag:
1274 1278 for key, value in sorted(extra.items()):
1275 1279 # i18n: column positioning for "hg log"
1276 1280 self.ui.write(_("extra: %s=%s\n")
1277 1281 % (key, value.encode('string_escape')),
1278 1282 label='ui.debug log.extra')
1279 1283
1280 1284 description = ctx.description().strip()
1281 1285 if description:
1282 1286 if self.ui.verbose:
1283 1287 self.ui.write(_("description:\n"),
1284 1288 label='ui.note log.description')
1285 1289 self.ui.write(description,
1286 1290 label='ui.note log.description')
1287 1291 self.ui.write("\n\n")
1288 1292 else:
1289 1293 # i18n: column positioning for "hg log"
1290 1294 self.ui.write(_("summary: %s\n") %
1291 1295 description.splitlines()[0],
1292 1296 label='log.summary')
1293 1297 self.ui.write("\n")
1294 1298
1295 1299 self.showpatch(ctx, matchfn)
1296 1300
1297 1301 def showpatch(self, ctx, matchfn):
1298 1302 if not matchfn:
1299 1303 matchfn = self.matchfn
1300 1304 if matchfn:
1301 1305 stat = self.diffopts.get('stat')
1302 1306 diff = self.diffopts.get('patch')
1303 1307 diffopts = patch.diffallopts(self.ui, self.diffopts)
1304 1308 node = ctx.node()
1305 1309 prev = ctx.p1().node()
1306 1310 if stat:
1307 1311 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1308 1312 match=matchfn, stat=True)
1309 1313 if diff:
1310 1314 if stat:
1311 1315 self.ui.write("\n")
1312 1316 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1313 1317 match=matchfn, stat=False)
1314 1318 self.ui.write("\n")
1315 1319
1316 1320 class jsonchangeset(changeset_printer):
1317 1321 '''format changeset information.'''
1318 1322
1319 1323 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1320 1324 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1321 1325 self.cache = {}
1322 1326 self._first = True
1323 1327
1324 1328 def close(self):
1325 1329 if not self._first:
1326 1330 self.ui.write("\n]\n")
1327 1331 else:
1328 1332 self.ui.write("[]\n")
1329 1333
1330 1334 def _show(self, ctx, copies, matchfn, props):
1331 1335 '''show a single changeset or file revision'''
1332 1336 rev = ctx.rev()
1333 1337 if rev is None:
1334 1338 jrev = jnode = 'null'
1335 1339 else:
1336 1340 jrev = str(rev)
1337 1341 jnode = '"%s"' % hex(ctx.node())
1338 1342 j = encoding.jsonescape
1339 1343
1340 1344 if self._first:
1341 1345 self.ui.write("[\n {")
1342 1346 self._first = False
1343 1347 else:
1344 1348 self.ui.write(",\n {")
1345 1349
1346 1350 if self.ui.quiet:
1347 1351 self.ui.write(('\n "rev": %s') % jrev)
1348 1352 self.ui.write((',\n "node": %s') % jnode)
1349 1353 self.ui.write('\n }')
1350 1354 return
1351 1355
1352 1356 self.ui.write(('\n "rev": %s') % jrev)
1353 1357 self.ui.write((',\n "node": %s') % jnode)
1354 1358 self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
1355 1359 self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
1356 1360 self.ui.write((',\n "user": "%s"') % j(ctx.user()))
1357 1361 self.ui.write((',\n "date": [%d, %d]') % ctx.date())
1358 1362 self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
1359 1363
1360 1364 self.ui.write((',\n "bookmarks": [%s]') %
1361 1365 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
1362 1366 self.ui.write((',\n "tags": [%s]') %
1363 1367 ", ".join('"%s"' % j(t) for t in ctx.tags()))
1364 1368 self.ui.write((',\n "parents": [%s]') %
1365 1369 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
1366 1370
1367 1371 if self.ui.debugflag:
1368 1372 if rev is None:
1369 1373 jmanifestnode = 'null'
1370 1374 else:
1371 1375 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
1372 1376 self.ui.write((',\n "manifest": %s') % jmanifestnode)
1373 1377
1374 1378 self.ui.write((',\n "extra": {%s}') %
1375 1379 ", ".join('"%s": "%s"' % (j(k), j(v))
1376 1380 for k, v in ctx.extra().items()))
1377 1381
1378 1382 files = ctx.p1().status(ctx)
1379 1383 self.ui.write((',\n "modified": [%s]') %
1380 1384 ", ".join('"%s"' % j(f) for f in files[0]))
1381 1385 self.ui.write((',\n "added": [%s]') %
1382 1386 ", ".join('"%s"' % j(f) for f in files[1]))
1383 1387 self.ui.write((',\n "removed": [%s]') %
1384 1388 ", ".join('"%s"' % j(f) for f in files[2]))
1385 1389
1386 1390 elif self.ui.verbose:
1387 1391 self.ui.write((',\n "files": [%s]') %
1388 1392 ", ".join('"%s"' % j(f) for f in ctx.files()))
1389 1393
1390 1394 if copies:
1391 1395 self.ui.write((',\n "copies": {%s}') %
1392 1396 ", ".join('"%s": "%s"' % (j(k), j(v))
1393 1397 for k, v in copies))
1394 1398
1395 1399 matchfn = self.matchfn
1396 1400 if matchfn:
1397 1401 stat = self.diffopts.get('stat')
1398 1402 diff = self.diffopts.get('patch')
1399 1403 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
1400 1404 node, prev = ctx.node(), ctx.p1().node()
1401 1405 if stat:
1402 1406 self.ui.pushbuffer()
1403 1407 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1404 1408 match=matchfn, stat=True)
1405 1409 self.ui.write((',\n "diffstat": "%s"')
1406 1410 % j(self.ui.popbuffer()))
1407 1411 if diff:
1408 1412 self.ui.pushbuffer()
1409 1413 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1410 1414 match=matchfn, stat=False)
1411 1415 self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
1412 1416
1413 1417 self.ui.write("\n }")
1414 1418
1415 1419 class changeset_templater(changeset_printer):
1416 1420 '''format changeset information.'''
1417 1421
1418 1422 def __init__(self, ui, repo, matchfn, diffopts, tmpl, mapfile, buffered):
1419 1423 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1420 1424 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
1421 1425 filters = {'formatnode': formatnode}
1422 1426 defaulttempl = {
1423 1427 'parent': '{rev}:{node|formatnode} ',
1424 1428 'manifest': '{rev}:{node|formatnode}',
1425 1429 'file_copy': '{name} ({source})',
1426 1430 'extra': '{key}={value|stringescape}'
1427 1431 }
1428 1432 # filecopy is preserved for compatibility reasons
1429 1433 defaulttempl['filecopy'] = defaulttempl['file_copy']
1430 1434 assert not (tmpl and mapfile)
1431 1435 if mapfile:
1432 1436 self.t = templater.templater.frommapfile(mapfile, filters=filters,
1433 1437 cache=defaulttempl)
1434 1438 else:
1435 1439 self.t = formatter.maketemplater(ui, 'changeset', tmpl,
1436 1440 filters=filters,
1437 1441 cache=defaulttempl)
1438 1442
1439 1443 self.cache = {}
1440 1444
1441 1445 # find correct templates for current mode
1442 1446 tmplmodes = [
1443 1447 (True, None),
1444 1448 (self.ui.verbose, 'verbose'),
1445 1449 (self.ui.quiet, 'quiet'),
1446 1450 (self.ui.debugflag, 'debug'),
1447 1451 ]
1448 1452
1449 1453 self._parts = {'header': '', 'footer': '', 'changeset': 'changeset',
1450 1454 'docheader': '', 'docfooter': ''}
1451 1455 for mode, postfix in tmplmodes:
1452 1456 for t in self._parts:
1453 1457 cur = t
1454 1458 if postfix:
1455 1459 cur += "_" + postfix
1456 1460 if mode and cur in self.t:
1457 1461 self._parts[t] = cur
1458 1462
1459 1463 if self._parts['docheader']:
1460 1464 self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
1461 1465
1462 1466 def close(self):
1463 1467 if self._parts['docfooter']:
1464 1468 if not self.footer:
1465 1469 self.footer = ""
1466 1470 self.footer += templater.stringify(self.t(self._parts['docfooter']))
1467 1471 return super(changeset_templater, self).close()
1468 1472
1469 1473 def _show(self, ctx, copies, matchfn, props):
1470 1474 '''show a single changeset or file revision'''
1471 1475 props = props.copy()
1472 1476 props.update(templatekw.keywords)
1473 1477 props['templ'] = self.t
1474 1478 props['ctx'] = ctx
1475 1479 props['repo'] = self.repo
1476 1480 props['ui'] = self.repo.ui
1477 1481 props['revcache'] = {'copies': copies}
1478 1482 props['cache'] = self.cache
1479 1483
1480 1484 # write header
1481 1485 if self._parts['header']:
1482 1486 h = templater.stringify(self.t(self._parts['header'], **props))
1483 1487 if self.buffered:
1484 1488 self.header[ctx.rev()] = h
1485 1489 else:
1486 1490 if self.lastheader != h:
1487 1491 self.lastheader = h
1488 1492 self.ui.write(h)
1489 1493
1490 1494 # write changeset metadata, then patch if requested
1491 1495 key = self._parts['changeset']
1492 1496 self.ui.write(templater.stringify(self.t(key, **props)))
1493 1497 self.showpatch(ctx, matchfn)
1494 1498
1495 1499 if self._parts['footer']:
1496 1500 if not self.footer:
1497 1501 self.footer = templater.stringify(
1498 1502 self.t(self._parts['footer'], **props))
1499 1503
1500 1504 def gettemplate(ui, tmpl, style):
1501 1505 """
1502 1506 Find the template matching the given template spec or style.
1503 1507 """
1504 1508
1505 1509 # ui settings
1506 1510 if not tmpl and not style: # template are stronger than style
1507 1511 tmpl = ui.config('ui', 'logtemplate')
1508 1512 if tmpl:
1509 1513 return templater.unquotestring(tmpl), None
1510 1514 else:
1511 1515 style = util.expandpath(ui.config('ui', 'style', ''))
1512 1516
1513 1517 if not tmpl and style:
1514 1518 mapfile = style
1515 1519 if not os.path.split(mapfile)[0]:
1516 1520 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1517 1521 or templater.templatepath(mapfile))
1518 1522 if mapname:
1519 1523 mapfile = mapname
1520 1524 return None, mapfile
1521 1525
1522 1526 if not tmpl:
1523 1527 return None, None
1524 1528
1525 1529 return formatter.lookuptemplate(ui, 'changeset', tmpl)
1526 1530
1527 1531 def show_changeset(ui, repo, opts, buffered=False):
1528 1532 """show one changeset using template or regular display.
1529 1533
1530 1534 Display format will be the first non-empty hit of:
1531 1535 1. option 'template'
1532 1536 2. option 'style'
1533 1537 3. [ui] setting 'logtemplate'
1534 1538 4. [ui] setting 'style'
1535 1539 If all of these values are either the unset or the empty string,
1536 1540 regular display via changeset_printer() is done.
1537 1541 """
1538 1542 # options
1539 1543 matchfn = None
1540 1544 if opts.get('patch') or opts.get('stat'):
1541 1545 matchfn = scmutil.matchall(repo)
1542 1546
1543 1547 if opts.get('template') == 'json':
1544 1548 return jsonchangeset(ui, repo, matchfn, opts, buffered)
1545 1549
1546 1550 tmpl, mapfile = gettemplate(ui, opts.get('template'), opts.get('style'))
1547 1551
1548 1552 if not tmpl and not mapfile:
1549 1553 return changeset_printer(ui, repo, matchfn, opts, buffered)
1550 1554
1551 1555 return changeset_templater(ui, repo, matchfn, opts, tmpl, mapfile, buffered)
1552 1556
1553 1557 def showmarker(fm, marker, index=None):
1554 1558 """utility function to display obsolescence marker in a readable way
1555 1559
1556 1560 To be used by debug function."""
1557 1561 if index is not None:
1558 1562 fm.write('index', '%i ', index)
1559 1563 fm.write('precnode', '%s ', hex(marker.precnode()))
1560 1564 succs = marker.succnodes()
1561 1565 fm.condwrite(succs, 'succnodes', '%s ',
1562 1566 fm.formatlist(map(hex, succs), name='node'))
1563 1567 fm.write('flag', '%X ', marker.flags())
1564 1568 parents = marker.parentnodes()
1565 1569 if parents is not None:
1566 1570 fm.write('parentnodes', '{%s} ',
1567 1571 fm.formatlist(map(hex, parents), name='node', sep=', '))
1568 1572 fm.write('date', '(%s) ', fm.formatdate(marker.date()))
1569 1573 meta = marker.metadata().copy()
1570 1574 meta.pop('date', None)
1571 1575 fm.write('metadata', '{%s}', fm.formatdict(meta, fmt='%r: %r', sep=', '))
1572 1576 fm.plain('\n')
1573 1577
1574 1578 def finddate(ui, repo, date):
1575 1579 """Find the tipmost changeset that matches the given date spec"""
1576 1580
1577 1581 df = util.matchdate(date)
1578 1582 m = scmutil.matchall(repo)
1579 1583 results = {}
1580 1584
1581 1585 def prep(ctx, fns):
1582 1586 d = ctx.date()
1583 1587 if df(d[0]):
1584 1588 results[ctx.rev()] = d
1585 1589
1586 1590 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1587 1591 rev = ctx.rev()
1588 1592 if rev in results:
1589 1593 ui.status(_("found revision %s from %s\n") %
1590 1594 (rev, util.datestr(results[rev])))
1591 1595 return str(rev)
1592 1596
1593 1597 raise error.Abort(_("revision matching date not found"))
1594 1598
1595 1599 def increasingwindows(windowsize=8, sizelimit=512):
1596 1600 while True:
1597 1601 yield windowsize
1598 1602 if windowsize < sizelimit:
1599 1603 windowsize *= 2
1600 1604
1601 1605 class FileWalkError(Exception):
1602 1606 pass
1603 1607
1604 1608 def walkfilerevs(repo, match, follow, revs, fncache):
1605 1609 '''Walks the file history for the matched files.
1606 1610
1607 1611 Returns the changeset revs that are involved in the file history.
1608 1612
1609 1613 Throws FileWalkError if the file history can't be walked using
1610 1614 filelogs alone.
1611 1615 '''
1612 1616 wanted = set()
1613 1617 copies = []
1614 1618 minrev, maxrev = min(revs), max(revs)
1615 1619 def filerevgen(filelog, last):
1616 1620 """
1617 1621 Only files, no patterns. Check the history of each file.
1618 1622
1619 1623 Examines filelog entries within minrev, maxrev linkrev range
1620 1624 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1621 1625 tuples in backwards order
1622 1626 """
1623 1627 cl_count = len(repo)
1624 1628 revs = []
1625 1629 for j in xrange(0, last + 1):
1626 1630 linkrev = filelog.linkrev(j)
1627 1631 if linkrev < minrev:
1628 1632 continue
1629 1633 # only yield rev for which we have the changelog, it can
1630 1634 # happen while doing "hg log" during a pull or commit
1631 1635 if linkrev >= cl_count:
1632 1636 break
1633 1637
1634 1638 parentlinkrevs = []
1635 1639 for p in filelog.parentrevs(j):
1636 1640 if p != nullrev:
1637 1641 parentlinkrevs.append(filelog.linkrev(p))
1638 1642 n = filelog.node(j)
1639 1643 revs.append((linkrev, parentlinkrevs,
1640 1644 follow and filelog.renamed(n)))
1641 1645
1642 1646 return reversed(revs)
1643 1647 def iterfiles():
1644 1648 pctx = repo['.']
1645 1649 for filename in match.files():
1646 1650 if follow:
1647 1651 if filename not in pctx:
1648 1652 raise error.Abort(_('cannot follow file not in parent '
1649 1653 'revision: "%s"') % filename)
1650 1654 yield filename, pctx[filename].filenode()
1651 1655 else:
1652 1656 yield filename, None
1653 1657 for filename_node in copies:
1654 1658 yield filename_node
1655 1659
1656 1660 for file_, node in iterfiles():
1657 1661 filelog = repo.file(file_)
1658 1662 if not len(filelog):
1659 1663 if node is None:
1660 1664 # A zero count may be a directory or deleted file, so
1661 1665 # try to find matching entries on the slow path.
1662 1666 if follow:
1663 1667 raise error.Abort(
1664 1668 _('cannot follow nonexistent file: "%s"') % file_)
1665 1669 raise FileWalkError("Cannot walk via filelog")
1666 1670 else:
1667 1671 continue
1668 1672
1669 1673 if node is None:
1670 1674 last = len(filelog) - 1
1671 1675 else:
1672 1676 last = filelog.rev(node)
1673 1677
1674 1678 # keep track of all ancestors of the file
1675 1679 ancestors = set([filelog.linkrev(last)])
1676 1680
1677 1681 # iterate from latest to oldest revision
1678 1682 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1679 1683 if not follow:
1680 1684 if rev > maxrev:
1681 1685 continue
1682 1686 else:
1683 1687 # Note that last might not be the first interesting
1684 1688 # rev to us:
1685 1689 # if the file has been changed after maxrev, we'll
1686 1690 # have linkrev(last) > maxrev, and we still need
1687 1691 # to explore the file graph
1688 1692 if rev not in ancestors:
1689 1693 continue
1690 1694 # XXX insert 1327 fix here
1691 1695 if flparentlinkrevs:
1692 1696 ancestors.update(flparentlinkrevs)
1693 1697
1694 1698 fncache.setdefault(rev, []).append(file_)
1695 1699 wanted.add(rev)
1696 1700 if copied:
1697 1701 copies.append(copied)
1698 1702
1699 1703 return wanted
1700 1704
1701 1705 class _followfilter(object):
1702 1706 def __init__(self, repo, onlyfirst=False):
1703 1707 self.repo = repo
1704 1708 self.startrev = nullrev
1705 1709 self.roots = set()
1706 1710 self.onlyfirst = onlyfirst
1707 1711
1708 1712 def match(self, rev):
1709 1713 def realparents(rev):
1710 1714 if self.onlyfirst:
1711 1715 return self.repo.changelog.parentrevs(rev)[0:1]
1712 1716 else:
1713 1717 return filter(lambda x: x != nullrev,
1714 1718 self.repo.changelog.parentrevs(rev))
1715 1719
1716 1720 if self.startrev == nullrev:
1717 1721 self.startrev = rev
1718 1722 return True
1719 1723
1720 1724 if rev > self.startrev:
1721 1725 # forward: all descendants
1722 1726 if not self.roots:
1723 1727 self.roots.add(self.startrev)
1724 1728 for parent in realparents(rev):
1725 1729 if parent in self.roots:
1726 1730 self.roots.add(rev)
1727 1731 return True
1728 1732 else:
1729 1733 # backwards: all parents
1730 1734 if not self.roots:
1731 1735 self.roots.update(realparents(self.startrev))
1732 1736 if rev in self.roots:
1733 1737 self.roots.remove(rev)
1734 1738 self.roots.update(realparents(rev))
1735 1739 return True
1736 1740
1737 1741 return False
1738 1742
1739 1743 def walkchangerevs(repo, match, opts, prepare):
1740 1744 '''Iterate over files and the revs in which they changed.
1741 1745
1742 1746 Callers most commonly need to iterate backwards over the history
1743 1747 in which they are interested. Doing so has awful (quadratic-looking)
1744 1748 performance, so we use iterators in a "windowed" way.
1745 1749
1746 1750 We walk a window of revisions in the desired order. Within the
1747 1751 window, we first walk forwards to gather data, then in the desired
1748 1752 order (usually backwards) to display it.
1749 1753
1750 1754 This function returns an iterator yielding contexts. Before
1751 1755 yielding each context, the iterator will first call the prepare
1752 1756 function on each context in the window in forward order.'''
1753 1757
1754 1758 follow = opts.get('follow') or opts.get('follow_first')
1755 1759 revs = _logrevs(repo, opts)
1756 1760 if not revs:
1757 1761 return []
1758 1762 wanted = set()
1759 1763 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
1760 1764 opts.get('removed'))
1761 1765 fncache = {}
1762 1766 change = repo.changectx
1763 1767
1764 1768 # First step is to fill wanted, the set of revisions that we want to yield.
1765 1769 # When it does not induce extra cost, we also fill fncache for revisions in
1766 1770 # wanted: a cache of filenames that were changed (ctx.files()) and that
1767 1771 # match the file filtering conditions.
1768 1772
1769 1773 if match.always():
1770 1774 # No files, no patterns. Display all revs.
1771 1775 wanted = revs
1772 1776 elif not slowpath:
1773 1777 # We only have to read through the filelog to find wanted revisions
1774 1778
1775 1779 try:
1776 1780 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1777 1781 except FileWalkError:
1778 1782 slowpath = True
1779 1783
1780 1784 # We decided to fall back to the slowpath because at least one
1781 1785 # of the paths was not a file. Check to see if at least one of them
1782 1786 # existed in history, otherwise simply return
1783 1787 for path in match.files():
1784 1788 if path == '.' or path in repo.store:
1785 1789 break
1786 1790 else:
1787 1791 return []
1788 1792
1789 1793 if slowpath:
1790 1794 # We have to read the changelog to match filenames against
1791 1795 # changed files
1792 1796
1793 1797 if follow:
1794 1798 raise error.Abort(_('can only follow copies/renames for explicit '
1795 1799 'filenames'))
1796 1800
1797 1801 # The slow path checks files modified in every changeset.
1798 1802 # This is really slow on large repos, so compute the set lazily.
1799 1803 class lazywantedset(object):
1800 1804 def __init__(self):
1801 1805 self.set = set()
1802 1806 self.revs = set(revs)
1803 1807
1804 1808 # No need to worry about locality here because it will be accessed
1805 1809 # in the same order as the increasing window below.
1806 1810 def __contains__(self, value):
1807 1811 if value in self.set:
1808 1812 return True
1809 1813 elif not value in self.revs:
1810 1814 return False
1811 1815 else:
1812 1816 self.revs.discard(value)
1813 1817 ctx = change(value)
1814 1818 matches = filter(match, ctx.files())
1815 1819 if matches:
1816 1820 fncache[value] = matches
1817 1821 self.set.add(value)
1818 1822 return True
1819 1823 return False
1820 1824
1821 1825 def discard(self, value):
1822 1826 self.revs.discard(value)
1823 1827 self.set.discard(value)
1824 1828
1825 1829 wanted = lazywantedset()
1826 1830
1827 1831 # it might be worthwhile to do this in the iterator if the rev range
1828 1832 # is descending and the prune args are all within that range
1829 1833 for rev in opts.get('prune', ()):
1830 1834 rev = repo[rev].rev()
1831 1835 ff = _followfilter(repo)
1832 1836 stop = min(revs[0], revs[-1])
1833 1837 for x in xrange(rev, stop - 1, -1):
1834 1838 if ff.match(x):
1835 1839 wanted = wanted - [x]
1836 1840
1837 1841 # Now that wanted is correctly initialized, we can iterate over the
1838 1842 # revision range, yielding only revisions in wanted.
1839 1843 def iterate():
1840 1844 if follow and match.always():
1841 1845 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
1842 1846 def want(rev):
1843 1847 return ff.match(rev) and rev in wanted
1844 1848 else:
1845 1849 def want(rev):
1846 1850 return rev in wanted
1847 1851
1848 1852 it = iter(revs)
1849 1853 stopiteration = False
1850 1854 for windowsize in increasingwindows():
1851 1855 nrevs = []
1852 1856 for i in xrange(windowsize):
1853 1857 rev = next(it, None)
1854 1858 if rev is None:
1855 1859 stopiteration = True
1856 1860 break
1857 1861 elif want(rev):
1858 1862 nrevs.append(rev)
1859 1863 for rev in sorted(nrevs):
1860 1864 fns = fncache.get(rev)
1861 1865 ctx = change(rev)
1862 1866 if not fns:
1863 1867 def fns_generator():
1864 1868 for f in ctx.files():
1865 1869 if match(f):
1866 1870 yield f
1867 1871 fns = fns_generator()
1868 1872 prepare(ctx, fns)
1869 1873 for rev in nrevs:
1870 1874 yield change(rev)
1871 1875
1872 1876 if stopiteration:
1873 1877 break
1874 1878
1875 1879 return iterate()
1876 1880
1877 1881 def _makefollowlogfilematcher(repo, files, followfirst):
1878 1882 # When displaying a revision with --patch --follow FILE, we have
1879 1883 # to know which file of the revision must be diffed. With
1880 1884 # --follow, we want the names of the ancestors of FILE in the
1881 1885 # revision, stored in "fcache". "fcache" is populated by
1882 1886 # reproducing the graph traversal already done by --follow revset
1883 1887 # and relating revs to file names (which is not "correct" but
1884 1888 # good enough).
1885 1889 fcache = {}
1886 1890 fcacheready = [False]
1887 1891 pctx = repo['.']
1888 1892
1889 1893 def populate():
1890 1894 for fn in files:
1891 1895 fctx = pctx[fn]
1892 1896 fcache.setdefault(fctx.introrev(), set()).add(fctx.path())
1893 1897 for c in fctx.ancestors(followfirst=followfirst):
1894 1898 fcache.setdefault(c.rev(), set()).add(c.path())
1895 1899
1896 1900 def filematcher(rev):
1897 1901 if not fcacheready[0]:
1898 1902 # Lazy initialization
1899 1903 fcacheready[0] = True
1900 1904 populate()
1901 1905 return scmutil.matchfiles(repo, fcache.get(rev, []))
1902 1906
1903 1907 return filematcher
1904 1908
1905 1909 def _makenofollowlogfilematcher(repo, pats, opts):
1906 1910 '''hook for extensions to override the filematcher for non-follow cases'''
1907 1911 return None
1908 1912
1909 1913 def _makelogrevset(repo, pats, opts, revs):
1910 1914 """Return (expr, filematcher) where expr is a revset string built
1911 1915 from log options and file patterns or None. If --stat or --patch
1912 1916 are not passed filematcher is None. Otherwise it is a callable
1913 1917 taking a revision number and returning a match objects filtering
1914 1918 the files to be detailed when displaying the revision.
1915 1919 """
1916 1920 opt2revset = {
1917 1921 'no_merges': ('not merge()', None),
1918 1922 'only_merges': ('merge()', None),
1919 1923 '_ancestors': ('ancestors(%(val)s)', None),
1920 1924 '_fancestors': ('_firstancestors(%(val)s)', None),
1921 1925 '_descendants': ('descendants(%(val)s)', None),
1922 1926 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1923 1927 '_matchfiles': ('_matchfiles(%(val)s)', None),
1924 1928 'date': ('date(%(val)r)', None),
1925 1929 'branch': ('branch(%(val)r)', ' or '),
1926 1930 '_patslog': ('filelog(%(val)r)', ' or '),
1927 1931 '_patsfollow': ('follow(%(val)r)', ' or '),
1928 1932 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1929 1933 'keyword': ('keyword(%(val)r)', ' or '),
1930 1934 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1931 1935 'user': ('user(%(val)r)', ' or '),
1932 1936 }
1933 1937
1934 1938 opts = dict(opts)
1935 1939 # follow or not follow?
1936 1940 follow = opts.get('follow') or opts.get('follow_first')
1937 1941 if opts.get('follow_first'):
1938 1942 followfirst = 1
1939 1943 else:
1940 1944 followfirst = 0
1941 1945 # --follow with FILE behavior depends on revs...
1942 1946 it = iter(revs)
1943 1947 startrev = next(it)
1944 1948 followdescendants = startrev < next(it, startrev)
1945 1949
1946 1950 # branch and only_branch are really aliases and must be handled at
1947 1951 # the same time
1948 1952 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1949 1953 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1950 1954 # pats/include/exclude are passed to match.match() directly in
1951 1955 # _matchfiles() revset but walkchangerevs() builds its matcher with
1952 1956 # scmutil.match(). The difference is input pats are globbed on
1953 1957 # platforms without shell expansion (windows).
1954 1958 wctx = repo[None]
1955 1959 match, pats = scmutil.matchandpats(wctx, pats, opts)
1956 1960 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
1957 1961 opts.get('removed'))
1958 1962 if not slowpath:
1959 1963 for f in match.files():
1960 1964 if follow and f not in wctx:
1961 1965 # If the file exists, it may be a directory, so let it
1962 1966 # take the slow path.
1963 1967 if os.path.exists(repo.wjoin(f)):
1964 1968 slowpath = True
1965 1969 continue
1966 1970 else:
1967 1971 raise error.Abort(_('cannot follow file not in parent '
1968 1972 'revision: "%s"') % f)
1969 1973 filelog = repo.file(f)
1970 1974 if not filelog:
1971 1975 # A zero count may be a directory or deleted file, so
1972 1976 # try to find matching entries on the slow path.
1973 1977 if follow:
1974 1978 raise error.Abort(
1975 1979 _('cannot follow nonexistent file: "%s"') % f)
1976 1980 slowpath = True
1977 1981
1978 1982 # We decided to fall back to the slowpath because at least one
1979 1983 # of the paths was not a file. Check to see if at least one of them
1980 1984 # existed in history - in that case, we'll continue down the
1981 1985 # slowpath; otherwise, we can turn off the slowpath
1982 1986 if slowpath:
1983 1987 for path in match.files():
1984 1988 if path == '.' or path in repo.store:
1985 1989 break
1986 1990 else:
1987 1991 slowpath = False
1988 1992
1989 1993 fpats = ('_patsfollow', '_patsfollowfirst')
1990 1994 fnopats = (('_ancestors', '_fancestors'),
1991 1995 ('_descendants', '_fdescendants'))
1992 1996 if slowpath:
1993 1997 # See walkchangerevs() slow path.
1994 1998 #
1995 1999 # pats/include/exclude cannot be represented as separate
1996 2000 # revset expressions as their filtering logic applies at file
1997 2001 # level. For instance "-I a -X a" matches a revision touching
1998 2002 # "a" and "b" while "file(a) and not file(b)" does
1999 2003 # not. Besides, filesets are evaluated against the working
2000 2004 # directory.
2001 2005 matchargs = ['r:', 'd:relpath']
2002 2006 for p in pats:
2003 2007 matchargs.append('p:' + p)
2004 2008 for p in opts.get('include', []):
2005 2009 matchargs.append('i:' + p)
2006 2010 for p in opts.get('exclude', []):
2007 2011 matchargs.append('x:' + p)
2008 2012 matchargs = ','.join(('%r' % p) for p in matchargs)
2009 2013 opts['_matchfiles'] = matchargs
2010 2014 if follow:
2011 2015 opts[fnopats[0][followfirst]] = '.'
2012 2016 else:
2013 2017 if follow:
2014 2018 if pats:
2015 2019 # follow() revset interprets its file argument as a
2016 2020 # manifest entry, so use match.files(), not pats.
2017 2021 opts[fpats[followfirst]] = list(match.files())
2018 2022 else:
2019 2023 op = fnopats[followdescendants][followfirst]
2020 2024 opts[op] = 'rev(%d)' % startrev
2021 2025 else:
2022 2026 opts['_patslog'] = list(pats)
2023 2027
2024 2028 filematcher = None
2025 2029 if opts.get('patch') or opts.get('stat'):
2026 2030 # When following files, track renames via a special matcher.
2027 2031 # If we're forced to take the slowpath it means we're following
2028 2032 # at least one pattern/directory, so don't bother with rename tracking.
2029 2033 if follow and not match.always() and not slowpath:
2030 2034 # _makefollowlogfilematcher expects its files argument to be
2031 2035 # relative to the repo root, so use match.files(), not pats.
2032 2036 filematcher = _makefollowlogfilematcher(repo, match.files(),
2033 2037 followfirst)
2034 2038 else:
2035 2039 filematcher = _makenofollowlogfilematcher(repo, pats, opts)
2036 2040 if filematcher is None:
2037 2041 filematcher = lambda rev: match
2038 2042
2039 2043 expr = []
2040 2044 for op, val in sorted(opts.iteritems()):
2041 2045 if not val:
2042 2046 continue
2043 2047 if op not in opt2revset:
2044 2048 continue
2045 2049 revop, andor = opt2revset[op]
2046 2050 if '%(val)' not in revop:
2047 2051 expr.append(revop)
2048 2052 else:
2049 2053 if not isinstance(val, list):
2050 2054 e = revop % {'val': val}
2051 2055 else:
2052 2056 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
2053 2057 expr.append(e)
2054 2058
2055 2059 if expr:
2056 2060 expr = '(' + ' and '.join(expr) + ')'
2057 2061 else:
2058 2062 expr = None
2059 2063 return expr, filematcher
2060 2064
2061 2065 def _logrevs(repo, opts):
2062 2066 # Default --rev value depends on --follow but --follow behavior
2063 2067 # depends on revisions resolved from --rev...
2064 2068 follow = opts.get('follow') or opts.get('follow_first')
2065 2069 if opts.get('rev'):
2066 2070 revs = scmutil.revrange(repo, opts['rev'])
2067 2071 elif follow and repo.dirstate.p1() == nullid:
2068 2072 revs = revset.baseset()
2069 2073 elif follow:
2070 2074 revs = repo.revs('reverse(:.)')
2071 2075 else:
2072 2076 revs = revset.spanset(repo)
2073 2077 revs.reverse()
2074 2078 return revs
2075 2079
2076 2080 def getgraphlogrevs(repo, pats, opts):
2077 2081 """Return (revs, expr, filematcher) where revs is an iterable of
2078 2082 revision numbers, expr is a revset string built from log options
2079 2083 and file patterns or None, and used to filter 'revs'. If --stat or
2080 2084 --patch are not passed filematcher is None. Otherwise it is a
2081 2085 callable taking a revision number and returning a match objects
2082 2086 filtering the files to be detailed when displaying the revision.
2083 2087 """
2084 2088 limit = loglimit(opts)
2085 2089 revs = _logrevs(repo, opts)
2086 2090 if not revs:
2087 2091 return revset.baseset(), None, None
2088 2092 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2089 2093 if opts.get('rev'):
2090 2094 # User-specified revs might be unsorted, but don't sort before
2091 2095 # _makelogrevset because it might depend on the order of revs
2092 2096 if not (revs.isdescending() or revs.istopo()):
2093 2097 revs.sort(reverse=True)
2094 2098 if expr:
2095 2099 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2096 2100 revs = matcher(repo, revs)
2097 2101 if limit is not None:
2098 2102 limitedrevs = []
2099 2103 for idx, rev in enumerate(revs):
2100 2104 if idx >= limit:
2101 2105 break
2102 2106 limitedrevs.append(rev)
2103 2107 revs = revset.baseset(limitedrevs)
2104 2108
2105 2109 return revs, expr, filematcher
2106 2110
2107 2111 def getlogrevs(repo, pats, opts):
2108 2112 """Return (revs, expr, filematcher) where revs is an iterable of
2109 2113 revision numbers, expr is a revset string built from log options
2110 2114 and file patterns or None, and used to filter 'revs'. If --stat or
2111 2115 --patch are not passed filematcher is None. Otherwise it is a
2112 2116 callable taking a revision number and returning a match objects
2113 2117 filtering the files to be detailed when displaying the revision.
2114 2118 """
2115 2119 limit = loglimit(opts)
2116 2120 revs = _logrevs(repo, opts)
2117 2121 if not revs:
2118 2122 return revset.baseset([]), None, None
2119 2123 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2120 2124 if expr:
2121 2125 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2122 2126 revs = matcher(repo, revs)
2123 2127 if limit is not None:
2124 2128 limitedrevs = []
2125 2129 for idx, r in enumerate(revs):
2126 2130 if limit <= idx:
2127 2131 break
2128 2132 limitedrevs.append(r)
2129 2133 revs = revset.baseset(limitedrevs)
2130 2134
2131 2135 return revs, expr, filematcher
2132 2136
2133 2137 def _graphnodeformatter(ui, displayer):
2134 2138 spec = ui.config('ui', 'graphnodetemplate')
2135 2139 if not spec:
2136 2140 return templatekw.showgraphnode # fast path for "{graphnode}"
2137 2141
2138 2142 templ = formatter.gettemplater(ui, 'graphnode', spec)
2139 2143 cache = {}
2140 2144 if isinstance(displayer, changeset_templater):
2141 2145 cache = displayer.cache # reuse cache of slow templates
2142 2146 props = templatekw.keywords.copy()
2143 2147 props['templ'] = templ
2144 2148 props['cache'] = cache
2145 2149 def formatnode(repo, ctx):
2146 2150 props['ctx'] = ctx
2147 2151 props['repo'] = repo
2148 2152 props['ui'] = repo.ui
2149 2153 props['revcache'] = {}
2150 2154 return templater.stringify(templ('graphnode', **props))
2151 2155 return formatnode
2152 2156
2153 2157 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
2154 2158 filematcher=None):
2155 2159 formatnode = _graphnodeformatter(ui, displayer)
2156 2160 state = graphmod.asciistate()
2157 2161 styles = state['styles']
2158 2162
2159 2163 # only set graph styling if HGPLAIN is not set.
2160 2164 if ui.plain('graph'):
2161 2165 # set all edge styles to |, the default pre-3.8 behaviour
2162 2166 styles.update(dict.fromkeys(styles, '|'))
2163 2167 else:
2164 2168 edgetypes = {
2165 2169 'parent': graphmod.PARENT,
2166 2170 'grandparent': graphmod.GRANDPARENT,
2167 2171 'missing': graphmod.MISSINGPARENT
2168 2172 }
2169 2173 for name, key in edgetypes.items():
2170 2174 # experimental config: experimental.graphstyle.*
2171 2175 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
2172 2176 styles[key])
2173 2177 if not styles[key]:
2174 2178 styles[key] = None
2175 2179
2176 2180 # experimental config: experimental.graphshorten
2177 2181 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
2178 2182
2179 2183 for rev, type, ctx, parents in dag:
2180 2184 char = formatnode(repo, ctx)
2181 2185 copies = None
2182 2186 if getrenamed and ctx.rev():
2183 2187 copies = []
2184 2188 for fn in ctx.files():
2185 2189 rename = getrenamed(fn, ctx.rev())
2186 2190 if rename:
2187 2191 copies.append((fn, rename[0]))
2188 2192 revmatchfn = None
2189 2193 if filematcher is not None:
2190 2194 revmatchfn = filematcher(ctx.rev())
2191 2195 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
2192 2196 lines = displayer.hunk.pop(rev).split('\n')
2193 2197 if not lines[-1]:
2194 2198 del lines[-1]
2195 2199 displayer.flush(ctx)
2196 2200 edges = edgefn(type, char, lines, state, rev, parents)
2197 2201 for type, char, lines, coldata in edges:
2198 2202 graphmod.ascii(ui, state, type, char, lines, coldata)
2199 2203 displayer.close()
2200 2204
2201 2205 def graphlog(ui, repo, *pats, **opts):
2202 2206 # Parameters are identical to log command ones
2203 2207 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
2204 2208 revdag = graphmod.dagwalker(repo, revs)
2205 2209
2206 2210 getrenamed = None
2207 2211 if opts.get('copies'):
2208 2212 endrev = None
2209 2213 if opts.get('rev'):
2210 2214 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
2211 2215 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
2212 2216 displayer = show_changeset(ui, repo, opts, buffered=True)
2213 2217 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
2214 2218 filematcher)
2215 2219
2216 2220 def checkunsupportedgraphflags(pats, opts):
2217 2221 for op in ["newest_first"]:
2218 2222 if op in opts and opts[op]:
2219 2223 raise error.Abort(_("-G/--graph option is incompatible with --%s")
2220 2224 % op.replace("_", "-"))
2221 2225
2222 2226 def graphrevs(repo, nodes, opts):
2223 2227 limit = loglimit(opts)
2224 2228 nodes.reverse()
2225 2229 if limit is not None:
2226 2230 nodes = nodes[:limit]
2227 2231 return graphmod.nodes(repo, nodes)
2228 2232
2229 2233 def add(ui, repo, match, prefix, explicitonly, **opts):
2230 2234 join = lambda f: os.path.join(prefix, f)
2231 2235 bad = []
2232 2236
2233 2237 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2234 2238 names = []
2235 2239 wctx = repo[None]
2236 2240 cca = None
2237 2241 abort, warn = scmutil.checkportabilityalert(ui)
2238 2242 if abort or warn:
2239 2243 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2240 2244
2241 2245 badmatch = matchmod.badmatch(match, badfn)
2242 2246 dirstate = repo.dirstate
2243 2247 # We don't want to just call wctx.walk here, since it would return a lot of
2244 2248 # clean files, which we aren't interested in and takes time.
2245 2249 for f in sorted(dirstate.walk(badmatch, sorted(wctx.substate),
2246 2250 True, False, full=False)):
2247 2251 exact = match.exact(f)
2248 2252 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2249 2253 if cca:
2250 2254 cca(f)
2251 2255 names.append(f)
2252 2256 if ui.verbose or not exact:
2253 2257 ui.status(_('adding %s\n') % match.rel(f))
2254 2258
2255 2259 for subpath in sorted(wctx.substate):
2256 2260 sub = wctx.sub(subpath)
2257 2261 try:
2258 2262 submatch = matchmod.subdirmatcher(subpath, match)
2259 2263 if opts.get('subrepos'):
2260 2264 bad.extend(sub.add(ui, submatch, prefix, False, **opts))
2261 2265 else:
2262 2266 bad.extend(sub.add(ui, submatch, prefix, True, **opts))
2263 2267 except error.LookupError:
2264 2268 ui.status(_("skipping missing subrepository: %s\n")
2265 2269 % join(subpath))
2266 2270
2267 2271 if not opts.get('dry_run'):
2268 2272 rejected = wctx.add(names, prefix)
2269 2273 bad.extend(f for f in rejected if f in match.files())
2270 2274 return bad
2271 2275
2272 2276 def forget(ui, repo, match, prefix, explicitonly):
2273 2277 join = lambda f: os.path.join(prefix, f)
2274 2278 bad = []
2275 2279 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2276 2280 wctx = repo[None]
2277 2281 forgot = []
2278 2282
2279 2283 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2280 2284 forget = sorted(s[0] + s[1] + s[3] + s[6])
2281 2285 if explicitonly:
2282 2286 forget = [f for f in forget if match.exact(f)]
2283 2287
2284 2288 for subpath in sorted(wctx.substate):
2285 2289 sub = wctx.sub(subpath)
2286 2290 try:
2287 2291 submatch = matchmod.subdirmatcher(subpath, match)
2288 2292 subbad, subforgot = sub.forget(submatch, prefix)
2289 2293 bad.extend([subpath + '/' + f for f in subbad])
2290 2294 forgot.extend([subpath + '/' + f for f in subforgot])
2291 2295 except error.LookupError:
2292 2296 ui.status(_("skipping missing subrepository: %s\n")
2293 2297 % join(subpath))
2294 2298
2295 2299 if not explicitonly:
2296 2300 for f in match.files():
2297 2301 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2298 2302 if f not in forgot:
2299 2303 if repo.wvfs.exists(f):
2300 2304 # Don't complain if the exact case match wasn't given.
2301 2305 # But don't do this until after checking 'forgot', so
2302 2306 # that subrepo files aren't normalized, and this op is
2303 2307 # purely from data cached by the status walk above.
2304 2308 if repo.dirstate.normalize(f) in repo.dirstate:
2305 2309 continue
2306 2310 ui.warn(_('not removing %s: '
2307 2311 'file is already untracked\n')
2308 2312 % match.rel(f))
2309 2313 bad.append(f)
2310 2314
2311 2315 for f in forget:
2312 2316 if ui.verbose or not match.exact(f):
2313 2317 ui.status(_('removing %s\n') % match.rel(f))
2314 2318
2315 2319 rejected = wctx.forget(forget, prefix)
2316 2320 bad.extend(f for f in rejected if f in match.files())
2317 2321 forgot.extend(f for f in forget if f not in rejected)
2318 2322 return bad, forgot
2319 2323
2320 2324 def files(ui, ctx, m, fm, fmt, subrepos):
2321 2325 rev = ctx.rev()
2322 2326 ret = 1
2323 2327 ds = ctx.repo().dirstate
2324 2328
2325 2329 for f in ctx.matches(m):
2326 2330 if rev is None and ds[f] == 'r':
2327 2331 continue
2328 2332 fm.startitem()
2329 2333 if ui.verbose:
2330 2334 fc = ctx[f]
2331 2335 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2332 2336 fm.data(abspath=f)
2333 2337 fm.write('path', fmt, m.rel(f))
2334 2338 ret = 0
2335 2339
2336 2340 for subpath in sorted(ctx.substate):
2337 2341 submatch = matchmod.subdirmatcher(subpath, m)
2338 2342 if (subrepos or m.exact(subpath) or any(submatch.files())):
2339 2343 sub = ctx.sub(subpath)
2340 2344 try:
2341 2345 recurse = m.exact(subpath) or subrepos
2342 2346 if sub.printfiles(ui, submatch, fm, fmt, recurse) == 0:
2343 2347 ret = 0
2344 2348 except error.LookupError:
2345 2349 ui.status(_("skipping missing subrepository: %s\n")
2346 2350 % m.abs(subpath))
2347 2351
2348 2352 return ret
2349 2353
2350 2354 def remove(ui, repo, m, prefix, after, force, subrepos, warnings=None):
2351 2355 join = lambda f: os.path.join(prefix, f)
2352 2356 ret = 0
2353 2357 s = repo.status(match=m, clean=True)
2354 2358 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2355 2359
2356 2360 wctx = repo[None]
2357 2361
2358 2362 if warnings is None:
2359 2363 warnings = []
2360 2364 warn = True
2361 2365 else:
2362 2366 warn = False
2363 2367
2364 2368 subs = sorted(wctx.substate)
2365 2369 total = len(subs)
2366 2370 count = 0
2367 2371 for subpath in subs:
2368 2372 count += 1
2369 2373 submatch = matchmod.subdirmatcher(subpath, m)
2370 2374 if subrepos or m.exact(subpath) or any(submatch.files()):
2371 2375 ui.progress(_('searching'), count, total=total, unit=_('subrepos'))
2372 2376 sub = wctx.sub(subpath)
2373 2377 try:
2374 2378 if sub.removefiles(submatch, prefix, after, force, subrepos,
2375 2379 warnings):
2376 2380 ret = 1
2377 2381 except error.LookupError:
2378 2382 warnings.append(_("skipping missing subrepository: %s\n")
2379 2383 % join(subpath))
2380 2384 ui.progress(_('searching'), None)
2381 2385
2382 2386 # warn about failure to delete explicit files/dirs
2383 2387 deleteddirs = util.dirs(deleted)
2384 2388 files = m.files()
2385 2389 total = len(files)
2386 2390 count = 0
2387 2391 for f in files:
2388 2392 def insubrepo():
2389 2393 for subpath in wctx.substate:
2390 2394 if f.startswith(subpath + '/'):
2391 2395 return True
2392 2396 return False
2393 2397
2394 2398 count += 1
2395 2399 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2396 2400 isdir = f in deleteddirs or wctx.hasdir(f)
2397 2401 if (f in repo.dirstate or isdir or f == '.'
2398 2402 or insubrepo() or f in subs):
2399 2403 continue
2400 2404
2401 2405 if repo.wvfs.exists(f):
2402 2406 if repo.wvfs.isdir(f):
2403 2407 warnings.append(_('not removing %s: no tracked files\n')
2404 2408 % m.rel(f))
2405 2409 else:
2406 2410 warnings.append(_('not removing %s: file is untracked\n')
2407 2411 % m.rel(f))
2408 2412 # missing files will generate a warning elsewhere
2409 2413 ret = 1
2410 2414 ui.progress(_('deleting'), None)
2411 2415
2412 2416 if force:
2413 2417 list = modified + deleted + clean + added
2414 2418 elif after:
2415 2419 list = deleted
2416 2420 remaining = modified + added + clean
2417 2421 total = len(remaining)
2418 2422 count = 0
2419 2423 for f in remaining:
2420 2424 count += 1
2421 2425 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2422 2426 warnings.append(_('not removing %s: file still exists\n')
2423 2427 % m.rel(f))
2424 2428 ret = 1
2425 2429 ui.progress(_('skipping'), None)
2426 2430 else:
2427 2431 list = deleted + clean
2428 2432 total = len(modified) + len(added)
2429 2433 count = 0
2430 2434 for f in modified:
2431 2435 count += 1
2432 2436 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2433 2437 warnings.append(_('not removing %s: file is modified (use -f'
2434 2438 ' to force removal)\n') % m.rel(f))
2435 2439 ret = 1
2436 2440 for f in added:
2437 2441 count += 1
2438 2442 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2439 2443 warnings.append(_("not removing %s: file has been marked for add"
2440 2444 " (use 'hg forget' to undo add)\n") % m.rel(f))
2441 2445 ret = 1
2442 2446 ui.progress(_('skipping'), None)
2443 2447
2444 2448 list = sorted(list)
2445 2449 total = len(list)
2446 2450 count = 0
2447 2451 for f in list:
2448 2452 count += 1
2449 2453 if ui.verbose or not m.exact(f):
2450 2454 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2451 2455 ui.status(_('removing %s\n') % m.rel(f))
2452 2456 ui.progress(_('deleting'), None)
2453 2457
2454 2458 with repo.wlock():
2455 2459 if not after:
2456 2460 for f in list:
2457 2461 if f in added:
2458 2462 continue # we never unlink added files on remove
2459 2463 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
2460 2464 repo[None].forget(list)
2461 2465
2462 2466 if warn:
2463 2467 for warning in warnings:
2464 2468 ui.warn(warning)
2465 2469
2466 2470 return ret
2467 2471
2468 2472 def cat(ui, repo, ctx, matcher, prefix, **opts):
2469 2473 err = 1
2470 2474
2471 2475 def write(path):
2472 2476 fp = makefileobj(repo, opts.get('output'), ctx.node(),
2473 2477 pathname=os.path.join(prefix, path))
2474 2478 data = ctx[path].data()
2475 2479 if opts.get('decode'):
2476 2480 data = repo.wwritedata(path, data)
2477 2481 fp.write(data)
2478 2482 fp.close()
2479 2483
2480 2484 # Automation often uses hg cat on single files, so special case it
2481 2485 # for performance to avoid the cost of parsing the manifest.
2482 2486 if len(matcher.files()) == 1 and not matcher.anypats():
2483 2487 file = matcher.files()[0]
2484 2488 mfl = repo.manifestlog
2485 2489 mfnode = ctx.manifestnode()
2486 2490 try:
2487 2491 if mfnode and mfl[mfnode].find(file)[0]:
2488 2492 write(file)
2489 2493 return 0
2490 2494 except KeyError:
2491 2495 pass
2492 2496
2493 2497 for abs in ctx.walk(matcher):
2494 2498 write(abs)
2495 2499 err = 0
2496 2500
2497 2501 for subpath in sorted(ctx.substate):
2498 2502 sub = ctx.sub(subpath)
2499 2503 try:
2500 2504 submatch = matchmod.subdirmatcher(subpath, matcher)
2501 2505
2502 2506 if not sub.cat(submatch, os.path.join(prefix, sub._path),
2503 2507 **opts):
2504 2508 err = 0
2505 2509 except error.RepoLookupError:
2506 2510 ui.status(_("skipping missing subrepository: %s\n")
2507 2511 % os.path.join(prefix, subpath))
2508 2512
2509 2513 return err
2510 2514
2511 2515 def commit(ui, repo, commitfunc, pats, opts):
2512 2516 '''commit the specified files or all outstanding changes'''
2513 2517 date = opts.get('date')
2514 2518 if date:
2515 2519 opts['date'] = util.parsedate(date)
2516 2520 message = logmessage(ui, opts)
2517 2521 matcher = scmutil.match(repo[None], pats, opts)
2518 2522
2519 2523 # extract addremove carefully -- this function can be called from a command
2520 2524 # that doesn't support addremove
2521 2525 if opts.get('addremove'):
2522 2526 if scmutil.addremove(repo, matcher, "", opts) != 0:
2523 2527 raise error.Abort(
2524 2528 _("failed to mark all new/missing files as added/removed"))
2525 2529
2526 2530 return commitfunc(ui, repo, message, matcher, opts)
2527 2531
2528 2532 def samefile(f, ctx1, ctx2):
2529 2533 if f in ctx1.manifest():
2530 2534 a = ctx1.filectx(f)
2531 2535 if f in ctx2.manifest():
2532 2536 b = ctx2.filectx(f)
2533 2537 return (not a.cmp(b)
2534 2538 and a.flags() == b.flags())
2535 2539 else:
2536 2540 return False
2537 2541 else:
2538 2542 return f not in ctx2.manifest()
2539 2543
2540 2544 def amend(ui, repo, commitfunc, old, extra, pats, opts):
2541 2545 # avoid cycle context -> subrepo -> cmdutil
2542 2546 from . import context
2543 2547
2544 2548 # amend will reuse the existing user if not specified, but the obsolete
2545 2549 # marker creation requires that the current user's name is specified.
2546 2550 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2547 2551 ui.username() # raise exception if username not set
2548 2552
2549 2553 ui.note(_('amending changeset %s\n') % old)
2550 2554 base = old.p1()
2551 2555 createmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
2552 2556
2553 2557 wlock = lock = newid = None
2554 2558 try:
2555 2559 wlock = repo.wlock()
2556 2560 lock = repo.lock()
2557 2561 with repo.transaction('amend') as tr:
2558 2562 # See if we got a message from -m or -l, if not, open the editor
2559 2563 # with the message of the changeset to amend
2560 2564 message = logmessage(ui, opts)
2561 2565 # ensure logfile does not conflict with later enforcement of the
2562 2566 # message. potential logfile content has been processed by
2563 2567 # `logmessage` anyway.
2564 2568 opts.pop('logfile')
2565 2569 # First, do a regular commit to record all changes in the working
2566 2570 # directory (if there are any)
2567 2571 ui.callhooks = False
2568 2572 activebookmark = repo._bookmarks.active
2569 2573 try:
2570 2574 repo._bookmarks.active = None
2571 2575 opts['message'] = 'temporary amend commit for %s' % old
2572 2576 node = commit(ui, repo, commitfunc, pats, opts)
2573 2577 finally:
2574 2578 repo._bookmarks.active = activebookmark
2575 2579 repo._bookmarks.recordchange(tr)
2576 2580 ui.callhooks = True
2577 2581 ctx = repo[node]
2578 2582
2579 2583 # Participating changesets:
2580 2584 #
2581 2585 # node/ctx o - new (intermediate) commit that contains changes
2582 2586 # | from working dir to go into amending commit
2583 2587 # | (or a workingctx if there were no changes)
2584 2588 # |
2585 2589 # old o - changeset to amend
2586 2590 # |
2587 2591 # base o - parent of amending changeset
2588 2592
2589 2593 # Update extra dict from amended commit (e.g. to preserve graft
2590 2594 # source)
2591 2595 extra.update(old.extra())
2592 2596
2593 2597 # Also update it from the intermediate commit or from the wctx
2594 2598 extra.update(ctx.extra())
2595 2599
2596 2600 if len(old.parents()) > 1:
2597 2601 # ctx.files() isn't reliable for merges, so fall back to the
2598 2602 # slower repo.status() method
2599 2603 files = set([fn for st in repo.status(base, old)[:3]
2600 2604 for fn in st])
2601 2605 else:
2602 2606 files = set(old.files())
2603 2607
2604 2608 # Second, we use either the commit we just did, or if there were no
2605 2609 # changes the parent of the working directory as the version of the
2606 2610 # files in the final amend commit
2607 2611 if node:
2608 2612 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
2609 2613
2610 2614 user = ctx.user()
2611 2615 date = ctx.date()
2612 2616 # Recompute copies (avoid recording a -> b -> a)
2613 2617 copied = copies.pathcopies(base, ctx)
2614 2618 if old.p2:
2615 2619 copied.update(copies.pathcopies(old.p2(), ctx))
2616 2620
2617 2621 # Prune files which were reverted by the updates: if old
2618 2622 # introduced file X and our intermediate commit, node,
2619 2623 # renamed that file, then those two files are the same and
2620 2624 # we can discard X from our list of files. Likewise if X
2621 2625 # was deleted, it's no longer relevant
2622 2626 files.update(ctx.files())
2623 2627 files = [f for f in files if not samefile(f, ctx, base)]
2624 2628
2625 2629 def filectxfn(repo, ctx_, path):
2626 2630 try:
2627 2631 fctx = ctx[path]
2628 2632 flags = fctx.flags()
2629 2633 mctx = context.memfilectx(repo,
2630 2634 fctx.path(), fctx.data(),
2631 2635 islink='l' in flags,
2632 2636 isexec='x' in flags,
2633 2637 copied=copied.get(path))
2634 2638 return mctx
2635 2639 except KeyError:
2636 2640 return None
2637 2641 else:
2638 2642 ui.note(_('copying changeset %s to %s\n') % (old, base))
2639 2643
2640 2644 # Use version of files as in the old cset
2641 2645 def filectxfn(repo, ctx_, path):
2642 2646 try:
2643 2647 return old.filectx(path)
2644 2648 except KeyError:
2645 2649 return None
2646 2650
2647 2651 user = opts.get('user') or old.user()
2648 2652 date = opts.get('date') or old.date()
2649 2653 editform = mergeeditform(old, 'commit.amend')
2650 2654 editor = getcommiteditor(editform=editform, **opts)
2651 2655 if not message:
2652 2656 editor = getcommiteditor(edit=True, editform=editform)
2653 2657 message = old.description()
2654 2658
2655 2659 pureextra = extra.copy()
2656 2660 extra['amend_source'] = old.hex()
2657 2661
2658 2662 new = context.memctx(repo,
2659 2663 parents=[base.node(), old.p2().node()],
2660 2664 text=message,
2661 2665 files=files,
2662 2666 filectxfn=filectxfn,
2663 2667 user=user,
2664 2668 date=date,
2665 2669 extra=extra,
2666 2670 editor=editor)
2667 2671
2668 2672 newdesc = changelog.stripdesc(new.description())
2669 2673 if ((not node)
2670 2674 and newdesc == old.description()
2671 2675 and user == old.user()
2672 2676 and date == old.date()
2673 2677 and pureextra == old.extra()):
2674 2678 # nothing changed. continuing here would create a new node
2675 2679 # anyway because of the amend_source noise.
2676 2680 #
2677 2681 # This not what we expect from amend.
2678 2682 return old.node()
2679 2683
2680 2684 ph = repo.ui.config('phases', 'new-commit', phases.draft)
2681 2685 try:
2682 2686 if opts.get('secret'):
2683 2687 commitphase = 'secret'
2684 2688 else:
2685 2689 commitphase = old.phase()
2686 2690 repo.ui.setconfig('phases', 'new-commit', commitphase, 'amend')
2687 2691 newid = repo.commitctx(new)
2688 2692 finally:
2689 2693 repo.ui.setconfig('phases', 'new-commit', ph, 'amend')
2690 2694 if newid != old.node():
2691 2695 # Reroute the working copy parent to the new changeset
2692 2696 repo.setparents(newid, nullid)
2693 2697
2694 2698 # Move bookmarks from old parent to amend commit
2695 2699 bms = repo.nodebookmarks(old.node())
2696 2700 if bms:
2697 2701 marks = repo._bookmarks
2698 2702 for bm in bms:
2699 2703 ui.debug('moving bookmarks %r from %s to %s\n' %
2700 2704 (marks, old.hex(), hex(newid)))
2701 2705 marks[bm] = newid
2702 2706 marks.recordchange(tr)
2703 2707 #commit the whole amend process
2704 2708 if createmarkers:
2705 2709 # mark the new changeset as successor of the rewritten one
2706 2710 new = repo[newid]
2707 2711 obs = [(old, (new,))]
2708 2712 if node:
2709 2713 obs.append((ctx, ()))
2710 2714
2711 2715 obsolete.createmarkers(repo, obs)
2712 2716 if not createmarkers and newid != old.node():
2713 2717 # Strip the intermediate commit (if there was one) and the amended
2714 2718 # commit
2715 2719 if node:
2716 2720 ui.note(_('stripping intermediate changeset %s\n') % ctx)
2717 2721 ui.note(_('stripping amended changeset %s\n') % old)
2718 2722 repair.strip(ui, repo, old.node(), topic='amend-backup')
2719 2723 finally:
2720 2724 lockmod.release(lock, wlock)
2721 2725 return newid
2722 2726
2723 2727 def commiteditor(repo, ctx, subs, editform=''):
2724 2728 if ctx.description():
2725 2729 return ctx.description()
2726 2730 return commitforceeditor(repo, ctx, subs, editform=editform,
2727 2731 unchangedmessagedetection=True)
2728 2732
2729 2733 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2730 2734 editform='', unchangedmessagedetection=False):
2731 2735 if not extramsg:
2732 2736 extramsg = _("Leave message empty to abort commit.")
2733 2737
2734 2738 forms = [e for e in editform.split('.') if e]
2735 2739 forms.insert(0, 'changeset')
2736 2740 templatetext = None
2737 2741 while forms:
2738 2742 tmpl = repo.ui.config('committemplate', '.'.join(forms))
2739 2743 if tmpl:
2740 2744 templatetext = committext = buildcommittemplate(
2741 2745 repo, ctx, subs, extramsg, tmpl)
2742 2746 break
2743 2747 forms.pop()
2744 2748 else:
2745 2749 committext = buildcommittext(repo, ctx, subs, extramsg)
2746 2750
2747 2751 # run editor in the repository root
2748 2752 olddir = pycompat.getcwd()
2749 2753 os.chdir(repo.root)
2750 2754
2751 2755 # make in-memory changes visible to external process
2752 2756 tr = repo.currenttransaction()
2753 2757 repo.dirstate.write(tr)
2754 2758 pending = tr and tr.writepending() and repo.root
2755 2759
2756 2760 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
2757 2761 editform=editform, pending=pending)
2758 2762 text = re.sub("(?m)^HG:.*(\n|$)", "", editortext)
2759 2763 os.chdir(olddir)
2760 2764
2761 2765 if finishdesc:
2762 2766 text = finishdesc(text)
2763 2767 if not text.strip():
2764 2768 raise error.Abort(_("empty commit message"))
2765 2769 if unchangedmessagedetection and editortext == templatetext:
2766 2770 raise error.Abort(_("commit message unchanged"))
2767 2771
2768 2772 return text
2769 2773
2770 2774 def buildcommittemplate(repo, ctx, subs, extramsg, tmpl):
2771 2775 ui = repo.ui
2772 2776 tmpl, mapfile = gettemplate(ui, tmpl, None)
2773 2777
2774 2778 t = changeset_templater(ui, repo, None, {}, tmpl, mapfile, False)
2775 2779
2776 2780 for k, v in repo.ui.configitems('committemplate'):
2777 2781 if k != 'changeset':
2778 2782 t.t.cache[k] = v
2779 2783
2780 2784 if not extramsg:
2781 2785 extramsg = '' # ensure that extramsg is string
2782 2786
2783 2787 ui.pushbuffer()
2784 2788 t.show(ctx, extramsg=extramsg)
2785 2789 return ui.popbuffer()
2786 2790
2787 2791 def hgprefix(msg):
2788 2792 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
2789 2793
2790 2794 def buildcommittext(repo, ctx, subs, extramsg):
2791 2795 edittext = []
2792 2796 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2793 2797 if ctx.description():
2794 2798 edittext.append(ctx.description())
2795 2799 edittext.append("")
2796 2800 edittext.append("") # Empty line between message and comments.
2797 2801 edittext.append(hgprefix(_("Enter commit message."
2798 2802 " Lines beginning with 'HG:' are removed.")))
2799 2803 edittext.append(hgprefix(extramsg))
2800 2804 edittext.append("HG: --")
2801 2805 edittext.append(hgprefix(_("user: %s") % ctx.user()))
2802 2806 if ctx.p2():
2803 2807 edittext.append(hgprefix(_("branch merge")))
2804 2808 if ctx.branch():
2805 2809 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
2806 2810 if bookmarks.isactivewdirparent(repo):
2807 2811 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
2808 2812 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
2809 2813 edittext.extend([hgprefix(_("added %s") % f) for f in added])
2810 2814 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
2811 2815 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
2812 2816 if not added and not modified and not removed:
2813 2817 edittext.append(hgprefix(_("no files changed")))
2814 2818 edittext.append("")
2815 2819
2816 2820 return "\n".join(edittext)
2817 2821
2818 2822 def commitstatus(repo, node, branch, bheads=None, opts=None):
2819 2823 if opts is None:
2820 2824 opts = {}
2821 2825 ctx = repo[node]
2822 2826 parents = ctx.parents()
2823 2827
2824 2828 if (not opts.get('amend') and bheads and node not in bheads and not
2825 2829 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2826 2830 repo.ui.status(_('created new head\n'))
2827 2831 # The message is not printed for initial roots. For the other
2828 2832 # changesets, it is printed in the following situations:
2829 2833 #
2830 2834 # Par column: for the 2 parents with ...
2831 2835 # N: null or no parent
2832 2836 # B: parent is on another named branch
2833 2837 # C: parent is a regular non head changeset
2834 2838 # H: parent was a branch head of the current branch
2835 2839 # Msg column: whether we print "created new head" message
2836 2840 # In the following, it is assumed that there already exists some
2837 2841 # initial branch heads of the current branch, otherwise nothing is
2838 2842 # printed anyway.
2839 2843 #
2840 2844 # Par Msg Comment
2841 2845 # N N y additional topo root
2842 2846 #
2843 2847 # B N y additional branch root
2844 2848 # C N y additional topo head
2845 2849 # H N n usual case
2846 2850 #
2847 2851 # B B y weird additional branch root
2848 2852 # C B y branch merge
2849 2853 # H B n merge with named branch
2850 2854 #
2851 2855 # C C y additional head from merge
2852 2856 # C H n merge with a head
2853 2857 #
2854 2858 # H H n head merge: head count decreases
2855 2859
2856 2860 if not opts.get('close_branch'):
2857 2861 for r in parents:
2858 2862 if r.closesbranch() and r.branch() == branch:
2859 2863 repo.ui.status(_('reopening closed branch head %d\n') % r)
2860 2864
2861 2865 if repo.ui.debugflag:
2862 2866 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
2863 2867 elif repo.ui.verbose:
2864 2868 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
2865 2869
2866 2870 def postcommitstatus(repo, pats, opts):
2867 2871 return repo.status(match=scmutil.match(repo[None], pats, opts))
2868 2872
2869 2873 def revert(ui, repo, ctx, parents, *pats, **opts):
2870 2874 parent, p2 = parents
2871 2875 node = ctx.node()
2872 2876
2873 2877 mf = ctx.manifest()
2874 2878 if node == p2:
2875 2879 parent = p2
2876 2880
2877 2881 # need all matching names in dirstate and manifest of target rev,
2878 2882 # so have to walk both. do not print errors if files exist in one
2879 2883 # but not other. in both cases, filesets should be evaluated against
2880 2884 # workingctx to get consistent result (issue4497). this means 'set:**'
2881 2885 # cannot be used to select missing files from target rev.
2882 2886
2883 2887 # `names` is a mapping for all elements in working copy and target revision
2884 2888 # The mapping is in the form:
2885 2889 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2886 2890 names = {}
2887 2891
2888 2892 with repo.wlock():
2889 2893 ## filling of the `names` mapping
2890 2894 # walk dirstate to fill `names`
2891 2895
2892 2896 interactive = opts.get('interactive', False)
2893 2897 wctx = repo[None]
2894 2898 m = scmutil.match(wctx, pats, opts)
2895 2899
2896 2900 # we'll need this later
2897 2901 targetsubs = sorted(s for s in wctx.substate if m(s))
2898 2902
2899 2903 if not m.always():
2900 2904 for abs in repo.walk(matchmod.badmatch(m, lambda x, y: False)):
2901 2905 names[abs] = m.rel(abs), m.exact(abs)
2902 2906
2903 2907 # walk target manifest to fill `names`
2904 2908
2905 2909 def badfn(path, msg):
2906 2910 if path in names:
2907 2911 return
2908 2912 if path in ctx.substate:
2909 2913 return
2910 2914 path_ = path + '/'
2911 2915 for f in names:
2912 2916 if f.startswith(path_):
2913 2917 return
2914 2918 ui.warn("%s: %s\n" % (m.rel(path), msg))
2915 2919
2916 2920 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
2917 2921 if abs not in names:
2918 2922 names[abs] = m.rel(abs), m.exact(abs)
2919 2923
2920 2924 # Find status of all file in `names`.
2921 2925 m = scmutil.matchfiles(repo, names)
2922 2926
2923 2927 changes = repo.status(node1=node, match=m,
2924 2928 unknown=True, ignored=True, clean=True)
2925 2929 else:
2926 2930 changes = repo.status(node1=node, match=m)
2927 2931 for kind in changes:
2928 2932 for abs in kind:
2929 2933 names[abs] = m.rel(abs), m.exact(abs)
2930 2934
2931 2935 m = scmutil.matchfiles(repo, names)
2932 2936
2933 2937 modified = set(changes.modified)
2934 2938 added = set(changes.added)
2935 2939 removed = set(changes.removed)
2936 2940 _deleted = set(changes.deleted)
2937 2941 unknown = set(changes.unknown)
2938 2942 unknown.update(changes.ignored)
2939 2943 clean = set(changes.clean)
2940 2944 modadded = set()
2941 2945
2942 2946 # split between files known in target manifest and the others
2943 2947 smf = set(mf)
2944 2948
2945 2949 # determine the exact nature of the deleted changesets
2946 2950 deladded = _deleted - smf
2947 2951 deleted = _deleted - deladded
2948 2952
2949 2953 # We need to account for the state of the file in the dirstate,
2950 2954 # even when we revert against something else than parent. This will
2951 2955 # slightly alter the behavior of revert (doing back up or not, delete
2952 2956 # or just forget etc).
2953 2957 if parent == node:
2954 2958 dsmodified = modified
2955 2959 dsadded = added
2956 2960 dsremoved = removed
2957 2961 # store all local modifications, useful later for rename detection
2958 2962 localchanges = dsmodified | dsadded
2959 2963 modified, added, removed = set(), set(), set()
2960 2964 else:
2961 2965 changes = repo.status(node1=parent, match=m)
2962 2966 dsmodified = set(changes.modified)
2963 2967 dsadded = set(changes.added)
2964 2968 dsremoved = set(changes.removed)
2965 2969 # store all local modifications, useful later for rename detection
2966 2970 localchanges = dsmodified | dsadded
2967 2971
2968 2972 # only take into account for removes between wc and target
2969 2973 clean |= dsremoved - removed
2970 2974 dsremoved &= removed
2971 2975 # distinct between dirstate remove and other
2972 2976 removed -= dsremoved
2973 2977
2974 2978 modadded = added & dsmodified
2975 2979 added -= modadded
2976 2980
2977 2981 # tell newly modified apart.
2978 2982 dsmodified &= modified
2979 2983 dsmodified |= modified & dsadded # dirstate added may need backup
2980 2984 modified -= dsmodified
2981 2985
2982 2986 # We need to wait for some post-processing to update this set
2983 2987 # before making the distinction. The dirstate will be used for
2984 2988 # that purpose.
2985 2989 dsadded = added
2986 2990
2987 2991 # in case of merge, files that are actually added can be reported as
2988 2992 # modified, we need to post process the result
2989 2993 if p2 != nullid:
2990 2994 mergeadd = dsmodified - smf
2991 2995 dsadded |= mergeadd
2992 2996 dsmodified -= mergeadd
2993 2997
2994 2998 # if f is a rename, update `names` to also revert the source
2995 2999 cwd = repo.getcwd()
2996 3000 for f in localchanges:
2997 3001 src = repo.dirstate.copied(f)
2998 3002 # XXX should we check for rename down to target node?
2999 3003 if src and src not in names and repo.dirstate[src] == 'r':
3000 3004 dsremoved.add(src)
3001 3005 names[src] = (repo.pathto(src, cwd), True)
3002 3006
3003 3007 # distinguish between file to forget and the other
3004 3008 added = set()
3005 3009 for abs in dsadded:
3006 3010 if repo.dirstate[abs] != 'a':
3007 3011 added.add(abs)
3008 3012 dsadded -= added
3009 3013
3010 3014 for abs in deladded:
3011 3015 if repo.dirstate[abs] == 'a':
3012 3016 dsadded.add(abs)
3013 3017 deladded -= dsadded
3014 3018
3015 3019 # For files marked as removed, we check if an unknown file is present at
3016 3020 # the same path. If a such file exists it may need to be backed up.
3017 3021 # Making the distinction at this stage helps have simpler backup
3018 3022 # logic.
3019 3023 removunk = set()
3020 3024 for abs in removed:
3021 3025 target = repo.wjoin(abs)
3022 3026 if os.path.lexists(target):
3023 3027 removunk.add(abs)
3024 3028 removed -= removunk
3025 3029
3026 3030 dsremovunk = set()
3027 3031 for abs in dsremoved:
3028 3032 target = repo.wjoin(abs)
3029 3033 if os.path.lexists(target):
3030 3034 dsremovunk.add(abs)
3031 3035 dsremoved -= dsremovunk
3032 3036
3033 3037 # action to be actually performed by revert
3034 3038 # (<list of file>, message>) tuple
3035 3039 actions = {'revert': ([], _('reverting %s\n')),
3036 3040 'add': ([], _('adding %s\n')),
3037 3041 'remove': ([], _('removing %s\n')),
3038 3042 'drop': ([], _('removing %s\n')),
3039 3043 'forget': ([], _('forgetting %s\n')),
3040 3044 'undelete': ([], _('undeleting %s\n')),
3041 3045 'noop': (None, _('no changes needed to %s\n')),
3042 3046 'unknown': (None, _('file not managed: %s\n')),
3043 3047 }
3044 3048
3045 3049 # "constant" that convey the backup strategy.
3046 3050 # All set to `discard` if `no-backup` is set do avoid checking
3047 3051 # no_backup lower in the code.
3048 3052 # These values are ordered for comparison purposes
3049 3053 backupinteractive = 3 # do backup if interactively modified
3050 3054 backup = 2 # unconditionally do backup
3051 3055 check = 1 # check if the existing file differs from target
3052 3056 discard = 0 # never do backup
3053 3057 if opts.get('no_backup'):
3054 3058 backupinteractive = backup = check = discard
3055 3059 if interactive:
3056 3060 dsmodifiedbackup = backupinteractive
3057 3061 else:
3058 3062 dsmodifiedbackup = backup
3059 3063 tobackup = set()
3060 3064
3061 3065 backupanddel = actions['remove']
3062 3066 if not opts.get('no_backup'):
3063 3067 backupanddel = actions['drop']
3064 3068
3065 3069 disptable = (
3066 3070 # dispatch table:
3067 3071 # file state
3068 3072 # action
3069 3073 # make backup
3070 3074
3071 3075 ## Sets that results that will change file on disk
3072 3076 # Modified compared to target, no local change
3073 3077 (modified, actions['revert'], discard),
3074 3078 # Modified compared to target, but local file is deleted
3075 3079 (deleted, actions['revert'], discard),
3076 3080 # Modified compared to target, local change
3077 3081 (dsmodified, actions['revert'], dsmodifiedbackup),
3078 3082 # Added since target
3079 3083 (added, actions['remove'], discard),
3080 3084 # Added in working directory
3081 3085 (dsadded, actions['forget'], discard),
3082 3086 # Added since target, have local modification
3083 3087 (modadded, backupanddel, backup),
3084 3088 # Added since target but file is missing in working directory
3085 3089 (deladded, actions['drop'], discard),
3086 3090 # Removed since target, before working copy parent
3087 3091 (removed, actions['add'], discard),
3088 3092 # Same as `removed` but an unknown file exists at the same path
3089 3093 (removunk, actions['add'], check),
3090 3094 # Removed since targe, marked as such in working copy parent
3091 3095 (dsremoved, actions['undelete'], discard),
3092 3096 # Same as `dsremoved` but an unknown file exists at the same path
3093 3097 (dsremovunk, actions['undelete'], check),
3094 3098 ## the following sets does not result in any file changes
3095 3099 # File with no modification
3096 3100 (clean, actions['noop'], discard),
3097 3101 # Existing file, not tracked anywhere
3098 3102 (unknown, actions['unknown'], discard),
3099 3103 )
3100 3104
3101 3105 for abs, (rel, exact) in sorted(names.items()):
3102 3106 # target file to be touch on disk (relative to cwd)
3103 3107 target = repo.wjoin(abs)
3104 3108 # search the entry in the dispatch table.
3105 3109 # if the file is in any of these sets, it was touched in the working
3106 3110 # directory parent and we are sure it needs to be reverted.
3107 3111 for table, (xlist, msg), dobackup in disptable:
3108 3112 if abs not in table:
3109 3113 continue
3110 3114 if xlist is not None:
3111 3115 xlist.append(abs)
3112 3116 if dobackup:
3113 3117 # If in interactive mode, don't automatically create
3114 3118 # .orig files (issue4793)
3115 3119 if dobackup == backupinteractive:
3116 3120 tobackup.add(abs)
3117 3121 elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
3118 3122 bakname = scmutil.origpath(ui, repo, rel)
3119 3123 ui.note(_('saving current version of %s as %s\n') %
3120 3124 (rel, bakname))
3121 3125 if not opts.get('dry_run'):
3122 3126 if interactive:
3123 3127 util.copyfile(target, bakname)
3124 3128 else:
3125 3129 util.rename(target, bakname)
3126 3130 if ui.verbose or not exact:
3127 3131 if not isinstance(msg, basestring):
3128 3132 msg = msg(abs)
3129 3133 ui.status(msg % rel)
3130 3134 elif exact:
3131 3135 ui.warn(msg % rel)
3132 3136 break
3133 3137
3134 3138 if not opts.get('dry_run'):
3135 3139 needdata = ('revert', 'add', 'undelete')
3136 3140 _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
3137 3141 _performrevert(repo, parents, ctx, actions, interactive, tobackup)
3138 3142
3139 3143 if targetsubs:
3140 3144 # Revert the subrepos on the revert list
3141 3145 for sub in targetsubs:
3142 3146 try:
3143 3147 wctx.sub(sub).revert(ctx.substate[sub], *pats, **opts)
3144 3148 except KeyError:
3145 3149 raise error.Abort("subrepository '%s' does not exist in %s!"
3146 3150 % (sub, short(ctx.node())))
3147 3151
3148 3152 def _revertprefetch(repo, ctx, *files):
3149 3153 """Let extension changing the storage layer prefetch content"""
3150 3154 pass
3151 3155
3152 3156 def _performrevert(repo, parents, ctx, actions, interactive=False,
3153 3157 tobackup=None):
3154 3158 """function that actually perform all the actions computed for revert
3155 3159
3156 3160 This is an independent function to let extension to plug in and react to
3157 3161 the imminent revert.
3158 3162
3159 3163 Make sure you have the working directory locked when calling this function.
3160 3164 """
3161 3165 parent, p2 = parents
3162 3166 node = ctx.node()
3163 3167 excluded_files = []
3164 3168 matcher_opts = {"exclude": excluded_files}
3165 3169
3166 3170 def checkout(f):
3167 3171 fc = ctx[f]
3168 3172 repo.wwrite(f, fc.data(), fc.flags())
3169 3173
3170 3174 def doremove(f):
3171 3175 try:
3172 3176 util.unlinkpath(repo.wjoin(f))
3173 3177 except OSError:
3174 3178 pass
3175 3179 repo.dirstate.remove(f)
3176 3180
3177 3181 audit_path = pathutil.pathauditor(repo.root)
3178 3182 for f in actions['forget'][0]:
3179 3183 if interactive:
3180 3184 choice = repo.ui.promptchoice(
3181 3185 _("forget added file %s (Yn)?$$ &Yes $$ &No") % f)
3182 3186 if choice == 0:
3183 3187 repo.dirstate.drop(f)
3184 3188 else:
3185 3189 excluded_files.append(repo.wjoin(f))
3186 3190 else:
3187 3191 repo.dirstate.drop(f)
3188 3192 for f in actions['remove'][0]:
3189 3193 audit_path(f)
3190 3194 if interactive:
3191 3195 choice = repo.ui.promptchoice(
3192 3196 _("remove added file %s (Yn)?$$ &Yes $$ &No") % f)
3193 3197 if choice == 0:
3194 3198 doremove(f)
3195 3199 else:
3196 3200 excluded_files.append(repo.wjoin(f))
3197 3201 else:
3198 3202 doremove(f)
3199 3203 for f in actions['drop'][0]:
3200 3204 audit_path(f)
3201 3205 repo.dirstate.remove(f)
3202 3206
3203 3207 normal = None
3204 3208 if node == parent:
3205 3209 # We're reverting to our parent. If possible, we'd like status
3206 3210 # to report the file as clean. We have to use normallookup for
3207 3211 # merges to avoid losing information about merged/dirty files.
3208 3212 if p2 != nullid:
3209 3213 normal = repo.dirstate.normallookup
3210 3214 else:
3211 3215 normal = repo.dirstate.normal
3212 3216
3213 3217 newlyaddedandmodifiedfiles = set()
3214 3218 if interactive:
3215 3219 # Prompt the user for changes to revert
3216 3220 torevert = [repo.wjoin(f) for f in actions['revert'][0]]
3217 3221 m = scmutil.match(ctx, torevert, matcher_opts)
3218 3222 diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
3219 3223 diffopts.nodates = True
3220 3224 diffopts.git = True
3221 3225 reversehunks = repo.ui.configbool('experimental',
3222 3226 'revertalternateinteractivemode',
3223 3227 True)
3224 3228 if reversehunks:
3225 3229 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3226 3230 else:
3227 3231 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3228 3232 originalchunks = patch.parsepatch(diff)
3229 3233 operation = 'discard' if node == parent else 'revert'
3230 3234
3231 3235 try:
3232 3236
3233 3237 chunks, opts = recordfilter(repo.ui, originalchunks,
3234 3238 operation=operation)
3235 3239 if reversehunks:
3236 3240 chunks = patch.reversehunks(chunks)
3237 3241
3238 3242 except patch.PatchError as err:
3239 3243 raise error.Abort(_('error parsing patch: %s') % err)
3240 3244
3241 3245 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
3242 3246 if tobackup is None:
3243 3247 tobackup = set()
3244 3248 # Apply changes
3245 3249 fp = stringio()
3246 3250 for c in chunks:
3247 3251 # Create a backup file only if this hunk should be backed up
3248 3252 if ishunk(c) and c.header.filename() in tobackup:
3249 3253 abs = c.header.filename()
3250 3254 target = repo.wjoin(abs)
3251 3255 bakname = scmutil.origpath(repo.ui, repo, m.rel(abs))
3252 3256 util.copyfile(target, bakname)
3253 3257 tobackup.remove(abs)
3254 3258 c.write(fp)
3255 3259 dopatch = fp.tell()
3256 3260 fp.seek(0)
3257 3261 if dopatch:
3258 3262 try:
3259 3263 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3260 3264 except patch.PatchError as err:
3261 3265 raise error.Abort(str(err))
3262 3266 del fp
3263 3267 else:
3264 3268 for f in actions['revert'][0]:
3265 3269 checkout(f)
3266 3270 if normal:
3267 3271 normal(f)
3268 3272
3269 3273 for f in actions['add'][0]:
3270 3274 # Don't checkout modified files, they are already created by the diff
3271 3275 if f not in newlyaddedandmodifiedfiles:
3272 3276 checkout(f)
3273 3277 repo.dirstate.add(f)
3274 3278
3275 3279 normal = repo.dirstate.normallookup
3276 3280 if node == parent and p2 == nullid:
3277 3281 normal = repo.dirstate.normal
3278 3282 for f in actions['undelete'][0]:
3279 3283 checkout(f)
3280 3284 normal(f)
3281 3285
3282 3286 copied = copies.pathcopies(repo[parent], ctx)
3283 3287
3284 3288 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3285 3289 if f in copied:
3286 3290 repo.dirstate.copy(copied[f], f)
3287 3291
3288 3292 def command(table):
3289 3293 """Returns a function object to be used as a decorator for making commands.
3290 3294
3291 3295 This function receives a command table as its argument. The table should
3292 3296 be a dict.
3293 3297
3294 3298 The returned function can be used as a decorator for adding commands
3295 3299 to that command table. This function accepts multiple arguments to define
3296 3300 a command.
3297 3301
3298 3302 The first argument is the command name.
3299 3303
3300 3304 The options argument is an iterable of tuples defining command arguments.
3301 3305 See ``mercurial.fancyopts.fancyopts()`` for the format of each tuple.
3302 3306
3303 3307 The synopsis argument defines a short, one line summary of how to use the
3304 3308 command. This shows up in the help output.
3305 3309
3306 3310 The norepo argument defines whether the command does not require a
3307 3311 local repository. Most commands operate against a repository, thus the
3308 3312 default is False.
3309 3313
3310 3314 The optionalrepo argument defines whether the command optionally requires
3311 3315 a local repository.
3312 3316
3313 3317 The inferrepo argument defines whether to try to find a repository from the
3314 3318 command line arguments. If True, arguments will be examined for potential
3315 3319 repository locations. See ``findrepo()``. If a repository is found, it
3316 3320 will be used.
3317 3321 """
3318 3322 def cmd(name, options=(), synopsis=None, norepo=False, optionalrepo=False,
3319 3323 inferrepo=False):
3320 3324 def decorator(func):
3321 3325 func.norepo = norepo
3322 3326 func.optionalrepo = optionalrepo
3323 3327 func.inferrepo = inferrepo
3324 3328 if synopsis:
3325 3329 table[name] = func, list(options), synopsis
3326 3330 else:
3327 3331 table[name] = func, list(options)
3328 3332 return func
3329 3333 return decorator
3330 3334
3331 3335 return cmd
3332 3336
3333 3337 def checkunresolved(ms):
3334 3338 ms._repo.ui.deprecwarn('checkunresolved moved from cmdutil to mergeutil',
3335 3339 '4.1')
3336 3340 return mergeutil.checkunresolved(ms)
3337 3341
3338 3342 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3339 3343 # commands.outgoing. "missing" is "missing" of the result of
3340 3344 # "findcommonoutgoing()"
3341 3345 outgoinghooks = util.hooks()
3342 3346
3343 3347 # a list of (ui, repo) functions called by commands.summary
3344 3348 summaryhooks = util.hooks()
3345 3349
3346 3350 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3347 3351 #
3348 3352 # functions should return tuple of booleans below, if 'changes' is None:
3349 3353 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3350 3354 #
3351 3355 # otherwise, 'changes' is a tuple of tuples below:
3352 3356 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3353 3357 # - (desturl, destbranch, destpeer, outgoing)
3354 3358 summaryremotehooks = util.hooks()
3355 3359
3356 3360 # A list of state files kept by multistep operations like graft.
3357 3361 # Since graft cannot be aborted, it is considered 'clearable' by update.
3358 3362 # note: bisect is intentionally excluded
3359 3363 # (state file, clearable, allowcommit, error, hint)
3360 3364 unfinishedstates = [
3361 3365 ('graftstate', True, False, _('graft in progress'),
3362 3366 _("use 'hg graft --continue' or 'hg update' to abort")),
3363 3367 ('updatestate', True, False, _('last update was interrupted'),
3364 3368 _("use 'hg update' to get a consistent checkout"))
3365 3369 ]
3366 3370
3367 3371 def checkunfinished(repo, commit=False):
3368 3372 '''Look for an unfinished multistep operation, like graft, and abort
3369 3373 if found. It's probably good to check this right before
3370 3374 bailifchanged().
3371 3375 '''
3372 3376 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3373 3377 if commit and allowcommit:
3374 3378 continue
3375 3379 if repo.vfs.exists(f):
3376 3380 raise error.Abort(msg, hint=hint)
3377 3381
3378 3382 def clearunfinished(repo):
3379 3383 '''Check for unfinished operations (as above), and clear the ones
3380 3384 that are clearable.
3381 3385 '''
3382 3386 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3383 3387 if not clearable and repo.vfs.exists(f):
3384 3388 raise error.Abort(msg, hint=hint)
3385 3389 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3386 3390 if clearable and repo.vfs.exists(f):
3387 3391 util.unlink(repo.join(f))
3388 3392
3389 3393 afterresolvedstates = [
3390 3394 ('graftstate',
3391 3395 _('hg graft --continue')),
3392 3396 ]
3393 3397
3394 3398 def howtocontinue(repo):
3395 3399 '''Check for an unfinished operation and return the command to finish
3396 3400 it.
3397 3401
3398 3402 afterresolvedstates tuples define a .hg/{file} and the corresponding
3399 3403 command needed to finish it.
3400 3404
3401 3405 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3402 3406 a boolean.
3403 3407 '''
3404 3408 contmsg = _("continue: %s")
3405 3409 for f, msg in afterresolvedstates:
3406 3410 if repo.vfs.exists(f):
3407 3411 return contmsg % msg, True
3408 3412 workingctx = repo[None]
3409 3413 dirty = any(repo.status()) or any(workingctx.sub(s).dirty()
3410 3414 for s in workingctx.substate)
3411 3415 if dirty:
3412 3416 return contmsg % _("hg commit"), False
3413 3417 return None, None
3414 3418
3415 3419 def checkafterresolved(repo):
3416 3420 '''Inform the user about the next action after completing hg resolve
3417 3421
3418 3422 If there's a matching afterresolvedstates, howtocontinue will yield
3419 3423 repo.ui.warn as the reporter.
3420 3424
3421 3425 Otherwise, it will yield repo.ui.note.
3422 3426 '''
3423 3427 msg, warning = howtocontinue(repo)
3424 3428 if msg is not None:
3425 3429 if warning:
3426 3430 repo.ui.warn("%s\n" % msg)
3427 3431 else:
3428 3432 repo.ui.note("%s\n" % msg)
3429 3433
3430 3434 def wrongtooltocontinue(repo, task):
3431 3435 '''Raise an abort suggesting how to properly continue if there is an
3432 3436 active task.
3433 3437
3434 3438 Uses howtocontinue() to find the active task.
3435 3439
3436 3440 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3437 3441 a hint.
3438 3442 '''
3439 3443 after = howtocontinue(repo)
3440 3444 hint = None
3441 3445 if after[1]:
3442 3446 hint = after[0]
3443 3447 raise error.Abort(_('no %s in progress') % task, hint=hint)
3444 3448
3445 3449 class dirstateguard(dirstateguardmod.dirstateguard):
3446 3450 def __init__(self, repo, name):
3447 3451 dirstateguardmod.dirstateguard.__init__(self, repo, name)
3448 3452 repo.ui.deprecwarn(
3449 3453 'dirstateguard has moved from cmdutil to dirstateguard',
3450 3454 '4.1')
General Comments 0
You need to be logged in to leave comments. Login now