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