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