##// END OF EJS Templates
cat: add formatter support...
Yuya Nishihara -
r32578:746e12a7 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,3586 +1,3586 b''
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import itertools
12 12 import os
13 13 import re
14 14 import tempfile
15 15
16 16 from .i18n import _
17 17 from .node import (
18 18 bin,
19 19 hex,
20 20 nullid,
21 21 nullrev,
22 22 short,
23 23 )
24 24
25 25 from . import (
26 26 bookmarks,
27 27 changelog,
28 28 copies,
29 29 crecord as crecordmod,
30 30 encoding,
31 31 error,
32 32 formatter,
33 33 graphmod,
34 34 lock as lockmod,
35 35 match as matchmod,
36 36 obsolete,
37 37 patch,
38 38 pathutil,
39 39 phases,
40 40 pycompat,
41 41 registrar,
42 42 repair,
43 43 revlog,
44 44 revset,
45 45 scmutil,
46 46 smartset,
47 47 templatekw,
48 48 templater,
49 49 util,
50 50 vfs as vfsmod,
51 51 )
52 52 stringio = util.stringio
53 53
54 54 # templates of common command options
55 55
56 56 dryrunopts = [
57 57 ('n', 'dry-run', None,
58 58 _('do not perform actions, just print output')),
59 59 ]
60 60
61 61 remoteopts = [
62 62 ('e', 'ssh', '',
63 63 _('specify ssh command to use'), _('CMD')),
64 64 ('', 'remotecmd', '',
65 65 _('specify hg command to run on the remote side'), _('CMD')),
66 66 ('', 'insecure', None,
67 67 _('do not verify server certificate (ignoring web.cacerts config)')),
68 68 ]
69 69
70 70 walkopts = [
71 71 ('I', 'include', [],
72 72 _('include names matching the given patterns'), _('PATTERN')),
73 73 ('X', 'exclude', [],
74 74 _('exclude names matching the given patterns'), _('PATTERN')),
75 75 ]
76 76
77 77 commitopts = [
78 78 ('m', 'message', '',
79 79 _('use text as commit message'), _('TEXT')),
80 80 ('l', 'logfile', '',
81 81 _('read commit message from file'), _('FILE')),
82 82 ]
83 83
84 84 commitopts2 = [
85 85 ('d', 'date', '',
86 86 _('record the specified date as commit date'), _('DATE')),
87 87 ('u', 'user', '',
88 88 _('record the specified user as committer'), _('USER')),
89 89 ]
90 90
91 91 # hidden for now
92 92 formatteropts = [
93 93 ('T', 'template', '',
94 94 _('display with template (EXPERIMENTAL)'), _('TEMPLATE')),
95 95 ]
96 96
97 97 templateopts = [
98 98 ('', 'style', '',
99 99 _('display using template map file (DEPRECATED)'), _('STYLE')),
100 100 ('T', 'template', '',
101 101 _('display with template'), _('TEMPLATE')),
102 102 ]
103 103
104 104 logopts = [
105 105 ('p', 'patch', None, _('show patch')),
106 106 ('g', 'git', None, _('use git extended diff format')),
107 107 ('l', 'limit', '',
108 108 _('limit number of changes displayed'), _('NUM')),
109 109 ('M', 'no-merges', None, _('do not show merges')),
110 110 ('', 'stat', None, _('output diffstat-style summary of changes')),
111 111 ('G', 'graph', None, _("show the revision DAG")),
112 112 ] + templateopts
113 113
114 114 diffopts = [
115 115 ('a', 'text', None, _('treat all files as text')),
116 116 ('g', 'git', None, _('use git extended diff format')),
117 117 ('', 'binary', None, _('generate binary diffs in git mode (default)')),
118 118 ('', 'nodates', None, _('omit dates from diff headers'))
119 119 ]
120 120
121 121 diffwsopts = [
122 122 ('w', 'ignore-all-space', None,
123 123 _('ignore white space when comparing lines')),
124 124 ('b', 'ignore-space-change', None,
125 125 _('ignore changes in the amount of white space')),
126 126 ('B', 'ignore-blank-lines', None,
127 127 _('ignore changes whose lines are all blank')),
128 128 ]
129 129
130 130 diffopts2 = [
131 131 ('', 'noprefix', None, _('omit a/ and b/ prefixes from filenames')),
132 132 ('p', 'show-function', None, _('show which function each change is in')),
133 133 ('', 'reverse', None, _('produce a diff that undoes the changes')),
134 134 ] + diffwsopts + [
135 135 ('U', 'unified', '',
136 136 _('number of lines of context to show'), _('NUM')),
137 137 ('', 'stat', None, _('output diffstat-style summary of changes')),
138 138 ('', 'root', '', _('produce diffs relative to subdirectory'), _('DIR')),
139 139 ]
140 140
141 141 mergetoolopts = [
142 142 ('t', 'tool', '', _('specify merge tool')),
143 143 ]
144 144
145 145 similarityopts = [
146 146 ('s', 'similarity', '',
147 147 _('guess renamed files by similarity (0<=s<=100)'), _('SIMILARITY'))
148 148 ]
149 149
150 150 subrepoopts = [
151 151 ('S', 'subrepos', None,
152 152 _('recurse into subrepositories'))
153 153 ]
154 154
155 155 debugrevlogopts = [
156 156 ('c', 'changelog', False, _('open changelog')),
157 157 ('m', 'manifest', False, _('open manifest')),
158 158 ('', 'dir', '', _('open directory manifest')),
159 159 ]
160 160
161 161 # special string such that everything below this line will be ingored in the
162 162 # editor text
163 163 _linebelow = "^HG: ------------------------ >8 ------------------------$"
164 164
165 165 def ishunk(x):
166 166 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
167 167 return isinstance(x, hunkclasses)
168 168
169 169 def newandmodified(chunks, originalchunks):
170 170 newlyaddedandmodifiedfiles = set()
171 171 for chunk in chunks:
172 172 if ishunk(chunk) and chunk.header.isnewfile() and chunk not in \
173 173 originalchunks:
174 174 newlyaddedandmodifiedfiles.add(chunk.header.filename())
175 175 return newlyaddedandmodifiedfiles
176 176
177 177 def parsealiases(cmd):
178 178 return cmd.lstrip("^").split("|")
179 179
180 180 def setupwrapcolorwrite(ui):
181 181 # wrap ui.write so diff output can be labeled/colorized
182 182 def wrapwrite(orig, *args, **kw):
183 183 label = kw.pop('label', '')
184 184 for chunk, l in patch.difflabel(lambda: args):
185 185 orig(chunk, label=label + l)
186 186
187 187 oldwrite = ui.write
188 188 def wrap(*args, **kwargs):
189 189 return wrapwrite(oldwrite, *args, **kwargs)
190 190 setattr(ui, 'write', wrap)
191 191 return oldwrite
192 192
193 193 def filterchunks(ui, originalhunks, usecurses, testfile, operation=None):
194 194 if usecurses:
195 195 if testfile:
196 196 recordfn = crecordmod.testdecorator(testfile,
197 197 crecordmod.testchunkselector)
198 198 else:
199 199 recordfn = crecordmod.chunkselector
200 200
201 201 return crecordmod.filterpatch(ui, originalhunks, recordfn, operation)
202 202
203 203 else:
204 204 return patch.filterpatch(ui, originalhunks, operation)
205 205
206 206 def recordfilter(ui, originalhunks, operation=None):
207 207 """ Prompts the user to filter the originalhunks and return a list of
208 208 selected hunks.
209 209 *operation* is used for to build ui messages to indicate the user what
210 210 kind of filtering they are doing: reverting, committing, shelving, etc.
211 211 (see patch.filterpatch).
212 212 """
213 213 usecurses = crecordmod.checkcurses(ui)
214 214 testfile = ui.config('experimental', 'crecordtest', None)
215 215 oldwrite = setupwrapcolorwrite(ui)
216 216 try:
217 217 newchunks, newopts = filterchunks(ui, originalhunks, usecurses,
218 218 testfile, operation)
219 219 finally:
220 220 ui.write = oldwrite
221 221 return newchunks, newopts
222 222
223 223 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
224 224 filterfn, *pats, **opts):
225 225 from . import merge as mergemod
226 226 opts = pycompat.byteskwargs(opts)
227 227 if not ui.interactive():
228 228 if cmdsuggest:
229 229 msg = _('running non-interactively, use %s instead') % cmdsuggest
230 230 else:
231 231 msg = _('running non-interactively')
232 232 raise error.Abort(msg)
233 233
234 234 # make sure username is set before going interactive
235 235 if not opts.get('user'):
236 236 ui.username() # raise exception, username not provided
237 237
238 238 def recordfunc(ui, repo, message, match, opts):
239 239 """This is generic record driver.
240 240
241 241 Its job is to interactively filter local changes, and
242 242 accordingly prepare working directory into a state in which the
243 243 job can be delegated to a non-interactive commit command such as
244 244 'commit' or 'qrefresh'.
245 245
246 246 After the actual job is done by non-interactive command, the
247 247 working directory is restored to its original state.
248 248
249 249 In the end we'll record interesting changes, and everything else
250 250 will be left in place, so the user can continue working.
251 251 """
252 252
253 253 checkunfinished(repo, commit=True)
254 254 wctx = repo[None]
255 255 merge = len(wctx.parents()) > 1
256 256 if merge:
257 257 raise error.Abort(_('cannot partially commit a merge '
258 258 '(use "hg commit" instead)'))
259 259
260 260 def fail(f, msg):
261 261 raise error.Abort('%s: %s' % (f, msg))
262 262
263 263 force = opts.get('force')
264 264 if not force:
265 265 vdirs = []
266 266 match.explicitdir = vdirs.append
267 267 match.bad = fail
268 268
269 269 status = repo.status(match=match)
270 270 if not force:
271 271 repo.checkcommitpatterns(wctx, vdirs, match, status, fail)
272 272 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
273 273 diffopts.nodates = True
274 274 diffopts.git = True
275 275 diffopts.showfunc = True
276 276 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
277 277 originalchunks = patch.parsepatch(originaldiff)
278 278
279 279 # 1. filter patch, since we are intending to apply subset of it
280 280 try:
281 281 chunks, newopts = filterfn(ui, originalchunks)
282 282 except patch.PatchError as err:
283 283 raise error.Abort(_('error parsing patch: %s') % err)
284 284 opts.update(newopts)
285 285
286 286 # We need to keep a backup of files that have been newly added and
287 287 # modified during the recording process because there is a previous
288 288 # version without the edit in the workdir
289 289 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
290 290 contenders = set()
291 291 for h in chunks:
292 292 try:
293 293 contenders.update(set(h.files()))
294 294 except AttributeError:
295 295 pass
296 296
297 297 changed = status.modified + status.added + status.removed
298 298 newfiles = [f for f in changed if f in contenders]
299 299 if not newfiles:
300 300 ui.status(_('no changes to record\n'))
301 301 return 0
302 302
303 303 modified = set(status.modified)
304 304
305 305 # 2. backup changed files, so we can restore them in the end
306 306
307 307 if backupall:
308 308 tobackup = changed
309 309 else:
310 310 tobackup = [f for f in newfiles if f in modified or f in \
311 311 newlyaddedandmodifiedfiles]
312 312 backups = {}
313 313 if tobackup:
314 314 backupdir = repo.vfs.join('record-backups')
315 315 try:
316 316 os.mkdir(backupdir)
317 317 except OSError as err:
318 318 if err.errno != errno.EEXIST:
319 319 raise
320 320 try:
321 321 # backup continues
322 322 for f in tobackup:
323 323 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
324 324 dir=backupdir)
325 325 os.close(fd)
326 326 ui.debug('backup %r as %r\n' % (f, tmpname))
327 327 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
328 328 backups[f] = tmpname
329 329
330 330 fp = stringio()
331 331 for c in chunks:
332 332 fname = c.filename()
333 333 if fname in backups:
334 334 c.write(fp)
335 335 dopatch = fp.tell()
336 336 fp.seek(0)
337 337
338 338 # 2.5 optionally review / modify patch in text editor
339 339 if opts.get('review', False):
340 340 patchtext = (crecordmod.diffhelptext
341 341 + crecordmod.patchhelptext
342 342 + fp.read())
343 343 reviewedpatch = ui.edit(patchtext, "",
344 344 extra={"suffix": ".diff"},
345 345 repopath=repo.path)
346 346 fp.truncate(0)
347 347 fp.write(reviewedpatch)
348 348 fp.seek(0)
349 349
350 350 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
351 351 # 3a. apply filtered patch to clean repo (clean)
352 352 if backups:
353 353 # Equivalent to hg.revert
354 354 m = scmutil.matchfiles(repo, backups.keys())
355 355 mergemod.update(repo, repo.dirstate.p1(),
356 356 False, True, matcher=m)
357 357
358 358 # 3b. (apply)
359 359 if dopatch:
360 360 try:
361 361 ui.debug('applying patch\n')
362 362 ui.debug(fp.getvalue())
363 363 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
364 364 except patch.PatchError as err:
365 365 raise error.Abort(str(err))
366 366 del fp
367 367
368 368 # 4. We prepared working directory according to filtered
369 369 # patch. Now is the time to delegate the job to
370 370 # commit/qrefresh or the like!
371 371
372 372 # Make all of the pathnames absolute.
373 373 newfiles = [repo.wjoin(nf) for nf in newfiles]
374 374 return commitfunc(ui, repo, *newfiles, **opts)
375 375 finally:
376 376 # 5. finally restore backed-up files
377 377 try:
378 378 dirstate = repo.dirstate
379 379 for realname, tmpname in backups.iteritems():
380 380 ui.debug('restoring %r to %r\n' % (tmpname, realname))
381 381
382 382 if dirstate[realname] == 'n':
383 383 # without normallookup, restoring timestamp
384 384 # may cause partially committed files
385 385 # to be treated as unmodified
386 386 dirstate.normallookup(realname)
387 387
388 388 # copystat=True here and above are a hack to trick any
389 389 # editors that have f open that we haven't modified them.
390 390 #
391 391 # Also note that this racy as an editor could notice the
392 392 # file's mtime before we've finished writing it.
393 393 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
394 394 os.unlink(tmpname)
395 395 if tobackup:
396 396 os.rmdir(backupdir)
397 397 except OSError:
398 398 pass
399 399
400 400 def recordinwlock(ui, repo, message, match, opts):
401 401 with repo.wlock():
402 402 return recordfunc(ui, repo, message, match, opts)
403 403
404 404 return commit(ui, repo, recordinwlock, pats, opts)
405 405
406 406 def findpossible(cmd, table, strict=False):
407 407 """
408 408 Return cmd -> (aliases, command table entry)
409 409 for each matching command.
410 410 Return debug commands (or their aliases) only if no normal command matches.
411 411 """
412 412 choice = {}
413 413 debugchoice = {}
414 414
415 415 if cmd in table:
416 416 # short-circuit exact matches, "log" alias beats "^log|history"
417 417 keys = [cmd]
418 418 else:
419 419 keys = table.keys()
420 420
421 421 allcmds = []
422 422 for e in keys:
423 423 aliases = parsealiases(e)
424 424 allcmds.extend(aliases)
425 425 found = None
426 426 if cmd in aliases:
427 427 found = cmd
428 428 elif not strict:
429 429 for a in aliases:
430 430 if a.startswith(cmd):
431 431 found = a
432 432 break
433 433 if found is not None:
434 434 if aliases[0].startswith("debug") or found.startswith("debug"):
435 435 debugchoice[found] = (aliases, table[e])
436 436 else:
437 437 choice[found] = (aliases, table[e])
438 438
439 439 if not choice and debugchoice:
440 440 choice = debugchoice
441 441
442 442 return choice, allcmds
443 443
444 444 def findcmd(cmd, table, strict=True):
445 445 """Return (aliases, command table entry) for command string."""
446 446 choice, allcmds = findpossible(cmd, table, strict)
447 447
448 448 if cmd in choice:
449 449 return choice[cmd]
450 450
451 451 if len(choice) > 1:
452 452 clist = sorted(choice)
453 453 raise error.AmbiguousCommand(cmd, clist)
454 454
455 455 if choice:
456 456 return choice.values()[0]
457 457
458 458 raise error.UnknownCommand(cmd, allcmds)
459 459
460 460 def findrepo(p):
461 461 while not os.path.isdir(os.path.join(p, ".hg")):
462 462 oldp, p = p, os.path.dirname(p)
463 463 if p == oldp:
464 464 return None
465 465
466 466 return p
467 467
468 468 def bailifchanged(repo, merge=True, hint=None):
469 469 """ enforce the precondition that working directory must be clean.
470 470
471 471 'merge' can be set to false if a pending uncommitted merge should be
472 472 ignored (such as when 'update --check' runs).
473 473
474 474 'hint' is the usual hint given to Abort exception.
475 475 """
476 476
477 477 if merge and repo.dirstate.p2() != nullid:
478 478 raise error.Abort(_('outstanding uncommitted merge'), hint=hint)
479 479 modified, added, removed, deleted = repo.status()[:4]
480 480 if modified or added or removed or deleted:
481 481 raise error.Abort(_('uncommitted changes'), hint=hint)
482 482 ctx = repo[None]
483 483 for s in sorted(ctx.substate):
484 484 ctx.sub(s).bailifchanged(hint=hint)
485 485
486 486 def logmessage(ui, opts):
487 487 """ get the log message according to -m and -l option """
488 488 message = opts.get('message')
489 489 logfile = opts.get('logfile')
490 490
491 491 if message and logfile:
492 492 raise error.Abort(_('options --message and --logfile are mutually '
493 493 'exclusive'))
494 494 if not message and logfile:
495 495 try:
496 496 if logfile == '-':
497 497 message = ui.fin.read()
498 498 else:
499 499 message = '\n'.join(util.readfile(logfile).splitlines())
500 500 except IOError as inst:
501 501 raise error.Abort(_("can't read commit message '%s': %s") %
502 502 (logfile, inst.strerror))
503 503 return message
504 504
505 505 def mergeeditform(ctxorbool, baseformname):
506 506 """return appropriate editform name (referencing a committemplate)
507 507
508 508 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
509 509 merging is committed.
510 510
511 511 This returns baseformname with '.merge' appended if it is a merge,
512 512 otherwise '.normal' is appended.
513 513 """
514 514 if isinstance(ctxorbool, bool):
515 515 if ctxorbool:
516 516 return baseformname + ".merge"
517 517 elif 1 < len(ctxorbool.parents()):
518 518 return baseformname + ".merge"
519 519
520 520 return baseformname + ".normal"
521 521
522 522 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
523 523 editform='', **opts):
524 524 """get appropriate commit message editor according to '--edit' option
525 525
526 526 'finishdesc' is a function to be called with edited commit message
527 527 (= 'description' of the new changeset) just after editing, but
528 528 before checking empty-ness. It should return actual text to be
529 529 stored into history. This allows to change description before
530 530 storing.
531 531
532 532 'extramsg' is a extra message to be shown in the editor instead of
533 533 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
534 534 is automatically added.
535 535
536 536 'editform' is a dot-separated list of names, to distinguish
537 537 the purpose of commit text editing.
538 538
539 539 'getcommiteditor' returns 'commitforceeditor' regardless of
540 540 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
541 541 they are specific for usage in MQ.
542 542 """
543 543 if edit or finishdesc or extramsg:
544 544 return lambda r, c, s: commitforceeditor(r, c, s,
545 545 finishdesc=finishdesc,
546 546 extramsg=extramsg,
547 547 editform=editform)
548 548 elif editform:
549 549 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
550 550 else:
551 551 return commiteditor
552 552
553 553 def loglimit(opts):
554 554 """get the log limit according to option -l/--limit"""
555 555 limit = opts.get('limit')
556 556 if limit:
557 557 try:
558 558 limit = int(limit)
559 559 except ValueError:
560 560 raise error.Abort(_('limit must be a positive integer'))
561 561 if limit <= 0:
562 562 raise error.Abort(_('limit must be positive'))
563 563 else:
564 564 limit = None
565 565 return limit
566 566
567 567 def makefilename(repo, pat, node, desc=None,
568 568 total=None, seqno=None, revwidth=None, pathname=None):
569 569 node_expander = {
570 570 'H': lambda: hex(node),
571 571 'R': lambda: str(repo.changelog.rev(node)),
572 572 'h': lambda: short(node),
573 573 'm': lambda: re.sub('[^\w]', '_', str(desc))
574 574 }
575 575 expander = {
576 576 '%': lambda: '%',
577 577 'b': lambda: os.path.basename(repo.root),
578 578 }
579 579
580 580 try:
581 581 if node:
582 582 expander.update(node_expander)
583 583 if node:
584 584 expander['r'] = (lambda:
585 585 str(repo.changelog.rev(node)).zfill(revwidth or 0))
586 586 if total is not None:
587 587 expander['N'] = lambda: str(total)
588 588 if seqno is not None:
589 589 expander['n'] = lambda: str(seqno)
590 590 if total is not None and seqno is not None:
591 591 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
592 592 if pathname is not None:
593 593 expander['s'] = lambda: os.path.basename(pathname)
594 594 expander['d'] = lambda: os.path.dirname(pathname) or '.'
595 595 expander['p'] = lambda: pathname
596 596
597 597 newname = []
598 598 patlen = len(pat)
599 599 i = 0
600 600 while i < patlen:
601 601 c = pat[i:i + 1]
602 602 if c == '%':
603 603 i += 1
604 604 c = pat[i:i + 1]
605 605 c = expander[c]()
606 606 newname.append(c)
607 607 i += 1
608 608 return ''.join(newname)
609 609 except KeyError as inst:
610 610 raise error.Abort(_("invalid format spec '%%%s' in output filename") %
611 611 inst.args[0])
612 612
613 613 def isstdiofilename(pat):
614 614 """True if the given pat looks like a filename denoting stdin/stdout"""
615 615 return not pat or pat == '-'
616 616
617 617 class _unclosablefile(object):
618 618 def __init__(self, fp):
619 619 self._fp = fp
620 620
621 621 def close(self):
622 622 pass
623 623
624 624 def __iter__(self):
625 625 return iter(self._fp)
626 626
627 627 def __getattr__(self, attr):
628 628 return getattr(self._fp, attr)
629 629
630 630 def __enter__(self):
631 631 return self
632 632
633 633 def __exit__(self, exc_type, exc_value, exc_tb):
634 634 pass
635 635
636 636 def makefileobj(repo, pat, node=None, desc=None, total=None,
637 637 seqno=None, revwidth=None, mode='wb', modemap=None,
638 638 pathname=None):
639 639
640 640 writable = mode not in ('r', 'rb')
641 641
642 642 if isstdiofilename(pat):
643 643 if writable:
644 644 fp = repo.ui.fout
645 645 else:
646 646 fp = repo.ui.fin
647 647 return _unclosablefile(fp)
648 648 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
649 649 if modemap is not None:
650 650 mode = modemap.get(fn, mode)
651 651 if mode == 'wb':
652 652 modemap[fn] = 'ab'
653 653 return open(fn, mode)
654 654
655 655 def openrevlog(repo, cmd, file_, opts):
656 656 """opens the changelog, manifest, a filelog or a given revlog"""
657 657 cl = opts['changelog']
658 658 mf = opts['manifest']
659 659 dir = opts['dir']
660 660 msg = None
661 661 if cl and mf:
662 662 msg = _('cannot specify --changelog and --manifest at the same time')
663 663 elif cl and dir:
664 664 msg = _('cannot specify --changelog and --dir at the same time')
665 665 elif cl or mf or dir:
666 666 if file_:
667 667 msg = _('cannot specify filename with --changelog or --manifest')
668 668 elif not repo:
669 669 msg = _('cannot specify --changelog or --manifest or --dir '
670 670 'without a repository')
671 671 if msg:
672 672 raise error.Abort(msg)
673 673
674 674 r = None
675 675 if repo:
676 676 if cl:
677 677 r = repo.unfiltered().changelog
678 678 elif dir:
679 679 if 'treemanifest' not in repo.requirements:
680 680 raise error.Abort(_("--dir can only be used on repos with "
681 681 "treemanifest enabled"))
682 682 dirlog = repo.manifestlog._revlog.dirlog(dir)
683 683 if len(dirlog):
684 684 r = dirlog
685 685 elif mf:
686 686 r = repo.manifestlog._revlog
687 687 elif file_:
688 688 filelog = repo.file(file_)
689 689 if len(filelog):
690 690 r = filelog
691 691 if not r:
692 692 if not file_:
693 693 raise error.CommandError(cmd, _('invalid arguments'))
694 694 if not os.path.isfile(file_):
695 695 raise error.Abort(_("revlog '%s' not found") % file_)
696 696 r = revlog.revlog(vfsmod.vfs(pycompat.getcwd(), audit=False),
697 697 file_[:-2] + ".i")
698 698 return r
699 699
700 700 def copy(ui, repo, pats, opts, rename=False):
701 701 # called with the repo lock held
702 702 #
703 703 # hgsep => pathname that uses "/" to separate directories
704 704 # ossep => pathname that uses os.sep to separate directories
705 705 cwd = repo.getcwd()
706 706 targets = {}
707 707 after = opts.get("after")
708 708 dryrun = opts.get("dry_run")
709 709 wctx = repo[None]
710 710
711 711 def walkpat(pat):
712 712 srcs = []
713 713 if after:
714 714 badstates = '?'
715 715 else:
716 716 badstates = '?r'
717 717 m = scmutil.match(wctx, [pat], opts, globbed=True)
718 718 for abs in wctx.walk(m):
719 719 state = repo.dirstate[abs]
720 720 rel = m.rel(abs)
721 721 exact = m.exact(abs)
722 722 if state in badstates:
723 723 if exact and state == '?':
724 724 ui.warn(_('%s: not copying - file is not managed\n') % rel)
725 725 if exact and state == 'r':
726 726 ui.warn(_('%s: not copying - file has been marked for'
727 727 ' remove\n') % rel)
728 728 continue
729 729 # abs: hgsep
730 730 # rel: ossep
731 731 srcs.append((abs, rel, exact))
732 732 return srcs
733 733
734 734 # abssrc: hgsep
735 735 # relsrc: ossep
736 736 # otarget: ossep
737 737 def copyfile(abssrc, relsrc, otarget, exact):
738 738 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
739 739 if '/' in abstarget:
740 740 # We cannot normalize abstarget itself, this would prevent
741 741 # case only renames, like a => A.
742 742 abspath, absname = abstarget.rsplit('/', 1)
743 743 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
744 744 reltarget = repo.pathto(abstarget, cwd)
745 745 target = repo.wjoin(abstarget)
746 746 src = repo.wjoin(abssrc)
747 747 state = repo.dirstate[abstarget]
748 748
749 749 scmutil.checkportable(ui, abstarget)
750 750
751 751 # check for collisions
752 752 prevsrc = targets.get(abstarget)
753 753 if prevsrc is not None:
754 754 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
755 755 (reltarget, repo.pathto(abssrc, cwd),
756 756 repo.pathto(prevsrc, cwd)))
757 757 return
758 758
759 759 # check for overwrites
760 760 exists = os.path.lexists(target)
761 761 samefile = False
762 762 if exists and abssrc != abstarget:
763 763 if (repo.dirstate.normalize(abssrc) ==
764 764 repo.dirstate.normalize(abstarget)):
765 765 if not rename:
766 766 ui.warn(_("%s: can't copy - same file\n") % reltarget)
767 767 return
768 768 exists = False
769 769 samefile = True
770 770
771 771 if not after and exists or after and state in 'mn':
772 772 if not opts['force']:
773 773 if state in 'mn':
774 774 msg = _('%s: not overwriting - file already committed\n')
775 775 if after:
776 776 flags = '--after --force'
777 777 else:
778 778 flags = '--force'
779 779 if rename:
780 780 hint = _('(hg rename %s to replace the file by '
781 781 'recording a rename)\n') % flags
782 782 else:
783 783 hint = _('(hg copy %s to replace the file by '
784 784 'recording a copy)\n') % flags
785 785 else:
786 786 msg = _('%s: not overwriting - file exists\n')
787 787 if rename:
788 788 hint = _('(hg rename --after to record the rename)\n')
789 789 else:
790 790 hint = _('(hg copy --after to record the copy)\n')
791 791 ui.warn(msg % reltarget)
792 792 ui.warn(hint)
793 793 return
794 794
795 795 if after:
796 796 if not exists:
797 797 if rename:
798 798 ui.warn(_('%s: not recording move - %s does not exist\n') %
799 799 (relsrc, reltarget))
800 800 else:
801 801 ui.warn(_('%s: not recording copy - %s does not exist\n') %
802 802 (relsrc, reltarget))
803 803 return
804 804 elif not dryrun:
805 805 try:
806 806 if exists:
807 807 os.unlink(target)
808 808 targetdir = os.path.dirname(target) or '.'
809 809 if not os.path.isdir(targetdir):
810 810 os.makedirs(targetdir)
811 811 if samefile:
812 812 tmp = target + "~hgrename"
813 813 os.rename(src, tmp)
814 814 os.rename(tmp, target)
815 815 else:
816 816 util.copyfile(src, target)
817 817 srcexists = True
818 818 except IOError as inst:
819 819 if inst.errno == errno.ENOENT:
820 820 ui.warn(_('%s: deleted in working directory\n') % relsrc)
821 821 srcexists = False
822 822 else:
823 823 ui.warn(_('%s: cannot copy - %s\n') %
824 824 (relsrc, inst.strerror))
825 825 return True # report a failure
826 826
827 827 if ui.verbose or not exact:
828 828 if rename:
829 829 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
830 830 else:
831 831 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
832 832
833 833 targets[abstarget] = abssrc
834 834
835 835 # fix up dirstate
836 836 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
837 837 dryrun=dryrun, cwd=cwd)
838 838 if rename and not dryrun:
839 839 if not after and srcexists and not samefile:
840 840 repo.wvfs.unlinkpath(abssrc)
841 841 wctx.forget([abssrc])
842 842
843 843 # pat: ossep
844 844 # dest ossep
845 845 # srcs: list of (hgsep, hgsep, ossep, bool)
846 846 # return: function that takes hgsep and returns ossep
847 847 def targetpathfn(pat, dest, srcs):
848 848 if os.path.isdir(pat):
849 849 abspfx = pathutil.canonpath(repo.root, cwd, pat)
850 850 abspfx = util.localpath(abspfx)
851 851 if destdirexists:
852 852 striplen = len(os.path.split(abspfx)[0])
853 853 else:
854 854 striplen = len(abspfx)
855 855 if striplen:
856 856 striplen += len(pycompat.ossep)
857 857 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
858 858 elif destdirexists:
859 859 res = lambda p: os.path.join(dest,
860 860 os.path.basename(util.localpath(p)))
861 861 else:
862 862 res = lambda p: dest
863 863 return res
864 864
865 865 # pat: ossep
866 866 # dest ossep
867 867 # srcs: list of (hgsep, hgsep, ossep, bool)
868 868 # return: function that takes hgsep and returns ossep
869 869 def targetpathafterfn(pat, dest, srcs):
870 870 if matchmod.patkind(pat):
871 871 # a mercurial pattern
872 872 res = lambda p: os.path.join(dest,
873 873 os.path.basename(util.localpath(p)))
874 874 else:
875 875 abspfx = pathutil.canonpath(repo.root, cwd, pat)
876 876 if len(abspfx) < len(srcs[0][0]):
877 877 # A directory. Either the target path contains the last
878 878 # component of the source path or it does not.
879 879 def evalpath(striplen):
880 880 score = 0
881 881 for s in srcs:
882 882 t = os.path.join(dest, util.localpath(s[0])[striplen:])
883 883 if os.path.lexists(t):
884 884 score += 1
885 885 return score
886 886
887 887 abspfx = util.localpath(abspfx)
888 888 striplen = len(abspfx)
889 889 if striplen:
890 890 striplen += len(pycompat.ossep)
891 891 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
892 892 score = evalpath(striplen)
893 893 striplen1 = len(os.path.split(abspfx)[0])
894 894 if striplen1:
895 895 striplen1 += len(pycompat.ossep)
896 896 if evalpath(striplen1) > score:
897 897 striplen = striplen1
898 898 res = lambda p: os.path.join(dest,
899 899 util.localpath(p)[striplen:])
900 900 else:
901 901 # a file
902 902 if destdirexists:
903 903 res = lambda p: os.path.join(dest,
904 904 os.path.basename(util.localpath(p)))
905 905 else:
906 906 res = lambda p: dest
907 907 return res
908 908
909 909 pats = scmutil.expandpats(pats)
910 910 if not pats:
911 911 raise error.Abort(_('no source or destination specified'))
912 912 if len(pats) == 1:
913 913 raise error.Abort(_('no destination specified'))
914 914 dest = pats.pop()
915 915 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
916 916 if not destdirexists:
917 917 if len(pats) > 1 or matchmod.patkind(pats[0]):
918 918 raise error.Abort(_('with multiple sources, destination must be an '
919 919 'existing directory'))
920 920 if util.endswithsep(dest):
921 921 raise error.Abort(_('destination %s is not a directory') % dest)
922 922
923 923 tfn = targetpathfn
924 924 if after:
925 925 tfn = targetpathafterfn
926 926 copylist = []
927 927 for pat in pats:
928 928 srcs = walkpat(pat)
929 929 if not srcs:
930 930 continue
931 931 copylist.append((tfn(pat, dest, srcs), srcs))
932 932 if not copylist:
933 933 raise error.Abort(_('no files to copy'))
934 934
935 935 errors = 0
936 936 for targetpath, srcs in copylist:
937 937 for abssrc, relsrc, exact in srcs:
938 938 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
939 939 errors += 1
940 940
941 941 if errors:
942 942 ui.warn(_('(consider using --after)\n'))
943 943
944 944 return errors != 0
945 945
946 946 ## facility to let extension process additional data into an import patch
947 947 # list of identifier to be executed in order
948 948 extrapreimport = [] # run before commit
949 949 extrapostimport = [] # run after commit
950 950 # mapping from identifier to actual import function
951 951 #
952 952 # 'preimport' are run before the commit is made and are provided the following
953 953 # arguments:
954 954 # - repo: the localrepository instance,
955 955 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
956 956 # - extra: the future extra dictionary of the changeset, please mutate it,
957 957 # - opts: the import options.
958 958 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
959 959 # mutation of in memory commit and more. Feel free to rework the code to get
960 960 # there.
961 961 extrapreimportmap = {}
962 962 # 'postimport' are run after the commit is made and are provided the following
963 963 # argument:
964 964 # - ctx: the changectx created by import.
965 965 extrapostimportmap = {}
966 966
967 967 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
968 968 """Utility function used by commands.import to import a single patch
969 969
970 970 This function is explicitly defined here to help the evolve extension to
971 971 wrap this part of the import logic.
972 972
973 973 The API is currently a bit ugly because it a simple code translation from
974 974 the import command. Feel free to make it better.
975 975
976 976 :hunk: a patch (as a binary string)
977 977 :parents: nodes that will be parent of the created commit
978 978 :opts: the full dict of option passed to the import command
979 979 :msgs: list to save commit message to.
980 980 (used in case we need to save it when failing)
981 981 :updatefunc: a function that update a repo to a given node
982 982 updatefunc(<repo>, <node>)
983 983 """
984 984 # avoid cycle context -> subrepo -> cmdutil
985 985 from . import context
986 986 extractdata = patch.extract(ui, hunk)
987 987 tmpname = extractdata.get('filename')
988 988 message = extractdata.get('message')
989 989 user = opts.get('user') or extractdata.get('user')
990 990 date = opts.get('date') or extractdata.get('date')
991 991 branch = extractdata.get('branch')
992 992 nodeid = extractdata.get('nodeid')
993 993 p1 = extractdata.get('p1')
994 994 p2 = extractdata.get('p2')
995 995
996 996 nocommit = opts.get('no_commit')
997 997 importbranch = opts.get('import_branch')
998 998 update = not opts.get('bypass')
999 999 strip = opts["strip"]
1000 1000 prefix = opts["prefix"]
1001 1001 sim = float(opts.get('similarity') or 0)
1002 1002 if not tmpname:
1003 1003 return (None, None, False)
1004 1004
1005 1005 rejects = False
1006 1006
1007 1007 try:
1008 1008 cmdline_message = logmessage(ui, opts)
1009 1009 if cmdline_message:
1010 1010 # pickup the cmdline msg
1011 1011 message = cmdline_message
1012 1012 elif message:
1013 1013 # pickup the patch msg
1014 1014 message = message.strip()
1015 1015 else:
1016 1016 # launch the editor
1017 1017 message = None
1018 1018 ui.debug('message:\n%s\n' % message)
1019 1019
1020 1020 if len(parents) == 1:
1021 1021 parents.append(repo[nullid])
1022 1022 if opts.get('exact'):
1023 1023 if not nodeid or not p1:
1024 1024 raise error.Abort(_('not a Mercurial patch'))
1025 1025 p1 = repo[p1]
1026 1026 p2 = repo[p2 or nullid]
1027 1027 elif p2:
1028 1028 try:
1029 1029 p1 = repo[p1]
1030 1030 p2 = repo[p2]
1031 1031 # Without any options, consider p2 only if the
1032 1032 # patch is being applied on top of the recorded
1033 1033 # first parent.
1034 1034 if p1 != parents[0]:
1035 1035 p1 = parents[0]
1036 1036 p2 = repo[nullid]
1037 1037 except error.RepoError:
1038 1038 p1, p2 = parents
1039 1039 if p2.node() == nullid:
1040 1040 ui.warn(_("warning: import the patch as a normal revision\n"
1041 1041 "(use --exact to import the patch as a merge)\n"))
1042 1042 else:
1043 1043 p1, p2 = parents
1044 1044
1045 1045 n = None
1046 1046 if update:
1047 1047 if p1 != parents[0]:
1048 1048 updatefunc(repo, p1.node())
1049 1049 if p2 != parents[1]:
1050 1050 repo.setparents(p1.node(), p2.node())
1051 1051
1052 1052 if opts.get('exact') or importbranch:
1053 1053 repo.dirstate.setbranch(branch or 'default')
1054 1054
1055 1055 partial = opts.get('partial', False)
1056 1056 files = set()
1057 1057 try:
1058 1058 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
1059 1059 files=files, eolmode=None, similarity=sim / 100.0)
1060 1060 except patch.PatchError as e:
1061 1061 if not partial:
1062 1062 raise error.Abort(str(e))
1063 1063 if partial:
1064 1064 rejects = True
1065 1065
1066 1066 files = list(files)
1067 1067 if nocommit:
1068 1068 if message:
1069 1069 msgs.append(message)
1070 1070 else:
1071 1071 if opts.get('exact') or p2:
1072 1072 # If you got here, you either use --force and know what
1073 1073 # you are doing or used --exact or a merge patch while
1074 1074 # being updated to its first parent.
1075 1075 m = None
1076 1076 else:
1077 1077 m = scmutil.matchfiles(repo, files or [])
1078 1078 editform = mergeeditform(repo[None], 'import.normal')
1079 1079 if opts.get('exact'):
1080 1080 editor = None
1081 1081 else:
1082 1082 editor = getcommiteditor(editform=editform, **opts)
1083 1083 extra = {}
1084 1084 for idfunc in extrapreimport:
1085 1085 extrapreimportmap[idfunc](repo, extractdata, extra, opts)
1086 1086 overrides = {}
1087 1087 if partial:
1088 1088 overrides[('ui', 'allowemptycommit')] = True
1089 1089 with repo.ui.configoverride(overrides, 'import'):
1090 1090 n = repo.commit(message, user,
1091 1091 date, match=m,
1092 1092 editor=editor, extra=extra)
1093 1093 for idfunc in extrapostimport:
1094 1094 extrapostimportmap[idfunc](repo[n])
1095 1095 else:
1096 1096 if opts.get('exact') or importbranch:
1097 1097 branch = branch or 'default'
1098 1098 else:
1099 1099 branch = p1.branch()
1100 1100 store = patch.filestore()
1101 1101 try:
1102 1102 files = set()
1103 1103 try:
1104 1104 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
1105 1105 files, eolmode=None)
1106 1106 except patch.PatchError as e:
1107 1107 raise error.Abort(str(e))
1108 1108 if opts.get('exact'):
1109 1109 editor = None
1110 1110 else:
1111 1111 editor = getcommiteditor(editform='import.bypass')
1112 1112 memctx = context.makememctx(repo, (p1.node(), p2.node()),
1113 1113 message,
1114 1114 user,
1115 1115 date,
1116 1116 branch, files, store,
1117 1117 editor=editor)
1118 1118 n = memctx.commit()
1119 1119 finally:
1120 1120 store.close()
1121 1121 if opts.get('exact') and nocommit:
1122 1122 # --exact with --no-commit is still useful in that it does merge
1123 1123 # and branch bits
1124 1124 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1125 1125 elif opts.get('exact') and hex(n) != nodeid:
1126 1126 raise error.Abort(_('patch is damaged or loses information'))
1127 1127 msg = _('applied to working directory')
1128 1128 if n:
1129 1129 # i18n: refers to a short changeset id
1130 1130 msg = _('created %s') % short(n)
1131 1131 return (msg, n, rejects)
1132 1132 finally:
1133 1133 os.unlink(tmpname)
1134 1134
1135 1135 # facility to let extensions include additional data in an exported patch
1136 1136 # list of identifiers to be executed in order
1137 1137 extraexport = []
1138 1138 # mapping from identifier to actual export function
1139 1139 # function as to return a string to be added to the header or None
1140 1140 # it is given two arguments (sequencenumber, changectx)
1141 1141 extraexportmap = {}
1142 1142
1143 1143 def _exportsingle(repo, ctx, match, switch_parent, rev, seqno, write, diffopts):
1144 1144 node = ctx.node()
1145 1145 parents = [p.node() for p in ctx.parents() if p]
1146 1146 branch = ctx.branch()
1147 1147 if switch_parent:
1148 1148 parents.reverse()
1149 1149
1150 1150 if parents:
1151 1151 prev = parents[0]
1152 1152 else:
1153 1153 prev = nullid
1154 1154
1155 1155 write("# HG changeset patch\n")
1156 1156 write("# User %s\n" % ctx.user())
1157 1157 write("# Date %d %d\n" % ctx.date())
1158 1158 write("# %s\n" % util.datestr(ctx.date()))
1159 1159 if branch and branch != 'default':
1160 1160 write("# Branch %s\n" % branch)
1161 1161 write("# Node ID %s\n" % hex(node))
1162 1162 write("# Parent %s\n" % hex(prev))
1163 1163 if len(parents) > 1:
1164 1164 write("# Parent %s\n" % hex(parents[1]))
1165 1165
1166 1166 for headerid in extraexport:
1167 1167 header = extraexportmap[headerid](seqno, ctx)
1168 1168 if header is not None:
1169 1169 write('# %s\n' % header)
1170 1170 write(ctx.description().rstrip())
1171 1171 write("\n\n")
1172 1172
1173 1173 for chunk, label in patch.diffui(repo, prev, node, match, opts=diffopts):
1174 1174 write(chunk, label=label)
1175 1175
1176 1176 def export(repo, revs, fntemplate='hg-%h.patch', fp=None, switch_parent=False,
1177 1177 opts=None, match=None):
1178 1178 '''export changesets as hg patches
1179 1179
1180 1180 Args:
1181 1181 repo: The repository from which we're exporting revisions.
1182 1182 revs: A list of revisions to export as revision numbers.
1183 1183 fntemplate: An optional string to use for generating patch file names.
1184 1184 fp: An optional file-like object to which patches should be written.
1185 1185 switch_parent: If True, show diffs against second parent when not nullid.
1186 1186 Default is false, which always shows diff against p1.
1187 1187 opts: diff options to use for generating the patch.
1188 1188 match: If specified, only export changes to files matching this matcher.
1189 1189
1190 1190 Returns:
1191 1191 Nothing.
1192 1192
1193 1193 Side Effect:
1194 1194 "HG Changeset Patch" data is emitted to one of the following
1195 1195 destinations:
1196 1196 fp is specified: All revs are written to the specified
1197 1197 file-like object.
1198 1198 fntemplate specified: Each rev is written to a unique file named using
1199 1199 the given template.
1200 1200 Neither fp nor template specified: All revs written to repo.ui.write()
1201 1201 '''
1202 1202
1203 1203 total = len(revs)
1204 1204 revwidth = max(len(str(rev)) for rev in revs)
1205 1205 filemode = {}
1206 1206
1207 1207 write = None
1208 1208 dest = '<unnamed>'
1209 1209 if fp:
1210 1210 dest = getattr(fp, 'name', dest)
1211 1211 def write(s, **kw):
1212 1212 fp.write(s)
1213 1213 elif not fntemplate:
1214 1214 write = repo.ui.write
1215 1215
1216 1216 for seqno, rev in enumerate(revs, 1):
1217 1217 ctx = repo[rev]
1218 1218 fo = None
1219 1219 if not fp and fntemplate:
1220 1220 desc_lines = ctx.description().rstrip().split('\n')
1221 1221 desc = desc_lines[0] #Commit always has a first line.
1222 1222 fo = makefileobj(repo, fntemplate, ctx.node(), desc=desc,
1223 1223 total=total, seqno=seqno, revwidth=revwidth,
1224 1224 mode='wb', modemap=filemode)
1225 1225 dest = fo.name
1226 1226 def write(s, **kw):
1227 1227 fo.write(s)
1228 1228 if not dest.startswith('<'):
1229 1229 repo.ui.note("%s\n" % dest)
1230 1230 _exportsingle(
1231 1231 repo, ctx, match, switch_parent, rev, seqno, write, opts)
1232 1232 if fo is not None:
1233 1233 fo.close()
1234 1234
1235 1235 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
1236 1236 changes=None, stat=False, fp=None, prefix='',
1237 1237 root='', listsubrepos=False):
1238 1238 '''show diff or diffstat.'''
1239 1239 if fp is None:
1240 1240 write = ui.write
1241 1241 else:
1242 1242 def write(s, **kw):
1243 1243 fp.write(s)
1244 1244
1245 1245 if root:
1246 1246 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
1247 1247 else:
1248 1248 relroot = ''
1249 1249 if relroot != '':
1250 1250 # XXX relative roots currently don't work if the root is within a
1251 1251 # subrepo
1252 1252 uirelroot = match.uipath(relroot)
1253 1253 relroot += '/'
1254 1254 for matchroot in match.files():
1255 1255 if not matchroot.startswith(relroot):
1256 1256 ui.warn(_('warning: %s not inside relative root %s\n') % (
1257 1257 match.uipath(matchroot), uirelroot))
1258 1258
1259 1259 if stat:
1260 1260 diffopts = diffopts.copy(context=0)
1261 1261 width = 80
1262 1262 if not ui.plain():
1263 1263 width = ui.termwidth()
1264 1264 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
1265 1265 prefix=prefix, relroot=relroot)
1266 1266 for chunk, label in patch.diffstatui(util.iterlines(chunks),
1267 1267 width=width):
1268 1268 write(chunk, label=label)
1269 1269 else:
1270 1270 for chunk, label in patch.diffui(repo, node1, node2, match,
1271 1271 changes, diffopts, prefix=prefix,
1272 1272 relroot=relroot):
1273 1273 write(chunk, label=label)
1274 1274
1275 1275 if listsubrepos:
1276 1276 ctx1 = repo[node1]
1277 1277 ctx2 = repo[node2]
1278 1278 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
1279 1279 tempnode2 = node2
1280 1280 try:
1281 1281 if node2 is not None:
1282 1282 tempnode2 = ctx2.substate[subpath][1]
1283 1283 except KeyError:
1284 1284 # A subrepo that existed in node1 was deleted between node1 and
1285 1285 # node2 (inclusive). Thus, ctx2's substate won't contain that
1286 1286 # subpath. The best we can do is to ignore it.
1287 1287 tempnode2 = None
1288 1288 submatch = matchmod.subdirmatcher(subpath, match)
1289 1289 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
1290 1290 stat=stat, fp=fp, prefix=prefix)
1291 1291
1292 1292 def _changesetlabels(ctx):
1293 1293 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
1294 1294 if ctx.obsolete():
1295 1295 labels.append('changeset.obsolete')
1296 1296 if ctx.troubled():
1297 1297 labels.append('changeset.troubled')
1298 1298 for trouble in ctx.troubles():
1299 1299 labels.append('trouble.%s' % trouble)
1300 1300 return ' '.join(labels)
1301 1301
1302 1302 class changeset_printer(object):
1303 1303 '''show changeset information when templating not requested.'''
1304 1304
1305 1305 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1306 1306 self.ui = ui
1307 1307 self.repo = repo
1308 1308 self.buffered = buffered
1309 1309 self.matchfn = matchfn
1310 1310 self.diffopts = diffopts
1311 1311 self.header = {}
1312 1312 self.hunk = {}
1313 1313 self.lastheader = None
1314 1314 self.footer = None
1315 1315
1316 1316 def flush(self, ctx):
1317 1317 rev = ctx.rev()
1318 1318 if rev in self.header:
1319 1319 h = self.header[rev]
1320 1320 if h != self.lastheader:
1321 1321 self.lastheader = h
1322 1322 self.ui.write(h)
1323 1323 del self.header[rev]
1324 1324 if rev in self.hunk:
1325 1325 self.ui.write(self.hunk[rev])
1326 1326 del self.hunk[rev]
1327 1327 return 1
1328 1328 return 0
1329 1329
1330 1330 def close(self):
1331 1331 if self.footer:
1332 1332 self.ui.write(self.footer)
1333 1333
1334 1334 def show(self, ctx, copies=None, matchfn=None, **props):
1335 1335 if self.buffered:
1336 1336 self.ui.pushbuffer(labeled=True)
1337 1337 self._show(ctx, copies, matchfn, props)
1338 1338 self.hunk[ctx.rev()] = self.ui.popbuffer()
1339 1339 else:
1340 1340 self._show(ctx, copies, matchfn, props)
1341 1341
1342 1342 def _show(self, ctx, copies, matchfn, props):
1343 1343 '''show a single changeset or file revision'''
1344 1344 changenode = ctx.node()
1345 1345 rev = ctx.rev()
1346 1346 if self.ui.debugflag:
1347 1347 hexfunc = hex
1348 1348 else:
1349 1349 hexfunc = short
1350 1350 # as of now, wctx.node() and wctx.rev() return None, but we want to
1351 1351 # show the same values as {node} and {rev} templatekw
1352 1352 revnode = (scmutil.intrev(rev), hexfunc(bin(ctx.hex())))
1353 1353
1354 1354 if self.ui.quiet:
1355 1355 self.ui.write("%d:%s\n" % revnode, label='log.node')
1356 1356 return
1357 1357
1358 1358 date = util.datestr(ctx.date())
1359 1359
1360 1360 # i18n: column positioning for "hg log"
1361 1361 self.ui.write(_("changeset: %d:%s\n") % revnode,
1362 1362 label=_changesetlabels(ctx))
1363 1363
1364 1364 # branches are shown first before any other names due to backwards
1365 1365 # compatibility
1366 1366 branch = ctx.branch()
1367 1367 # don't show the default branch name
1368 1368 if branch != 'default':
1369 1369 # i18n: column positioning for "hg log"
1370 1370 self.ui.write(_("branch: %s\n") % branch,
1371 1371 label='log.branch')
1372 1372
1373 1373 for nsname, ns in self.repo.names.iteritems():
1374 1374 # branches has special logic already handled above, so here we just
1375 1375 # skip it
1376 1376 if nsname == 'branches':
1377 1377 continue
1378 1378 # we will use the templatename as the color name since those two
1379 1379 # should be the same
1380 1380 for name in ns.names(self.repo, changenode):
1381 1381 self.ui.write(ns.logfmt % name,
1382 1382 label='log.%s' % ns.colorname)
1383 1383 if self.ui.debugflag:
1384 1384 # i18n: column positioning for "hg log"
1385 1385 self.ui.write(_("phase: %s\n") % ctx.phasestr(),
1386 1386 label='log.phase')
1387 1387 for pctx in scmutil.meaningfulparents(self.repo, ctx):
1388 1388 label = 'log.parent changeset.%s' % pctx.phasestr()
1389 1389 # i18n: column positioning for "hg log"
1390 1390 self.ui.write(_("parent: %d:%s\n")
1391 1391 % (pctx.rev(), hexfunc(pctx.node())),
1392 1392 label=label)
1393 1393
1394 1394 if self.ui.debugflag and rev is not None:
1395 1395 mnode = ctx.manifestnode()
1396 1396 # i18n: column positioning for "hg log"
1397 1397 self.ui.write(_("manifest: %d:%s\n") %
1398 1398 (self.repo.manifestlog._revlog.rev(mnode),
1399 1399 hex(mnode)),
1400 1400 label='ui.debug log.manifest')
1401 1401 # i18n: column positioning for "hg log"
1402 1402 self.ui.write(_("user: %s\n") % ctx.user(),
1403 1403 label='log.user')
1404 1404 # i18n: column positioning for "hg log"
1405 1405 self.ui.write(_("date: %s\n") % date,
1406 1406 label='log.date')
1407 1407
1408 1408 if ctx.troubled():
1409 1409 # i18n: column positioning for "hg log"
1410 1410 self.ui.write(_("trouble: %s\n") % ', '.join(ctx.troubles()),
1411 1411 label='log.trouble')
1412 1412
1413 1413 if self.ui.debugflag:
1414 1414 files = ctx.p1().status(ctx)[:3]
1415 1415 for key, value in zip([# i18n: column positioning for "hg log"
1416 1416 _("files:"),
1417 1417 # i18n: column positioning for "hg log"
1418 1418 _("files+:"),
1419 1419 # i18n: column positioning for "hg log"
1420 1420 _("files-:")], files):
1421 1421 if value:
1422 1422 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
1423 1423 label='ui.debug log.files')
1424 1424 elif ctx.files() and self.ui.verbose:
1425 1425 # i18n: column positioning for "hg log"
1426 1426 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
1427 1427 label='ui.note log.files')
1428 1428 if copies and self.ui.verbose:
1429 1429 copies = ['%s (%s)' % c for c in copies]
1430 1430 # i18n: column positioning for "hg log"
1431 1431 self.ui.write(_("copies: %s\n") % ' '.join(copies),
1432 1432 label='ui.note log.copies')
1433 1433
1434 1434 extra = ctx.extra()
1435 1435 if extra and self.ui.debugflag:
1436 1436 for key, value in sorted(extra.items()):
1437 1437 # i18n: column positioning for "hg log"
1438 1438 self.ui.write(_("extra: %s=%s\n")
1439 1439 % (key, util.escapestr(value)),
1440 1440 label='ui.debug log.extra')
1441 1441
1442 1442 description = ctx.description().strip()
1443 1443 if description:
1444 1444 if self.ui.verbose:
1445 1445 self.ui.write(_("description:\n"),
1446 1446 label='ui.note log.description')
1447 1447 self.ui.write(description,
1448 1448 label='ui.note log.description')
1449 1449 self.ui.write("\n\n")
1450 1450 else:
1451 1451 # i18n: column positioning for "hg log"
1452 1452 self.ui.write(_("summary: %s\n") %
1453 1453 description.splitlines()[0],
1454 1454 label='log.summary')
1455 1455 self.ui.write("\n")
1456 1456
1457 1457 self.showpatch(ctx, matchfn)
1458 1458
1459 1459 def showpatch(self, ctx, matchfn):
1460 1460 if not matchfn:
1461 1461 matchfn = self.matchfn
1462 1462 if matchfn:
1463 1463 stat = self.diffopts.get('stat')
1464 1464 diff = self.diffopts.get('patch')
1465 1465 diffopts = patch.diffallopts(self.ui, self.diffopts)
1466 1466 node = ctx.node()
1467 1467 prev = ctx.p1().node()
1468 1468 if stat:
1469 1469 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1470 1470 match=matchfn, stat=True)
1471 1471 if diff:
1472 1472 if stat:
1473 1473 self.ui.write("\n")
1474 1474 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1475 1475 match=matchfn, stat=False)
1476 1476 self.ui.write("\n")
1477 1477
1478 1478 class jsonchangeset(changeset_printer):
1479 1479 '''format changeset information.'''
1480 1480
1481 1481 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1482 1482 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1483 1483 self.cache = {}
1484 1484 self._first = True
1485 1485
1486 1486 def close(self):
1487 1487 if not self._first:
1488 1488 self.ui.write("\n]\n")
1489 1489 else:
1490 1490 self.ui.write("[]\n")
1491 1491
1492 1492 def _show(self, ctx, copies, matchfn, props):
1493 1493 '''show a single changeset or file revision'''
1494 1494 rev = ctx.rev()
1495 1495 if rev is None:
1496 1496 jrev = jnode = 'null'
1497 1497 else:
1498 1498 jrev = '%d' % rev
1499 1499 jnode = '"%s"' % hex(ctx.node())
1500 1500 j = encoding.jsonescape
1501 1501
1502 1502 if self._first:
1503 1503 self.ui.write("[\n {")
1504 1504 self._first = False
1505 1505 else:
1506 1506 self.ui.write(",\n {")
1507 1507
1508 1508 if self.ui.quiet:
1509 1509 self.ui.write(('\n "rev": %s') % jrev)
1510 1510 self.ui.write((',\n "node": %s') % jnode)
1511 1511 self.ui.write('\n }')
1512 1512 return
1513 1513
1514 1514 self.ui.write(('\n "rev": %s') % jrev)
1515 1515 self.ui.write((',\n "node": %s') % jnode)
1516 1516 self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
1517 1517 self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
1518 1518 self.ui.write((',\n "user": "%s"') % j(ctx.user()))
1519 1519 self.ui.write((',\n "date": [%d, %d]') % ctx.date())
1520 1520 self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
1521 1521
1522 1522 self.ui.write((',\n "bookmarks": [%s]') %
1523 1523 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
1524 1524 self.ui.write((',\n "tags": [%s]') %
1525 1525 ", ".join('"%s"' % j(t) for t in ctx.tags()))
1526 1526 self.ui.write((',\n "parents": [%s]') %
1527 1527 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
1528 1528
1529 1529 if self.ui.debugflag:
1530 1530 if rev is None:
1531 1531 jmanifestnode = 'null'
1532 1532 else:
1533 1533 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
1534 1534 self.ui.write((',\n "manifest": %s') % jmanifestnode)
1535 1535
1536 1536 self.ui.write((',\n "extra": {%s}') %
1537 1537 ", ".join('"%s": "%s"' % (j(k), j(v))
1538 1538 for k, v in ctx.extra().items()))
1539 1539
1540 1540 files = ctx.p1().status(ctx)
1541 1541 self.ui.write((',\n "modified": [%s]') %
1542 1542 ", ".join('"%s"' % j(f) for f in files[0]))
1543 1543 self.ui.write((',\n "added": [%s]') %
1544 1544 ", ".join('"%s"' % j(f) for f in files[1]))
1545 1545 self.ui.write((',\n "removed": [%s]') %
1546 1546 ", ".join('"%s"' % j(f) for f in files[2]))
1547 1547
1548 1548 elif self.ui.verbose:
1549 1549 self.ui.write((',\n "files": [%s]') %
1550 1550 ", ".join('"%s"' % j(f) for f in ctx.files()))
1551 1551
1552 1552 if copies:
1553 1553 self.ui.write((',\n "copies": {%s}') %
1554 1554 ", ".join('"%s": "%s"' % (j(k), j(v))
1555 1555 for k, v in copies))
1556 1556
1557 1557 matchfn = self.matchfn
1558 1558 if matchfn:
1559 1559 stat = self.diffopts.get('stat')
1560 1560 diff = self.diffopts.get('patch')
1561 1561 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
1562 1562 node, prev = ctx.node(), ctx.p1().node()
1563 1563 if stat:
1564 1564 self.ui.pushbuffer()
1565 1565 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1566 1566 match=matchfn, stat=True)
1567 1567 self.ui.write((',\n "diffstat": "%s"')
1568 1568 % j(self.ui.popbuffer()))
1569 1569 if diff:
1570 1570 self.ui.pushbuffer()
1571 1571 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1572 1572 match=matchfn, stat=False)
1573 1573 self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
1574 1574
1575 1575 self.ui.write("\n }")
1576 1576
1577 1577 class changeset_templater(changeset_printer):
1578 1578 '''format changeset information.'''
1579 1579
1580 1580 def __init__(self, ui, repo, matchfn, diffopts, tmpl, mapfile, buffered):
1581 1581 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1582 1582 assert not (tmpl and mapfile)
1583 1583 defaulttempl = templatekw.defaulttempl
1584 1584 if mapfile:
1585 1585 self.t = templater.templater.frommapfile(mapfile,
1586 1586 cache=defaulttempl)
1587 1587 else:
1588 1588 self.t = formatter.maketemplater(ui, 'changeset', tmpl,
1589 1589 cache=defaulttempl)
1590 1590
1591 1591 self._counter = itertools.count()
1592 1592 self.cache = {}
1593 1593
1594 1594 # find correct templates for current mode
1595 1595 tmplmodes = [
1596 1596 (True, None),
1597 1597 (self.ui.verbose, 'verbose'),
1598 1598 (self.ui.quiet, 'quiet'),
1599 1599 (self.ui.debugflag, 'debug'),
1600 1600 ]
1601 1601
1602 1602 self._parts = {'header': '', 'footer': '', 'changeset': 'changeset',
1603 1603 'docheader': '', 'docfooter': ''}
1604 1604 for mode, postfix in tmplmodes:
1605 1605 for t in self._parts:
1606 1606 cur = t
1607 1607 if postfix:
1608 1608 cur += "_" + postfix
1609 1609 if mode and cur in self.t:
1610 1610 self._parts[t] = cur
1611 1611
1612 1612 if self._parts['docheader']:
1613 1613 self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
1614 1614
1615 1615 def close(self):
1616 1616 if self._parts['docfooter']:
1617 1617 if not self.footer:
1618 1618 self.footer = ""
1619 1619 self.footer += templater.stringify(self.t(self._parts['docfooter']))
1620 1620 return super(changeset_templater, self).close()
1621 1621
1622 1622 def _show(self, ctx, copies, matchfn, props):
1623 1623 '''show a single changeset or file revision'''
1624 1624 props = props.copy()
1625 1625 props.update(templatekw.keywords)
1626 1626 props['templ'] = self.t
1627 1627 props['ctx'] = ctx
1628 1628 props['repo'] = self.repo
1629 1629 props['ui'] = self.repo.ui
1630 1630 props['index'] = next(self._counter)
1631 1631 props['revcache'] = {'copies': copies}
1632 1632 props['cache'] = self.cache
1633 1633 props = pycompat.strkwargs(props)
1634 1634
1635 1635 # write header
1636 1636 if self._parts['header']:
1637 1637 h = templater.stringify(self.t(self._parts['header'], **props))
1638 1638 if self.buffered:
1639 1639 self.header[ctx.rev()] = h
1640 1640 else:
1641 1641 if self.lastheader != h:
1642 1642 self.lastheader = h
1643 1643 self.ui.write(h)
1644 1644
1645 1645 # write changeset metadata, then patch if requested
1646 1646 key = self._parts['changeset']
1647 1647 self.ui.write(templater.stringify(self.t(key, **props)))
1648 1648 self.showpatch(ctx, matchfn)
1649 1649
1650 1650 if self._parts['footer']:
1651 1651 if not self.footer:
1652 1652 self.footer = templater.stringify(
1653 1653 self.t(self._parts['footer'], **props))
1654 1654
1655 1655 def gettemplate(ui, tmpl, style):
1656 1656 """
1657 1657 Find the template matching the given template spec or style.
1658 1658 """
1659 1659
1660 1660 # ui settings
1661 1661 if not tmpl and not style: # template are stronger than style
1662 1662 tmpl = ui.config('ui', 'logtemplate')
1663 1663 if tmpl:
1664 1664 return templater.unquotestring(tmpl), None
1665 1665 else:
1666 1666 style = util.expandpath(ui.config('ui', 'style', ''))
1667 1667
1668 1668 if not tmpl and style:
1669 1669 mapfile = style
1670 1670 if not os.path.split(mapfile)[0]:
1671 1671 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1672 1672 or templater.templatepath(mapfile))
1673 1673 if mapname:
1674 1674 mapfile = mapname
1675 1675 return None, mapfile
1676 1676
1677 1677 if not tmpl:
1678 1678 return None, None
1679 1679
1680 1680 return formatter.lookuptemplate(ui, 'changeset', tmpl)
1681 1681
1682 1682 def show_changeset(ui, repo, opts, buffered=False):
1683 1683 """show one changeset using template or regular display.
1684 1684
1685 1685 Display format will be the first non-empty hit of:
1686 1686 1. option 'template'
1687 1687 2. option 'style'
1688 1688 3. [ui] setting 'logtemplate'
1689 1689 4. [ui] setting 'style'
1690 1690 If all of these values are either the unset or the empty string,
1691 1691 regular display via changeset_printer() is done.
1692 1692 """
1693 1693 # options
1694 1694 matchfn = None
1695 1695 if opts.get('patch') or opts.get('stat'):
1696 1696 matchfn = scmutil.matchall(repo)
1697 1697
1698 1698 if opts.get('template') == 'json':
1699 1699 return jsonchangeset(ui, repo, matchfn, opts, buffered)
1700 1700
1701 1701 tmpl, mapfile = gettemplate(ui, opts.get('template'), opts.get('style'))
1702 1702
1703 1703 if not tmpl and not mapfile:
1704 1704 return changeset_printer(ui, repo, matchfn, opts, buffered)
1705 1705
1706 1706 return changeset_templater(ui, repo, matchfn, opts, tmpl, mapfile, buffered)
1707 1707
1708 1708 def showmarker(fm, marker, index=None):
1709 1709 """utility function to display obsolescence marker in a readable way
1710 1710
1711 1711 To be used by debug function."""
1712 1712 if index is not None:
1713 1713 fm.write('index', '%i ', index)
1714 1714 fm.write('precnode', '%s ', hex(marker.precnode()))
1715 1715 succs = marker.succnodes()
1716 1716 fm.condwrite(succs, 'succnodes', '%s ',
1717 1717 fm.formatlist(map(hex, succs), name='node'))
1718 1718 fm.write('flag', '%X ', marker.flags())
1719 1719 parents = marker.parentnodes()
1720 1720 if parents is not None:
1721 1721 fm.write('parentnodes', '{%s} ',
1722 1722 fm.formatlist(map(hex, parents), name='node', sep=', '))
1723 1723 fm.write('date', '(%s) ', fm.formatdate(marker.date()))
1724 1724 meta = marker.metadata().copy()
1725 1725 meta.pop('date', None)
1726 1726 fm.write('metadata', '{%s}', fm.formatdict(meta, fmt='%r: %r', sep=', '))
1727 1727 fm.plain('\n')
1728 1728
1729 1729 def finddate(ui, repo, date):
1730 1730 """Find the tipmost changeset that matches the given date spec"""
1731 1731
1732 1732 df = util.matchdate(date)
1733 1733 m = scmutil.matchall(repo)
1734 1734 results = {}
1735 1735
1736 1736 def prep(ctx, fns):
1737 1737 d = ctx.date()
1738 1738 if df(d[0]):
1739 1739 results[ctx.rev()] = d
1740 1740
1741 1741 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1742 1742 rev = ctx.rev()
1743 1743 if rev in results:
1744 1744 ui.status(_("found revision %s from %s\n") %
1745 1745 (rev, util.datestr(results[rev])))
1746 1746 return '%d' % rev
1747 1747
1748 1748 raise error.Abort(_("revision matching date not found"))
1749 1749
1750 1750 def increasingwindows(windowsize=8, sizelimit=512):
1751 1751 while True:
1752 1752 yield windowsize
1753 1753 if windowsize < sizelimit:
1754 1754 windowsize *= 2
1755 1755
1756 1756 class FileWalkError(Exception):
1757 1757 pass
1758 1758
1759 1759 def walkfilerevs(repo, match, follow, revs, fncache):
1760 1760 '''Walks the file history for the matched files.
1761 1761
1762 1762 Returns the changeset revs that are involved in the file history.
1763 1763
1764 1764 Throws FileWalkError if the file history can't be walked using
1765 1765 filelogs alone.
1766 1766 '''
1767 1767 wanted = set()
1768 1768 copies = []
1769 1769 minrev, maxrev = min(revs), max(revs)
1770 1770 def filerevgen(filelog, last):
1771 1771 """
1772 1772 Only files, no patterns. Check the history of each file.
1773 1773
1774 1774 Examines filelog entries within minrev, maxrev linkrev range
1775 1775 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1776 1776 tuples in backwards order
1777 1777 """
1778 1778 cl_count = len(repo)
1779 1779 revs = []
1780 1780 for j in xrange(0, last + 1):
1781 1781 linkrev = filelog.linkrev(j)
1782 1782 if linkrev < minrev:
1783 1783 continue
1784 1784 # only yield rev for which we have the changelog, it can
1785 1785 # happen while doing "hg log" during a pull or commit
1786 1786 if linkrev >= cl_count:
1787 1787 break
1788 1788
1789 1789 parentlinkrevs = []
1790 1790 for p in filelog.parentrevs(j):
1791 1791 if p != nullrev:
1792 1792 parentlinkrevs.append(filelog.linkrev(p))
1793 1793 n = filelog.node(j)
1794 1794 revs.append((linkrev, parentlinkrevs,
1795 1795 follow and filelog.renamed(n)))
1796 1796
1797 1797 return reversed(revs)
1798 1798 def iterfiles():
1799 1799 pctx = repo['.']
1800 1800 for filename in match.files():
1801 1801 if follow:
1802 1802 if filename not in pctx:
1803 1803 raise error.Abort(_('cannot follow file not in parent '
1804 1804 'revision: "%s"') % filename)
1805 1805 yield filename, pctx[filename].filenode()
1806 1806 else:
1807 1807 yield filename, None
1808 1808 for filename_node in copies:
1809 1809 yield filename_node
1810 1810
1811 1811 for file_, node in iterfiles():
1812 1812 filelog = repo.file(file_)
1813 1813 if not len(filelog):
1814 1814 if node is None:
1815 1815 # A zero count may be a directory or deleted file, so
1816 1816 # try to find matching entries on the slow path.
1817 1817 if follow:
1818 1818 raise error.Abort(
1819 1819 _('cannot follow nonexistent file: "%s"') % file_)
1820 1820 raise FileWalkError("Cannot walk via filelog")
1821 1821 else:
1822 1822 continue
1823 1823
1824 1824 if node is None:
1825 1825 last = len(filelog) - 1
1826 1826 else:
1827 1827 last = filelog.rev(node)
1828 1828
1829 1829 # keep track of all ancestors of the file
1830 1830 ancestors = {filelog.linkrev(last)}
1831 1831
1832 1832 # iterate from latest to oldest revision
1833 1833 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1834 1834 if not follow:
1835 1835 if rev > maxrev:
1836 1836 continue
1837 1837 else:
1838 1838 # Note that last might not be the first interesting
1839 1839 # rev to us:
1840 1840 # if the file has been changed after maxrev, we'll
1841 1841 # have linkrev(last) > maxrev, and we still need
1842 1842 # to explore the file graph
1843 1843 if rev not in ancestors:
1844 1844 continue
1845 1845 # XXX insert 1327 fix here
1846 1846 if flparentlinkrevs:
1847 1847 ancestors.update(flparentlinkrevs)
1848 1848
1849 1849 fncache.setdefault(rev, []).append(file_)
1850 1850 wanted.add(rev)
1851 1851 if copied:
1852 1852 copies.append(copied)
1853 1853
1854 1854 return wanted
1855 1855
1856 1856 class _followfilter(object):
1857 1857 def __init__(self, repo, onlyfirst=False):
1858 1858 self.repo = repo
1859 1859 self.startrev = nullrev
1860 1860 self.roots = set()
1861 1861 self.onlyfirst = onlyfirst
1862 1862
1863 1863 def match(self, rev):
1864 1864 def realparents(rev):
1865 1865 if self.onlyfirst:
1866 1866 return self.repo.changelog.parentrevs(rev)[0:1]
1867 1867 else:
1868 1868 return filter(lambda x: x != nullrev,
1869 1869 self.repo.changelog.parentrevs(rev))
1870 1870
1871 1871 if self.startrev == nullrev:
1872 1872 self.startrev = rev
1873 1873 return True
1874 1874
1875 1875 if rev > self.startrev:
1876 1876 # forward: all descendants
1877 1877 if not self.roots:
1878 1878 self.roots.add(self.startrev)
1879 1879 for parent in realparents(rev):
1880 1880 if parent in self.roots:
1881 1881 self.roots.add(rev)
1882 1882 return True
1883 1883 else:
1884 1884 # backwards: all parents
1885 1885 if not self.roots:
1886 1886 self.roots.update(realparents(self.startrev))
1887 1887 if rev in self.roots:
1888 1888 self.roots.remove(rev)
1889 1889 self.roots.update(realparents(rev))
1890 1890 return True
1891 1891
1892 1892 return False
1893 1893
1894 1894 def walkchangerevs(repo, match, opts, prepare):
1895 1895 '''Iterate over files and the revs in which they changed.
1896 1896
1897 1897 Callers most commonly need to iterate backwards over the history
1898 1898 in which they are interested. Doing so has awful (quadratic-looking)
1899 1899 performance, so we use iterators in a "windowed" way.
1900 1900
1901 1901 We walk a window of revisions in the desired order. Within the
1902 1902 window, we first walk forwards to gather data, then in the desired
1903 1903 order (usually backwards) to display it.
1904 1904
1905 1905 This function returns an iterator yielding contexts. Before
1906 1906 yielding each context, the iterator will first call the prepare
1907 1907 function on each context in the window in forward order.'''
1908 1908
1909 1909 follow = opts.get('follow') or opts.get('follow_first')
1910 1910 revs = _logrevs(repo, opts)
1911 1911 if not revs:
1912 1912 return []
1913 1913 wanted = set()
1914 1914 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
1915 1915 opts.get('removed'))
1916 1916 fncache = {}
1917 1917 change = repo.changectx
1918 1918
1919 1919 # First step is to fill wanted, the set of revisions that we want to yield.
1920 1920 # When it does not induce extra cost, we also fill fncache for revisions in
1921 1921 # wanted: a cache of filenames that were changed (ctx.files()) and that
1922 1922 # match the file filtering conditions.
1923 1923
1924 1924 if match.always():
1925 1925 # No files, no patterns. Display all revs.
1926 1926 wanted = revs
1927 1927 elif not slowpath:
1928 1928 # We only have to read through the filelog to find wanted revisions
1929 1929
1930 1930 try:
1931 1931 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1932 1932 except FileWalkError:
1933 1933 slowpath = True
1934 1934
1935 1935 # We decided to fall back to the slowpath because at least one
1936 1936 # of the paths was not a file. Check to see if at least one of them
1937 1937 # existed in history, otherwise simply return
1938 1938 for path in match.files():
1939 1939 if path == '.' or path in repo.store:
1940 1940 break
1941 1941 else:
1942 1942 return []
1943 1943
1944 1944 if slowpath:
1945 1945 # We have to read the changelog to match filenames against
1946 1946 # changed files
1947 1947
1948 1948 if follow:
1949 1949 raise error.Abort(_('can only follow copies/renames for explicit '
1950 1950 'filenames'))
1951 1951
1952 1952 # The slow path checks files modified in every changeset.
1953 1953 # This is really slow on large repos, so compute the set lazily.
1954 1954 class lazywantedset(object):
1955 1955 def __init__(self):
1956 1956 self.set = set()
1957 1957 self.revs = set(revs)
1958 1958
1959 1959 # No need to worry about locality here because it will be accessed
1960 1960 # in the same order as the increasing window below.
1961 1961 def __contains__(self, value):
1962 1962 if value in self.set:
1963 1963 return True
1964 1964 elif not value in self.revs:
1965 1965 return False
1966 1966 else:
1967 1967 self.revs.discard(value)
1968 1968 ctx = change(value)
1969 1969 matches = filter(match, ctx.files())
1970 1970 if matches:
1971 1971 fncache[value] = matches
1972 1972 self.set.add(value)
1973 1973 return True
1974 1974 return False
1975 1975
1976 1976 def discard(self, value):
1977 1977 self.revs.discard(value)
1978 1978 self.set.discard(value)
1979 1979
1980 1980 wanted = lazywantedset()
1981 1981
1982 1982 # it might be worthwhile to do this in the iterator if the rev range
1983 1983 # is descending and the prune args are all within that range
1984 1984 for rev in opts.get('prune', ()):
1985 1985 rev = repo[rev].rev()
1986 1986 ff = _followfilter(repo)
1987 1987 stop = min(revs[0], revs[-1])
1988 1988 for x in xrange(rev, stop - 1, -1):
1989 1989 if ff.match(x):
1990 1990 wanted = wanted - [x]
1991 1991
1992 1992 # Now that wanted is correctly initialized, we can iterate over the
1993 1993 # revision range, yielding only revisions in wanted.
1994 1994 def iterate():
1995 1995 if follow and match.always():
1996 1996 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
1997 1997 def want(rev):
1998 1998 return ff.match(rev) and rev in wanted
1999 1999 else:
2000 2000 def want(rev):
2001 2001 return rev in wanted
2002 2002
2003 2003 it = iter(revs)
2004 2004 stopiteration = False
2005 2005 for windowsize in increasingwindows():
2006 2006 nrevs = []
2007 2007 for i in xrange(windowsize):
2008 2008 rev = next(it, None)
2009 2009 if rev is None:
2010 2010 stopiteration = True
2011 2011 break
2012 2012 elif want(rev):
2013 2013 nrevs.append(rev)
2014 2014 for rev in sorted(nrevs):
2015 2015 fns = fncache.get(rev)
2016 2016 ctx = change(rev)
2017 2017 if not fns:
2018 2018 def fns_generator():
2019 2019 for f in ctx.files():
2020 2020 if match(f):
2021 2021 yield f
2022 2022 fns = fns_generator()
2023 2023 prepare(ctx, fns)
2024 2024 for rev in nrevs:
2025 2025 yield change(rev)
2026 2026
2027 2027 if stopiteration:
2028 2028 break
2029 2029
2030 2030 return iterate()
2031 2031
2032 2032 def _makefollowlogfilematcher(repo, files, followfirst):
2033 2033 # When displaying a revision with --patch --follow FILE, we have
2034 2034 # to know which file of the revision must be diffed. With
2035 2035 # --follow, we want the names of the ancestors of FILE in the
2036 2036 # revision, stored in "fcache". "fcache" is populated by
2037 2037 # reproducing the graph traversal already done by --follow revset
2038 2038 # and relating revs to file names (which is not "correct" but
2039 2039 # good enough).
2040 2040 fcache = {}
2041 2041 fcacheready = [False]
2042 2042 pctx = repo['.']
2043 2043
2044 2044 def populate():
2045 2045 for fn in files:
2046 2046 fctx = pctx[fn]
2047 2047 fcache.setdefault(fctx.introrev(), set()).add(fctx.path())
2048 2048 for c in fctx.ancestors(followfirst=followfirst):
2049 2049 fcache.setdefault(c.rev(), set()).add(c.path())
2050 2050
2051 2051 def filematcher(rev):
2052 2052 if not fcacheready[0]:
2053 2053 # Lazy initialization
2054 2054 fcacheready[0] = True
2055 2055 populate()
2056 2056 return scmutil.matchfiles(repo, fcache.get(rev, []))
2057 2057
2058 2058 return filematcher
2059 2059
2060 2060 def _makenofollowlogfilematcher(repo, pats, opts):
2061 2061 '''hook for extensions to override the filematcher for non-follow cases'''
2062 2062 return None
2063 2063
2064 2064 def _makelogrevset(repo, pats, opts, revs):
2065 2065 """Return (expr, filematcher) where expr is a revset string built
2066 2066 from log options and file patterns or None. If --stat or --patch
2067 2067 are not passed filematcher is None. Otherwise it is a callable
2068 2068 taking a revision number and returning a match objects filtering
2069 2069 the files to be detailed when displaying the revision.
2070 2070 """
2071 2071 opt2revset = {
2072 2072 'no_merges': ('not merge()', None),
2073 2073 'only_merges': ('merge()', None),
2074 2074 '_ancestors': ('ancestors(%(val)s)', None),
2075 2075 '_fancestors': ('_firstancestors(%(val)s)', None),
2076 2076 '_descendants': ('descendants(%(val)s)', None),
2077 2077 '_fdescendants': ('_firstdescendants(%(val)s)', None),
2078 2078 '_matchfiles': ('_matchfiles(%(val)s)', None),
2079 2079 'date': ('date(%(val)r)', None),
2080 2080 'branch': ('branch(%(val)r)', ' or '),
2081 2081 '_patslog': ('filelog(%(val)r)', ' or '),
2082 2082 '_patsfollow': ('follow(%(val)r)', ' or '),
2083 2083 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
2084 2084 'keyword': ('keyword(%(val)r)', ' or '),
2085 2085 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
2086 2086 'user': ('user(%(val)r)', ' or '),
2087 2087 }
2088 2088
2089 2089 opts = dict(opts)
2090 2090 # follow or not follow?
2091 2091 follow = opts.get('follow') or opts.get('follow_first')
2092 2092 if opts.get('follow_first'):
2093 2093 followfirst = 1
2094 2094 else:
2095 2095 followfirst = 0
2096 2096 # --follow with FILE behavior depends on revs...
2097 2097 it = iter(revs)
2098 2098 startrev = next(it)
2099 2099 followdescendants = startrev < next(it, startrev)
2100 2100
2101 2101 # branch and only_branch are really aliases and must be handled at
2102 2102 # the same time
2103 2103 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
2104 2104 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
2105 2105 # pats/include/exclude are passed to match.match() directly in
2106 2106 # _matchfiles() revset but walkchangerevs() builds its matcher with
2107 2107 # scmutil.match(). The difference is input pats are globbed on
2108 2108 # platforms without shell expansion (windows).
2109 2109 wctx = repo[None]
2110 2110 match, pats = scmutil.matchandpats(wctx, pats, opts)
2111 2111 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
2112 2112 opts.get('removed'))
2113 2113 if not slowpath:
2114 2114 for f in match.files():
2115 2115 if follow and f not in wctx:
2116 2116 # If the file exists, it may be a directory, so let it
2117 2117 # take the slow path.
2118 2118 if os.path.exists(repo.wjoin(f)):
2119 2119 slowpath = True
2120 2120 continue
2121 2121 else:
2122 2122 raise error.Abort(_('cannot follow file not in parent '
2123 2123 'revision: "%s"') % f)
2124 2124 filelog = repo.file(f)
2125 2125 if not filelog:
2126 2126 # A zero count may be a directory or deleted file, so
2127 2127 # try to find matching entries on the slow path.
2128 2128 if follow:
2129 2129 raise error.Abort(
2130 2130 _('cannot follow nonexistent file: "%s"') % f)
2131 2131 slowpath = True
2132 2132
2133 2133 # We decided to fall back to the slowpath because at least one
2134 2134 # of the paths was not a file. Check to see if at least one of them
2135 2135 # existed in history - in that case, we'll continue down the
2136 2136 # slowpath; otherwise, we can turn off the slowpath
2137 2137 if slowpath:
2138 2138 for path in match.files():
2139 2139 if path == '.' or path in repo.store:
2140 2140 break
2141 2141 else:
2142 2142 slowpath = False
2143 2143
2144 2144 fpats = ('_patsfollow', '_patsfollowfirst')
2145 2145 fnopats = (('_ancestors', '_fancestors'),
2146 2146 ('_descendants', '_fdescendants'))
2147 2147 if slowpath:
2148 2148 # See walkchangerevs() slow path.
2149 2149 #
2150 2150 # pats/include/exclude cannot be represented as separate
2151 2151 # revset expressions as their filtering logic applies at file
2152 2152 # level. For instance "-I a -X a" matches a revision touching
2153 2153 # "a" and "b" while "file(a) and not file(b)" does
2154 2154 # not. Besides, filesets are evaluated against the working
2155 2155 # directory.
2156 2156 matchargs = ['r:', 'd:relpath']
2157 2157 for p in pats:
2158 2158 matchargs.append('p:' + p)
2159 2159 for p in opts.get('include', []):
2160 2160 matchargs.append('i:' + p)
2161 2161 for p in opts.get('exclude', []):
2162 2162 matchargs.append('x:' + p)
2163 2163 matchargs = ','.join(('%r' % p) for p in matchargs)
2164 2164 opts['_matchfiles'] = matchargs
2165 2165 if follow:
2166 2166 opts[fnopats[0][followfirst]] = '.'
2167 2167 else:
2168 2168 if follow:
2169 2169 if pats:
2170 2170 # follow() revset interprets its file argument as a
2171 2171 # manifest entry, so use match.files(), not pats.
2172 2172 opts[fpats[followfirst]] = list(match.files())
2173 2173 else:
2174 2174 op = fnopats[followdescendants][followfirst]
2175 2175 opts[op] = 'rev(%d)' % startrev
2176 2176 else:
2177 2177 opts['_patslog'] = list(pats)
2178 2178
2179 2179 filematcher = None
2180 2180 if opts.get('patch') or opts.get('stat'):
2181 2181 # When following files, track renames via a special matcher.
2182 2182 # If we're forced to take the slowpath it means we're following
2183 2183 # at least one pattern/directory, so don't bother with rename tracking.
2184 2184 if follow and not match.always() and not slowpath:
2185 2185 # _makefollowlogfilematcher expects its files argument to be
2186 2186 # relative to the repo root, so use match.files(), not pats.
2187 2187 filematcher = _makefollowlogfilematcher(repo, match.files(),
2188 2188 followfirst)
2189 2189 else:
2190 2190 filematcher = _makenofollowlogfilematcher(repo, pats, opts)
2191 2191 if filematcher is None:
2192 2192 filematcher = lambda rev: match
2193 2193
2194 2194 expr = []
2195 2195 for op, val in sorted(opts.iteritems()):
2196 2196 if not val:
2197 2197 continue
2198 2198 if op not in opt2revset:
2199 2199 continue
2200 2200 revop, andor = opt2revset[op]
2201 2201 if '%(val)' not in revop:
2202 2202 expr.append(revop)
2203 2203 else:
2204 2204 if not isinstance(val, list):
2205 2205 e = revop % {'val': val}
2206 2206 else:
2207 2207 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
2208 2208 expr.append(e)
2209 2209
2210 2210 if expr:
2211 2211 expr = '(' + ' and '.join(expr) + ')'
2212 2212 else:
2213 2213 expr = None
2214 2214 return expr, filematcher
2215 2215
2216 2216 def _logrevs(repo, opts):
2217 2217 # Default --rev value depends on --follow but --follow behavior
2218 2218 # depends on revisions resolved from --rev...
2219 2219 follow = opts.get('follow') or opts.get('follow_first')
2220 2220 if opts.get('rev'):
2221 2221 revs = scmutil.revrange(repo, opts['rev'])
2222 2222 elif follow and repo.dirstate.p1() == nullid:
2223 2223 revs = smartset.baseset()
2224 2224 elif follow:
2225 2225 revs = repo.revs('reverse(:.)')
2226 2226 else:
2227 2227 revs = smartset.spanset(repo)
2228 2228 revs.reverse()
2229 2229 return revs
2230 2230
2231 2231 def getgraphlogrevs(repo, pats, opts):
2232 2232 """Return (revs, expr, filematcher) where revs is an iterable of
2233 2233 revision numbers, expr is a revset string built from log options
2234 2234 and file patterns or None, and used to filter 'revs'. If --stat or
2235 2235 --patch are not passed filematcher is None. Otherwise it is a
2236 2236 callable taking a revision number and returning a match objects
2237 2237 filtering the files to be detailed when displaying the revision.
2238 2238 """
2239 2239 limit = loglimit(opts)
2240 2240 revs = _logrevs(repo, opts)
2241 2241 if not revs:
2242 2242 return smartset.baseset(), None, None
2243 2243 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2244 2244 if opts.get('rev'):
2245 2245 # User-specified revs might be unsorted, but don't sort before
2246 2246 # _makelogrevset because it might depend on the order of revs
2247 2247 if not (revs.isdescending() or revs.istopo()):
2248 2248 revs.sort(reverse=True)
2249 2249 if expr:
2250 2250 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2251 2251 revs = matcher(repo, revs)
2252 2252 if limit is not None:
2253 2253 limitedrevs = []
2254 2254 for idx, rev in enumerate(revs):
2255 2255 if idx >= limit:
2256 2256 break
2257 2257 limitedrevs.append(rev)
2258 2258 revs = smartset.baseset(limitedrevs)
2259 2259
2260 2260 return revs, expr, filematcher
2261 2261
2262 2262 def getlogrevs(repo, pats, opts):
2263 2263 """Return (revs, expr, filematcher) where revs is an iterable of
2264 2264 revision numbers, expr is a revset string built from log options
2265 2265 and file patterns or None, and used to filter 'revs'. If --stat or
2266 2266 --patch are not passed filematcher is None. Otherwise it is a
2267 2267 callable taking a revision number and returning a match objects
2268 2268 filtering the files to be detailed when displaying the revision.
2269 2269 """
2270 2270 limit = loglimit(opts)
2271 2271 revs = _logrevs(repo, opts)
2272 2272 if not revs:
2273 2273 return smartset.baseset([]), None, None
2274 2274 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2275 2275 if expr:
2276 2276 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2277 2277 revs = matcher(repo, revs)
2278 2278 if limit is not None:
2279 2279 limitedrevs = []
2280 2280 for idx, r in enumerate(revs):
2281 2281 if limit <= idx:
2282 2282 break
2283 2283 limitedrevs.append(r)
2284 2284 revs = smartset.baseset(limitedrevs)
2285 2285
2286 2286 return revs, expr, filematcher
2287 2287
2288 2288 def _graphnodeformatter(ui, displayer):
2289 2289 spec = ui.config('ui', 'graphnodetemplate')
2290 2290 if not spec:
2291 2291 return templatekw.showgraphnode # fast path for "{graphnode}"
2292 2292
2293 2293 spec = templater.unquotestring(spec)
2294 2294 templ = formatter.gettemplater(ui, 'graphnode', spec)
2295 2295 cache = {}
2296 2296 if isinstance(displayer, changeset_templater):
2297 2297 cache = displayer.cache # reuse cache of slow templates
2298 2298 props = templatekw.keywords.copy()
2299 2299 props['templ'] = templ
2300 2300 props['cache'] = cache
2301 2301 def formatnode(repo, ctx):
2302 2302 props['ctx'] = ctx
2303 2303 props['repo'] = repo
2304 2304 props['ui'] = repo.ui
2305 2305 props['revcache'] = {}
2306 2306 return templater.stringify(templ('graphnode', **props))
2307 2307 return formatnode
2308 2308
2309 2309 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
2310 2310 filematcher=None):
2311 2311 formatnode = _graphnodeformatter(ui, displayer)
2312 2312 state = graphmod.asciistate()
2313 2313 styles = state['styles']
2314 2314
2315 2315 # only set graph styling if HGPLAIN is not set.
2316 2316 if ui.plain('graph'):
2317 2317 # set all edge styles to |, the default pre-3.8 behaviour
2318 2318 styles.update(dict.fromkeys(styles, '|'))
2319 2319 else:
2320 2320 edgetypes = {
2321 2321 'parent': graphmod.PARENT,
2322 2322 'grandparent': graphmod.GRANDPARENT,
2323 2323 'missing': graphmod.MISSINGPARENT
2324 2324 }
2325 2325 for name, key in edgetypes.items():
2326 2326 # experimental config: experimental.graphstyle.*
2327 2327 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
2328 2328 styles[key])
2329 2329 if not styles[key]:
2330 2330 styles[key] = None
2331 2331
2332 2332 # experimental config: experimental.graphshorten
2333 2333 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
2334 2334
2335 2335 for rev, type, ctx, parents in dag:
2336 2336 char = formatnode(repo, ctx)
2337 2337 copies = None
2338 2338 if getrenamed and ctx.rev():
2339 2339 copies = []
2340 2340 for fn in ctx.files():
2341 2341 rename = getrenamed(fn, ctx.rev())
2342 2342 if rename:
2343 2343 copies.append((fn, rename[0]))
2344 2344 revmatchfn = None
2345 2345 if filematcher is not None:
2346 2346 revmatchfn = filematcher(ctx.rev())
2347 2347 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
2348 2348 lines = displayer.hunk.pop(rev).split('\n')
2349 2349 if not lines[-1]:
2350 2350 del lines[-1]
2351 2351 displayer.flush(ctx)
2352 2352 edges = edgefn(type, char, lines, state, rev, parents)
2353 2353 for type, char, lines, coldata in edges:
2354 2354 graphmod.ascii(ui, state, type, char, lines, coldata)
2355 2355 displayer.close()
2356 2356
2357 2357 def graphlog(ui, repo, pats, opts):
2358 2358 # Parameters are identical to log command ones
2359 2359 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
2360 2360 revdag = graphmod.dagwalker(repo, revs)
2361 2361
2362 2362 getrenamed = None
2363 2363 if opts.get('copies'):
2364 2364 endrev = None
2365 2365 if opts.get('rev'):
2366 2366 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
2367 2367 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
2368 2368
2369 2369 ui.pager('log')
2370 2370 displayer = show_changeset(ui, repo, opts, buffered=True)
2371 2371 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
2372 2372 filematcher)
2373 2373
2374 2374 def checkunsupportedgraphflags(pats, opts):
2375 2375 for op in ["newest_first"]:
2376 2376 if op in opts and opts[op]:
2377 2377 raise error.Abort(_("-G/--graph option is incompatible with --%s")
2378 2378 % op.replace("_", "-"))
2379 2379
2380 2380 def graphrevs(repo, nodes, opts):
2381 2381 limit = loglimit(opts)
2382 2382 nodes.reverse()
2383 2383 if limit is not None:
2384 2384 nodes = nodes[:limit]
2385 2385 return graphmod.nodes(repo, nodes)
2386 2386
2387 2387 def add(ui, repo, match, prefix, explicitonly, **opts):
2388 2388 join = lambda f: os.path.join(prefix, f)
2389 2389 bad = []
2390 2390
2391 2391 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2392 2392 names = []
2393 2393 wctx = repo[None]
2394 2394 cca = None
2395 2395 abort, warn = scmutil.checkportabilityalert(ui)
2396 2396 if abort or warn:
2397 2397 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2398 2398
2399 2399 badmatch = matchmod.badmatch(match, badfn)
2400 2400 dirstate = repo.dirstate
2401 2401 # We don't want to just call wctx.walk here, since it would return a lot of
2402 2402 # clean files, which we aren't interested in and takes time.
2403 2403 for f in sorted(dirstate.walk(badmatch, sorted(wctx.substate),
2404 2404 True, False, full=False)):
2405 2405 exact = match.exact(f)
2406 2406 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2407 2407 if cca:
2408 2408 cca(f)
2409 2409 names.append(f)
2410 2410 if ui.verbose or not exact:
2411 2411 ui.status(_('adding %s\n') % match.rel(f))
2412 2412
2413 2413 for subpath in sorted(wctx.substate):
2414 2414 sub = wctx.sub(subpath)
2415 2415 try:
2416 2416 submatch = matchmod.subdirmatcher(subpath, match)
2417 2417 if opts.get(r'subrepos'):
2418 2418 bad.extend(sub.add(ui, submatch, prefix, False, **opts))
2419 2419 else:
2420 2420 bad.extend(sub.add(ui, submatch, prefix, True, **opts))
2421 2421 except error.LookupError:
2422 2422 ui.status(_("skipping missing subrepository: %s\n")
2423 2423 % join(subpath))
2424 2424
2425 2425 if not opts.get(r'dry_run'):
2426 2426 rejected = wctx.add(names, prefix)
2427 2427 bad.extend(f for f in rejected if f in match.files())
2428 2428 return bad
2429 2429
2430 2430 def addwebdirpath(repo, serverpath, webconf):
2431 2431 webconf[serverpath] = repo.root
2432 2432 repo.ui.debug('adding %s = %s\n' % (serverpath, repo.root))
2433 2433
2434 2434 for r in repo.revs('filelog("path:.hgsub")'):
2435 2435 ctx = repo[r]
2436 2436 for subpath in ctx.substate:
2437 2437 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2438 2438
2439 2439 def forget(ui, repo, match, prefix, explicitonly):
2440 2440 join = lambda f: os.path.join(prefix, f)
2441 2441 bad = []
2442 2442 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2443 2443 wctx = repo[None]
2444 2444 forgot = []
2445 2445
2446 2446 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2447 2447 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2448 2448 if explicitonly:
2449 2449 forget = [f for f in forget if match.exact(f)]
2450 2450
2451 2451 for subpath in sorted(wctx.substate):
2452 2452 sub = wctx.sub(subpath)
2453 2453 try:
2454 2454 submatch = matchmod.subdirmatcher(subpath, match)
2455 2455 subbad, subforgot = sub.forget(submatch, prefix)
2456 2456 bad.extend([subpath + '/' + f for f in subbad])
2457 2457 forgot.extend([subpath + '/' + f for f in subforgot])
2458 2458 except error.LookupError:
2459 2459 ui.status(_("skipping missing subrepository: %s\n")
2460 2460 % join(subpath))
2461 2461
2462 2462 if not explicitonly:
2463 2463 for f in match.files():
2464 2464 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2465 2465 if f not in forgot:
2466 2466 if repo.wvfs.exists(f):
2467 2467 # Don't complain if the exact case match wasn't given.
2468 2468 # But don't do this until after checking 'forgot', so
2469 2469 # that subrepo files aren't normalized, and this op is
2470 2470 # purely from data cached by the status walk above.
2471 2471 if repo.dirstate.normalize(f) in repo.dirstate:
2472 2472 continue
2473 2473 ui.warn(_('not removing %s: '
2474 2474 'file is already untracked\n')
2475 2475 % match.rel(f))
2476 2476 bad.append(f)
2477 2477
2478 2478 for f in forget:
2479 2479 if ui.verbose or not match.exact(f):
2480 2480 ui.status(_('removing %s\n') % match.rel(f))
2481 2481
2482 2482 rejected = wctx.forget(forget, prefix)
2483 2483 bad.extend(f for f in rejected if f in match.files())
2484 2484 forgot.extend(f for f in forget if f not in rejected)
2485 2485 return bad, forgot
2486 2486
2487 2487 def files(ui, ctx, m, fm, fmt, subrepos):
2488 2488 rev = ctx.rev()
2489 2489 ret = 1
2490 2490 ds = ctx.repo().dirstate
2491 2491
2492 2492 for f in ctx.matches(m):
2493 2493 if rev is None and ds[f] == 'r':
2494 2494 continue
2495 2495 fm.startitem()
2496 2496 if ui.verbose:
2497 2497 fc = ctx[f]
2498 2498 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2499 2499 fm.data(abspath=f)
2500 2500 fm.write('path', fmt, m.rel(f))
2501 2501 ret = 0
2502 2502
2503 2503 for subpath in sorted(ctx.substate):
2504 2504 submatch = matchmod.subdirmatcher(subpath, m)
2505 2505 if (subrepos or m.exact(subpath) or any(submatch.files())):
2506 2506 sub = ctx.sub(subpath)
2507 2507 try:
2508 2508 recurse = m.exact(subpath) or subrepos
2509 2509 if sub.printfiles(ui, submatch, fm, fmt, recurse) == 0:
2510 2510 ret = 0
2511 2511 except error.LookupError:
2512 2512 ui.status(_("skipping missing subrepository: %s\n")
2513 2513 % m.abs(subpath))
2514 2514
2515 2515 return ret
2516 2516
2517 2517 def remove(ui, repo, m, prefix, after, force, subrepos, warnings=None):
2518 2518 join = lambda f: os.path.join(prefix, f)
2519 2519 ret = 0
2520 2520 s = repo.status(match=m, clean=True)
2521 2521 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2522 2522
2523 2523 wctx = repo[None]
2524 2524
2525 2525 if warnings is None:
2526 2526 warnings = []
2527 2527 warn = True
2528 2528 else:
2529 2529 warn = False
2530 2530
2531 2531 subs = sorted(wctx.substate)
2532 2532 total = len(subs)
2533 2533 count = 0
2534 2534 for subpath in subs:
2535 2535 count += 1
2536 2536 submatch = matchmod.subdirmatcher(subpath, m)
2537 2537 if subrepos or m.exact(subpath) or any(submatch.files()):
2538 2538 ui.progress(_('searching'), count, total=total, unit=_('subrepos'))
2539 2539 sub = wctx.sub(subpath)
2540 2540 try:
2541 2541 if sub.removefiles(submatch, prefix, after, force, subrepos,
2542 2542 warnings):
2543 2543 ret = 1
2544 2544 except error.LookupError:
2545 2545 warnings.append(_("skipping missing subrepository: %s\n")
2546 2546 % join(subpath))
2547 2547 ui.progress(_('searching'), None)
2548 2548
2549 2549 # warn about failure to delete explicit files/dirs
2550 2550 deleteddirs = util.dirs(deleted)
2551 2551 files = m.files()
2552 2552 total = len(files)
2553 2553 count = 0
2554 2554 for f in files:
2555 2555 def insubrepo():
2556 2556 for subpath in wctx.substate:
2557 2557 if f.startswith(subpath + '/'):
2558 2558 return True
2559 2559 return False
2560 2560
2561 2561 count += 1
2562 2562 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2563 2563 isdir = f in deleteddirs or wctx.hasdir(f)
2564 2564 if (f in repo.dirstate or isdir or f == '.'
2565 2565 or insubrepo() or f in subs):
2566 2566 continue
2567 2567
2568 2568 if repo.wvfs.exists(f):
2569 2569 if repo.wvfs.isdir(f):
2570 2570 warnings.append(_('not removing %s: no tracked files\n')
2571 2571 % m.rel(f))
2572 2572 else:
2573 2573 warnings.append(_('not removing %s: file is untracked\n')
2574 2574 % m.rel(f))
2575 2575 # missing files will generate a warning elsewhere
2576 2576 ret = 1
2577 2577 ui.progress(_('deleting'), None)
2578 2578
2579 2579 if force:
2580 2580 list = modified + deleted + clean + added
2581 2581 elif after:
2582 2582 list = deleted
2583 2583 remaining = modified + added + clean
2584 2584 total = len(remaining)
2585 2585 count = 0
2586 2586 for f in remaining:
2587 2587 count += 1
2588 2588 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2589 2589 warnings.append(_('not removing %s: file still exists\n')
2590 2590 % m.rel(f))
2591 2591 ret = 1
2592 2592 ui.progress(_('skipping'), None)
2593 2593 else:
2594 2594 list = deleted + clean
2595 2595 total = len(modified) + len(added)
2596 2596 count = 0
2597 2597 for f in modified:
2598 2598 count += 1
2599 2599 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2600 2600 warnings.append(_('not removing %s: file is modified (use -f'
2601 2601 ' to force removal)\n') % m.rel(f))
2602 2602 ret = 1
2603 2603 for f in added:
2604 2604 count += 1
2605 2605 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2606 2606 warnings.append(_("not removing %s: file has been marked for add"
2607 2607 " (use 'hg forget' to undo add)\n") % m.rel(f))
2608 2608 ret = 1
2609 2609 ui.progress(_('skipping'), None)
2610 2610
2611 2611 list = sorted(list)
2612 2612 total = len(list)
2613 2613 count = 0
2614 2614 for f in list:
2615 2615 count += 1
2616 2616 if ui.verbose or not m.exact(f):
2617 2617 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2618 2618 ui.status(_('removing %s\n') % m.rel(f))
2619 2619 ui.progress(_('deleting'), None)
2620 2620
2621 2621 with repo.wlock():
2622 2622 if not after:
2623 2623 for f in list:
2624 2624 if f in added:
2625 2625 continue # we never unlink added files on remove
2626 2626 repo.wvfs.unlinkpath(f, ignoremissing=True)
2627 2627 repo[None].forget(list)
2628 2628
2629 2629 if warn:
2630 2630 for warning in warnings:
2631 2631 ui.warn(warning)
2632 2632
2633 2633 return ret
2634 2634
2635 def cat(ui, repo, ctx, matcher, fntemplate, prefix, **opts):
2635 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2636 2636 err = 1
2637 2637
2638 2638 def write(path):
2639 filename = None
2639 2640 if fntemplate:
2640 2641 filename = makefilename(repo, fntemplate, ctx.node(),
2641 2642 pathname=os.path.join(prefix, path))
2642 fp = open(filename, 'wb')
2643 else:
2644 fp = _unclosablefile(ui.fout)
2645 with fp:
2643 with formatter.maybereopen(basefm, filename, opts) as fm:
2646 2644 data = ctx[path].data()
2647 2645 if opts.get('decode'):
2648 2646 data = repo.wwritedata(path, data)
2649 fp.write(data)
2647 fm.startitem()
2648 fm.write('data', '%s', data)
2649 fm.data(abspath=path, path=matcher.rel(path))
2650 2650
2651 2651 # Automation often uses hg cat on single files, so special case it
2652 2652 # for performance to avoid the cost of parsing the manifest.
2653 2653 if len(matcher.files()) == 1 and not matcher.anypats():
2654 2654 file = matcher.files()[0]
2655 2655 mfl = repo.manifestlog
2656 2656 mfnode = ctx.manifestnode()
2657 2657 try:
2658 2658 if mfnode and mfl[mfnode].find(file)[0]:
2659 2659 write(file)
2660 2660 return 0
2661 2661 except KeyError:
2662 2662 pass
2663 2663
2664 2664 for abs in ctx.walk(matcher):
2665 2665 write(abs)
2666 2666 err = 0
2667 2667
2668 2668 for subpath in sorted(ctx.substate):
2669 2669 sub = ctx.sub(subpath)
2670 2670 try:
2671 2671 submatch = matchmod.subdirmatcher(subpath, matcher)
2672 2672
2673 if not sub.cat(submatch, fntemplate,
2673 if not sub.cat(submatch, basefm, fntemplate,
2674 2674 os.path.join(prefix, sub._path), **opts):
2675 2675 err = 0
2676 2676 except error.RepoLookupError:
2677 2677 ui.status(_("skipping missing subrepository: %s\n")
2678 2678 % os.path.join(prefix, subpath))
2679 2679
2680 2680 return err
2681 2681
2682 2682 def commit(ui, repo, commitfunc, pats, opts):
2683 2683 '''commit the specified files or all outstanding changes'''
2684 2684 date = opts.get('date')
2685 2685 if date:
2686 2686 opts['date'] = util.parsedate(date)
2687 2687 message = logmessage(ui, opts)
2688 2688 matcher = scmutil.match(repo[None], pats, opts)
2689 2689
2690 2690 # extract addremove carefully -- this function can be called from a command
2691 2691 # that doesn't support addremove
2692 2692 if opts.get('addremove'):
2693 2693 if scmutil.addremove(repo, matcher, "", opts) != 0:
2694 2694 raise error.Abort(
2695 2695 _("failed to mark all new/missing files as added/removed"))
2696 2696
2697 2697 return commitfunc(ui, repo, message, matcher, opts)
2698 2698
2699 2699 def samefile(f, ctx1, ctx2):
2700 2700 if f in ctx1.manifest():
2701 2701 a = ctx1.filectx(f)
2702 2702 if f in ctx2.manifest():
2703 2703 b = ctx2.filectx(f)
2704 2704 return (not a.cmp(b)
2705 2705 and a.flags() == b.flags())
2706 2706 else:
2707 2707 return False
2708 2708 else:
2709 2709 return f not in ctx2.manifest()
2710 2710
2711 2711 def amend(ui, repo, commitfunc, old, extra, pats, opts):
2712 2712 # avoid cycle context -> subrepo -> cmdutil
2713 2713 from . import context
2714 2714
2715 2715 # amend will reuse the existing user if not specified, but the obsolete
2716 2716 # marker creation requires that the current user's name is specified.
2717 2717 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2718 2718 ui.username() # raise exception if username not set
2719 2719
2720 2720 ui.note(_('amending changeset %s\n') % old)
2721 2721 base = old.p1()
2722 2722 createmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
2723 2723
2724 2724 wlock = lock = newid = None
2725 2725 try:
2726 2726 wlock = repo.wlock()
2727 2727 lock = repo.lock()
2728 2728 with repo.transaction('amend') as tr:
2729 2729 # See if we got a message from -m or -l, if not, open the editor
2730 2730 # with the message of the changeset to amend
2731 2731 message = logmessage(ui, opts)
2732 2732 # ensure logfile does not conflict with later enforcement of the
2733 2733 # message. potential logfile content has been processed by
2734 2734 # `logmessage` anyway.
2735 2735 opts.pop('logfile')
2736 2736 # First, do a regular commit to record all changes in the working
2737 2737 # directory (if there are any)
2738 2738 ui.callhooks = False
2739 2739 activebookmark = repo._bookmarks.active
2740 2740 try:
2741 2741 repo._bookmarks.active = None
2742 2742 opts['message'] = 'temporary amend commit for %s' % old
2743 2743 node = commit(ui, repo, commitfunc, pats, opts)
2744 2744 finally:
2745 2745 repo._bookmarks.active = activebookmark
2746 2746 repo._bookmarks.recordchange(tr)
2747 2747 ui.callhooks = True
2748 2748 ctx = repo[node]
2749 2749
2750 2750 # Participating changesets:
2751 2751 #
2752 2752 # node/ctx o - new (intermediate) commit that contains changes
2753 2753 # | from working dir to go into amending commit
2754 2754 # | (or a workingctx if there were no changes)
2755 2755 # |
2756 2756 # old o - changeset to amend
2757 2757 # |
2758 2758 # base o - parent of amending changeset
2759 2759
2760 2760 # Update extra dict from amended commit (e.g. to preserve graft
2761 2761 # source)
2762 2762 extra.update(old.extra())
2763 2763
2764 2764 # Also update it from the intermediate commit or from the wctx
2765 2765 extra.update(ctx.extra())
2766 2766
2767 2767 if len(old.parents()) > 1:
2768 2768 # ctx.files() isn't reliable for merges, so fall back to the
2769 2769 # slower repo.status() method
2770 2770 files = set([fn for st in repo.status(base, old)[:3]
2771 2771 for fn in st])
2772 2772 else:
2773 2773 files = set(old.files())
2774 2774
2775 2775 # Second, we use either the commit we just did, or if there were no
2776 2776 # changes the parent of the working directory as the version of the
2777 2777 # files in the final amend commit
2778 2778 if node:
2779 2779 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
2780 2780
2781 2781 user = ctx.user()
2782 2782 date = ctx.date()
2783 2783 # Recompute copies (avoid recording a -> b -> a)
2784 2784 copied = copies.pathcopies(base, ctx)
2785 2785 if old.p2:
2786 2786 copied.update(copies.pathcopies(old.p2(), ctx))
2787 2787
2788 2788 # Prune files which were reverted by the updates: if old
2789 2789 # introduced file X and our intermediate commit, node,
2790 2790 # renamed that file, then those two files are the same and
2791 2791 # we can discard X from our list of files. Likewise if X
2792 2792 # was deleted, it's no longer relevant
2793 2793 files.update(ctx.files())
2794 2794 files = [f for f in files if not samefile(f, ctx, base)]
2795 2795
2796 2796 def filectxfn(repo, ctx_, path):
2797 2797 try:
2798 2798 fctx = ctx[path]
2799 2799 flags = fctx.flags()
2800 2800 mctx = context.memfilectx(repo,
2801 2801 fctx.path(), fctx.data(),
2802 2802 islink='l' in flags,
2803 2803 isexec='x' in flags,
2804 2804 copied=copied.get(path))
2805 2805 return mctx
2806 2806 except KeyError:
2807 2807 return None
2808 2808 else:
2809 2809 ui.note(_('copying changeset %s to %s\n') % (old, base))
2810 2810
2811 2811 # Use version of files as in the old cset
2812 2812 def filectxfn(repo, ctx_, path):
2813 2813 try:
2814 2814 return old.filectx(path)
2815 2815 except KeyError:
2816 2816 return None
2817 2817
2818 2818 user = opts.get('user') or old.user()
2819 2819 date = opts.get('date') or old.date()
2820 2820 editform = mergeeditform(old, 'commit.amend')
2821 2821 editor = getcommiteditor(editform=editform, **opts)
2822 2822 if not message:
2823 2823 editor = getcommiteditor(edit=True, editform=editform)
2824 2824 message = old.description()
2825 2825
2826 2826 pureextra = extra.copy()
2827 2827 extra['amend_source'] = old.hex()
2828 2828
2829 2829 new = context.memctx(repo,
2830 2830 parents=[base.node(), old.p2().node()],
2831 2831 text=message,
2832 2832 files=files,
2833 2833 filectxfn=filectxfn,
2834 2834 user=user,
2835 2835 date=date,
2836 2836 extra=extra,
2837 2837 editor=editor)
2838 2838
2839 2839 newdesc = changelog.stripdesc(new.description())
2840 2840 if ((not node)
2841 2841 and newdesc == old.description()
2842 2842 and user == old.user()
2843 2843 and date == old.date()
2844 2844 and pureextra == old.extra()):
2845 2845 # nothing changed. continuing here would create a new node
2846 2846 # anyway because of the amend_source noise.
2847 2847 #
2848 2848 # This not what we expect from amend.
2849 2849 return old.node()
2850 2850
2851 2851 ph = repo.ui.config('phases', 'new-commit', phases.draft)
2852 2852 try:
2853 2853 if opts.get('secret'):
2854 2854 commitphase = 'secret'
2855 2855 else:
2856 2856 commitphase = old.phase()
2857 2857 repo.ui.setconfig('phases', 'new-commit', commitphase, 'amend')
2858 2858 newid = repo.commitctx(new)
2859 2859 finally:
2860 2860 repo.ui.setconfig('phases', 'new-commit', ph, 'amend')
2861 2861 if newid != old.node():
2862 2862 # Reroute the working copy parent to the new changeset
2863 2863 repo.setparents(newid, nullid)
2864 2864
2865 2865 # Move bookmarks from old parent to amend commit
2866 2866 bms = repo.nodebookmarks(old.node())
2867 2867 if bms:
2868 2868 marks = repo._bookmarks
2869 2869 for bm in bms:
2870 2870 ui.debug('moving bookmarks %r from %s to %s\n' %
2871 2871 (marks, old.hex(), hex(newid)))
2872 2872 marks[bm] = newid
2873 2873 marks.recordchange(tr)
2874 2874 #commit the whole amend process
2875 2875 if createmarkers:
2876 2876 # mark the new changeset as successor of the rewritten one
2877 2877 new = repo[newid]
2878 2878 obs = [(old, (new,))]
2879 2879 if node:
2880 2880 obs.append((ctx, ()))
2881 2881
2882 2882 obsolete.createmarkers(repo, obs, operation='amend')
2883 2883 if not createmarkers and newid != old.node():
2884 2884 # Strip the intermediate commit (if there was one) and the amended
2885 2885 # commit
2886 2886 if node:
2887 2887 ui.note(_('stripping intermediate changeset %s\n') % ctx)
2888 2888 ui.note(_('stripping amended changeset %s\n') % old)
2889 2889 repair.strip(ui, repo, old.node(), topic='amend-backup')
2890 2890 finally:
2891 2891 lockmod.release(lock, wlock)
2892 2892 return newid
2893 2893
2894 2894 def commiteditor(repo, ctx, subs, editform=''):
2895 2895 if ctx.description():
2896 2896 return ctx.description()
2897 2897 return commitforceeditor(repo, ctx, subs, editform=editform,
2898 2898 unchangedmessagedetection=True)
2899 2899
2900 2900 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2901 2901 editform='', unchangedmessagedetection=False):
2902 2902 if not extramsg:
2903 2903 extramsg = _("Leave message empty to abort commit.")
2904 2904
2905 2905 forms = [e for e in editform.split('.') if e]
2906 2906 forms.insert(0, 'changeset')
2907 2907 templatetext = None
2908 2908 while forms:
2909 2909 tmpl = repo.ui.config('committemplate', '.'.join(forms))
2910 2910 if tmpl:
2911 2911 tmpl = templater.unquotestring(tmpl)
2912 2912 templatetext = committext = buildcommittemplate(
2913 2913 repo, ctx, subs, extramsg, tmpl)
2914 2914 break
2915 2915 forms.pop()
2916 2916 else:
2917 2917 committext = buildcommittext(repo, ctx, subs, extramsg)
2918 2918
2919 2919 # run editor in the repository root
2920 2920 olddir = pycompat.getcwd()
2921 2921 os.chdir(repo.root)
2922 2922
2923 2923 # make in-memory changes visible to external process
2924 2924 tr = repo.currenttransaction()
2925 2925 repo.dirstate.write(tr)
2926 2926 pending = tr and tr.writepending() and repo.root
2927 2927
2928 2928 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
2929 2929 editform=editform, pending=pending,
2930 2930 repopath=repo.path)
2931 2931 text = editortext
2932 2932
2933 2933 # strip away anything below this special string (used for editors that want
2934 2934 # to display the diff)
2935 2935 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
2936 2936 if stripbelow:
2937 2937 text = text[:stripbelow.start()]
2938 2938
2939 2939 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2940 2940 os.chdir(olddir)
2941 2941
2942 2942 if finishdesc:
2943 2943 text = finishdesc(text)
2944 2944 if not text.strip():
2945 2945 raise error.Abort(_("empty commit message"))
2946 2946 if unchangedmessagedetection and editortext == templatetext:
2947 2947 raise error.Abort(_("commit message unchanged"))
2948 2948
2949 2949 return text
2950 2950
2951 2951 def buildcommittemplate(repo, ctx, subs, extramsg, tmpl):
2952 2952 ui = repo.ui
2953 2953 tmpl, mapfile = gettemplate(ui, tmpl, None)
2954 2954
2955 2955 t = changeset_templater(ui, repo, None, {}, tmpl, mapfile, False)
2956 2956
2957 2957 for k, v in repo.ui.configitems('committemplate'):
2958 2958 if k != 'changeset':
2959 2959 t.t.cache[k] = v
2960 2960
2961 2961 if not extramsg:
2962 2962 extramsg = '' # ensure that extramsg is string
2963 2963
2964 2964 ui.pushbuffer()
2965 2965 t.show(ctx, extramsg=extramsg)
2966 2966 return ui.popbuffer()
2967 2967
2968 2968 def hgprefix(msg):
2969 2969 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
2970 2970
2971 2971 def buildcommittext(repo, ctx, subs, extramsg):
2972 2972 edittext = []
2973 2973 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2974 2974 if ctx.description():
2975 2975 edittext.append(ctx.description())
2976 2976 edittext.append("")
2977 2977 edittext.append("") # Empty line between message and comments.
2978 2978 edittext.append(hgprefix(_("Enter commit message."
2979 2979 " Lines beginning with 'HG:' are removed.")))
2980 2980 edittext.append(hgprefix(extramsg))
2981 2981 edittext.append("HG: --")
2982 2982 edittext.append(hgprefix(_("user: %s") % ctx.user()))
2983 2983 if ctx.p2():
2984 2984 edittext.append(hgprefix(_("branch merge")))
2985 2985 if ctx.branch():
2986 2986 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
2987 2987 if bookmarks.isactivewdirparent(repo):
2988 2988 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
2989 2989 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
2990 2990 edittext.extend([hgprefix(_("added %s") % f) for f in added])
2991 2991 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
2992 2992 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
2993 2993 if not added and not modified and not removed:
2994 2994 edittext.append(hgprefix(_("no files changed")))
2995 2995 edittext.append("")
2996 2996
2997 2997 return "\n".join(edittext)
2998 2998
2999 2999 def commitstatus(repo, node, branch, bheads=None, opts=None):
3000 3000 if opts is None:
3001 3001 opts = {}
3002 3002 ctx = repo[node]
3003 3003 parents = ctx.parents()
3004 3004
3005 3005 if (not opts.get('amend') and bheads and node not in bheads and not
3006 3006 [x for x in parents if x.node() in bheads and x.branch() == branch]):
3007 3007 repo.ui.status(_('created new head\n'))
3008 3008 # The message is not printed for initial roots. For the other
3009 3009 # changesets, it is printed in the following situations:
3010 3010 #
3011 3011 # Par column: for the 2 parents with ...
3012 3012 # N: null or no parent
3013 3013 # B: parent is on another named branch
3014 3014 # C: parent is a regular non head changeset
3015 3015 # H: parent was a branch head of the current branch
3016 3016 # Msg column: whether we print "created new head" message
3017 3017 # In the following, it is assumed that there already exists some
3018 3018 # initial branch heads of the current branch, otherwise nothing is
3019 3019 # printed anyway.
3020 3020 #
3021 3021 # Par Msg Comment
3022 3022 # N N y additional topo root
3023 3023 #
3024 3024 # B N y additional branch root
3025 3025 # C N y additional topo head
3026 3026 # H N n usual case
3027 3027 #
3028 3028 # B B y weird additional branch root
3029 3029 # C B y branch merge
3030 3030 # H B n merge with named branch
3031 3031 #
3032 3032 # C C y additional head from merge
3033 3033 # C H n merge with a head
3034 3034 #
3035 3035 # H H n head merge: head count decreases
3036 3036
3037 3037 if not opts.get('close_branch'):
3038 3038 for r in parents:
3039 3039 if r.closesbranch() and r.branch() == branch:
3040 3040 repo.ui.status(_('reopening closed branch head %d\n') % r)
3041 3041
3042 3042 if repo.ui.debugflag:
3043 3043 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
3044 3044 elif repo.ui.verbose:
3045 3045 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
3046 3046
3047 3047 def postcommitstatus(repo, pats, opts):
3048 3048 return repo.status(match=scmutil.match(repo[None], pats, opts))
3049 3049
3050 3050 def revert(ui, repo, ctx, parents, *pats, **opts):
3051 3051 parent, p2 = parents
3052 3052 node = ctx.node()
3053 3053
3054 3054 mf = ctx.manifest()
3055 3055 if node == p2:
3056 3056 parent = p2
3057 3057
3058 3058 # need all matching names in dirstate and manifest of target rev,
3059 3059 # so have to walk both. do not print errors if files exist in one
3060 3060 # but not other. in both cases, filesets should be evaluated against
3061 3061 # workingctx to get consistent result (issue4497). this means 'set:**'
3062 3062 # cannot be used to select missing files from target rev.
3063 3063
3064 3064 # `names` is a mapping for all elements in working copy and target revision
3065 3065 # The mapping is in the form:
3066 3066 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3067 3067 names = {}
3068 3068
3069 3069 with repo.wlock():
3070 3070 ## filling of the `names` mapping
3071 3071 # walk dirstate to fill `names`
3072 3072
3073 3073 interactive = opts.get('interactive', False)
3074 3074 wctx = repo[None]
3075 3075 m = scmutil.match(wctx, pats, opts)
3076 3076
3077 3077 # we'll need this later
3078 3078 targetsubs = sorted(s for s in wctx.substate if m(s))
3079 3079
3080 3080 if not m.always():
3081 3081 matcher = matchmod.badmatch(m, lambda x, y: False)
3082 3082 for abs in wctx.walk(matcher):
3083 3083 names[abs] = m.rel(abs), m.exact(abs)
3084 3084
3085 3085 # walk target manifest to fill `names`
3086 3086
3087 3087 def badfn(path, msg):
3088 3088 if path in names:
3089 3089 return
3090 3090 if path in ctx.substate:
3091 3091 return
3092 3092 path_ = path + '/'
3093 3093 for f in names:
3094 3094 if f.startswith(path_):
3095 3095 return
3096 3096 ui.warn("%s: %s\n" % (m.rel(path), msg))
3097 3097
3098 3098 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3099 3099 if abs not in names:
3100 3100 names[abs] = m.rel(abs), m.exact(abs)
3101 3101
3102 3102 # Find status of all file in `names`.
3103 3103 m = scmutil.matchfiles(repo, names)
3104 3104
3105 3105 changes = repo.status(node1=node, match=m,
3106 3106 unknown=True, ignored=True, clean=True)
3107 3107 else:
3108 3108 changes = repo.status(node1=node, match=m)
3109 3109 for kind in changes:
3110 3110 for abs in kind:
3111 3111 names[abs] = m.rel(abs), m.exact(abs)
3112 3112
3113 3113 m = scmutil.matchfiles(repo, names)
3114 3114
3115 3115 modified = set(changes.modified)
3116 3116 added = set(changes.added)
3117 3117 removed = set(changes.removed)
3118 3118 _deleted = set(changes.deleted)
3119 3119 unknown = set(changes.unknown)
3120 3120 unknown.update(changes.ignored)
3121 3121 clean = set(changes.clean)
3122 3122 modadded = set()
3123 3123
3124 3124 # We need to account for the state of the file in the dirstate,
3125 3125 # even when we revert against something else than parent. This will
3126 3126 # slightly alter the behavior of revert (doing back up or not, delete
3127 3127 # or just forget etc).
3128 3128 if parent == node:
3129 3129 dsmodified = modified
3130 3130 dsadded = added
3131 3131 dsremoved = removed
3132 3132 # store all local modifications, useful later for rename detection
3133 3133 localchanges = dsmodified | dsadded
3134 3134 modified, added, removed = set(), set(), set()
3135 3135 else:
3136 3136 changes = repo.status(node1=parent, match=m)
3137 3137 dsmodified = set(changes.modified)
3138 3138 dsadded = set(changes.added)
3139 3139 dsremoved = set(changes.removed)
3140 3140 # store all local modifications, useful later for rename detection
3141 3141 localchanges = dsmodified | dsadded
3142 3142
3143 3143 # only take into account for removes between wc and target
3144 3144 clean |= dsremoved - removed
3145 3145 dsremoved &= removed
3146 3146 # distinct between dirstate remove and other
3147 3147 removed -= dsremoved
3148 3148
3149 3149 modadded = added & dsmodified
3150 3150 added -= modadded
3151 3151
3152 3152 # tell newly modified apart.
3153 3153 dsmodified &= modified
3154 3154 dsmodified |= modified & dsadded # dirstate added may need backup
3155 3155 modified -= dsmodified
3156 3156
3157 3157 # We need to wait for some post-processing to update this set
3158 3158 # before making the distinction. The dirstate will be used for
3159 3159 # that purpose.
3160 3160 dsadded = added
3161 3161
3162 3162 # in case of merge, files that are actually added can be reported as
3163 3163 # modified, we need to post process the result
3164 3164 if p2 != nullid:
3165 3165 mergeadd = set(dsmodified)
3166 3166 for path in dsmodified:
3167 3167 if path in mf:
3168 3168 mergeadd.remove(path)
3169 3169 dsadded |= mergeadd
3170 3170 dsmodified -= mergeadd
3171 3171
3172 3172 # if f is a rename, update `names` to also revert the source
3173 3173 cwd = repo.getcwd()
3174 3174 for f in localchanges:
3175 3175 src = repo.dirstate.copied(f)
3176 3176 # XXX should we check for rename down to target node?
3177 3177 if src and src not in names and repo.dirstate[src] == 'r':
3178 3178 dsremoved.add(src)
3179 3179 names[src] = (repo.pathto(src, cwd), True)
3180 3180
3181 3181 # determine the exact nature of the deleted changesets
3182 3182 deladded = set(_deleted)
3183 3183 for path in _deleted:
3184 3184 if path in mf:
3185 3185 deladded.remove(path)
3186 3186 deleted = _deleted - deladded
3187 3187
3188 3188 # distinguish between file to forget and the other
3189 3189 added = set()
3190 3190 for abs in dsadded:
3191 3191 if repo.dirstate[abs] != 'a':
3192 3192 added.add(abs)
3193 3193 dsadded -= added
3194 3194
3195 3195 for abs in deladded:
3196 3196 if repo.dirstate[abs] == 'a':
3197 3197 dsadded.add(abs)
3198 3198 deladded -= dsadded
3199 3199
3200 3200 # For files marked as removed, we check if an unknown file is present at
3201 3201 # the same path. If a such file exists it may need to be backed up.
3202 3202 # Making the distinction at this stage helps have simpler backup
3203 3203 # logic.
3204 3204 removunk = set()
3205 3205 for abs in removed:
3206 3206 target = repo.wjoin(abs)
3207 3207 if os.path.lexists(target):
3208 3208 removunk.add(abs)
3209 3209 removed -= removunk
3210 3210
3211 3211 dsremovunk = set()
3212 3212 for abs in dsremoved:
3213 3213 target = repo.wjoin(abs)
3214 3214 if os.path.lexists(target):
3215 3215 dsremovunk.add(abs)
3216 3216 dsremoved -= dsremovunk
3217 3217
3218 3218 # action to be actually performed by revert
3219 3219 # (<list of file>, message>) tuple
3220 3220 actions = {'revert': ([], _('reverting %s\n')),
3221 3221 'add': ([], _('adding %s\n')),
3222 3222 'remove': ([], _('removing %s\n')),
3223 3223 'drop': ([], _('removing %s\n')),
3224 3224 'forget': ([], _('forgetting %s\n')),
3225 3225 'undelete': ([], _('undeleting %s\n')),
3226 3226 'noop': (None, _('no changes needed to %s\n')),
3227 3227 'unknown': (None, _('file not managed: %s\n')),
3228 3228 }
3229 3229
3230 3230 # "constant" that convey the backup strategy.
3231 3231 # All set to `discard` if `no-backup` is set do avoid checking
3232 3232 # no_backup lower in the code.
3233 3233 # These values are ordered for comparison purposes
3234 3234 backupinteractive = 3 # do backup if interactively modified
3235 3235 backup = 2 # unconditionally do backup
3236 3236 check = 1 # check if the existing file differs from target
3237 3237 discard = 0 # never do backup
3238 3238 if opts.get('no_backup'):
3239 3239 backupinteractive = backup = check = discard
3240 3240 if interactive:
3241 3241 dsmodifiedbackup = backupinteractive
3242 3242 else:
3243 3243 dsmodifiedbackup = backup
3244 3244 tobackup = set()
3245 3245
3246 3246 backupanddel = actions['remove']
3247 3247 if not opts.get('no_backup'):
3248 3248 backupanddel = actions['drop']
3249 3249
3250 3250 disptable = (
3251 3251 # dispatch table:
3252 3252 # file state
3253 3253 # action
3254 3254 # make backup
3255 3255
3256 3256 ## Sets that results that will change file on disk
3257 3257 # Modified compared to target, no local change
3258 3258 (modified, actions['revert'], discard),
3259 3259 # Modified compared to target, but local file is deleted
3260 3260 (deleted, actions['revert'], discard),
3261 3261 # Modified compared to target, local change
3262 3262 (dsmodified, actions['revert'], dsmodifiedbackup),
3263 3263 # Added since target
3264 3264 (added, actions['remove'], discard),
3265 3265 # Added in working directory
3266 3266 (dsadded, actions['forget'], discard),
3267 3267 # Added since target, have local modification
3268 3268 (modadded, backupanddel, backup),
3269 3269 # Added since target but file is missing in working directory
3270 3270 (deladded, actions['drop'], discard),
3271 3271 # Removed since target, before working copy parent
3272 3272 (removed, actions['add'], discard),
3273 3273 # Same as `removed` but an unknown file exists at the same path
3274 3274 (removunk, actions['add'], check),
3275 3275 # Removed since targe, marked as such in working copy parent
3276 3276 (dsremoved, actions['undelete'], discard),
3277 3277 # Same as `dsremoved` but an unknown file exists at the same path
3278 3278 (dsremovunk, actions['undelete'], check),
3279 3279 ## the following sets does not result in any file changes
3280 3280 # File with no modification
3281 3281 (clean, actions['noop'], discard),
3282 3282 # Existing file, not tracked anywhere
3283 3283 (unknown, actions['unknown'], discard),
3284 3284 )
3285 3285
3286 3286 for abs, (rel, exact) in sorted(names.items()):
3287 3287 # target file to be touch on disk (relative to cwd)
3288 3288 target = repo.wjoin(abs)
3289 3289 # search the entry in the dispatch table.
3290 3290 # if the file is in any of these sets, it was touched in the working
3291 3291 # directory parent and we are sure it needs to be reverted.
3292 3292 for table, (xlist, msg), dobackup in disptable:
3293 3293 if abs not in table:
3294 3294 continue
3295 3295 if xlist is not None:
3296 3296 xlist.append(abs)
3297 3297 if dobackup:
3298 3298 # If in interactive mode, don't automatically create
3299 3299 # .orig files (issue4793)
3300 3300 if dobackup == backupinteractive:
3301 3301 tobackup.add(abs)
3302 3302 elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
3303 3303 bakname = scmutil.origpath(ui, repo, rel)
3304 3304 ui.note(_('saving current version of %s as %s\n') %
3305 3305 (rel, bakname))
3306 3306 if not opts.get('dry_run'):
3307 3307 if interactive:
3308 3308 util.copyfile(target, bakname)
3309 3309 else:
3310 3310 util.rename(target, bakname)
3311 3311 if ui.verbose or not exact:
3312 3312 if not isinstance(msg, basestring):
3313 3313 msg = msg(abs)
3314 3314 ui.status(msg % rel)
3315 3315 elif exact:
3316 3316 ui.warn(msg % rel)
3317 3317 break
3318 3318
3319 3319 if not opts.get('dry_run'):
3320 3320 needdata = ('revert', 'add', 'undelete')
3321 3321 _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
3322 3322 _performrevert(repo, parents, ctx, actions, interactive, tobackup)
3323 3323
3324 3324 if targetsubs:
3325 3325 # Revert the subrepos on the revert list
3326 3326 for sub in targetsubs:
3327 3327 try:
3328 3328 wctx.sub(sub).revert(ctx.substate[sub], *pats, **opts)
3329 3329 except KeyError:
3330 3330 raise error.Abort("subrepository '%s' does not exist in %s!"
3331 3331 % (sub, short(ctx.node())))
3332 3332
3333 3333 def _revertprefetch(repo, ctx, *files):
3334 3334 """Let extension changing the storage layer prefetch content"""
3335 3335 pass
3336 3336
3337 3337 def _performrevert(repo, parents, ctx, actions, interactive=False,
3338 3338 tobackup=None):
3339 3339 """function that actually perform all the actions computed for revert
3340 3340
3341 3341 This is an independent function to let extension to plug in and react to
3342 3342 the imminent revert.
3343 3343
3344 3344 Make sure you have the working directory locked when calling this function.
3345 3345 """
3346 3346 parent, p2 = parents
3347 3347 node = ctx.node()
3348 3348 excluded_files = []
3349 3349 matcher_opts = {"exclude": excluded_files}
3350 3350
3351 3351 def checkout(f):
3352 3352 fc = ctx[f]
3353 3353 repo.wwrite(f, fc.data(), fc.flags())
3354 3354
3355 3355 def doremove(f):
3356 3356 try:
3357 3357 repo.wvfs.unlinkpath(f)
3358 3358 except OSError:
3359 3359 pass
3360 3360 repo.dirstate.remove(f)
3361 3361
3362 3362 audit_path = pathutil.pathauditor(repo.root)
3363 3363 for f in actions['forget'][0]:
3364 3364 if interactive:
3365 3365 choice = repo.ui.promptchoice(
3366 3366 _("forget added file %s (Yn)?$$ &Yes $$ &No") % f)
3367 3367 if choice == 0:
3368 3368 repo.dirstate.drop(f)
3369 3369 else:
3370 3370 excluded_files.append(repo.wjoin(f))
3371 3371 else:
3372 3372 repo.dirstate.drop(f)
3373 3373 for f in actions['remove'][0]:
3374 3374 audit_path(f)
3375 3375 if interactive:
3376 3376 choice = repo.ui.promptchoice(
3377 3377 _("remove added file %s (Yn)?$$ &Yes $$ &No") % f)
3378 3378 if choice == 0:
3379 3379 doremove(f)
3380 3380 else:
3381 3381 excluded_files.append(repo.wjoin(f))
3382 3382 else:
3383 3383 doremove(f)
3384 3384 for f in actions['drop'][0]:
3385 3385 audit_path(f)
3386 3386 repo.dirstate.remove(f)
3387 3387
3388 3388 normal = None
3389 3389 if node == parent:
3390 3390 # We're reverting to our parent. If possible, we'd like status
3391 3391 # to report the file as clean. We have to use normallookup for
3392 3392 # merges to avoid losing information about merged/dirty files.
3393 3393 if p2 != nullid:
3394 3394 normal = repo.dirstate.normallookup
3395 3395 else:
3396 3396 normal = repo.dirstate.normal
3397 3397
3398 3398 newlyaddedandmodifiedfiles = set()
3399 3399 if interactive:
3400 3400 # Prompt the user for changes to revert
3401 3401 torevert = [repo.wjoin(f) for f in actions['revert'][0]]
3402 3402 m = scmutil.match(ctx, torevert, matcher_opts)
3403 3403 diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
3404 3404 diffopts.nodates = True
3405 3405 diffopts.git = True
3406 3406 operation = 'discard'
3407 3407 reversehunks = True
3408 3408 if node != parent:
3409 3409 operation = 'revert'
3410 3410 reversehunks = repo.ui.configbool('experimental',
3411 3411 'revertalternateinteractivemode',
3412 3412 True)
3413 3413 if reversehunks:
3414 3414 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3415 3415 else:
3416 3416 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3417 3417 originalchunks = patch.parsepatch(diff)
3418 3418
3419 3419 try:
3420 3420
3421 3421 chunks, opts = recordfilter(repo.ui, originalchunks,
3422 3422 operation=operation)
3423 3423 if reversehunks:
3424 3424 chunks = patch.reversehunks(chunks)
3425 3425
3426 3426 except patch.PatchError as err:
3427 3427 raise error.Abort(_('error parsing patch: %s') % err)
3428 3428
3429 3429 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
3430 3430 if tobackup is None:
3431 3431 tobackup = set()
3432 3432 # Apply changes
3433 3433 fp = stringio()
3434 3434 for c in chunks:
3435 3435 # Create a backup file only if this hunk should be backed up
3436 3436 if ishunk(c) and c.header.filename() in tobackup:
3437 3437 abs = c.header.filename()
3438 3438 target = repo.wjoin(abs)
3439 3439 bakname = scmutil.origpath(repo.ui, repo, m.rel(abs))
3440 3440 util.copyfile(target, bakname)
3441 3441 tobackup.remove(abs)
3442 3442 c.write(fp)
3443 3443 dopatch = fp.tell()
3444 3444 fp.seek(0)
3445 3445 if dopatch:
3446 3446 try:
3447 3447 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3448 3448 except patch.PatchError as err:
3449 3449 raise error.Abort(str(err))
3450 3450 del fp
3451 3451 else:
3452 3452 for f in actions['revert'][0]:
3453 3453 checkout(f)
3454 3454 if normal:
3455 3455 normal(f)
3456 3456
3457 3457 for f in actions['add'][0]:
3458 3458 # Don't checkout modified files, they are already created by the diff
3459 3459 if f not in newlyaddedandmodifiedfiles:
3460 3460 checkout(f)
3461 3461 repo.dirstate.add(f)
3462 3462
3463 3463 normal = repo.dirstate.normallookup
3464 3464 if node == parent and p2 == nullid:
3465 3465 normal = repo.dirstate.normal
3466 3466 for f in actions['undelete'][0]:
3467 3467 checkout(f)
3468 3468 normal(f)
3469 3469
3470 3470 copied = copies.pathcopies(repo[parent], ctx)
3471 3471
3472 3472 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3473 3473 if f in copied:
3474 3474 repo.dirstate.copy(copied[f], f)
3475 3475
3476 3476 class command(registrar.command):
3477 3477 def _doregister(self, func, name, *args, **kwargs):
3478 3478 func._deprecatedregistrar = True # flag for deprecwarn in extensions.py
3479 3479 return super(command, self)._doregister(func, name, *args, **kwargs)
3480 3480
3481 3481 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3482 3482 # commands.outgoing. "missing" is "missing" of the result of
3483 3483 # "findcommonoutgoing()"
3484 3484 outgoinghooks = util.hooks()
3485 3485
3486 3486 # a list of (ui, repo) functions called by commands.summary
3487 3487 summaryhooks = util.hooks()
3488 3488
3489 3489 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3490 3490 #
3491 3491 # functions should return tuple of booleans below, if 'changes' is None:
3492 3492 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3493 3493 #
3494 3494 # otherwise, 'changes' is a tuple of tuples below:
3495 3495 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3496 3496 # - (desturl, destbranch, destpeer, outgoing)
3497 3497 summaryremotehooks = util.hooks()
3498 3498
3499 3499 # A list of state files kept by multistep operations like graft.
3500 3500 # Since graft cannot be aborted, it is considered 'clearable' by update.
3501 3501 # note: bisect is intentionally excluded
3502 3502 # (state file, clearable, allowcommit, error, hint)
3503 3503 unfinishedstates = [
3504 3504 ('graftstate', True, False, _('graft in progress'),
3505 3505 _("use 'hg graft --continue' or 'hg update' to abort")),
3506 3506 ('updatestate', True, False, _('last update was interrupted'),
3507 3507 _("use 'hg update' to get a consistent checkout"))
3508 3508 ]
3509 3509
3510 3510 def checkunfinished(repo, commit=False):
3511 3511 '''Look for an unfinished multistep operation, like graft, and abort
3512 3512 if found. It's probably good to check this right before
3513 3513 bailifchanged().
3514 3514 '''
3515 3515 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3516 3516 if commit and allowcommit:
3517 3517 continue
3518 3518 if repo.vfs.exists(f):
3519 3519 raise error.Abort(msg, hint=hint)
3520 3520
3521 3521 def clearunfinished(repo):
3522 3522 '''Check for unfinished operations (as above), and clear the ones
3523 3523 that are clearable.
3524 3524 '''
3525 3525 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3526 3526 if not clearable and repo.vfs.exists(f):
3527 3527 raise error.Abort(msg, hint=hint)
3528 3528 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3529 3529 if clearable and repo.vfs.exists(f):
3530 3530 util.unlink(repo.vfs.join(f))
3531 3531
3532 3532 afterresolvedstates = [
3533 3533 ('graftstate',
3534 3534 _('hg graft --continue')),
3535 3535 ]
3536 3536
3537 3537 def howtocontinue(repo):
3538 3538 '''Check for an unfinished operation and return the command to finish
3539 3539 it.
3540 3540
3541 3541 afterresolvedstates tuples define a .hg/{file} and the corresponding
3542 3542 command needed to finish it.
3543 3543
3544 3544 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3545 3545 a boolean.
3546 3546 '''
3547 3547 contmsg = _("continue: %s")
3548 3548 for f, msg in afterresolvedstates:
3549 3549 if repo.vfs.exists(f):
3550 3550 return contmsg % msg, True
3551 3551 workingctx = repo[None]
3552 3552 dirty = any(repo.status()) or any(workingctx.sub(s).dirty()
3553 3553 for s in workingctx.substate)
3554 3554 if dirty:
3555 3555 return contmsg % _("hg commit"), False
3556 3556 return None, None
3557 3557
3558 3558 def checkafterresolved(repo):
3559 3559 '''Inform the user about the next action after completing hg resolve
3560 3560
3561 3561 If there's a matching afterresolvedstates, howtocontinue will yield
3562 3562 repo.ui.warn as the reporter.
3563 3563
3564 3564 Otherwise, it will yield repo.ui.note.
3565 3565 '''
3566 3566 msg, warning = howtocontinue(repo)
3567 3567 if msg is not None:
3568 3568 if warning:
3569 3569 repo.ui.warn("%s\n" % msg)
3570 3570 else:
3571 3571 repo.ui.note("%s\n" % msg)
3572 3572
3573 3573 def wrongtooltocontinue(repo, task):
3574 3574 '''Raise an abort suggesting how to properly continue if there is an
3575 3575 active task.
3576 3576
3577 3577 Uses howtocontinue() to find the active task.
3578 3578
3579 3579 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3580 3580 a hint.
3581 3581 '''
3582 3582 after = howtocontinue(repo)
3583 3583 hint = None
3584 3584 if after[1]:
3585 3585 hint = after[0]
3586 3586 raise error.Abort(_('no %s in progress') % task, hint=hint)
@@ -1,5495 +1,5500 b''
1 1 # commands.py - command processing for 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 difflib
11 11 import errno
12 12 import os
13 13 import re
14 14 import sys
15 15
16 16 from .i18n import _
17 17 from .node import (
18 18 hex,
19 19 nullid,
20 20 nullrev,
21 21 short,
22 22 )
23 23 from . import (
24 24 archival,
25 25 bookmarks,
26 26 bundle2,
27 27 changegroup,
28 28 cmdutil,
29 29 copies,
30 30 debugcommands as debugcommandsmod,
31 31 destutil,
32 32 dirstateguard,
33 33 discovery,
34 34 encoding,
35 35 error,
36 36 exchange,
37 37 extensions,
38 formatter,
38 39 graphmod,
39 40 hbisect,
40 41 help,
41 42 hg,
42 43 lock as lockmod,
43 44 merge as mergemod,
44 45 obsolete,
45 46 patch,
46 47 phases,
47 48 pycompat,
48 49 rcutil,
49 50 registrar,
50 51 revsetlang,
51 52 scmutil,
52 53 server,
53 54 sshserver,
54 55 streamclone,
55 56 tags as tagsmod,
56 57 templatekw,
57 58 ui as uimod,
58 59 util,
59 60 )
60 61
61 62 release = lockmod.release
62 63
63 64 table = {}
64 65 table.update(debugcommandsmod.command._table)
65 66
66 67 command = registrar.command(table)
67 68
68 69 # label constants
69 70 # until 3.5, bookmarks.current was the advertised name, not
70 71 # bookmarks.active, so we must use both to avoid breaking old
71 72 # custom styles
72 73 activebookmarklabel = 'bookmarks.active bookmarks.current'
73 74
74 75 # common command options
75 76
76 77 globalopts = [
77 78 ('R', 'repository', '',
78 79 _('repository root directory or name of overlay bundle file'),
79 80 _('REPO')),
80 81 ('', 'cwd', '',
81 82 _('change working directory'), _('DIR')),
82 83 ('y', 'noninteractive', None,
83 84 _('do not prompt, automatically pick the first choice for all prompts')),
84 85 ('q', 'quiet', None, _('suppress output')),
85 86 ('v', 'verbose', None, _('enable additional output')),
86 87 ('', 'color', '',
87 88 # i18n: 'always', 'auto', 'never', and 'debug' are keywords
88 89 # and should not be translated
89 90 _("when to colorize (boolean, always, auto, never, or debug)"),
90 91 _('TYPE')),
91 92 ('', 'config', [],
92 93 _('set/override config option (use \'section.name=value\')'),
93 94 _('CONFIG')),
94 95 ('', 'debug', None, _('enable debugging output')),
95 96 ('', 'debugger', None, _('start debugger')),
96 97 ('', 'encoding', encoding.encoding, _('set the charset encoding'),
97 98 _('ENCODE')),
98 99 ('', 'encodingmode', encoding.encodingmode,
99 100 _('set the charset encoding mode'), _('MODE')),
100 101 ('', 'traceback', None, _('always print a traceback on exception')),
101 102 ('', 'time', None, _('time how long the command takes')),
102 103 ('', 'profile', None, _('print command execution profile')),
103 104 ('', 'version', None, _('output version information and exit')),
104 105 ('h', 'help', None, _('display help and exit')),
105 106 ('', 'hidden', False, _('consider hidden changesets')),
106 107 ('', 'pager', 'auto',
107 108 _("when to paginate (boolean, always, auto, or never)"), _('TYPE')),
108 109 ]
109 110
110 111 dryrunopts = cmdutil.dryrunopts
111 112 remoteopts = cmdutil.remoteopts
112 113 walkopts = cmdutil.walkopts
113 114 commitopts = cmdutil.commitopts
114 115 commitopts2 = cmdutil.commitopts2
115 116 formatteropts = cmdutil.formatteropts
116 117 templateopts = cmdutil.templateopts
117 118 logopts = cmdutil.logopts
118 119 diffopts = cmdutil.diffopts
119 120 diffwsopts = cmdutil.diffwsopts
120 121 diffopts2 = cmdutil.diffopts2
121 122 mergetoolopts = cmdutil.mergetoolopts
122 123 similarityopts = cmdutil.similarityopts
123 124 subrepoopts = cmdutil.subrepoopts
124 125 debugrevlogopts = cmdutil.debugrevlogopts
125 126
126 127 # Commands start here, listed alphabetically
127 128
128 129 @command('^add',
129 130 walkopts + subrepoopts + dryrunopts,
130 131 _('[OPTION]... [FILE]...'),
131 132 inferrepo=True)
132 133 def add(ui, repo, *pats, **opts):
133 134 """add the specified files on the next commit
134 135
135 136 Schedule files to be version controlled and added to the
136 137 repository.
137 138
138 139 The files will be added to the repository at the next commit. To
139 140 undo an add before that, see :hg:`forget`.
140 141
141 142 If no names are given, add all files to the repository (except
142 143 files matching ``.hgignore``).
143 144
144 145 .. container:: verbose
145 146
146 147 Examples:
147 148
148 149 - New (unknown) files are added
149 150 automatically by :hg:`add`::
150 151
151 152 $ ls
152 153 foo.c
153 154 $ hg status
154 155 ? foo.c
155 156 $ hg add
156 157 adding foo.c
157 158 $ hg status
158 159 A foo.c
159 160
160 161 - Specific files to be added can be specified::
161 162
162 163 $ ls
163 164 bar.c foo.c
164 165 $ hg status
165 166 ? bar.c
166 167 ? foo.c
167 168 $ hg add bar.c
168 169 $ hg status
169 170 A bar.c
170 171 ? foo.c
171 172
172 173 Returns 0 if all files are successfully added.
173 174 """
174 175
175 176 m = scmutil.match(repo[None], pats, pycompat.byteskwargs(opts))
176 177 rejected = cmdutil.add(ui, repo, m, "", False, **opts)
177 178 return rejected and 1 or 0
178 179
179 180 @command('addremove',
180 181 similarityopts + subrepoopts + walkopts + dryrunopts,
181 182 _('[OPTION]... [FILE]...'),
182 183 inferrepo=True)
183 184 def addremove(ui, repo, *pats, **opts):
184 185 """add all new files, delete all missing files
185 186
186 187 Add all new files and remove all missing files from the
187 188 repository.
188 189
189 190 Unless names are given, new files are ignored if they match any of
190 191 the patterns in ``.hgignore``. As with add, these changes take
191 192 effect at the next commit.
192 193
193 194 Use the -s/--similarity option to detect renamed files. This
194 195 option takes a percentage between 0 (disabled) and 100 (files must
195 196 be identical) as its parameter. With a parameter greater than 0,
196 197 this compares every removed file with every added file and records
197 198 those similar enough as renames. Detecting renamed files this way
198 199 can be expensive. After using this option, :hg:`status -C` can be
199 200 used to check which files were identified as moved or renamed. If
200 201 not specified, -s/--similarity defaults to 100 and only renames of
201 202 identical files are detected.
202 203
203 204 .. container:: verbose
204 205
205 206 Examples:
206 207
207 208 - A number of files (bar.c and foo.c) are new,
208 209 while foobar.c has been removed (without using :hg:`remove`)
209 210 from the repository::
210 211
211 212 $ ls
212 213 bar.c foo.c
213 214 $ hg status
214 215 ! foobar.c
215 216 ? bar.c
216 217 ? foo.c
217 218 $ hg addremove
218 219 adding bar.c
219 220 adding foo.c
220 221 removing foobar.c
221 222 $ hg status
222 223 A bar.c
223 224 A foo.c
224 225 R foobar.c
225 226
226 227 - A file foobar.c was moved to foo.c without using :hg:`rename`.
227 228 Afterwards, it was edited slightly::
228 229
229 230 $ ls
230 231 foo.c
231 232 $ hg status
232 233 ! foobar.c
233 234 ? foo.c
234 235 $ hg addremove --similarity 90
235 236 removing foobar.c
236 237 adding foo.c
237 238 recording removal of foobar.c as rename to foo.c (94% similar)
238 239 $ hg status -C
239 240 A foo.c
240 241 foobar.c
241 242 R foobar.c
242 243
243 244 Returns 0 if all files are successfully added.
244 245 """
245 246 opts = pycompat.byteskwargs(opts)
246 247 try:
247 248 sim = float(opts.get('similarity') or 100)
248 249 except ValueError:
249 250 raise error.Abort(_('similarity must be a number'))
250 251 if sim < 0 or sim > 100:
251 252 raise error.Abort(_('similarity must be between 0 and 100'))
252 253 matcher = scmutil.match(repo[None], pats, opts)
253 254 return scmutil.addremove(repo, matcher, "", opts, similarity=sim / 100.0)
254 255
255 256 @command('^annotate|blame',
256 257 [('r', 'rev', '', _('annotate the specified revision'), _('REV')),
257 258 ('', 'follow', None,
258 259 _('follow copies/renames and list the filename (DEPRECATED)')),
259 260 ('', 'no-follow', None, _("don't follow copies and renames")),
260 261 ('a', 'text', None, _('treat all files as text')),
261 262 ('u', 'user', None, _('list the author (long with -v)')),
262 263 ('f', 'file', None, _('list the filename')),
263 264 ('d', 'date', None, _('list the date (short with -q)')),
264 265 ('n', 'number', None, _('list the revision number (default)')),
265 266 ('c', 'changeset', None, _('list the changeset')),
266 267 ('l', 'line-number', None, _('show line number at the first appearance')),
267 268 ('', 'skip', [], _('revision to not display (EXPERIMENTAL)'), _('REV')),
268 269 ] + diffwsopts + walkopts + formatteropts,
269 270 _('[-r REV] [-f] [-a] [-u] [-d] [-n] [-c] [-l] FILE...'),
270 271 inferrepo=True)
271 272 def annotate(ui, repo, *pats, **opts):
272 273 """show changeset information by line for each file
273 274
274 275 List changes in files, showing the revision id responsible for
275 276 each line.
276 277
277 278 This command is useful for discovering when a change was made and
278 279 by whom.
279 280
280 281 If you include --file, --user, or --date, the revision number is
281 282 suppressed unless you also include --number.
282 283
283 284 Without the -a/--text option, annotate will avoid processing files
284 285 it detects as binary. With -a, annotate will annotate the file
285 286 anyway, although the results will probably be neither useful
286 287 nor desirable.
287 288
288 289 Returns 0 on success.
289 290 """
290 291 opts = pycompat.byteskwargs(opts)
291 292 if not pats:
292 293 raise error.Abort(_('at least one filename or pattern is required'))
293 294
294 295 if opts.get('follow'):
295 296 # --follow is deprecated and now just an alias for -f/--file
296 297 # to mimic the behavior of Mercurial before version 1.5
297 298 opts['file'] = True
298 299
299 300 ctx = scmutil.revsingle(repo, opts.get('rev'))
300 301
301 302 fm = ui.formatter('annotate', opts)
302 303 if ui.quiet:
303 304 datefunc = util.shortdate
304 305 else:
305 306 datefunc = util.datestr
306 307 if ctx.rev() is None:
307 308 def hexfn(node):
308 309 if node is None:
309 310 return None
310 311 else:
311 312 return fm.hexfunc(node)
312 313 if opts.get('changeset'):
313 314 # omit "+" suffix which is appended to node hex
314 315 def formatrev(rev):
315 316 if rev is None:
316 317 return '%d' % ctx.p1().rev()
317 318 else:
318 319 return '%d' % rev
319 320 else:
320 321 def formatrev(rev):
321 322 if rev is None:
322 323 return '%d+' % ctx.p1().rev()
323 324 else:
324 325 return '%d ' % rev
325 326 def formathex(hex):
326 327 if hex is None:
327 328 return '%s+' % fm.hexfunc(ctx.p1().node())
328 329 else:
329 330 return '%s ' % hex
330 331 else:
331 332 hexfn = fm.hexfunc
332 333 formatrev = formathex = str
333 334
334 335 opmap = [('user', ' ', lambda x: x[0].user(), ui.shortuser),
335 336 ('number', ' ', lambda x: x[0].rev(), formatrev),
336 337 ('changeset', ' ', lambda x: hexfn(x[0].node()), formathex),
337 338 ('date', ' ', lambda x: x[0].date(), util.cachefunc(datefunc)),
338 339 ('file', ' ', lambda x: x[0].path(), str),
339 340 ('line_number', ':', lambda x: x[1], str),
340 341 ]
341 342 fieldnamemap = {'number': 'rev', 'changeset': 'node'}
342 343
343 344 if (not opts.get('user') and not opts.get('changeset')
344 345 and not opts.get('date') and not opts.get('file')):
345 346 opts['number'] = True
346 347
347 348 linenumber = opts.get('line_number') is not None
348 349 if linenumber and (not opts.get('changeset')) and (not opts.get('number')):
349 350 raise error.Abort(_('at least one of -n/-c is required for -l'))
350 351
351 352 ui.pager('annotate')
352 353
353 354 if fm.isplain():
354 355 def makefunc(get, fmt):
355 356 return lambda x: fmt(get(x))
356 357 else:
357 358 def makefunc(get, fmt):
358 359 return get
359 360 funcmap = [(makefunc(get, fmt), sep) for op, sep, get, fmt in opmap
360 361 if opts.get(op)]
361 362 funcmap[0] = (funcmap[0][0], '') # no separator in front of first column
362 363 fields = ' '.join(fieldnamemap.get(op, op) for op, sep, get, fmt in opmap
363 364 if opts.get(op))
364 365
365 366 def bad(x, y):
366 367 raise error.Abort("%s: %s" % (x, y))
367 368
368 369 m = scmutil.match(ctx, pats, opts, badfn=bad)
369 370
370 371 follow = not opts.get('no_follow')
371 372 diffopts = patch.difffeatureopts(ui, opts, section='annotate',
372 373 whitespace=True)
373 374 skiprevs = opts.get('skip')
374 375 if skiprevs:
375 376 skiprevs = scmutil.revrange(repo, skiprevs)
376 377
377 378 for abs in ctx.walk(m):
378 379 fctx = ctx[abs]
379 380 if not opts.get('text') and fctx.isbinary():
380 381 fm.plain(_("%s: binary file\n") % ((pats and m.rel(abs)) or abs))
381 382 continue
382 383
383 384 lines = fctx.annotate(follow=follow, linenumber=linenumber,
384 385 skiprevs=skiprevs, diffopts=diffopts)
385 386 if not lines:
386 387 continue
387 388 formats = []
388 389 pieces = []
389 390
390 391 for f, sep in funcmap:
391 392 l = [f(n) for n, dummy in lines]
392 393 if fm.isplain():
393 394 sizes = [encoding.colwidth(x) for x in l]
394 395 ml = max(sizes)
395 396 formats.append([sep + ' ' * (ml - w) + '%s' for w in sizes])
396 397 else:
397 398 formats.append(['%s' for x in l])
398 399 pieces.append(l)
399 400
400 401 for f, p, l in zip(zip(*formats), zip(*pieces), lines):
401 402 fm.startitem()
402 403 fm.write(fields, "".join(f), *p)
403 404 fm.write('line', ": %s", l[1])
404 405
405 406 if not lines[-1][1].endswith('\n'):
406 407 fm.plain('\n')
407 408
408 409 fm.end()
409 410
410 411 @command('archive',
411 412 [('', 'no-decode', None, _('do not pass files through decoders')),
412 413 ('p', 'prefix', '', _('directory prefix for files in archive'),
413 414 _('PREFIX')),
414 415 ('r', 'rev', '', _('revision to distribute'), _('REV')),
415 416 ('t', 'type', '', _('type of distribution to create'), _('TYPE')),
416 417 ] + subrepoopts + walkopts,
417 418 _('[OPTION]... DEST'))
418 419 def archive(ui, repo, dest, **opts):
419 420 '''create an unversioned archive of a repository revision
420 421
421 422 By default, the revision used is the parent of the working
422 423 directory; use -r/--rev to specify a different revision.
423 424
424 425 The archive type is automatically detected based on file
425 426 extension (to override, use -t/--type).
426 427
427 428 .. container:: verbose
428 429
429 430 Examples:
430 431
431 432 - create a zip file containing the 1.0 release::
432 433
433 434 hg archive -r 1.0 project-1.0.zip
434 435
435 436 - create a tarball excluding .hg files::
436 437
437 438 hg archive project.tar.gz -X ".hg*"
438 439
439 440 Valid types are:
440 441
441 442 :``files``: a directory full of files (default)
442 443 :``tar``: tar archive, uncompressed
443 444 :``tbz2``: tar archive, compressed using bzip2
444 445 :``tgz``: tar archive, compressed using gzip
445 446 :``uzip``: zip archive, uncompressed
446 447 :``zip``: zip archive, compressed using deflate
447 448
448 449 The exact name of the destination archive or directory is given
449 450 using a format string; see :hg:`help export` for details.
450 451
451 452 Each member added to an archive file has a directory prefix
452 453 prepended. Use -p/--prefix to specify a format string for the
453 454 prefix. The default is the basename of the archive, with suffixes
454 455 removed.
455 456
456 457 Returns 0 on success.
457 458 '''
458 459
459 460 opts = pycompat.byteskwargs(opts)
460 461 ctx = scmutil.revsingle(repo, opts.get('rev'))
461 462 if not ctx:
462 463 raise error.Abort(_('no working directory: please specify a revision'))
463 464 node = ctx.node()
464 465 dest = cmdutil.makefilename(repo, dest, node)
465 466 if os.path.realpath(dest) == repo.root:
466 467 raise error.Abort(_('repository root cannot be destination'))
467 468
468 469 kind = opts.get('type') or archival.guesskind(dest) or 'files'
469 470 prefix = opts.get('prefix')
470 471
471 472 if dest == '-':
472 473 if kind == 'files':
473 474 raise error.Abort(_('cannot archive plain files to stdout'))
474 475 dest = cmdutil.makefileobj(repo, dest)
475 476 if not prefix:
476 477 prefix = os.path.basename(repo.root) + '-%h'
477 478
478 479 prefix = cmdutil.makefilename(repo, prefix, node)
479 480 matchfn = scmutil.match(ctx, [], opts)
480 481 archival.archive(repo, dest, node, kind, not opts.get('no_decode'),
481 482 matchfn, prefix, subrepos=opts.get('subrepos'))
482 483
483 484 @command('backout',
484 485 [('', 'merge', None, _('merge with old dirstate parent after backout')),
485 486 ('', 'commit', None,
486 487 _('commit if no conflicts were encountered (DEPRECATED)')),
487 488 ('', 'no-commit', None, _('do not commit')),
488 489 ('', 'parent', '',
489 490 _('parent to choose when backing out merge (DEPRECATED)'), _('REV')),
490 491 ('r', 'rev', '', _('revision to backout'), _('REV')),
491 492 ('e', 'edit', False, _('invoke editor on commit messages')),
492 493 ] + mergetoolopts + walkopts + commitopts + commitopts2,
493 494 _('[OPTION]... [-r] REV'))
494 495 def backout(ui, repo, node=None, rev=None, **opts):
495 496 '''reverse effect of earlier changeset
496 497
497 498 Prepare a new changeset with the effect of REV undone in the
498 499 current working directory. If no conflicts were encountered,
499 500 it will be committed immediately.
500 501
501 502 If REV is the parent of the working directory, then this new changeset
502 503 is committed automatically (unless --no-commit is specified).
503 504
504 505 .. note::
505 506
506 507 :hg:`backout` cannot be used to fix either an unwanted or
507 508 incorrect merge.
508 509
509 510 .. container:: verbose
510 511
511 512 Examples:
512 513
513 514 - Reverse the effect of the parent of the working directory.
514 515 This backout will be committed immediately::
515 516
516 517 hg backout -r .
517 518
518 519 - Reverse the effect of previous bad revision 23::
519 520
520 521 hg backout -r 23
521 522
522 523 - Reverse the effect of previous bad revision 23 and
523 524 leave changes uncommitted::
524 525
525 526 hg backout -r 23 --no-commit
526 527 hg commit -m "Backout revision 23"
527 528
528 529 By default, the pending changeset will have one parent,
529 530 maintaining a linear history. With --merge, the pending
530 531 changeset will instead have two parents: the old parent of the
531 532 working directory and a new child of REV that simply undoes REV.
532 533
533 534 Before version 1.7, the behavior without --merge was equivalent
534 535 to specifying --merge followed by :hg:`update --clean .` to
535 536 cancel the merge and leave the child of REV as a head to be
536 537 merged separately.
537 538
538 539 See :hg:`help dates` for a list of formats valid for -d/--date.
539 540
540 541 See :hg:`help revert` for a way to restore files to the state
541 542 of another revision.
542 543
543 544 Returns 0 on success, 1 if nothing to backout or there are unresolved
544 545 files.
545 546 '''
546 547 wlock = lock = None
547 548 try:
548 549 wlock = repo.wlock()
549 550 lock = repo.lock()
550 551 return _dobackout(ui, repo, node, rev, **opts)
551 552 finally:
552 553 release(lock, wlock)
553 554
554 555 def _dobackout(ui, repo, node=None, rev=None, **opts):
555 556 opts = pycompat.byteskwargs(opts)
556 557 if opts.get('commit') and opts.get('no_commit'):
557 558 raise error.Abort(_("cannot use --commit with --no-commit"))
558 559 if opts.get('merge') and opts.get('no_commit'):
559 560 raise error.Abort(_("cannot use --merge with --no-commit"))
560 561
561 562 if rev and node:
562 563 raise error.Abort(_("please specify just one revision"))
563 564
564 565 if not rev:
565 566 rev = node
566 567
567 568 if not rev:
568 569 raise error.Abort(_("please specify a revision to backout"))
569 570
570 571 date = opts.get('date')
571 572 if date:
572 573 opts['date'] = util.parsedate(date)
573 574
574 575 cmdutil.checkunfinished(repo)
575 576 cmdutil.bailifchanged(repo)
576 577 node = scmutil.revsingle(repo, rev).node()
577 578
578 579 op1, op2 = repo.dirstate.parents()
579 580 if not repo.changelog.isancestor(node, op1):
580 581 raise error.Abort(_('cannot backout change that is not an ancestor'))
581 582
582 583 p1, p2 = repo.changelog.parents(node)
583 584 if p1 == nullid:
584 585 raise error.Abort(_('cannot backout a change with no parents'))
585 586 if p2 != nullid:
586 587 if not opts.get('parent'):
587 588 raise error.Abort(_('cannot backout a merge changeset'))
588 589 p = repo.lookup(opts['parent'])
589 590 if p not in (p1, p2):
590 591 raise error.Abort(_('%s is not a parent of %s') %
591 592 (short(p), short(node)))
592 593 parent = p
593 594 else:
594 595 if opts.get('parent'):
595 596 raise error.Abort(_('cannot use --parent on non-merge changeset'))
596 597 parent = p1
597 598
598 599 # the backout should appear on the same branch
599 600 branch = repo.dirstate.branch()
600 601 bheads = repo.branchheads(branch)
601 602 rctx = scmutil.revsingle(repo, hex(parent))
602 603 if not opts.get('merge') and op1 != node:
603 604 dsguard = dirstateguard.dirstateguard(repo, 'backout')
604 605 try:
605 606 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
606 607 'backout')
607 608 stats = mergemod.update(repo, parent, True, True, node, False)
608 609 repo.setparents(op1, op2)
609 610 dsguard.close()
610 611 hg._showstats(repo, stats)
611 612 if stats[3]:
612 613 repo.ui.status(_("use 'hg resolve' to retry unresolved "
613 614 "file merges\n"))
614 615 return 1
615 616 finally:
616 617 ui.setconfig('ui', 'forcemerge', '', '')
617 618 lockmod.release(dsguard)
618 619 else:
619 620 hg.clean(repo, node, show_stats=False)
620 621 repo.dirstate.setbranch(branch)
621 622 cmdutil.revert(ui, repo, rctx, repo.dirstate.parents())
622 623
623 624 if opts.get('no_commit'):
624 625 msg = _("changeset %s backed out, "
625 626 "don't forget to commit.\n")
626 627 ui.status(msg % short(node))
627 628 return 0
628 629
629 630 def commitfunc(ui, repo, message, match, opts):
630 631 editform = 'backout'
631 632 e = cmdutil.getcommiteditor(editform=editform,
632 633 **pycompat.strkwargs(opts))
633 634 if not message:
634 635 # we don't translate commit messages
635 636 message = "Backed out changeset %s" % short(node)
636 637 e = cmdutil.getcommiteditor(edit=True, editform=editform)
637 638 return repo.commit(message, opts.get('user'), opts.get('date'),
638 639 match, editor=e)
639 640 newnode = cmdutil.commit(ui, repo, commitfunc, [], opts)
640 641 if not newnode:
641 642 ui.status(_("nothing changed\n"))
642 643 return 1
643 644 cmdutil.commitstatus(repo, newnode, branch, bheads)
644 645
645 646 def nice(node):
646 647 return '%d:%s' % (repo.changelog.rev(node), short(node))
647 648 ui.status(_('changeset %s backs out changeset %s\n') %
648 649 (nice(repo.changelog.tip()), nice(node)))
649 650 if opts.get('merge') and op1 != node:
650 651 hg.clean(repo, op1, show_stats=False)
651 652 ui.status(_('merging with changeset %s\n')
652 653 % nice(repo.changelog.tip()))
653 654 try:
654 655 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
655 656 'backout')
656 657 return hg.merge(repo, hex(repo.changelog.tip()))
657 658 finally:
658 659 ui.setconfig('ui', 'forcemerge', '', '')
659 660 return 0
660 661
661 662 @command('bisect',
662 663 [('r', 'reset', False, _('reset bisect state')),
663 664 ('g', 'good', False, _('mark changeset good')),
664 665 ('b', 'bad', False, _('mark changeset bad')),
665 666 ('s', 'skip', False, _('skip testing changeset')),
666 667 ('e', 'extend', False, _('extend the bisect range')),
667 668 ('c', 'command', '', _('use command to check changeset state'), _('CMD')),
668 669 ('U', 'noupdate', False, _('do not update to target'))],
669 670 _("[-gbsr] [-U] [-c CMD] [REV]"))
670 671 def bisect(ui, repo, rev=None, extra=None, command=None,
671 672 reset=None, good=None, bad=None, skip=None, extend=None,
672 673 noupdate=None):
673 674 """subdivision search of changesets
674 675
675 676 This command helps to find changesets which introduce problems. To
676 677 use, mark the earliest changeset you know exhibits the problem as
677 678 bad, then mark the latest changeset which is free from the problem
678 679 as good. Bisect will update your working directory to a revision
679 680 for testing (unless the -U/--noupdate option is specified). Once
680 681 you have performed tests, mark the working directory as good or
681 682 bad, and bisect will either update to another candidate changeset
682 683 or announce that it has found the bad revision.
683 684
684 685 As a shortcut, you can also use the revision argument to mark a
685 686 revision as good or bad without checking it out first.
686 687
687 688 If you supply a command, it will be used for automatic bisection.
688 689 The environment variable HG_NODE will contain the ID of the
689 690 changeset being tested. The exit status of the command will be
690 691 used to mark revisions as good or bad: status 0 means good, 125
691 692 means to skip the revision, 127 (command not found) will abort the
692 693 bisection, and any other non-zero exit status means the revision
693 694 is bad.
694 695
695 696 .. container:: verbose
696 697
697 698 Some examples:
698 699
699 700 - start a bisection with known bad revision 34, and good revision 12::
700 701
701 702 hg bisect --bad 34
702 703 hg bisect --good 12
703 704
704 705 - advance the current bisection by marking current revision as good or
705 706 bad::
706 707
707 708 hg bisect --good
708 709 hg bisect --bad
709 710
710 711 - mark the current revision, or a known revision, to be skipped (e.g. if
711 712 that revision is not usable because of another issue)::
712 713
713 714 hg bisect --skip
714 715 hg bisect --skip 23
715 716
716 717 - skip all revisions that do not touch directories ``foo`` or ``bar``::
717 718
718 719 hg bisect --skip "!( file('path:foo') & file('path:bar') )"
719 720
720 721 - forget the current bisection::
721 722
722 723 hg bisect --reset
723 724
724 725 - use 'make && make tests' to automatically find the first broken
725 726 revision::
726 727
727 728 hg bisect --reset
728 729 hg bisect --bad 34
729 730 hg bisect --good 12
730 731 hg bisect --command "make && make tests"
731 732
732 733 - see all changesets whose states are already known in the current
733 734 bisection::
734 735
735 736 hg log -r "bisect(pruned)"
736 737
737 738 - see the changeset currently being bisected (especially useful
738 739 if running with -U/--noupdate)::
739 740
740 741 hg log -r "bisect(current)"
741 742
742 743 - see all changesets that took part in the current bisection::
743 744
744 745 hg log -r "bisect(range)"
745 746
746 747 - you can even get a nice graph::
747 748
748 749 hg log --graph -r "bisect(range)"
749 750
750 751 See :hg:`help revisions.bisect` for more about the `bisect()` predicate.
751 752
752 753 Returns 0 on success.
753 754 """
754 755 # backward compatibility
755 756 if rev in "good bad reset init".split():
756 757 ui.warn(_("(use of 'hg bisect <cmd>' is deprecated)\n"))
757 758 cmd, rev, extra = rev, extra, None
758 759 if cmd == "good":
759 760 good = True
760 761 elif cmd == "bad":
761 762 bad = True
762 763 else:
763 764 reset = True
764 765 elif extra or good + bad + skip + reset + extend + bool(command) > 1:
765 766 raise error.Abort(_('incompatible arguments'))
766 767
767 768 if reset:
768 769 hbisect.resetstate(repo)
769 770 return
770 771
771 772 state = hbisect.load_state(repo)
772 773
773 774 # update state
774 775 if good or bad or skip:
775 776 if rev:
776 777 nodes = [repo.lookup(i) for i in scmutil.revrange(repo, [rev])]
777 778 else:
778 779 nodes = [repo.lookup('.')]
779 780 if good:
780 781 state['good'] += nodes
781 782 elif bad:
782 783 state['bad'] += nodes
783 784 elif skip:
784 785 state['skip'] += nodes
785 786 hbisect.save_state(repo, state)
786 787 if not (state['good'] and state['bad']):
787 788 return
788 789
789 790 def mayupdate(repo, node, show_stats=True):
790 791 """common used update sequence"""
791 792 if noupdate:
792 793 return
793 794 cmdutil.checkunfinished(repo)
794 795 cmdutil.bailifchanged(repo)
795 796 return hg.clean(repo, node, show_stats=show_stats)
796 797
797 798 displayer = cmdutil.show_changeset(ui, repo, {})
798 799
799 800 if command:
800 801 changesets = 1
801 802 if noupdate:
802 803 try:
803 804 node = state['current'][0]
804 805 except LookupError:
805 806 raise error.Abort(_('current bisect revision is unknown - '
806 807 'start a new bisect to fix'))
807 808 else:
808 809 node, p2 = repo.dirstate.parents()
809 810 if p2 != nullid:
810 811 raise error.Abort(_('current bisect revision is a merge'))
811 812 if rev:
812 813 node = repo[scmutil.revsingle(repo, rev, node)].node()
813 814 try:
814 815 while changesets:
815 816 # update state
816 817 state['current'] = [node]
817 818 hbisect.save_state(repo, state)
818 819 status = ui.system(command, environ={'HG_NODE': hex(node)},
819 820 blockedtag='bisect_check')
820 821 if status == 125:
821 822 transition = "skip"
822 823 elif status == 0:
823 824 transition = "good"
824 825 # status < 0 means process was killed
825 826 elif status == 127:
826 827 raise error.Abort(_("failed to execute %s") % command)
827 828 elif status < 0:
828 829 raise error.Abort(_("%s killed") % command)
829 830 else:
830 831 transition = "bad"
831 832 state[transition].append(node)
832 833 ctx = repo[node]
833 834 ui.status(_('changeset %d:%s: %s\n') % (ctx, ctx, transition))
834 835 hbisect.checkstate(state)
835 836 # bisect
836 837 nodes, changesets, bgood = hbisect.bisect(repo.changelog, state)
837 838 # update to next check
838 839 node = nodes[0]
839 840 mayupdate(repo, node, show_stats=False)
840 841 finally:
841 842 state['current'] = [node]
842 843 hbisect.save_state(repo, state)
843 844 hbisect.printresult(ui, repo, state, displayer, nodes, bgood)
844 845 return
845 846
846 847 hbisect.checkstate(state)
847 848
848 849 # actually bisect
849 850 nodes, changesets, good = hbisect.bisect(repo.changelog, state)
850 851 if extend:
851 852 if not changesets:
852 853 extendnode = hbisect.extendrange(repo, state, nodes, good)
853 854 if extendnode is not None:
854 855 ui.write(_("Extending search to changeset %d:%s\n")
855 856 % (extendnode.rev(), extendnode))
856 857 state['current'] = [extendnode.node()]
857 858 hbisect.save_state(repo, state)
858 859 return mayupdate(repo, extendnode.node())
859 860 raise error.Abort(_("nothing to extend"))
860 861
861 862 if changesets == 0:
862 863 hbisect.printresult(ui, repo, state, displayer, nodes, good)
863 864 else:
864 865 assert len(nodes) == 1 # only a single node can be tested next
865 866 node = nodes[0]
866 867 # compute the approximate number of remaining tests
867 868 tests, size = 0, 2
868 869 while size <= changesets:
869 870 tests, size = tests + 1, size * 2
870 871 rev = repo.changelog.rev(node)
871 872 ui.write(_("Testing changeset %d:%s "
872 873 "(%d changesets remaining, ~%d tests)\n")
873 874 % (rev, short(node), changesets, tests))
874 875 state['current'] = [node]
875 876 hbisect.save_state(repo, state)
876 877 return mayupdate(repo, node)
877 878
878 879 @command('bookmarks|bookmark',
879 880 [('f', 'force', False, _('force')),
880 881 ('r', 'rev', '', _('revision for bookmark action'), _('REV')),
881 882 ('d', 'delete', False, _('delete a given bookmark')),
882 883 ('m', 'rename', '', _('rename a given bookmark'), _('OLD')),
883 884 ('i', 'inactive', False, _('mark a bookmark inactive')),
884 885 ] + formatteropts,
885 886 _('hg bookmarks [OPTIONS]... [NAME]...'))
886 887 def bookmark(ui, repo, *names, **opts):
887 888 '''create a new bookmark or list existing bookmarks
888 889
889 890 Bookmarks are labels on changesets to help track lines of development.
890 891 Bookmarks are unversioned and can be moved, renamed and deleted.
891 892 Deleting or moving a bookmark has no effect on the associated changesets.
892 893
893 894 Creating or updating to a bookmark causes it to be marked as 'active'.
894 895 The active bookmark is indicated with a '*'.
895 896 When a commit is made, the active bookmark will advance to the new commit.
896 897 A plain :hg:`update` will also advance an active bookmark, if possible.
897 898 Updating away from a bookmark will cause it to be deactivated.
898 899
899 900 Bookmarks can be pushed and pulled between repositories (see
900 901 :hg:`help push` and :hg:`help pull`). If a shared bookmark has
901 902 diverged, a new 'divergent bookmark' of the form 'name@path' will
902 903 be created. Using :hg:`merge` will resolve the divergence.
903 904
904 905 A bookmark named '@' has the special property that :hg:`clone` will
905 906 check it out by default if it exists.
906 907
907 908 .. container:: verbose
908 909
909 910 Examples:
910 911
911 912 - create an active bookmark for a new line of development::
912 913
913 914 hg book new-feature
914 915
915 916 - create an inactive bookmark as a place marker::
916 917
917 918 hg book -i reviewed
918 919
919 920 - create an inactive bookmark on another changeset::
920 921
921 922 hg book -r .^ tested
922 923
923 924 - rename bookmark turkey to dinner::
924 925
925 926 hg book -m turkey dinner
926 927
927 928 - move the '@' bookmark from another branch::
928 929
929 930 hg book -f @
930 931 '''
931 932 opts = pycompat.byteskwargs(opts)
932 933 force = opts.get('force')
933 934 rev = opts.get('rev')
934 935 delete = opts.get('delete')
935 936 rename = opts.get('rename')
936 937 inactive = opts.get('inactive')
937 938
938 939 def checkformat(mark):
939 940 mark = mark.strip()
940 941 if not mark:
941 942 raise error.Abort(_("bookmark names cannot consist entirely of "
942 943 "whitespace"))
943 944 scmutil.checknewlabel(repo, mark, 'bookmark')
944 945 return mark
945 946
946 947 def checkconflict(repo, mark, cur, force=False, target=None):
947 948 if mark in marks and not force:
948 949 if target:
949 950 if marks[mark] == target and target == cur:
950 951 # re-activating a bookmark
951 952 return
952 953 anc = repo.changelog.ancestors([repo[target].rev()])
953 954 bmctx = repo[marks[mark]]
954 955 divs = [repo[b].node() for b in marks
955 956 if b.split('@', 1)[0] == mark.split('@', 1)[0]]
956 957
957 958 # allow resolving a single divergent bookmark even if moving
958 959 # the bookmark across branches when a revision is specified
959 960 # that contains a divergent bookmark
960 961 if bmctx.rev() not in anc and target in divs:
961 962 bookmarks.deletedivergent(repo, [target], mark)
962 963 return
963 964
964 965 deletefrom = [b for b in divs
965 966 if repo[b].rev() in anc or b == target]
966 967 bookmarks.deletedivergent(repo, deletefrom, mark)
967 968 if bookmarks.validdest(repo, bmctx, repo[target]):
968 969 ui.status(_("moving bookmark '%s' forward from %s\n") %
969 970 (mark, short(bmctx.node())))
970 971 return
971 972 raise error.Abort(_("bookmark '%s' already exists "
972 973 "(use -f to force)") % mark)
973 974 if ((mark in repo.branchmap() or mark == repo.dirstate.branch())
974 975 and not force):
975 976 raise error.Abort(
976 977 _("a bookmark cannot have the name of an existing branch"))
977 978 if len(mark) > 3 and not force:
978 979 try:
979 980 shadowhash = (mark in repo)
980 981 except error.LookupError: # ambiguous identifier
981 982 shadowhash = False
982 983 if shadowhash:
983 984 repo.ui.warn(
984 985 _("bookmark %s matches a changeset hash\n"
985 986 "(did you leave a -r out of an 'hg bookmark' command?)\n")
986 987 % mark)
987 988
988 989 if delete and rename:
989 990 raise error.Abort(_("--delete and --rename are incompatible"))
990 991 if delete and rev:
991 992 raise error.Abort(_("--rev is incompatible with --delete"))
992 993 if rename and rev:
993 994 raise error.Abort(_("--rev is incompatible with --rename"))
994 995 if not names and (delete or rev):
995 996 raise error.Abort(_("bookmark name required"))
996 997
997 998 if delete or rename or names or inactive:
998 999 wlock = lock = tr = None
999 1000 try:
1000 1001 wlock = repo.wlock()
1001 1002 lock = repo.lock()
1002 1003 cur = repo.changectx('.').node()
1003 1004 marks = repo._bookmarks
1004 1005 if delete:
1005 1006 tr = repo.transaction('bookmark')
1006 1007 for mark in names:
1007 1008 if mark not in marks:
1008 1009 raise error.Abort(_("bookmark '%s' does not exist") %
1009 1010 mark)
1010 1011 if mark == repo._activebookmark:
1011 1012 bookmarks.deactivate(repo)
1012 1013 del marks[mark]
1013 1014
1014 1015 elif rename:
1015 1016 tr = repo.transaction('bookmark')
1016 1017 if not names:
1017 1018 raise error.Abort(_("new bookmark name required"))
1018 1019 elif len(names) > 1:
1019 1020 raise error.Abort(_("only one new bookmark name allowed"))
1020 1021 mark = checkformat(names[0])
1021 1022 if rename not in marks:
1022 1023 raise error.Abort(_("bookmark '%s' does not exist")
1023 1024 % rename)
1024 1025 checkconflict(repo, mark, cur, force)
1025 1026 marks[mark] = marks[rename]
1026 1027 if repo._activebookmark == rename and not inactive:
1027 1028 bookmarks.activate(repo, mark)
1028 1029 del marks[rename]
1029 1030 elif names:
1030 1031 tr = repo.transaction('bookmark')
1031 1032 newact = None
1032 1033 for mark in names:
1033 1034 mark = checkformat(mark)
1034 1035 if newact is None:
1035 1036 newact = mark
1036 1037 if inactive and mark == repo._activebookmark:
1037 1038 bookmarks.deactivate(repo)
1038 1039 return
1039 1040 tgt = cur
1040 1041 if rev:
1041 1042 tgt = scmutil.revsingle(repo, rev).node()
1042 1043 checkconflict(repo, mark, cur, force, tgt)
1043 1044 marks[mark] = tgt
1044 1045 if not inactive and cur == marks[newact] and not rev:
1045 1046 bookmarks.activate(repo, newact)
1046 1047 elif cur != tgt and newact == repo._activebookmark:
1047 1048 bookmarks.deactivate(repo)
1048 1049 elif inactive:
1049 1050 if len(marks) == 0:
1050 1051 ui.status(_("no bookmarks set\n"))
1051 1052 elif not repo._activebookmark:
1052 1053 ui.status(_("no active bookmark\n"))
1053 1054 else:
1054 1055 bookmarks.deactivate(repo)
1055 1056 if tr is not None:
1056 1057 marks.recordchange(tr)
1057 1058 tr.close()
1058 1059 finally:
1059 1060 lockmod.release(tr, lock, wlock)
1060 1061 else: # show bookmarks
1061 1062 fm = ui.formatter('bookmarks', opts)
1062 1063 hexfn = fm.hexfunc
1063 1064 marks = repo._bookmarks
1064 1065 if len(marks) == 0 and fm.isplain():
1065 1066 ui.status(_("no bookmarks set\n"))
1066 1067 for bmark, n in sorted(marks.iteritems()):
1067 1068 active = repo._activebookmark
1068 1069 if bmark == active:
1069 1070 prefix, label = '*', activebookmarklabel
1070 1071 else:
1071 1072 prefix, label = ' ', ''
1072 1073
1073 1074 fm.startitem()
1074 1075 if not ui.quiet:
1075 1076 fm.plain(' %s ' % prefix, label=label)
1076 1077 fm.write('bookmark', '%s', bmark, label=label)
1077 1078 pad = " " * (25 - encoding.colwidth(bmark))
1078 1079 fm.condwrite(not ui.quiet, 'rev node', pad + ' %d:%s',
1079 1080 repo.changelog.rev(n), hexfn(n), label=label)
1080 1081 fm.data(active=(bmark == active))
1081 1082 fm.plain('\n')
1082 1083 fm.end()
1083 1084
1084 1085 @command('branch',
1085 1086 [('f', 'force', None,
1086 1087 _('set branch name even if it shadows an existing branch')),
1087 1088 ('C', 'clean', None, _('reset branch name to parent branch name'))],
1088 1089 _('[-fC] [NAME]'))
1089 1090 def branch(ui, repo, label=None, **opts):
1090 1091 """set or show the current branch name
1091 1092
1092 1093 .. note::
1093 1094
1094 1095 Branch names are permanent and global. Use :hg:`bookmark` to create a
1095 1096 light-weight bookmark instead. See :hg:`help glossary` for more
1096 1097 information about named branches and bookmarks.
1097 1098
1098 1099 With no argument, show the current branch name. With one argument,
1099 1100 set the working directory branch name (the branch will not exist
1100 1101 in the repository until the next commit). Standard practice
1101 1102 recommends that primary development take place on the 'default'
1102 1103 branch.
1103 1104
1104 1105 Unless -f/--force is specified, branch will not let you set a
1105 1106 branch name that already exists.
1106 1107
1107 1108 Use -C/--clean to reset the working directory branch to that of
1108 1109 the parent of the working directory, negating a previous branch
1109 1110 change.
1110 1111
1111 1112 Use the command :hg:`update` to switch to an existing branch. Use
1112 1113 :hg:`commit --close-branch` to mark this branch head as closed.
1113 1114 When all heads of a branch are closed, the branch will be
1114 1115 considered closed.
1115 1116
1116 1117 Returns 0 on success.
1117 1118 """
1118 1119 opts = pycompat.byteskwargs(opts)
1119 1120 if label:
1120 1121 label = label.strip()
1121 1122
1122 1123 if not opts.get('clean') and not label:
1123 1124 ui.write("%s\n" % repo.dirstate.branch())
1124 1125 return
1125 1126
1126 1127 with repo.wlock():
1127 1128 if opts.get('clean'):
1128 1129 label = repo[None].p1().branch()
1129 1130 repo.dirstate.setbranch(label)
1130 1131 ui.status(_('reset working directory to branch %s\n') % label)
1131 1132 elif label:
1132 1133 if not opts.get('force') and label in repo.branchmap():
1133 1134 if label not in [p.branch() for p in repo[None].parents()]:
1134 1135 raise error.Abort(_('a branch of the same name already'
1135 1136 ' exists'),
1136 1137 # i18n: "it" refers to an existing branch
1137 1138 hint=_("use 'hg update' to switch to it"))
1138 1139 scmutil.checknewlabel(repo, label, 'branch')
1139 1140 repo.dirstate.setbranch(label)
1140 1141 ui.status(_('marked working directory as branch %s\n') % label)
1141 1142
1142 1143 # find any open named branches aside from default
1143 1144 others = [n for n, h, t, c in repo.branchmap().iterbranches()
1144 1145 if n != "default" and not c]
1145 1146 if not others:
1146 1147 ui.status(_('(branches are permanent and global, '
1147 1148 'did you want a bookmark?)\n'))
1148 1149
1149 1150 @command('branches',
1150 1151 [('a', 'active', False,
1151 1152 _('show only branches that have unmerged heads (DEPRECATED)')),
1152 1153 ('c', 'closed', False, _('show normal and closed branches')),
1153 1154 ] + formatteropts,
1154 1155 _('[-c]'))
1155 1156 def branches(ui, repo, active=False, closed=False, **opts):
1156 1157 """list repository named branches
1157 1158
1158 1159 List the repository's named branches, indicating which ones are
1159 1160 inactive. If -c/--closed is specified, also list branches which have
1160 1161 been marked closed (see :hg:`commit --close-branch`).
1161 1162
1162 1163 Use the command :hg:`update` to switch to an existing branch.
1163 1164
1164 1165 Returns 0.
1165 1166 """
1166 1167
1167 1168 opts = pycompat.byteskwargs(opts)
1168 1169 ui.pager('branches')
1169 1170 fm = ui.formatter('branches', opts)
1170 1171 hexfunc = fm.hexfunc
1171 1172
1172 1173 allheads = set(repo.heads())
1173 1174 branches = []
1174 1175 for tag, heads, tip, isclosed in repo.branchmap().iterbranches():
1175 1176 isactive = not isclosed and bool(set(heads) & allheads)
1176 1177 branches.append((tag, repo[tip], isactive, not isclosed))
1177 1178 branches.sort(key=lambda i: (i[2], i[1].rev(), i[0], i[3]),
1178 1179 reverse=True)
1179 1180
1180 1181 for tag, ctx, isactive, isopen in branches:
1181 1182 if active and not isactive:
1182 1183 continue
1183 1184 if isactive:
1184 1185 label = 'branches.active'
1185 1186 notice = ''
1186 1187 elif not isopen:
1187 1188 if not closed:
1188 1189 continue
1189 1190 label = 'branches.closed'
1190 1191 notice = _(' (closed)')
1191 1192 else:
1192 1193 label = 'branches.inactive'
1193 1194 notice = _(' (inactive)')
1194 1195 current = (tag == repo.dirstate.branch())
1195 1196 if current:
1196 1197 label = 'branches.current'
1197 1198
1198 1199 fm.startitem()
1199 1200 fm.write('branch', '%s', tag, label=label)
1200 1201 rev = ctx.rev()
1201 1202 padsize = max(31 - len(str(rev)) - encoding.colwidth(tag), 0)
1202 1203 fmt = ' ' * padsize + ' %d:%s'
1203 1204 fm.condwrite(not ui.quiet, 'rev node', fmt, rev, hexfunc(ctx.node()),
1204 1205 label='log.changeset changeset.%s' % ctx.phasestr())
1205 1206 fm.context(ctx=ctx)
1206 1207 fm.data(active=isactive, closed=not isopen, current=current)
1207 1208 if not ui.quiet:
1208 1209 fm.plain(notice)
1209 1210 fm.plain('\n')
1210 1211 fm.end()
1211 1212
1212 1213 @command('bundle',
1213 1214 [('f', 'force', None, _('run even when the destination is unrelated')),
1214 1215 ('r', 'rev', [], _('a changeset intended to be added to the destination'),
1215 1216 _('REV')),
1216 1217 ('b', 'branch', [], _('a specific branch you would like to bundle'),
1217 1218 _('BRANCH')),
1218 1219 ('', 'base', [],
1219 1220 _('a base changeset assumed to be available at the destination'),
1220 1221 _('REV')),
1221 1222 ('a', 'all', None, _('bundle all changesets in the repository')),
1222 1223 ('t', 'type', 'bzip2', _('bundle compression type to use'), _('TYPE')),
1223 1224 ] + remoteopts,
1224 1225 _('[-f] [-t BUNDLESPEC] [-a] [-r REV]... [--base REV]... FILE [DEST]'))
1225 1226 def bundle(ui, repo, fname, dest=None, **opts):
1226 1227 """create a bundle file
1227 1228
1228 1229 Generate a bundle file containing data to be added to a repository.
1229 1230
1230 1231 To create a bundle containing all changesets, use -a/--all
1231 1232 (or --base null). Otherwise, hg assumes the destination will have
1232 1233 all the nodes you specify with --base parameters. Otherwise, hg
1233 1234 will assume the repository has all the nodes in destination, or
1234 1235 default-push/default if no destination is specified.
1235 1236
1236 1237 You can change bundle format with the -t/--type option. See
1237 1238 :hg:`help bundlespec` for documentation on this format. By default,
1238 1239 the most appropriate format is used and compression defaults to
1239 1240 bzip2.
1240 1241
1241 1242 The bundle file can then be transferred using conventional means
1242 1243 and applied to another repository with the unbundle or pull
1243 1244 command. This is useful when direct push and pull are not
1244 1245 available or when exporting an entire repository is undesirable.
1245 1246
1246 1247 Applying bundles preserves all changeset contents including
1247 1248 permissions, copy/rename information, and revision history.
1248 1249
1249 1250 Returns 0 on success, 1 if no changes found.
1250 1251 """
1251 1252 opts = pycompat.byteskwargs(opts)
1252 1253 revs = None
1253 1254 if 'rev' in opts:
1254 1255 revstrings = opts['rev']
1255 1256 revs = scmutil.revrange(repo, revstrings)
1256 1257 if revstrings and not revs:
1257 1258 raise error.Abort(_('no commits to bundle'))
1258 1259
1259 1260 bundletype = opts.get('type', 'bzip2').lower()
1260 1261 try:
1261 1262 bcompression, cgversion, params = exchange.parsebundlespec(
1262 1263 repo, bundletype, strict=False)
1263 1264 except error.UnsupportedBundleSpecification as e:
1264 1265 raise error.Abort(str(e),
1265 1266 hint=_("see 'hg help bundlespec' for supported "
1266 1267 "values for --type"))
1267 1268
1268 1269 # Packed bundles are a pseudo bundle format for now.
1269 1270 if cgversion == 's1':
1270 1271 raise error.Abort(_('packed bundles cannot be produced by "hg bundle"'),
1271 1272 hint=_("use 'hg debugcreatestreamclonebundle'"))
1272 1273
1273 1274 if opts.get('all'):
1274 1275 if dest:
1275 1276 raise error.Abort(_("--all is incompatible with specifying "
1276 1277 "a destination"))
1277 1278 if opts.get('base'):
1278 1279 ui.warn(_("ignoring --base because --all was specified\n"))
1279 1280 base = ['null']
1280 1281 else:
1281 1282 base = scmutil.revrange(repo, opts.get('base'))
1282 1283 if cgversion not in changegroup.supportedoutgoingversions(repo):
1283 1284 raise error.Abort(_("repository does not support bundle version %s") %
1284 1285 cgversion)
1285 1286
1286 1287 if base:
1287 1288 if dest:
1288 1289 raise error.Abort(_("--base is incompatible with specifying "
1289 1290 "a destination"))
1290 1291 common = [repo.lookup(rev) for rev in base]
1291 1292 heads = revs and map(repo.lookup, revs) or None
1292 1293 outgoing = discovery.outgoing(repo, common, heads)
1293 1294 else:
1294 1295 dest = ui.expandpath(dest or 'default-push', dest or 'default')
1295 1296 dest, branches = hg.parseurl(dest, opts.get('branch'))
1296 1297 other = hg.peer(repo, opts, dest)
1297 1298 revs, checkout = hg.addbranchrevs(repo, repo, branches, revs)
1298 1299 heads = revs and map(repo.lookup, revs) or revs
1299 1300 outgoing = discovery.findcommonoutgoing(repo, other,
1300 1301 onlyheads=heads,
1301 1302 force=opts.get('force'),
1302 1303 portable=True)
1303 1304
1304 1305 if not outgoing.missing:
1305 1306 scmutil.nochangesfound(ui, repo, not base and outgoing.excluded)
1306 1307 return 1
1307 1308
1308 1309 if cgversion == '01': #bundle1
1309 1310 if bcompression is None:
1310 1311 bcompression = 'UN'
1311 1312 bversion = 'HG10' + bcompression
1312 1313 bcompression = None
1313 1314 elif cgversion in ('02', '03'):
1314 1315 bversion = 'HG20'
1315 1316 else:
1316 1317 raise error.ProgrammingError(
1317 1318 'bundle: unexpected changegroup version %s' % cgversion)
1318 1319
1319 1320 # TODO compression options should be derived from bundlespec parsing.
1320 1321 # This is a temporary hack to allow adjusting bundle compression
1321 1322 # level without a) formalizing the bundlespec changes to declare it
1322 1323 # b) introducing a command flag.
1323 1324 compopts = {}
1324 1325 complevel = ui.configint('experimental', 'bundlecomplevel')
1325 1326 if complevel is not None:
1326 1327 compopts['level'] = complevel
1327 1328
1328 1329
1329 1330 contentopts = {'cg.version': cgversion}
1330 1331 if repo.ui.configbool('experimental', 'evolution.bundle-obsmarker', False):
1331 1332 contentopts['obsolescence'] = True
1332 1333 bundle2.writenewbundle(ui, repo, 'bundle', fname, bversion, outgoing,
1333 1334 contentopts, compression=bcompression,
1334 1335 compopts=compopts)
1335 1336
1336 1337 @command('cat',
1337 1338 [('o', 'output', '',
1338 1339 _('print output to file with formatted name'), _('FORMAT')),
1339 1340 ('r', 'rev', '', _('print the given revision'), _('REV')),
1340 1341 ('', 'decode', None, _('apply any matching decode filter')),
1341 ] + walkopts,
1342 ] + walkopts + formatteropts,
1342 1343 _('[OPTION]... FILE...'),
1343 1344 inferrepo=True)
1344 1345 def cat(ui, repo, file1, *pats, **opts):
1345 1346 """output the current or given revision of files
1346 1347
1347 1348 Print the specified files as they were at the given revision. If
1348 1349 no revision is given, the parent of the working directory is used.
1349 1350
1350 1351 Output may be to a file, in which case the name of the file is
1351 1352 given using a format string. The formatting rules as follows:
1352 1353
1353 1354 :``%%``: literal "%" character
1354 1355 :``%s``: basename of file being printed
1355 1356 :``%d``: dirname of file being printed, or '.' if in repository root
1356 1357 :``%p``: root-relative path name of file being printed
1357 1358 :``%H``: changeset hash (40 hexadecimal digits)
1358 1359 :``%R``: changeset revision number
1359 1360 :``%h``: short-form changeset hash (12 hexadecimal digits)
1360 1361 :``%r``: zero-padded changeset revision number
1361 1362 :``%b``: basename of the exporting repository
1362 1363
1363 1364 Returns 0 on success.
1364 1365 """
1365 1366 ctx = scmutil.revsingle(repo, opts.get('rev'))
1366 1367 m = scmutil.match(ctx, (file1,) + pats, opts)
1367 1368 fntemplate = opts.pop('output', '')
1368 1369 if cmdutil.isstdiofilename(fntemplate):
1369 1370 fntemplate = ''
1370 1371
1371 if not fntemplate:
1372 if fntemplate:
1373 fm = formatter.nullformatter(ui, 'cat')
1374 else:
1372 1375 ui.pager('cat')
1373 return cmdutil.cat(ui, repo, ctx, m, fntemplate, '', **opts)
1376 fm = ui.formatter('cat', opts)
1377 with fm:
1378 return cmdutil.cat(ui, repo, ctx, m, fm, fntemplate, '', **opts)
1374 1379
1375 1380 @command('^clone',
1376 1381 [('U', 'noupdate', None, _('the clone will include an empty working '
1377 1382 'directory (only a repository)')),
1378 1383 ('u', 'updaterev', '', _('revision, tag, or branch to check out'),
1379 1384 _('REV')),
1380 1385 ('r', 'rev', [], _('include the specified changeset'), _('REV')),
1381 1386 ('b', 'branch', [], _('clone only the specified branch'), _('BRANCH')),
1382 1387 ('', 'pull', None, _('use pull protocol to copy metadata')),
1383 1388 ('', 'uncompressed', None, _('use uncompressed transfer (fast over LAN)')),
1384 1389 ] + remoteopts,
1385 1390 _('[OPTION]... SOURCE [DEST]'),
1386 1391 norepo=True)
1387 1392 def clone(ui, source, dest=None, **opts):
1388 1393 """make a copy of an existing repository
1389 1394
1390 1395 Create a copy of an existing repository in a new directory.
1391 1396
1392 1397 If no destination directory name is specified, it defaults to the
1393 1398 basename of the source.
1394 1399
1395 1400 The location of the source is added to the new repository's
1396 1401 ``.hg/hgrc`` file, as the default to be used for future pulls.
1397 1402
1398 1403 Only local paths and ``ssh://`` URLs are supported as
1399 1404 destinations. For ``ssh://`` destinations, no working directory or
1400 1405 ``.hg/hgrc`` will be created on the remote side.
1401 1406
1402 1407 If the source repository has a bookmark called '@' set, that
1403 1408 revision will be checked out in the new repository by default.
1404 1409
1405 1410 To check out a particular version, use -u/--update, or
1406 1411 -U/--noupdate to create a clone with no working directory.
1407 1412
1408 1413 To pull only a subset of changesets, specify one or more revisions
1409 1414 identifiers with -r/--rev or branches with -b/--branch. The
1410 1415 resulting clone will contain only the specified changesets and
1411 1416 their ancestors. These options (or 'clone src#rev dest') imply
1412 1417 --pull, even for local source repositories.
1413 1418
1414 1419 .. note::
1415 1420
1416 1421 Specifying a tag will include the tagged changeset but not the
1417 1422 changeset containing the tag.
1418 1423
1419 1424 .. container:: verbose
1420 1425
1421 1426 For efficiency, hardlinks are used for cloning whenever the
1422 1427 source and destination are on the same filesystem (note this
1423 1428 applies only to the repository data, not to the working
1424 1429 directory). Some filesystems, such as AFS, implement hardlinking
1425 1430 incorrectly, but do not report errors. In these cases, use the
1426 1431 --pull option to avoid hardlinking.
1427 1432
1428 1433 In some cases, you can clone repositories and the working
1429 1434 directory using full hardlinks with ::
1430 1435
1431 1436 $ cp -al REPO REPOCLONE
1432 1437
1433 1438 This is the fastest way to clone, but it is not always safe. The
1434 1439 operation is not atomic (making sure REPO is not modified during
1435 1440 the operation is up to you) and you have to make sure your
1436 1441 editor breaks hardlinks (Emacs and most Linux Kernel tools do
1437 1442 so). Also, this is not compatible with certain extensions that
1438 1443 place their metadata under the .hg directory, such as mq.
1439 1444
1440 1445 Mercurial will update the working directory to the first applicable
1441 1446 revision from this list:
1442 1447
1443 1448 a) null if -U or the source repository has no changesets
1444 1449 b) if -u . and the source repository is local, the first parent of
1445 1450 the source repository's working directory
1446 1451 c) the changeset specified with -u (if a branch name, this means the
1447 1452 latest head of that branch)
1448 1453 d) the changeset specified with -r
1449 1454 e) the tipmost head specified with -b
1450 1455 f) the tipmost head specified with the url#branch source syntax
1451 1456 g) the revision marked with the '@' bookmark, if present
1452 1457 h) the tipmost head of the default branch
1453 1458 i) tip
1454 1459
1455 1460 When cloning from servers that support it, Mercurial may fetch
1456 1461 pre-generated data from a server-advertised URL. When this is done,
1457 1462 hooks operating on incoming changesets and changegroups may fire twice,
1458 1463 once for the bundle fetched from the URL and another for any additional
1459 1464 data not fetched from this URL. In addition, if an error occurs, the
1460 1465 repository may be rolled back to a partial clone. This behavior may
1461 1466 change in future releases. See :hg:`help -e clonebundles` for more.
1462 1467
1463 1468 Examples:
1464 1469
1465 1470 - clone a remote repository to a new directory named hg/::
1466 1471
1467 1472 hg clone https://www.mercurial-scm.org/repo/hg/
1468 1473
1469 1474 - create a lightweight local clone::
1470 1475
1471 1476 hg clone project/ project-feature/
1472 1477
1473 1478 - clone from an absolute path on an ssh server (note double-slash)::
1474 1479
1475 1480 hg clone ssh://user@server//home/projects/alpha/
1476 1481
1477 1482 - do a high-speed clone over a LAN while checking out a
1478 1483 specified version::
1479 1484
1480 1485 hg clone --uncompressed http://server/repo -u 1.5
1481 1486
1482 1487 - create a repository without changesets after a particular revision::
1483 1488
1484 1489 hg clone -r 04e544 experimental/ good/
1485 1490
1486 1491 - clone (and track) a particular named branch::
1487 1492
1488 1493 hg clone https://www.mercurial-scm.org/repo/hg/#stable
1489 1494
1490 1495 See :hg:`help urls` for details on specifying URLs.
1491 1496
1492 1497 Returns 0 on success.
1493 1498 """
1494 1499 opts = pycompat.byteskwargs(opts)
1495 1500 if opts.get('noupdate') and opts.get('updaterev'):
1496 1501 raise error.Abort(_("cannot specify both --noupdate and --updaterev"))
1497 1502
1498 1503 r = hg.clone(ui, opts, source, dest,
1499 1504 pull=opts.get('pull'),
1500 1505 stream=opts.get('uncompressed'),
1501 1506 rev=opts.get('rev'),
1502 1507 update=opts.get('updaterev') or not opts.get('noupdate'),
1503 1508 branch=opts.get('branch'),
1504 1509 shareopts=opts.get('shareopts'))
1505 1510
1506 1511 return r is None
1507 1512
1508 1513 @command('^commit|ci',
1509 1514 [('A', 'addremove', None,
1510 1515 _('mark new/missing files as added/removed before committing')),
1511 1516 ('', 'close-branch', None,
1512 1517 _('mark a branch head as closed')),
1513 1518 ('', 'amend', None, _('amend the parent of the working directory')),
1514 1519 ('s', 'secret', None, _('use the secret phase for committing')),
1515 1520 ('e', 'edit', None, _('invoke editor on commit messages')),
1516 1521 ('i', 'interactive', None, _('use interactive mode')),
1517 1522 ] + walkopts + commitopts + commitopts2 + subrepoopts,
1518 1523 _('[OPTION]... [FILE]...'),
1519 1524 inferrepo=True)
1520 1525 def commit(ui, repo, *pats, **opts):
1521 1526 """commit the specified files or all outstanding changes
1522 1527
1523 1528 Commit changes to the given files into the repository. Unlike a
1524 1529 centralized SCM, this operation is a local operation. See
1525 1530 :hg:`push` for a way to actively distribute your changes.
1526 1531
1527 1532 If a list of files is omitted, all changes reported by :hg:`status`
1528 1533 will be committed.
1529 1534
1530 1535 If you are committing the result of a merge, do not provide any
1531 1536 filenames or -I/-X filters.
1532 1537
1533 1538 If no commit message is specified, Mercurial starts your
1534 1539 configured editor where you can enter a message. In case your
1535 1540 commit fails, you will find a backup of your message in
1536 1541 ``.hg/last-message.txt``.
1537 1542
1538 1543 The --close-branch flag can be used to mark the current branch
1539 1544 head closed. When all heads of a branch are closed, the branch
1540 1545 will be considered closed and no longer listed.
1541 1546
1542 1547 The --amend flag can be used to amend the parent of the
1543 1548 working directory with a new commit that contains the changes
1544 1549 in the parent in addition to those currently reported by :hg:`status`,
1545 1550 if there are any. The old commit is stored in a backup bundle in
1546 1551 ``.hg/strip-backup`` (see :hg:`help bundle` and :hg:`help unbundle`
1547 1552 on how to restore it).
1548 1553
1549 1554 Message, user and date are taken from the amended commit unless
1550 1555 specified. When a message isn't specified on the command line,
1551 1556 the editor will open with the message of the amended commit.
1552 1557
1553 1558 It is not possible to amend public changesets (see :hg:`help phases`)
1554 1559 or changesets that have children.
1555 1560
1556 1561 See :hg:`help dates` for a list of formats valid for -d/--date.
1557 1562
1558 1563 Returns 0 on success, 1 if nothing changed.
1559 1564
1560 1565 .. container:: verbose
1561 1566
1562 1567 Examples:
1563 1568
1564 1569 - commit all files ending in .py::
1565 1570
1566 1571 hg commit --include "set:**.py"
1567 1572
1568 1573 - commit all non-binary files::
1569 1574
1570 1575 hg commit --exclude "set:binary()"
1571 1576
1572 1577 - amend the current commit and set the date to now::
1573 1578
1574 1579 hg commit --amend --date now
1575 1580 """
1576 1581 wlock = lock = None
1577 1582 try:
1578 1583 wlock = repo.wlock()
1579 1584 lock = repo.lock()
1580 1585 return _docommit(ui, repo, *pats, **opts)
1581 1586 finally:
1582 1587 release(lock, wlock)
1583 1588
1584 1589 def _docommit(ui, repo, *pats, **opts):
1585 1590 if opts.get(r'interactive'):
1586 1591 opts.pop(r'interactive')
1587 1592 ret = cmdutil.dorecord(ui, repo, commit, None, False,
1588 1593 cmdutil.recordfilter, *pats,
1589 1594 **opts)
1590 1595 # ret can be 0 (no changes to record) or the value returned by
1591 1596 # commit(), 1 if nothing changed or None on success.
1592 1597 return 1 if ret == 0 else ret
1593 1598
1594 1599 opts = pycompat.byteskwargs(opts)
1595 1600 if opts.get('subrepos'):
1596 1601 if opts.get('amend'):
1597 1602 raise error.Abort(_('cannot amend with --subrepos'))
1598 1603 # Let --subrepos on the command line override config setting.
1599 1604 ui.setconfig('ui', 'commitsubrepos', True, 'commit')
1600 1605
1601 1606 cmdutil.checkunfinished(repo, commit=True)
1602 1607
1603 1608 branch = repo[None].branch()
1604 1609 bheads = repo.branchheads(branch)
1605 1610
1606 1611 extra = {}
1607 1612 if opts.get('close_branch'):
1608 1613 extra['close'] = 1
1609 1614
1610 1615 if not bheads:
1611 1616 raise error.Abort(_('can only close branch heads'))
1612 1617 elif opts.get('amend'):
1613 1618 if repo[None].parents()[0].p1().branch() != branch and \
1614 1619 repo[None].parents()[0].p2().branch() != branch:
1615 1620 raise error.Abort(_('can only close branch heads'))
1616 1621
1617 1622 if opts.get('amend'):
1618 1623 if ui.configbool('ui', 'commitsubrepos'):
1619 1624 raise error.Abort(_('cannot amend with ui.commitsubrepos enabled'))
1620 1625
1621 1626 old = repo['.']
1622 1627 if not old.mutable():
1623 1628 raise error.Abort(_('cannot amend public changesets'))
1624 1629 if len(repo[None].parents()) > 1:
1625 1630 raise error.Abort(_('cannot amend while merging'))
1626 1631 allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt)
1627 1632 if not allowunstable and old.children():
1628 1633 raise error.Abort(_('cannot amend changeset with children'))
1629 1634
1630 1635 # Currently histedit gets confused if an amend happens while histedit
1631 1636 # is in progress. Since we have a checkunfinished command, we are
1632 1637 # temporarily honoring it.
1633 1638 #
1634 1639 # Note: eventually this guard will be removed. Please do not expect
1635 1640 # this behavior to remain.
1636 1641 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1637 1642 cmdutil.checkunfinished(repo)
1638 1643
1639 1644 # commitfunc is used only for temporary amend commit by cmdutil.amend
1640 1645 def commitfunc(ui, repo, message, match, opts):
1641 1646 return repo.commit(message,
1642 1647 opts.get('user') or old.user(),
1643 1648 opts.get('date') or old.date(),
1644 1649 match,
1645 1650 extra=extra)
1646 1651
1647 1652 node = cmdutil.amend(ui, repo, commitfunc, old, extra, pats, opts)
1648 1653 if node == old.node():
1649 1654 ui.status(_("nothing changed\n"))
1650 1655 return 1
1651 1656 else:
1652 1657 def commitfunc(ui, repo, message, match, opts):
1653 1658 overrides = {}
1654 1659 if opts.get('secret'):
1655 1660 overrides[('phases', 'new-commit')] = 'secret'
1656 1661
1657 1662 baseui = repo.baseui
1658 1663 with baseui.configoverride(overrides, 'commit'):
1659 1664 with ui.configoverride(overrides, 'commit'):
1660 1665 editform = cmdutil.mergeeditform(repo[None],
1661 1666 'commit.normal')
1662 1667 editor = cmdutil.getcommiteditor(
1663 1668 editform=editform, **pycompat.strkwargs(opts))
1664 1669 return repo.commit(message,
1665 1670 opts.get('user'),
1666 1671 opts.get('date'),
1667 1672 match,
1668 1673 editor=editor,
1669 1674 extra=extra)
1670 1675
1671 1676 node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
1672 1677
1673 1678 if not node:
1674 1679 stat = cmdutil.postcommitstatus(repo, pats, opts)
1675 1680 if stat[3]:
1676 1681 ui.status(_("nothing changed (%d missing files, see "
1677 1682 "'hg status')\n") % len(stat[3]))
1678 1683 else:
1679 1684 ui.status(_("nothing changed\n"))
1680 1685 return 1
1681 1686
1682 1687 cmdutil.commitstatus(repo, node, branch, bheads, opts)
1683 1688
1684 1689 @command('config|showconfig|debugconfig',
1685 1690 [('u', 'untrusted', None, _('show untrusted configuration options')),
1686 1691 ('e', 'edit', None, _('edit user config')),
1687 1692 ('l', 'local', None, _('edit repository config')),
1688 1693 ('g', 'global', None, _('edit global config'))] + formatteropts,
1689 1694 _('[-u] [NAME]...'),
1690 1695 optionalrepo=True)
1691 1696 def config(ui, repo, *values, **opts):
1692 1697 """show combined config settings from all hgrc files
1693 1698
1694 1699 With no arguments, print names and values of all config items.
1695 1700
1696 1701 With one argument of the form section.name, print just the value
1697 1702 of that config item.
1698 1703
1699 1704 With multiple arguments, print names and values of all config
1700 1705 items with matching section names.
1701 1706
1702 1707 With --edit, start an editor on the user-level config file. With
1703 1708 --global, edit the system-wide config file. With --local, edit the
1704 1709 repository-level config file.
1705 1710
1706 1711 With --debug, the source (filename and line number) is printed
1707 1712 for each config item.
1708 1713
1709 1714 See :hg:`help config` for more information about config files.
1710 1715
1711 1716 Returns 0 on success, 1 if NAME does not exist.
1712 1717
1713 1718 """
1714 1719
1715 1720 opts = pycompat.byteskwargs(opts)
1716 1721 if opts.get('edit') or opts.get('local') or opts.get('global'):
1717 1722 if opts.get('local') and opts.get('global'):
1718 1723 raise error.Abort(_("can't use --local and --global together"))
1719 1724
1720 1725 if opts.get('local'):
1721 1726 if not repo:
1722 1727 raise error.Abort(_("can't use --local outside a repository"))
1723 1728 paths = [repo.vfs.join('hgrc')]
1724 1729 elif opts.get('global'):
1725 1730 paths = rcutil.systemrcpath()
1726 1731 else:
1727 1732 paths = rcutil.userrcpath()
1728 1733
1729 1734 for f in paths:
1730 1735 if os.path.exists(f):
1731 1736 break
1732 1737 else:
1733 1738 if opts.get('global'):
1734 1739 samplehgrc = uimod.samplehgrcs['global']
1735 1740 elif opts.get('local'):
1736 1741 samplehgrc = uimod.samplehgrcs['local']
1737 1742 else:
1738 1743 samplehgrc = uimod.samplehgrcs['user']
1739 1744
1740 1745 f = paths[0]
1741 1746 fp = open(f, "w")
1742 1747 fp.write(samplehgrc)
1743 1748 fp.close()
1744 1749
1745 1750 editor = ui.geteditor()
1746 1751 ui.system("%s \"%s\"" % (editor, f),
1747 1752 onerr=error.Abort, errprefix=_("edit failed"),
1748 1753 blockedtag='config_edit')
1749 1754 return
1750 1755 ui.pager('config')
1751 1756 fm = ui.formatter('config', opts)
1752 1757 for t, f in rcutil.rccomponents():
1753 1758 if t == 'path':
1754 1759 ui.debug('read config from: %s\n' % f)
1755 1760 elif t == 'items':
1756 1761 for section, name, value, source in f:
1757 1762 ui.debug('set config by: %s\n' % source)
1758 1763 else:
1759 1764 raise error.ProgrammingError('unknown rctype: %s' % t)
1760 1765 untrusted = bool(opts.get('untrusted'))
1761 1766 if values:
1762 1767 sections = [v for v in values if '.' not in v]
1763 1768 items = [v for v in values if '.' in v]
1764 1769 if len(items) > 1 or items and sections:
1765 1770 raise error.Abort(_('only one config item permitted'))
1766 1771 matched = False
1767 1772 for section, name, value in ui.walkconfig(untrusted=untrusted):
1768 1773 source = ui.configsource(section, name, untrusted)
1769 1774 value = pycompat.bytestr(value)
1770 1775 if fm.isplain():
1771 1776 source = source or 'none'
1772 1777 value = value.replace('\n', '\\n')
1773 1778 entryname = section + '.' + name
1774 1779 if values:
1775 1780 for v in values:
1776 1781 if v == section:
1777 1782 fm.startitem()
1778 1783 fm.condwrite(ui.debugflag, 'source', '%s: ', source)
1779 1784 fm.write('name value', '%s=%s\n', entryname, value)
1780 1785 matched = True
1781 1786 elif v == entryname:
1782 1787 fm.startitem()
1783 1788 fm.condwrite(ui.debugflag, 'source', '%s: ', source)
1784 1789 fm.write('value', '%s\n', value)
1785 1790 fm.data(name=entryname)
1786 1791 matched = True
1787 1792 else:
1788 1793 fm.startitem()
1789 1794 fm.condwrite(ui.debugflag, 'source', '%s: ', source)
1790 1795 fm.write('name value', '%s=%s\n', entryname, value)
1791 1796 matched = True
1792 1797 fm.end()
1793 1798 if matched:
1794 1799 return 0
1795 1800 return 1
1796 1801
1797 1802 @command('copy|cp',
1798 1803 [('A', 'after', None, _('record a copy that has already occurred')),
1799 1804 ('f', 'force', None, _('forcibly copy over an existing managed file')),
1800 1805 ] + walkopts + dryrunopts,
1801 1806 _('[OPTION]... [SOURCE]... DEST'))
1802 1807 def copy(ui, repo, *pats, **opts):
1803 1808 """mark files as copied for the next commit
1804 1809
1805 1810 Mark dest as having copies of source files. If dest is a
1806 1811 directory, copies are put in that directory. If dest is a file,
1807 1812 the source must be a single file.
1808 1813
1809 1814 By default, this command copies the contents of files as they
1810 1815 exist in the working directory. If invoked with -A/--after, the
1811 1816 operation is recorded, but no copying is performed.
1812 1817
1813 1818 This command takes effect with the next commit. To undo a copy
1814 1819 before that, see :hg:`revert`.
1815 1820
1816 1821 Returns 0 on success, 1 if errors are encountered.
1817 1822 """
1818 1823 opts = pycompat.byteskwargs(opts)
1819 1824 with repo.wlock(False):
1820 1825 return cmdutil.copy(ui, repo, pats, opts)
1821 1826
1822 1827 @command('debugcommands', [], _('[COMMAND]'), norepo=True)
1823 1828 def debugcommands(ui, cmd='', *args):
1824 1829 """list all available commands and options"""
1825 1830 for cmd, vals in sorted(table.iteritems()):
1826 1831 cmd = cmd.split('|')[0].strip('^')
1827 1832 opts = ', '.join([i[1] for i in vals[1]])
1828 1833 ui.write('%s: %s\n' % (cmd, opts))
1829 1834
1830 1835 @command('debugcomplete',
1831 1836 [('o', 'options', None, _('show the command options'))],
1832 1837 _('[-o] CMD'),
1833 1838 norepo=True)
1834 1839 def debugcomplete(ui, cmd='', **opts):
1835 1840 """returns the completion list associated with the given command"""
1836 1841
1837 1842 if opts.get('options'):
1838 1843 options = []
1839 1844 otables = [globalopts]
1840 1845 if cmd:
1841 1846 aliases, entry = cmdutil.findcmd(cmd, table, False)
1842 1847 otables.append(entry[1])
1843 1848 for t in otables:
1844 1849 for o in t:
1845 1850 if "(DEPRECATED)" in o[3]:
1846 1851 continue
1847 1852 if o[0]:
1848 1853 options.append('-%s' % o[0])
1849 1854 options.append('--%s' % o[1])
1850 1855 ui.write("%s\n" % "\n".join(options))
1851 1856 return
1852 1857
1853 1858 cmdlist, unused_allcmds = cmdutil.findpossible(cmd, table)
1854 1859 if ui.verbose:
1855 1860 cmdlist = [' '.join(c[0]) for c in cmdlist.values()]
1856 1861 ui.write("%s\n" % "\n".join(sorted(cmdlist)))
1857 1862
1858 1863 @command('^diff',
1859 1864 [('r', 'rev', [], _('revision'), _('REV')),
1860 1865 ('c', 'change', '', _('change made by revision'), _('REV'))
1861 1866 ] + diffopts + diffopts2 + walkopts + subrepoopts,
1862 1867 _('[OPTION]... ([-c REV] | [-r REV1 [-r REV2]]) [FILE]...'),
1863 1868 inferrepo=True)
1864 1869 def diff(ui, repo, *pats, **opts):
1865 1870 """diff repository (or selected files)
1866 1871
1867 1872 Show differences between revisions for the specified files.
1868 1873
1869 1874 Differences between files are shown using the unified diff format.
1870 1875
1871 1876 .. note::
1872 1877
1873 1878 :hg:`diff` may generate unexpected results for merges, as it will
1874 1879 default to comparing against the working directory's first
1875 1880 parent changeset if no revisions are specified.
1876 1881
1877 1882 When two revision arguments are given, then changes are shown
1878 1883 between those revisions. If only one revision is specified then
1879 1884 that revision is compared to the working directory, and, when no
1880 1885 revisions are specified, the working directory files are compared
1881 1886 to its first parent.
1882 1887
1883 1888 Alternatively you can specify -c/--change with a revision to see
1884 1889 the changes in that changeset relative to its first parent.
1885 1890
1886 1891 Without the -a/--text option, diff will avoid generating diffs of
1887 1892 files it detects as binary. With -a, diff will generate a diff
1888 1893 anyway, probably with undesirable results.
1889 1894
1890 1895 Use the -g/--git option to generate diffs in the git extended diff
1891 1896 format. For more information, read :hg:`help diffs`.
1892 1897
1893 1898 .. container:: verbose
1894 1899
1895 1900 Examples:
1896 1901
1897 1902 - compare a file in the current working directory to its parent::
1898 1903
1899 1904 hg diff foo.c
1900 1905
1901 1906 - compare two historical versions of a directory, with rename info::
1902 1907
1903 1908 hg diff --git -r 1.0:1.2 lib/
1904 1909
1905 1910 - get change stats relative to the last change on some date::
1906 1911
1907 1912 hg diff --stat -r "date('may 2')"
1908 1913
1909 1914 - diff all newly-added files that contain a keyword::
1910 1915
1911 1916 hg diff "set:added() and grep(GNU)"
1912 1917
1913 1918 - compare a revision and its parents::
1914 1919
1915 1920 hg diff -c 9353 # compare against first parent
1916 1921 hg diff -r 9353^:9353 # same using revset syntax
1917 1922 hg diff -r 9353^2:9353 # compare against the second parent
1918 1923
1919 1924 Returns 0 on success.
1920 1925 """
1921 1926
1922 1927 opts = pycompat.byteskwargs(opts)
1923 1928 revs = opts.get('rev')
1924 1929 change = opts.get('change')
1925 1930 stat = opts.get('stat')
1926 1931 reverse = opts.get('reverse')
1927 1932
1928 1933 if revs and change:
1929 1934 msg = _('cannot specify --rev and --change at the same time')
1930 1935 raise error.Abort(msg)
1931 1936 elif change:
1932 1937 node2 = scmutil.revsingle(repo, change, None).node()
1933 1938 node1 = repo[node2].p1().node()
1934 1939 else:
1935 1940 node1, node2 = scmutil.revpair(repo, revs)
1936 1941
1937 1942 if reverse:
1938 1943 node1, node2 = node2, node1
1939 1944
1940 1945 diffopts = patch.diffallopts(ui, opts)
1941 1946 m = scmutil.match(repo[node2], pats, opts)
1942 1947 ui.pager('diff')
1943 1948 cmdutil.diffordiffstat(ui, repo, diffopts, node1, node2, m, stat=stat,
1944 1949 listsubrepos=opts.get('subrepos'),
1945 1950 root=opts.get('root'))
1946 1951
1947 1952 @command('^export',
1948 1953 [('o', 'output', '',
1949 1954 _('print output to file with formatted name'), _('FORMAT')),
1950 1955 ('', 'switch-parent', None, _('diff against the second parent')),
1951 1956 ('r', 'rev', [], _('revisions to export'), _('REV')),
1952 1957 ] + diffopts,
1953 1958 _('[OPTION]... [-o OUTFILESPEC] [-r] [REV]...'))
1954 1959 def export(ui, repo, *changesets, **opts):
1955 1960 """dump the header and diffs for one or more changesets
1956 1961
1957 1962 Print the changeset header and diffs for one or more revisions.
1958 1963 If no revision is given, the parent of the working directory is used.
1959 1964
1960 1965 The information shown in the changeset header is: author, date,
1961 1966 branch name (if non-default), changeset hash, parent(s) and commit
1962 1967 comment.
1963 1968
1964 1969 .. note::
1965 1970
1966 1971 :hg:`export` may generate unexpected diff output for merge
1967 1972 changesets, as it will compare the merge changeset against its
1968 1973 first parent only.
1969 1974
1970 1975 Output may be to a file, in which case the name of the file is
1971 1976 given using a format string. The formatting rules are as follows:
1972 1977
1973 1978 :``%%``: literal "%" character
1974 1979 :``%H``: changeset hash (40 hexadecimal digits)
1975 1980 :``%N``: number of patches being generated
1976 1981 :``%R``: changeset revision number
1977 1982 :``%b``: basename of the exporting repository
1978 1983 :``%h``: short-form changeset hash (12 hexadecimal digits)
1979 1984 :``%m``: first line of the commit message (only alphanumeric characters)
1980 1985 :``%n``: zero-padded sequence number, starting at 1
1981 1986 :``%r``: zero-padded changeset revision number
1982 1987
1983 1988 Without the -a/--text option, export will avoid generating diffs
1984 1989 of files it detects as binary. With -a, export will generate a
1985 1990 diff anyway, probably with undesirable results.
1986 1991
1987 1992 Use the -g/--git option to generate diffs in the git extended diff
1988 1993 format. See :hg:`help diffs` for more information.
1989 1994
1990 1995 With the --switch-parent option, the diff will be against the
1991 1996 second parent. It can be useful to review a merge.
1992 1997
1993 1998 .. container:: verbose
1994 1999
1995 2000 Examples:
1996 2001
1997 2002 - use export and import to transplant a bugfix to the current
1998 2003 branch::
1999 2004
2000 2005 hg export -r 9353 | hg import -
2001 2006
2002 2007 - export all the changesets between two revisions to a file with
2003 2008 rename information::
2004 2009
2005 2010 hg export --git -r 123:150 > changes.txt
2006 2011
2007 2012 - split outgoing changes into a series of patches with
2008 2013 descriptive names::
2009 2014
2010 2015 hg export -r "outgoing()" -o "%n-%m.patch"
2011 2016
2012 2017 Returns 0 on success.
2013 2018 """
2014 2019 opts = pycompat.byteskwargs(opts)
2015 2020 changesets += tuple(opts.get('rev', []))
2016 2021 if not changesets:
2017 2022 changesets = ['.']
2018 2023 revs = scmutil.revrange(repo, changesets)
2019 2024 if not revs:
2020 2025 raise error.Abort(_("export requires at least one changeset"))
2021 2026 if len(revs) > 1:
2022 2027 ui.note(_('exporting patches:\n'))
2023 2028 else:
2024 2029 ui.note(_('exporting patch:\n'))
2025 2030 ui.pager('export')
2026 2031 cmdutil.export(repo, revs, fntemplate=opts.get('output'),
2027 2032 switch_parent=opts.get('switch_parent'),
2028 2033 opts=patch.diffallopts(ui, opts))
2029 2034
2030 2035 @command('files',
2031 2036 [('r', 'rev', '', _('search the repository as it is in REV'), _('REV')),
2032 2037 ('0', 'print0', None, _('end filenames with NUL, for use with xargs')),
2033 2038 ] + walkopts + formatteropts + subrepoopts,
2034 2039 _('[OPTION]... [FILE]...'))
2035 2040 def files(ui, repo, *pats, **opts):
2036 2041 """list tracked files
2037 2042
2038 2043 Print files under Mercurial control in the working directory or
2039 2044 specified revision for given files (excluding removed files).
2040 2045 Files can be specified as filenames or filesets.
2041 2046
2042 2047 If no files are given to match, this command prints the names
2043 2048 of all files under Mercurial control.
2044 2049
2045 2050 .. container:: verbose
2046 2051
2047 2052 Examples:
2048 2053
2049 2054 - list all files under the current directory::
2050 2055
2051 2056 hg files .
2052 2057
2053 2058 - shows sizes and flags for current revision::
2054 2059
2055 2060 hg files -vr .
2056 2061
2057 2062 - list all files named README::
2058 2063
2059 2064 hg files -I "**/README"
2060 2065
2061 2066 - list all binary files::
2062 2067
2063 2068 hg files "set:binary()"
2064 2069
2065 2070 - find files containing a regular expression::
2066 2071
2067 2072 hg files "set:grep('bob')"
2068 2073
2069 2074 - search tracked file contents with xargs and grep::
2070 2075
2071 2076 hg files -0 | xargs -0 grep foo
2072 2077
2073 2078 See :hg:`help patterns` and :hg:`help filesets` for more information
2074 2079 on specifying file patterns.
2075 2080
2076 2081 Returns 0 if a match is found, 1 otherwise.
2077 2082
2078 2083 """
2079 2084
2080 2085 opts = pycompat.byteskwargs(opts)
2081 2086 ctx = scmutil.revsingle(repo, opts.get('rev'), None)
2082 2087
2083 2088 end = '\n'
2084 2089 if opts.get('print0'):
2085 2090 end = '\0'
2086 2091 fmt = '%s' + end
2087 2092
2088 2093 m = scmutil.match(ctx, pats, opts)
2089 2094 ui.pager('files')
2090 2095 with ui.formatter('files', opts) as fm:
2091 2096 return cmdutil.files(ui, ctx, m, fm, fmt, opts.get('subrepos'))
2092 2097
2093 2098 @command('^forget', walkopts, _('[OPTION]... FILE...'), inferrepo=True)
2094 2099 def forget(ui, repo, *pats, **opts):
2095 2100 """forget the specified files on the next commit
2096 2101
2097 2102 Mark the specified files so they will no longer be tracked
2098 2103 after the next commit.
2099 2104
2100 2105 This only removes files from the current branch, not from the
2101 2106 entire project history, and it does not delete them from the
2102 2107 working directory.
2103 2108
2104 2109 To delete the file from the working directory, see :hg:`remove`.
2105 2110
2106 2111 To undo a forget before the next commit, see :hg:`add`.
2107 2112
2108 2113 .. container:: verbose
2109 2114
2110 2115 Examples:
2111 2116
2112 2117 - forget newly-added binary files::
2113 2118
2114 2119 hg forget "set:added() and binary()"
2115 2120
2116 2121 - forget files that would be excluded by .hgignore::
2117 2122
2118 2123 hg forget "set:hgignore()"
2119 2124
2120 2125 Returns 0 on success.
2121 2126 """
2122 2127
2123 2128 opts = pycompat.byteskwargs(opts)
2124 2129 if not pats:
2125 2130 raise error.Abort(_('no files specified'))
2126 2131
2127 2132 m = scmutil.match(repo[None], pats, opts)
2128 2133 rejected = cmdutil.forget(ui, repo, m, prefix="", explicitonly=False)[0]
2129 2134 return rejected and 1 or 0
2130 2135
2131 2136 @command(
2132 2137 'graft',
2133 2138 [('r', 'rev', [], _('revisions to graft'), _('REV')),
2134 2139 ('c', 'continue', False, _('resume interrupted graft')),
2135 2140 ('e', 'edit', False, _('invoke editor on commit messages')),
2136 2141 ('', 'log', None, _('append graft info to log message')),
2137 2142 ('f', 'force', False, _('force graft')),
2138 2143 ('D', 'currentdate', False,
2139 2144 _('record the current date as commit date')),
2140 2145 ('U', 'currentuser', False,
2141 2146 _('record the current user as committer'), _('DATE'))]
2142 2147 + commitopts2 + mergetoolopts + dryrunopts,
2143 2148 _('[OPTION]... [-r REV]... REV...'))
2144 2149 def graft(ui, repo, *revs, **opts):
2145 2150 '''copy changes from other branches onto the current branch
2146 2151
2147 2152 This command uses Mercurial's merge logic to copy individual
2148 2153 changes from other branches without merging branches in the
2149 2154 history graph. This is sometimes known as 'backporting' or
2150 2155 'cherry-picking'. By default, graft will copy user, date, and
2151 2156 description from the source changesets.
2152 2157
2153 2158 Changesets that are ancestors of the current revision, that have
2154 2159 already been grafted, or that are merges will be skipped.
2155 2160
2156 2161 If --log is specified, log messages will have a comment appended
2157 2162 of the form::
2158 2163
2159 2164 (grafted from CHANGESETHASH)
2160 2165
2161 2166 If --force is specified, revisions will be grafted even if they
2162 2167 are already ancestors of or have been grafted to the destination.
2163 2168 This is useful when the revisions have since been backed out.
2164 2169
2165 2170 If a graft merge results in conflicts, the graft process is
2166 2171 interrupted so that the current merge can be manually resolved.
2167 2172 Once all conflicts are addressed, the graft process can be
2168 2173 continued with the -c/--continue option.
2169 2174
2170 2175 .. note::
2171 2176
2172 2177 The -c/--continue option does not reapply earlier options, except
2173 2178 for --force.
2174 2179
2175 2180 .. container:: verbose
2176 2181
2177 2182 Examples:
2178 2183
2179 2184 - copy a single change to the stable branch and edit its description::
2180 2185
2181 2186 hg update stable
2182 2187 hg graft --edit 9393
2183 2188
2184 2189 - graft a range of changesets with one exception, updating dates::
2185 2190
2186 2191 hg graft -D "2085::2093 and not 2091"
2187 2192
2188 2193 - continue a graft after resolving conflicts::
2189 2194
2190 2195 hg graft -c
2191 2196
2192 2197 - show the source of a grafted changeset::
2193 2198
2194 2199 hg log --debug -r .
2195 2200
2196 2201 - show revisions sorted by date::
2197 2202
2198 2203 hg log -r "sort(all(), date)"
2199 2204
2200 2205 See :hg:`help revisions` for more about specifying revisions.
2201 2206
2202 2207 Returns 0 on successful completion.
2203 2208 '''
2204 2209 with repo.wlock():
2205 2210 return _dograft(ui, repo, *revs, **opts)
2206 2211
2207 2212 def _dograft(ui, repo, *revs, **opts):
2208 2213 opts = pycompat.byteskwargs(opts)
2209 2214 if revs and opts.get('rev'):
2210 2215 ui.warn(_('warning: inconsistent use of --rev might give unexpected '
2211 2216 'revision ordering!\n'))
2212 2217
2213 2218 revs = list(revs)
2214 2219 revs.extend(opts.get('rev'))
2215 2220
2216 2221 if not opts.get('user') and opts.get('currentuser'):
2217 2222 opts['user'] = ui.username()
2218 2223 if not opts.get('date') and opts.get('currentdate'):
2219 2224 opts['date'] = "%d %d" % util.makedate()
2220 2225
2221 2226 editor = cmdutil.getcommiteditor(editform='graft',
2222 2227 **pycompat.strkwargs(opts))
2223 2228
2224 2229 cont = False
2225 2230 if opts.get('continue'):
2226 2231 cont = True
2227 2232 if revs:
2228 2233 raise error.Abort(_("can't specify --continue and revisions"))
2229 2234 # read in unfinished revisions
2230 2235 try:
2231 2236 nodes = repo.vfs.read('graftstate').splitlines()
2232 2237 revs = [repo[node].rev() for node in nodes]
2233 2238 except IOError as inst:
2234 2239 if inst.errno != errno.ENOENT:
2235 2240 raise
2236 2241 cmdutil.wrongtooltocontinue(repo, _('graft'))
2237 2242 else:
2238 2243 cmdutil.checkunfinished(repo)
2239 2244 cmdutil.bailifchanged(repo)
2240 2245 if not revs:
2241 2246 raise error.Abort(_('no revisions specified'))
2242 2247 revs = scmutil.revrange(repo, revs)
2243 2248
2244 2249 skipped = set()
2245 2250 # check for merges
2246 2251 for rev in repo.revs('%ld and merge()', revs):
2247 2252 ui.warn(_('skipping ungraftable merge revision %s\n') % rev)
2248 2253 skipped.add(rev)
2249 2254 revs = [r for r in revs if r not in skipped]
2250 2255 if not revs:
2251 2256 return -1
2252 2257
2253 2258 # Don't check in the --continue case, in effect retaining --force across
2254 2259 # --continues. That's because without --force, any revisions we decided to
2255 2260 # skip would have been filtered out here, so they wouldn't have made their
2256 2261 # way to the graftstate. With --force, any revisions we would have otherwise
2257 2262 # skipped would not have been filtered out, and if they hadn't been applied
2258 2263 # already, they'd have been in the graftstate.
2259 2264 if not (cont or opts.get('force')):
2260 2265 # check for ancestors of dest branch
2261 2266 crev = repo['.'].rev()
2262 2267 ancestors = repo.changelog.ancestors([crev], inclusive=True)
2263 2268 # XXX make this lazy in the future
2264 2269 # don't mutate while iterating, create a copy
2265 2270 for rev in list(revs):
2266 2271 if rev in ancestors:
2267 2272 ui.warn(_('skipping ancestor revision %d:%s\n') %
2268 2273 (rev, repo[rev]))
2269 2274 # XXX remove on list is slow
2270 2275 revs.remove(rev)
2271 2276 if not revs:
2272 2277 return -1
2273 2278
2274 2279 # analyze revs for earlier grafts
2275 2280 ids = {}
2276 2281 for ctx in repo.set("%ld", revs):
2277 2282 ids[ctx.hex()] = ctx.rev()
2278 2283 n = ctx.extra().get('source')
2279 2284 if n:
2280 2285 ids[n] = ctx.rev()
2281 2286
2282 2287 # check ancestors for earlier grafts
2283 2288 ui.debug('scanning for duplicate grafts\n')
2284 2289
2285 2290 # The only changesets we can be sure doesn't contain grafts of any
2286 2291 # revs, are the ones that are common ancestors of *all* revs:
2287 2292 for rev in repo.revs('only(%d,ancestor(%ld))', crev, revs):
2288 2293 ctx = repo[rev]
2289 2294 n = ctx.extra().get('source')
2290 2295 if n in ids:
2291 2296 try:
2292 2297 r = repo[n].rev()
2293 2298 except error.RepoLookupError:
2294 2299 r = None
2295 2300 if r in revs:
2296 2301 ui.warn(_('skipping revision %d:%s '
2297 2302 '(already grafted to %d:%s)\n')
2298 2303 % (r, repo[r], rev, ctx))
2299 2304 revs.remove(r)
2300 2305 elif ids[n] in revs:
2301 2306 if r is None:
2302 2307 ui.warn(_('skipping already grafted revision %d:%s '
2303 2308 '(%d:%s also has unknown origin %s)\n')
2304 2309 % (ids[n], repo[ids[n]], rev, ctx, n[:12]))
2305 2310 else:
2306 2311 ui.warn(_('skipping already grafted revision %d:%s '
2307 2312 '(%d:%s also has origin %d:%s)\n')
2308 2313 % (ids[n], repo[ids[n]], rev, ctx, r, n[:12]))
2309 2314 revs.remove(ids[n])
2310 2315 elif ctx.hex() in ids:
2311 2316 r = ids[ctx.hex()]
2312 2317 ui.warn(_('skipping already grafted revision %d:%s '
2313 2318 '(was grafted from %d:%s)\n') %
2314 2319 (r, repo[r], rev, ctx))
2315 2320 revs.remove(r)
2316 2321 if not revs:
2317 2322 return -1
2318 2323
2319 2324 for pos, ctx in enumerate(repo.set("%ld", revs)):
2320 2325 desc = '%d:%s "%s"' % (ctx.rev(), ctx,
2321 2326 ctx.description().split('\n', 1)[0])
2322 2327 names = repo.nodetags(ctx.node()) + repo.nodebookmarks(ctx.node())
2323 2328 if names:
2324 2329 desc += ' (%s)' % ' '.join(names)
2325 2330 ui.status(_('grafting %s\n') % desc)
2326 2331 if opts.get('dry_run'):
2327 2332 continue
2328 2333
2329 2334 source = ctx.extra().get('source')
2330 2335 extra = {}
2331 2336 if source:
2332 2337 extra['source'] = source
2333 2338 extra['intermediate-source'] = ctx.hex()
2334 2339 else:
2335 2340 extra['source'] = ctx.hex()
2336 2341 user = ctx.user()
2337 2342 if opts.get('user'):
2338 2343 user = opts['user']
2339 2344 date = ctx.date()
2340 2345 if opts.get('date'):
2341 2346 date = opts['date']
2342 2347 message = ctx.description()
2343 2348 if opts.get('log'):
2344 2349 message += '\n(grafted from %s)' % ctx.hex()
2345 2350
2346 2351 # we don't merge the first commit when continuing
2347 2352 if not cont:
2348 2353 # perform the graft merge with p1(rev) as 'ancestor'
2349 2354 try:
2350 2355 # ui.forcemerge is an internal variable, do not document
2351 2356 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
2352 2357 'graft')
2353 2358 stats = mergemod.graft(repo, ctx, ctx.p1(),
2354 2359 ['local', 'graft'])
2355 2360 finally:
2356 2361 repo.ui.setconfig('ui', 'forcemerge', '', 'graft')
2357 2362 # report any conflicts
2358 2363 if stats and stats[3] > 0:
2359 2364 # write out state for --continue
2360 2365 nodelines = [repo[rev].hex() + "\n" for rev in revs[pos:]]
2361 2366 repo.vfs.write('graftstate', ''.join(nodelines))
2362 2367 extra = ''
2363 2368 if opts.get('user'):
2364 2369 extra += ' --user %s' % util.shellquote(opts['user'])
2365 2370 if opts.get('date'):
2366 2371 extra += ' --date %s' % util.shellquote(opts['date'])
2367 2372 if opts.get('log'):
2368 2373 extra += ' --log'
2369 2374 hint=_("use 'hg resolve' and 'hg graft --continue%s'") % extra
2370 2375 raise error.Abort(
2371 2376 _("unresolved conflicts, can't continue"),
2372 2377 hint=hint)
2373 2378 else:
2374 2379 cont = False
2375 2380
2376 2381 # commit
2377 2382 node = repo.commit(text=message, user=user,
2378 2383 date=date, extra=extra, editor=editor)
2379 2384 if node is None:
2380 2385 ui.warn(
2381 2386 _('note: graft of %d:%s created no changes to commit\n') %
2382 2387 (ctx.rev(), ctx))
2383 2388
2384 2389 # remove state when we complete successfully
2385 2390 if not opts.get('dry_run'):
2386 2391 repo.vfs.unlinkpath('graftstate', ignoremissing=True)
2387 2392
2388 2393 return 0
2389 2394
2390 2395 @command('grep',
2391 2396 [('0', 'print0', None, _('end fields with NUL')),
2392 2397 ('', 'all', None, _('print all revisions that match')),
2393 2398 ('a', 'text', None, _('treat all files as text')),
2394 2399 ('f', 'follow', None,
2395 2400 _('follow changeset history,'
2396 2401 ' or file history across copies and renames')),
2397 2402 ('i', 'ignore-case', None, _('ignore case when matching')),
2398 2403 ('l', 'files-with-matches', None,
2399 2404 _('print only filenames and revisions that match')),
2400 2405 ('n', 'line-number', None, _('print matching line numbers')),
2401 2406 ('r', 'rev', [],
2402 2407 _('only search files changed within revision range'), _('REV')),
2403 2408 ('u', 'user', None, _('list the author (long with -v)')),
2404 2409 ('d', 'date', None, _('list the date (short with -q)')),
2405 2410 ] + formatteropts + walkopts,
2406 2411 _('[OPTION]... PATTERN [FILE]...'),
2407 2412 inferrepo=True)
2408 2413 def grep(ui, repo, pattern, *pats, **opts):
2409 2414 """search revision history for a pattern in specified files
2410 2415
2411 2416 Search revision history for a regular expression in the specified
2412 2417 files or the entire project.
2413 2418
2414 2419 By default, grep prints the most recent revision number for each
2415 2420 file in which it finds a match. To get it to print every revision
2416 2421 that contains a change in match status ("-" for a match that becomes
2417 2422 a non-match, or "+" for a non-match that becomes a match), use the
2418 2423 --all flag.
2419 2424
2420 2425 PATTERN can be any Python (roughly Perl-compatible) regular
2421 2426 expression.
2422 2427
2423 2428 If no FILEs are specified (and -f/--follow isn't set), all files in
2424 2429 the repository are searched, including those that don't exist in the
2425 2430 current branch or have been deleted in a prior changeset.
2426 2431
2427 2432 Returns 0 if a match is found, 1 otherwise.
2428 2433 """
2429 2434 opts = pycompat.byteskwargs(opts)
2430 2435 reflags = re.M
2431 2436 if opts.get('ignore_case'):
2432 2437 reflags |= re.I
2433 2438 try:
2434 2439 regexp = util.re.compile(pattern, reflags)
2435 2440 except re.error as inst:
2436 2441 ui.warn(_("grep: invalid match pattern: %s\n") % inst)
2437 2442 return 1
2438 2443 sep, eol = ':', '\n'
2439 2444 if opts.get('print0'):
2440 2445 sep = eol = '\0'
2441 2446
2442 2447 getfile = util.lrucachefunc(repo.file)
2443 2448
2444 2449 def matchlines(body):
2445 2450 begin = 0
2446 2451 linenum = 0
2447 2452 while begin < len(body):
2448 2453 match = regexp.search(body, begin)
2449 2454 if not match:
2450 2455 break
2451 2456 mstart, mend = match.span()
2452 2457 linenum += body.count('\n', begin, mstart) + 1
2453 2458 lstart = body.rfind('\n', begin, mstart) + 1 or begin
2454 2459 begin = body.find('\n', mend) + 1 or len(body) + 1
2455 2460 lend = begin - 1
2456 2461 yield linenum, mstart - lstart, mend - lstart, body[lstart:lend]
2457 2462
2458 2463 class linestate(object):
2459 2464 def __init__(self, line, linenum, colstart, colend):
2460 2465 self.line = line
2461 2466 self.linenum = linenum
2462 2467 self.colstart = colstart
2463 2468 self.colend = colend
2464 2469
2465 2470 def __hash__(self):
2466 2471 return hash((self.linenum, self.line))
2467 2472
2468 2473 def __eq__(self, other):
2469 2474 return self.line == other.line
2470 2475
2471 2476 def findpos(self):
2472 2477 """Iterate all (start, end) indices of matches"""
2473 2478 yield self.colstart, self.colend
2474 2479 p = self.colend
2475 2480 while p < len(self.line):
2476 2481 m = regexp.search(self.line, p)
2477 2482 if not m:
2478 2483 break
2479 2484 yield m.span()
2480 2485 p = m.end()
2481 2486
2482 2487 matches = {}
2483 2488 copies = {}
2484 2489 def grepbody(fn, rev, body):
2485 2490 matches[rev].setdefault(fn, [])
2486 2491 m = matches[rev][fn]
2487 2492 for lnum, cstart, cend, line in matchlines(body):
2488 2493 s = linestate(line, lnum, cstart, cend)
2489 2494 m.append(s)
2490 2495
2491 2496 def difflinestates(a, b):
2492 2497 sm = difflib.SequenceMatcher(None, a, b)
2493 2498 for tag, alo, ahi, blo, bhi in sm.get_opcodes():
2494 2499 if tag == 'insert':
2495 2500 for i in xrange(blo, bhi):
2496 2501 yield ('+', b[i])
2497 2502 elif tag == 'delete':
2498 2503 for i in xrange(alo, ahi):
2499 2504 yield ('-', a[i])
2500 2505 elif tag == 'replace':
2501 2506 for i in xrange(alo, ahi):
2502 2507 yield ('-', a[i])
2503 2508 for i in xrange(blo, bhi):
2504 2509 yield ('+', b[i])
2505 2510
2506 2511 def display(fm, fn, ctx, pstates, states):
2507 2512 rev = ctx.rev()
2508 2513 if fm.isplain():
2509 2514 formatuser = ui.shortuser
2510 2515 else:
2511 2516 formatuser = str
2512 2517 if ui.quiet:
2513 2518 datefmt = '%Y-%m-%d'
2514 2519 else:
2515 2520 datefmt = '%a %b %d %H:%M:%S %Y %1%2'
2516 2521 found = False
2517 2522 @util.cachefunc
2518 2523 def binary():
2519 2524 flog = getfile(fn)
2520 2525 return util.binary(flog.read(ctx.filenode(fn)))
2521 2526
2522 2527 fieldnamemap = {'filename': 'file', 'linenumber': 'line_number'}
2523 2528 if opts.get('all'):
2524 2529 iter = difflinestates(pstates, states)
2525 2530 else:
2526 2531 iter = [('', l) for l in states]
2527 2532 for change, l in iter:
2528 2533 fm.startitem()
2529 2534 fm.data(node=fm.hexfunc(ctx.node()))
2530 2535 cols = [
2531 2536 ('filename', fn, True),
2532 2537 ('rev', rev, True),
2533 2538 ('linenumber', l.linenum, opts.get('line_number')),
2534 2539 ]
2535 2540 if opts.get('all'):
2536 2541 cols.append(('change', change, True))
2537 2542 cols.extend([
2538 2543 ('user', formatuser(ctx.user()), opts.get('user')),
2539 2544 ('date', fm.formatdate(ctx.date(), datefmt), opts.get('date')),
2540 2545 ])
2541 2546 lastcol = next(name for name, data, cond in reversed(cols) if cond)
2542 2547 for name, data, cond in cols:
2543 2548 field = fieldnamemap.get(name, name)
2544 2549 fm.condwrite(cond, field, '%s', data, label='grep.%s' % name)
2545 2550 if cond and name != lastcol:
2546 2551 fm.plain(sep, label='grep.sep')
2547 2552 if not opts.get('files_with_matches'):
2548 2553 fm.plain(sep, label='grep.sep')
2549 2554 if not opts.get('text') and binary():
2550 2555 fm.plain(_(" Binary file matches"))
2551 2556 else:
2552 2557 displaymatches(fm.nested('texts'), l)
2553 2558 fm.plain(eol)
2554 2559 found = True
2555 2560 if opts.get('files_with_matches'):
2556 2561 break
2557 2562 return found
2558 2563
2559 2564 def displaymatches(fm, l):
2560 2565 p = 0
2561 2566 for s, e in l.findpos():
2562 2567 if p < s:
2563 2568 fm.startitem()
2564 2569 fm.write('text', '%s', l.line[p:s])
2565 2570 fm.data(matched=False)
2566 2571 fm.startitem()
2567 2572 fm.write('text', '%s', l.line[s:e], label='grep.match')
2568 2573 fm.data(matched=True)
2569 2574 p = e
2570 2575 if p < len(l.line):
2571 2576 fm.startitem()
2572 2577 fm.write('text', '%s', l.line[p:])
2573 2578 fm.data(matched=False)
2574 2579 fm.end()
2575 2580
2576 2581 skip = {}
2577 2582 revfiles = {}
2578 2583 matchfn = scmutil.match(repo[None], pats, opts)
2579 2584 found = False
2580 2585 follow = opts.get('follow')
2581 2586
2582 2587 def prep(ctx, fns):
2583 2588 rev = ctx.rev()
2584 2589 pctx = ctx.p1()
2585 2590 parent = pctx.rev()
2586 2591 matches.setdefault(rev, {})
2587 2592 matches.setdefault(parent, {})
2588 2593 files = revfiles.setdefault(rev, [])
2589 2594 for fn in fns:
2590 2595 flog = getfile(fn)
2591 2596 try:
2592 2597 fnode = ctx.filenode(fn)
2593 2598 except error.LookupError:
2594 2599 continue
2595 2600
2596 2601 copied = flog.renamed(fnode)
2597 2602 copy = follow and copied and copied[0]
2598 2603 if copy:
2599 2604 copies.setdefault(rev, {})[fn] = copy
2600 2605 if fn in skip:
2601 2606 if copy:
2602 2607 skip[copy] = True
2603 2608 continue
2604 2609 files.append(fn)
2605 2610
2606 2611 if fn not in matches[rev]:
2607 2612 grepbody(fn, rev, flog.read(fnode))
2608 2613
2609 2614 pfn = copy or fn
2610 2615 if pfn not in matches[parent]:
2611 2616 try:
2612 2617 fnode = pctx.filenode(pfn)
2613 2618 grepbody(pfn, parent, flog.read(fnode))
2614 2619 except error.LookupError:
2615 2620 pass
2616 2621
2617 2622 ui.pager('grep')
2618 2623 fm = ui.formatter('grep', opts)
2619 2624 for ctx in cmdutil.walkchangerevs(repo, matchfn, opts, prep):
2620 2625 rev = ctx.rev()
2621 2626 parent = ctx.p1().rev()
2622 2627 for fn in sorted(revfiles.get(rev, [])):
2623 2628 states = matches[rev][fn]
2624 2629 copy = copies.get(rev, {}).get(fn)
2625 2630 if fn in skip:
2626 2631 if copy:
2627 2632 skip[copy] = True
2628 2633 continue
2629 2634 pstates = matches.get(parent, {}).get(copy or fn, [])
2630 2635 if pstates or states:
2631 2636 r = display(fm, fn, ctx, pstates, states)
2632 2637 found = found or r
2633 2638 if r and not opts.get('all'):
2634 2639 skip[fn] = True
2635 2640 if copy:
2636 2641 skip[copy] = True
2637 2642 del matches[rev]
2638 2643 del revfiles[rev]
2639 2644 fm.end()
2640 2645
2641 2646 return not found
2642 2647
2643 2648 @command('heads',
2644 2649 [('r', 'rev', '',
2645 2650 _('show only heads which are descendants of STARTREV'), _('STARTREV')),
2646 2651 ('t', 'topo', False, _('show topological heads only')),
2647 2652 ('a', 'active', False, _('show active branchheads only (DEPRECATED)')),
2648 2653 ('c', 'closed', False, _('show normal and closed branch heads')),
2649 2654 ] + templateopts,
2650 2655 _('[-ct] [-r STARTREV] [REV]...'))
2651 2656 def heads(ui, repo, *branchrevs, **opts):
2652 2657 """show branch heads
2653 2658
2654 2659 With no arguments, show all open branch heads in the repository.
2655 2660 Branch heads are changesets that have no descendants on the
2656 2661 same branch. They are where development generally takes place and
2657 2662 are the usual targets for update and merge operations.
2658 2663
2659 2664 If one or more REVs are given, only open branch heads on the
2660 2665 branches associated with the specified changesets are shown. This
2661 2666 means that you can use :hg:`heads .` to see the heads on the
2662 2667 currently checked-out branch.
2663 2668
2664 2669 If -c/--closed is specified, also show branch heads marked closed
2665 2670 (see :hg:`commit --close-branch`).
2666 2671
2667 2672 If STARTREV is specified, only those heads that are descendants of
2668 2673 STARTREV will be displayed.
2669 2674
2670 2675 If -t/--topo is specified, named branch mechanics will be ignored and only
2671 2676 topological heads (changesets with no children) will be shown.
2672 2677
2673 2678 Returns 0 if matching heads are found, 1 if not.
2674 2679 """
2675 2680
2676 2681 opts = pycompat.byteskwargs(opts)
2677 2682 start = None
2678 2683 if 'rev' in opts:
2679 2684 start = scmutil.revsingle(repo, opts['rev'], None).node()
2680 2685
2681 2686 if opts.get('topo'):
2682 2687 heads = [repo[h] for h in repo.heads(start)]
2683 2688 else:
2684 2689 heads = []
2685 2690 for branch in repo.branchmap():
2686 2691 heads += repo.branchheads(branch, start, opts.get('closed'))
2687 2692 heads = [repo[h] for h in heads]
2688 2693
2689 2694 if branchrevs:
2690 2695 branches = set(repo[br].branch() for br in branchrevs)
2691 2696 heads = [h for h in heads if h.branch() in branches]
2692 2697
2693 2698 if opts.get('active') and branchrevs:
2694 2699 dagheads = repo.heads(start)
2695 2700 heads = [h for h in heads if h.node() in dagheads]
2696 2701
2697 2702 if branchrevs:
2698 2703 haveheads = set(h.branch() for h in heads)
2699 2704 if branches - haveheads:
2700 2705 headless = ', '.join(b for b in branches - haveheads)
2701 2706 msg = _('no open branch heads found on branches %s')
2702 2707 if opts.get('rev'):
2703 2708 msg += _(' (started at %s)') % opts['rev']
2704 2709 ui.warn((msg + '\n') % headless)
2705 2710
2706 2711 if not heads:
2707 2712 return 1
2708 2713
2709 2714 ui.pager('heads')
2710 2715 heads = sorted(heads, key=lambda x: -x.rev())
2711 2716 displayer = cmdutil.show_changeset(ui, repo, opts)
2712 2717 for ctx in heads:
2713 2718 displayer.show(ctx)
2714 2719 displayer.close()
2715 2720
2716 2721 @command('help',
2717 2722 [('e', 'extension', None, _('show only help for extensions')),
2718 2723 ('c', 'command', None, _('show only help for commands')),
2719 2724 ('k', 'keyword', None, _('show topics matching keyword')),
2720 2725 ('s', 'system', [], _('show help for specific platform(s)')),
2721 2726 ],
2722 2727 _('[-ecks] [TOPIC]'),
2723 2728 norepo=True)
2724 2729 def help_(ui, name=None, **opts):
2725 2730 """show help for a given topic or a help overview
2726 2731
2727 2732 With no arguments, print a list of commands with short help messages.
2728 2733
2729 2734 Given a topic, extension, or command name, print help for that
2730 2735 topic.
2731 2736
2732 2737 Returns 0 if successful.
2733 2738 """
2734 2739
2735 2740 keep = opts.get(r'system') or []
2736 2741 if len(keep) == 0:
2737 2742 if pycompat.sysplatform.startswith('win'):
2738 2743 keep.append('windows')
2739 2744 elif pycompat.sysplatform == 'OpenVMS':
2740 2745 keep.append('vms')
2741 2746 elif pycompat.sysplatform == 'plan9':
2742 2747 keep.append('plan9')
2743 2748 else:
2744 2749 keep.append('unix')
2745 2750 keep.append(pycompat.sysplatform.lower())
2746 2751 if ui.verbose:
2747 2752 keep.append('verbose')
2748 2753
2749 2754 commands = sys.modules[__name__]
2750 2755 formatted = help.formattedhelp(ui, commands, name, keep=keep, **opts)
2751 2756 ui.pager('help')
2752 2757 ui.write(formatted)
2753 2758
2754 2759
2755 2760 @command('identify|id',
2756 2761 [('r', 'rev', '',
2757 2762 _('identify the specified revision'), _('REV')),
2758 2763 ('n', 'num', None, _('show local revision number')),
2759 2764 ('i', 'id', None, _('show global revision id')),
2760 2765 ('b', 'branch', None, _('show branch')),
2761 2766 ('t', 'tags', None, _('show tags')),
2762 2767 ('B', 'bookmarks', None, _('show bookmarks')),
2763 2768 ] + remoteopts,
2764 2769 _('[-nibtB] [-r REV] [SOURCE]'),
2765 2770 optionalrepo=True)
2766 2771 def identify(ui, repo, source=None, rev=None,
2767 2772 num=None, id=None, branch=None, tags=None, bookmarks=None, **opts):
2768 2773 """identify the working directory or specified revision
2769 2774
2770 2775 Print a summary identifying the repository state at REV using one or
2771 2776 two parent hash identifiers, followed by a "+" if the working
2772 2777 directory has uncommitted changes, the branch name (if not default),
2773 2778 a list of tags, and a list of bookmarks.
2774 2779
2775 2780 When REV is not given, print a summary of the current state of the
2776 2781 repository.
2777 2782
2778 2783 Specifying a path to a repository root or Mercurial bundle will
2779 2784 cause lookup to operate on that repository/bundle.
2780 2785
2781 2786 .. container:: verbose
2782 2787
2783 2788 Examples:
2784 2789
2785 2790 - generate a build identifier for the working directory::
2786 2791
2787 2792 hg id --id > build-id.dat
2788 2793
2789 2794 - find the revision corresponding to a tag::
2790 2795
2791 2796 hg id -n -r 1.3
2792 2797
2793 2798 - check the most recent revision of a remote repository::
2794 2799
2795 2800 hg id -r tip https://www.mercurial-scm.org/repo/hg/
2796 2801
2797 2802 See :hg:`log` for generating more information about specific revisions,
2798 2803 including full hash identifiers.
2799 2804
2800 2805 Returns 0 if successful.
2801 2806 """
2802 2807
2803 2808 opts = pycompat.byteskwargs(opts)
2804 2809 if not repo and not source:
2805 2810 raise error.Abort(_("there is no Mercurial repository here "
2806 2811 "(.hg not found)"))
2807 2812
2808 2813 if ui.debugflag:
2809 2814 hexfunc = hex
2810 2815 else:
2811 2816 hexfunc = short
2812 2817 default = not (num or id or branch or tags or bookmarks)
2813 2818 output = []
2814 2819 revs = []
2815 2820
2816 2821 if source:
2817 2822 source, branches = hg.parseurl(ui.expandpath(source))
2818 2823 peer = hg.peer(repo or ui, opts, source) # only pass ui when no repo
2819 2824 repo = peer.local()
2820 2825 revs, checkout = hg.addbranchrevs(repo, peer, branches, None)
2821 2826
2822 2827 if not repo:
2823 2828 if num or branch or tags:
2824 2829 raise error.Abort(
2825 2830 _("can't query remote revision number, branch, or tags"))
2826 2831 if not rev and revs:
2827 2832 rev = revs[0]
2828 2833 if not rev:
2829 2834 rev = "tip"
2830 2835
2831 2836 remoterev = peer.lookup(rev)
2832 2837 if default or id:
2833 2838 output = [hexfunc(remoterev)]
2834 2839
2835 2840 def getbms():
2836 2841 bms = []
2837 2842
2838 2843 if 'bookmarks' in peer.listkeys('namespaces'):
2839 2844 hexremoterev = hex(remoterev)
2840 2845 bms = [bm for bm, bmr in peer.listkeys('bookmarks').iteritems()
2841 2846 if bmr == hexremoterev]
2842 2847
2843 2848 return sorted(bms)
2844 2849
2845 2850 if bookmarks:
2846 2851 output.extend(getbms())
2847 2852 elif default and not ui.quiet:
2848 2853 # multiple bookmarks for a single parent separated by '/'
2849 2854 bm = '/'.join(getbms())
2850 2855 if bm:
2851 2856 output.append(bm)
2852 2857 else:
2853 2858 ctx = scmutil.revsingle(repo, rev, None)
2854 2859
2855 2860 if ctx.rev() is None:
2856 2861 ctx = repo[None]
2857 2862 parents = ctx.parents()
2858 2863 taglist = []
2859 2864 for p in parents:
2860 2865 taglist.extend(p.tags())
2861 2866
2862 2867 changed = ""
2863 2868 if default or id or num:
2864 2869 if (any(repo.status())
2865 2870 or any(ctx.sub(s).dirty() for s in ctx.substate)):
2866 2871 changed = '+'
2867 2872 if default or id:
2868 2873 output = ["%s%s" %
2869 2874 ('+'.join([hexfunc(p.node()) for p in parents]), changed)]
2870 2875 if num:
2871 2876 output.append("%s%s" %
2872 2877 ('+'.join([str(p.rev()) for p in parents]), changed))
2873 2878 else:
2874 2879 if default or id:
2875 2880 output = [hexfunc(ctx.node())]
2876 2881 if num:
2877 2882 output.append(str(ctx.rev()))
2878 2883 taglist = ctx.tags()
2879 2884
2880 2885 if default and not ui.quiet:
2881 2886 b = ctx.branch()
2882 2887 if b != 'default':
2883 2888 output.append("(%s)" % b)
2884 2889
2885 2890 # multiple tags for a single parent separated by '/'
2886 2891 t = '/'.join(taglist)
2887 2892 if t:
2888 2893 output.append(t)
2889 2894
2890 2895 # multiple bookmarks for a single parent separated by '/'
2891 2896 bm = '/'.join(ctx.bookmarks())
2892 2897 if bm:
2893 2898 output.append(bm)
2894 2899 else:
2895 2900 if branch:
2896 2901 output.append(ctx.branch())
2897 2902
2898 2903 if tags:
2899 2904 output.extend(taglist)
2900 2905
2901 2906 if bookmarks:
2902 2907 output.extend(ctx.bookmarks())
2903 2908
2904 2909 ui.write("%s\n" % ' '.join(output))
2905 2910
2906 2911 @command('import|patch',
2907 2912 [('p', 'strip', 1,
2908 2913 _('directory strip option for patch. This has the same '
2909 2914 'meaning as the corresponding patch option'), _('NUM')),
2910 2915 ('b', 'base', '', _('base path (DEPRECATED)'), _('PATH')),
2911 2916 ('e', 'edit', False, _('invoke editor on commit messages')),
2912 2917 ('f', 'force', None,
2913 2918 _('skip check for outstanding uncommitted changes (DEPRECATED)')),
2914 2919 ('', 'no-commit', None,
2915 2920 _("don't commit, just update the working directory")),
2916 2921 ('', 'bypass', None,
2917 2922 _("apply patch without touching the working directory")),
2918 2923 ('', 'partial', None,
2919 2924 _('commit even if some hunks fail')),
2920 2925 ('', 'exact', None,
2921 2926 _('abort if patch would apply lossily')),
2922 2927 ('', 'prefix', '',
2923 2928 _('apply patch to subdirectory'), _('DIR')),
2924 2929 ('', 'import-branch', None,
2925 2930 _('use any branch information in patch (implied by --exact)'))] +
2926 2931 commitopts + commitopts2 + similarityopts,
2927 2932 _('[OPTION]... PATCH...'))
2928 2933 def import_(ui, repo, patch1=None, *patches, **opts):
2929 2934 """import an ordered set of patches
2930 2935
2931 2936 Import a list of patches and commit them individually (unless
2932 2937 --no-commit is specified).
2933 2938
2934 2939 To read a patch from standard input (stdin), use "-" as the patch
2935 2940 name. If a URL is specified, the patch will be downloaded from
2936 2941 there.
2937 2942
2938 2943 Import first applies changes to the working directory (unless
2939 2944 --bypass is specified), import will abort if there are outstanding
2940 2945 changes.
2941 2946
2942 2947 Use --bypass to apply and commit patches directly to the
2943 2948 repository, without affecting the working directory. Without
2944 2949 --exact, patches will be applied on top of the working directory
2945 2950 parent revision.
2946 2951
2947 2952 You can import a patch straight from a mail message. Even patches
2948 2953 as attachments work (to use the body part, it must have type
2949 2954 text/plain or text/x-patch). From and Subject headers of email
2950 2955 message are used as default committer and commit message. All
2951 2956 text/plain body parts before first diff are added to the commit
2952 2957 message.
2953 2958
2954 2959 If the imported patch was generated by :hg:`export`, user and
2955 2960 description from patch override values from message headers and
2956 2961 body. Values given on command line with -m/--message and -u/--user
2957 2962 override these.
2958 2963
2959 2964 If --exact is specified, import will set the working directory to
2960 2965 the parent of each patch before applying it, and will abort if the
2961 2966 resulting changeset has a different ID than the one recorded in
2962 2967 the patch. This will guard against various ways that portable
2963 2968 patch formats and mail systems might fail to transfer Mercurial
2964 2969 data or metadata. See :hg:`bundle` for lossless transmission.
2965 2970
2966 2971 Use --partial to ensure a changeset will be created from the patch
2967 2972 even if some hunks fail to apply. Hunks that fail to apply will be
2968 2973 written to a <target-file>.rej file. Conflicts can then be resolved
2969 2974 by hand before :hg:`commit --amend` is run to update the created
2970 2975 changeset. This flag exists to let people import patches that
2971 2976 partially apply without losing the associated metadata (author,
2972 2977 date, description, ...).
2973 2978
2974 2979 .. note::
2975 2980
2976 2981 When no hunks apply cleanly, :hg:`import --partial` will create
2977 2982 an empty changeset, importing only the patch metadata.
2978 2983
2979 2984 With -s/--similarity, hg will attempt to discover renames and
2980 2985 copies in the patch in the same way as :hg:`addremove`.
2981 2986
2982 2987 It is possible to use external patch programs to perform the patch
2983 2988 by setting the ``ui.patch`` configuration option. For the default
2984 2989 internal tool, the fuzz can also be configured via ``patch.fuzz``.
2985 2990 See :hg:`help config` for more information about configuration
2986 2991 files and how to use these options.
2987 2992
2988 2993 See :hg:`help dates` for a list of formats valid for -d/--date.
2989 2994
2990 2995 .. container:: verbose
2991 2996
2992 2997 Examples:
2993 2998
2994 2999 - import a traditional patch from a website and detect renames::
2995 3000
2996 3001 hg import -s 80 http://example.com/bugfix.patch
2997 3002
2998 3003 - import a changeset from an hgweb server::
2999 3004
3000 3005 hg import https://www.mercurial-scm.org/repo/hg/rev/5ca8c111e9aa
3001 3006
3002 3007 - import all the patches in an Unix-style mbox::
3003 3008
3004 3009 hg import incoming-patches.mbox
3005 3010
3006 3011 - import patches from stdin::
3007 3012
3008 3013 hg import -
3009 3014
3010 3015 - attempt to exactly restore an exported changeset (not always
3011 3016 possible)::
3012 3017
3013 3018 hg import --exact proposed-fix.patch
3014 3019
3015 3020 - use an external tool to apply a patch which is too fuzzy for
3016 3021 the default internal tool.
3017 3022
3018 3023 hg import --config ui.patch="patch --merge" fuzzy.patch
3019 3024
3020 3025 - change the default fuzzing from 2 to a less strict 7
3021 3026
3022 3027 hg import --config ui.fuzz=7 fuzz.patch
3023 3028
3024 3029 Returns 0 on success, 1 on partial success (see --partial).
3025 3030 """
3026 3031
3027 3032 opts = pycompat.byteskwargs(opts)
3028 3033 if not patch1:
3029 3034 raise error.Abort(_('need at least one patch to import'))
3030 3035
3031 3036 patches = (patch1,) + patches
3032 3037
3033 3038 date = opts.get('date')
3034 3039 if date:
3035 3040 opts['date'] = util.parsedate(date)
3036 3041
3037 3042 exact = opts.get('exact')
3038 3043 update = not opts.get('bypass')
3039 3044 if not update and opts.get('no_commit'):
3040 3045 raise error.Abort(_('cannot use --no-commit with --bypass'))
3041 3046 try:
3042 3047 sim = float(opts.get('similarity') or 0)
3043 3048 except ValueError:
3044 3049 raise error.Abort(_('similarity must be a number'))
3045 3050 if sim < 0 or sim > 100:
3046 3051 raise error.Abort(_('similarity must be between 0 and 100'))
3047 3052 if sim and not update:
3048 3053 raise error.Abort(_('cannot use --similarity with --bypass'))
3049 3054 if exact:
3050 3055 if opts.get('edit'):
3051 3056 raise error.Abort(_('cannot use --exact with --edit'))
3052 3057 if opts.get('prefix'):
3053 3058 raise error.Abort(_('cannot use --exact with --prefix'))
3054 3059
3055 3060 base = opts["base"]
3056 3061 wlock = dsguard = lock = tr = None
3057 3062 msgs = []
3058 3063 ret = 0
3059 3064
3060 3065
3061 3066 try:
3062 3067 wlock = repo.wlock()
3063 3068
3064 3069 if update:
3065 3070 cmdutil.checkunfinished(repo)
3066 3071 if (exact or not opts.get('force')):
3067 3072 cmdutil.bailifchanged(repo)
3068 3073
3069 3074 if not opts.get('no_commit'):
3070 3075 lock = repo.lock()
3071 3076 tr = repo.transaction('import')
3072 3077 else:
3073 3078 dsguard = dirstateguard.dirstateguard(repo, 'import')
3074 3079 parents = repo[None].parents()
3075 3080 for patchurl in patches:
3076 3081 if patchurl == '-':
3077 3082 ui.status(_('applying patch from stdin\n'))
3078 3083 patchfile = ui.fin
3079 3084 patchurl = 'stdin' # for error message
3080 3085 else:
3081 3086 patchurl = os.path.join(base, patchurl)
3082 3087 ui.status(_('applying %s\n') % patchurl)
3083 3088 patchfile = hg.openpath(ui, patchurl)
3084 3089
3085 3090 haspatch = False
3086 3091 for hunk in patch.split(patchfile):
3087 3092 (msg, node, rej) = cmdutil.tryimportone(ui, repo, hunk,
3088 3093 parents, opts,
3089 3094 msgs, hg.clean)
3090 3095 if msg:
3091 3096 haspatch = True
3092 3097 ui.note(msg + '\n')
3093 3098 if update or exact:
3094 3099 parents = repo[None].parents()
3095 3100 else:
3096 3101 parents = [repo[node]]
3097 3102 if rej:
3098 3103 ui.write_err(_("patch applied partially\n"))
3099 3104 ui.write_err(_("(fix the .rej files and run "
3100 3105 "`hg commit --amend`)\n"))
3101 3106 ret = 1
3102 3107 break
3103 3108
3104 3109 if not haspatch:
3105 3110 raise error.Abort(_('%s: no diffs found') % patchurl)
3106 3111
3107 3112 if tr:
3108 3113 tr.close()
3109 3114 if msgs:
3110 3115 repo.savecommitmessage('\n* * *\n'.join(msgs))
3111 3116 if dsguard:
3112 3117 dsguard.close()
3113 3118 return ret
3114 3119 finally:
3115 3120 if tr:
3116 3121 tr.release()
3117 3122 release(lock, dsguard, wlock)
3118 3123
3119 3124 @command('incoming|in',
3120 3125 [('f', 'force', None,
3121 3126 _('run even if remote repository is unrelated')),
3122 3127 ('n', 'newest-first', None, _('show newest record first')),
3123 3128 ('', 'bundle', '',
3124 3129 _('file to store the bundles into'), _('FILE')),
3125 3130 ('r', 'rev', [], _('a remote changeset intended to be added'), _('REV')),
3126 3131 ('B', 'bookmarks', False, _("compare bookmarks")),
3127 3132 ('b', 'branch', [],
3128 3133 _('a specific branch you would like to pull'), _('BRANCH')),
3129 3134 ] + logopts + remoteopts + subrepoopts,
3130 3135 _('[-p] [-n] [-M] [-f] [-r REV]... [--bundle FILENAME] [SOURCE]'))
3131 3136 def incoming(ui, repo, source="default", **opts):
3132 3137 """show new changesets found in source
3133 3138
3134 3139 Show new changesets found in the specified path/URL or the default
3135 3140 pull location. These are the changesets that would have been pulled
3136 3141 if a pull at the time you issued this command.
3137 3142
3138 3143 See pull for valid source format details.
3139 3144
3140 3145 .. container:: verbose
3141 3146
3142 3147 With -B/--bookmarks, the result of bookmark comparison between
3143 3148 local and remote repositories is displayed. With -v/--verbose,
3144 3149 status is also displayed for each bookmark like below::
3145 3150
3146 3151 BM1 01234567890a added
3147 3152 BM2 1234567890ab advanced
3148 3153 BM3 234567890abc diverged
3149 3154 BM4 34567890abcd changed
3150 3155
3151 3156 The action taken locally when pulling depends on the
3152 3157 status of each bookmark:
3153 3158
3154 3159 :``added``: pull will create it
3155 3160 :``advanced``: pull will update it
3156 3161 :``diverged``: pull will create a divergent bookmark
3157 3162 :``changed``: result depends on remote changesets
3158 3163
3159 3164 From the point of view of pulling behavior, bookmark
3160 3165 existing only in the remote repository are treated as ``added``,
3161 3166 even if it is in fact locally deleted.
3162 3167
3163 3168 .. container:: verbose
3164 3169
3165 3170 For remote repository, using --bundle avoids downloading the
3166 3171 changesets twice if the incoming is followed by a pull.
3167 3172
3168 3173 Examples:
3169 3174
3170 3175 - show incoming changes with patches and full description::
3171 3176
3172 3177 hg incoming -vp
3173 3178
3174 3179 - show incoming changes excluding merges, store a bundle::
3175 3180
3176 3181 hg in -vpM --bundle incoming.hg
3177 3182 hg pull incoming.hg
3178 3183
3179 3184 - briefly list changes inside a bundle::
3180 3185
3181 3186 hg in changes.hg -T "{desc|firstline}\\n"
3182 3187
3183 3188 Returns 0 if there are incoming changes, 1 otherwise.
3184 3189 """
3185 3190 opts = pycompat.byteskwargs(opts)
3186 3191 if opts.get('graph'):
3187 3192 cmdutil.checkunsupportedgraphflags([], opts)
3188 3193 def display(other, chlist, displayer):
3189 3194 revdag = cmdutil.graphrevs(other, chlist, opts)
3190 3195 cmdutil.displaygraph(ui, repo, revdag, displayer,
3191 3196 graphmod.asciiedges)
3192 3197
3193 3198 hg._incoming(display, lambda: 1, ui, repo, source, opts, buffered=True)
3194 3199 return 0
3195 3200
3196 3201 if opts.get('bundle') and opts.get('subrepos'):
3197 3202 raise error.Abort(_('cannot combine --bundle and --subrepos'))
3198 3203
3199 3204 if opts.get('bookmarks'):
3200 3205 source, branches = hg.parseurl(ui.expandpath(source),
3201 3206 opts.get('branch'))
3202 3207 other = hg.peer(repo, opts, source)
3203 3208 if 'bookmarks' not in other.listkeys('namespaces'):
3204 3209 ui.warn(_("remote doesn't support bookmarks\n"))
3205 3210 return 0
3206 3211 ui.pager('incoming')
3207 3212 ui.status(_('comparing with %s\n') % util.hidepassword(source))
3208 3213 return bookmarks.incoming(ui, repo, other)
3209 3214
3210 3215 repo._subtoppath = ui.expandpath(source)
3211 3216 try:
3212 3217 return hg.incoming(ui, repo, source, opts)
3213 3218 finally:
3214 3219 del repo._subtoppath
3215 3220
3216 3221
3217 3222 @command('^init', remoteopts, _('[-e CMD] [--remotecmd CMD] [DEST]'),
3218 3223 norepo=True)
3219 3224 def init(ui, dest=".", **opts):
3220 3225 """create a new repository in the given directory
3221 3226
3222 3227 Initialize a new repository in the given directory. If the given
3223 3228 directory does not exist, it will be created.
3224 3229
3225 3230 If no directory is given, the current directory is used.
3226 3231
3227 3232 It is possible to specify an ``ssh://`` URL as the destination.
3228 3233 See :hg:`help urls` for more information.
3229 3234
3230 3235 Returns 0 on success.
3231 3236 """
3232 3237 opts = pycompat.byteskwargs(opts)
3233 3238 hg.peer(ui, opts, ui.expandpath(dest), create=True)
3234 3239
3235 3240 @command('locate',
3236 3241 [('r', 'rev', '', _('search the repository as it is in REV'), _('REV')),
3237 3242 ('0', 'print0', None, _('end filenames with NUL, for use with xargs')),
3238 3243 ('f', 'fullpath', None, _('print complete paths from the filesystem root')),
3239 3244 ] + walkopts,
3240 3245 _('[OPTION]... [PATTERN]...'))
3241 3246 def locate(ui, repo, *pats, **opts):
3242 3247 """locate files matching specific patterns (DEPRECATED)
3243 3248
3244 3249 Print files under Mercurial control in the working directory whose
3245 3250 names match the given patterns.
3246 3251
3247 3252 By default, this command searches all directories in the working
3248 3253 directory. To search just the current directory and its
3249 3254 subdirectories, use "--include .".
3250 3255
3251 3256 If no patterns are given to match, this command prints the names
3252 3257 of all files under Mercurial control in the working directory.
3253 3258
3254 3259 If you want to feed the output of this command into the "xargs"
3255 3260 command, use the -0 option to both this command and "xargs". This
3256 3261 will avoid the problem of "xargs" treating single filenames that
3257 3262 contain whitespace as multiple filenames.
3258 3263
3259 3264 See :hg:`help files` for a more versatile command.
3260 3265
3261 3266 Returns 0 if a match is found, 1 otherwise.
3262 3267 """
3263 3268 opts = pycompat.byteskwargs(opts)
3264 3269 if opts.get('print0'):
3265 3270 end = '\0'
3266 3271 else:
3267 3272 end = '\n'
3268 3273 rev = scmutil.revsingle(repo, opts.get('rev'), None).node()
3269 3274
3270 3275 ret = 1
3271 3276 ctx = repo[rev]
3272 3277 m = scmutil.match(ctx, pats, opts, default='relglob',
3273 3278 badfn=lambda x, y: False)
3274 3279
3275 3280 ui.pager('locate')
3276 3281 for abs in ctx.matches(m):
3277 3282 if opts.get('fullpath'):
3278 3283 ui.write(repo.wjoin(abs), end)
3279 3284 else:
3280 3285 ui.write(((pats and m.rel(abs)) or abs), end)
3281 3286 ret = 0
3282 3287
3283 3288 return ret
3284 3289
3285 3290 @command('^log|history',
3286 3291 [('f', 'follow', None,
3287 3292 _('follow changeset history, or file history across copies and renames')),
3288 3293 ('', 'follow-first', None,
3289 3294 _('only follow the first parent of merge changesets (DEPRECATED)')),
3290 3295 ('d', 'date', '', _('show revisions matching date spec'), _('DATE')),
3291 3296 ('C', 'copies', None, _('show copied files')),
3292 3297 ('k', 'keyword', [],
3293 3298 _('do case-insensitive search for a given text'), _('TEXT')),
3294 3299 ('r', 'rev', [], _('show the specified revision or revset'), _('REV')),
3295 3300 ('', 'removed', None, _('include revisions where files were removed')),
3296 3301 ('m', 'only-merges', None, _('show only merges (DEPRECATED)')),
3297 3302 ('u', 'user', [], _('revisions committed by user'), _('USER')),
3298 3303 ('', 'only-branch', [],
3299 3304 _('show only changesets within the given named branch (DEPRECATED)'),
3300 3305 _('BRANCH')),
3301 3306 ('b', 'branch', [],
3302 3307 _('show changesets within the given named branch'), _('BRANCH')),
3303 3308 ('P', 'prune', [],
3304 3309 _('do not display revision or any of its ancestors'), _('REV')),
3305 3310 ] + logopts + walkopts,
3306 3311 _('[OPTION]... [FILE]'),
3307 3312 inferrepo=True)
3308 3313 def log(ui, repo, *pats, **opts):
3309 3314 """show revision history of entire repository or files
3310 3315
3311 3316 Print the revision history of the specified files or the entire
3312 3317 project.
3313 3318
3314 3319 If no revision range is specified, the default is ``tip:0`` unless
3315 3320 --follow is set, in which case the working directory parent is
3316 3321 used as the starting revision.
3317 3322
3318 3323 File history is shown without following rename or copy history of
3319 3324 files. Use -f/--follow with a filename to follow history across
3320 3325 renames and copies. --follow without a filename will only show
3321 3326 ancestors or descendants of the starting revision.
3322 3327
3323 3328 By default this command prints revision number and changeset id,
3324 3329 tags, non-trivial parents, user, date and time, and a summary for
3325 3330 each commit. When the -v/--verbose switch is used, the list of
3326 3331 changed files and full commit message are shown.
3327 3332
3328 3333 With --graph the revisions are shown as an ASCII art DAG with the most
3329 3334 recent changeset at the top.
3330 3335 'o' is a changeset, '@' is a working directory parent, 'x' is obsolete,
3331 3336 and '+' represents a fork where the changeset from the lines below is a
3332 3337 parent of the 'o' merge on the same line.
3333 3338 Paths in the DAG are represented with '|', '/' and so forth. ':' in place
3334 3339 of a '|' indicates one or more revisions in a path are omitted.
3335 3340
3336 3341 .. note::
3337 3342
3338 3343 :hg:`log --patch` may generate unexpected diff output for merge
3339 3344 changesets, as it will only compare the merge changeset against
3340 3345 its first parent. Also, only files different from BOTH parents
3341 3346 will appear in files:.
3342 3347
3343 3348 .. note::
3344 3349
3345 3350 For performance reasons, :hg:`log FILE` may omit duplicate changes
3346 3351 made on branches and will not show removals or mode changes. To
3347 3352 see all such changes, use the --removed switch.
3348 3353
3349 3354 .. container:: verbose
3350 3355
3351 3356 Some examples:
3352 3357
3353 3358 - changesets with full descriptions and file lists::
3354 3359
3355 3360 hg log -v
3356 3361
3357 3362 - changesets ancestral to the working directory::
3358 3363
3359 3364 hg log -f
3360 3365
3361 3366 - last 10 commits on the current branch::
3362 3367
3363 3368 hg log -l 10 -b .
3364 3369
3365 3370 - changesets showing all modifications of a file, including removals::
3366 3371
3367 3372 hg log --removed file.c
3368 3373
3369 3374 - all changesets that touch a directory, with diffs, excluding merges::
3370 3375
3371 3376 hg log -Mp lib/
3372 3377
3373 3378 - all revision numbers that match a keyword::
3374 3379
3375 3380 hg log -k bug --template "{rev}\\n"
3376 3381
3377 3382 - the full hash identifier of the working directory parent::
3378 3383
3379 3384 hg log -r . --template "{node}\\n"
3380 3385
3381 3386 - list available log templates::
3382 3387
3383 3388 hg log -T list
3384 3389
3385 3390 - check if a given changeset is included in a tagged release::
3386 3391
3387 3392 hg log -r "a21ccf and ancestor(1.9)"
3388 3393
3389 3394 - find all changesets by some user in a date range::
3390 3395
3391 3396 hg log -k alice -d "may 2008 to jul 2008"
3392 3397
3393 3398 - summary of all changesets after the last tag::
3394 3399
3395 3400 hg log -r "last(tagged())::" --template "{desc|firstline}\\n"
3396 3401
3397 3402 See :hg:`help dates` for a list of formats valid for -d/--date.
3398 3403
3399 3404 See :hg:`help revisions` for more about specifying and ordering
3400 3405 revisions.
3401 3406
3402 3407 See :hg:`help templates` for more about pre-packaged styles and
3403 3408 specifying custom templates.
3404 3409
3405 3410 Returns 0 on success.
3406 3411
3407 3412 """
3408 3413 opts = pycompat.byteskwargs(opts)
3409 3414 if opts.get('follow') and opts.get('rev'):
3410 3415 opts['rev'] = [revsetlang.formatspec('reverse(::%lr)', opts.get('rev'))]
3411 3416 del opts['follow']
3412 3417
3413 3418 if opts.get('graph'):
3414 3419 return cmdutil.graphlog(ui, repo, pats, opts)
3415 3420
3416 3421 revs, expr, filematcher = cmdutil.getlogrevs(repo, pats, opts)
3417 3422 limit = cmdutil.loglimit(opts)
3418 3423 count = 0
3419 3424
3420 3425 getrenamed = None
3421 3426 if opts.get('copies'):
3422 3427 endrev = None
3423 3428 if opts.get('rev'):
3424 3429 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
3425 3430 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
3426 3431
3427 3432 ui.pager('log')
3428 3433 displayer = cmdutil.show_changeset(ui, repo, opts, buffered=True)
3429 3434 for rev in revs:
3430 3435 if count == limit:
3431 3436 break
3432 3437 ctx = repo[rev]
3433 3438 copies = None
3434 3439 if getrenamed is not None and rev:
3435 3440 copies = []
3436 3441 for fn in ctx.files():
3437 3442 rename = getrenamed(fn, rev)
3438 3443 if rename:
3439 3444 copies.append((fn, rename[0]))
3440 3445 if filematcher:
3441 3446 revmatchfn = filematcher(ctx.rev())
3442 3447 else:
3443 3448 revmatchfn = None
3444 3449 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
3445 3450 if displayer.flush(ctx):
3446 3451 count += 1
3447 3452
3448 3453 displayer.close()
3449 3454
3450 3455 @command('manifest',
3451 3456 [('r', 'rev', '', _('revision to display'), _('REV')),
3452 3457 ('', 'all', False, _("list files from all revisions"))]
3453 3458 + formatteropts,
3454 3459 _('[-r REV]'))
3455 3460 def manifest(ui, repo, node=None, rev=None, **opts):
3456 3461 """output the current or given revision of the project manifest
3457 3462
3458 3463 Print a list of version controlled files for the given revision.
3459 3464 If no revision is given, the first parent of the working directory
3460 3465 is used, or the null revision if no revision is checked out.
3461 3466
3462 3467 With -v, print file permissions, symlink and executable bits.
3463 3468 With --debug, print file revision hashes.
3464 3469
3465 3470 If option --all is specified, the list of all files from all revisions
3466 3471 is printed. This includes deleted and renamed files.
3467 3472
3468 3473 Returns 0 on success.
3469 3474 """
3470 3475 opts = pycompat.byteskwargs(opts)
3471 3476 fm = ui.formatter('manifest', opts)
3472 3477
3473 3478 if opts.get('all'):
3474 3479 if rev or node:
3475 3480 raise error.Abort(_("can't specify a revision with --all"))
3476 3481
3477 3482 res = []
3478 3483 prefix = "data/"
3479 3484 suffix = ".i"
3480 3485 plen = len(prefix)
3481 3486 slen = len(suffix)
3482 3487 with repo.lock():
3483 3488 for fn, b, size in repo.store.datafiles():
3484 3489 if size != 0 and fn[-slen:] == suffix and fn[:plen] == prefix:
3485 3490 res.append(fn[plen:-slen])
3486 3491 ui.pager('manifest')
3487 3492 for f in res:
3488 3493 fm.startitem()
3489 3494 fm.write("path", '%s\n', f)
3490 3495 fm.end()
3491 3496 return
3492 3497
3493 3498 if rev and node:
3494 3499 raise error.Abort(_("please specify just one revision"))
3495 3500
3496 3501 if not node:
3497 3502 node = rev
3498 3503
3499 3504 char = {'l': '@', 'x': '*', '': ''}
3500 3505 mode = {'l': '644', 'x': '755', '': '644'}
3501 3506 ctx = scmutil.revsingle(repo, node)
3502 3507 mf = ctx.manifest()
3503 3508 ui.pager('manifest')
3504 3509 for f in ctx:
3505 3510 fm.startitem()
3506 3511 fl = ctx[f].flags()
3507 3512 fm.condwrite(ui.debugflag, 'hash', '%s ', hex(mf[f]))
3508 3513 fm.condwrite(ui.verbose, 'mode type', '%s %1s ', mode[fl], char[fl])
3509 3514 fm.write('path', '%s\n', f)
3510 3515 fm.end()
3511 3516
3512 3517 @command('^merge',
3513 3518 [('f', 'force', None,
3514 3519 _('force a merge including outstanding changes (DEPRECATED)')),
3515 3520 ('r', 'rev', '', _('revision to merge'), _('REV')),
3516 3521 ('P', 'preview', None,
3517 3522 _('review revisions to merge (no merge is performed)'))
3518 3523 ] + mergetoolopts,
3519 3524 _('[-P] [[-r] REV]'))
3520 3525 def merge(ui, repo, node=None, **opts):
3521 3526 """merge another revision into working directory
3522 3527
3523 3528 The current working directory is updated with all changes made in
3524 3529 the requested revision since the last common predecessor revision.
3525 3530
3526 3531 Files that changed between either parent are marked as changed for
3527 3532 the next commit and a commit must be performed before any further
3528 3533 updates to the repository are allowed. The next commit will have
3529 3534 two parents.
3530 3535
3531 3536 ``--tool`` can be used to specify the merge tool used for file
3532 3537 merges. It overrides the HGMERGE environment variable and your
3533 3538 configuration files. See :hg:`help merge-tools` for options.
3534 3539
3535 3540 If no revision is specified, the working directory's parent is a
3536 3541 head revision, and the current branch contains exactly one other
3537 3542 head, the other head is merged with by default. Otherwise, an
3538 3543 explicit revision with which to merge with must be provided.
3539 3544
3540 3545 See :hg:`help resolve` for information on handling file conflicts.
3541 3546
3542 3547 To undo an uncommitted merge, use :hg:`update --clean .` which
3543 3548 will check out a clean copy of the original merge parent, losing
3544 3549 all changes.
3545 3550
3546 3551 Returns 0 on success, 1 if there are unresolved files.
3547 3552 """
3548 3553
3549 3554 opts = pycompat.byteskwargs(opts)
3550 3555 if opts.get('rev') and node:
3551 3556 raise error.Abort(_("please specify just one revision"))
3552 3557 if not node:
3553 3558 node = opts.get('rev')
3554 3559
3555 3560 if node:
3556 3561 node = scmutil.revsingle(repo, node).node()
3557 3562
3558 3563 if not node:
3559 3564 node = repo[destutil.destmerge(repo)].node()
3560 3565
3561 3566 if opts.get('preview'):
3562 3567 # find nodes that are ancestors of p2 but not of p1
3563 3568 p1 = repo.lookup('.')
3564 3569 p2 = repo.lookup(node)
3565 3570 nodes = repo.changelog.findmissing(common=[p1], heads=[p2])
3566 3571
3567 3572 displayer = cmdutil.show_changeset(ui, repo, opts)
3568 3573 for node in nodes:
3569 3574 displayer.show(repo[node])
3570 3575 displayer.close()
3571 3576 return 0
3572 3577
3573 3578 try:
3574 3579 # ui.forcemerge is an internal variable, do not document
3575 3580 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''), 'merge')
3576 3581 force = opts.get('force')
3577 3582 labels = ['working copy', 'merge rev']
3578 3583 return hg.merge(repo, node, force=force, mergeforce=force,
3579 3584 labels=labels)
3580 3585 finally:
3581 3586 ui.setconfig('ui', 'forcemerge', '', 'merge')
3582 3587
3583 3588 @command('outgoing|out',
3584 3589 [('f', 'force', None, _('run even when the destination is unrelated')),
3585 3590 ('r', 'rev', [],
3586 3591 _('a changeset intended to be included in the destination'), _('REV')),
3587 3592 ('n', 'newest-first', None, _('show newest record first')),
3588 3593 ('B', 'bookmarks', False, _('compare bookmarks')),
3589 3594 ('b', 'branch', [], _('a specific branch you would like to push'),
3590 3595 _('BRANCH')),
3591 3596 ] + logopts + remoteopts + subrepoopts,
3592 3597 _('[-M] [-p] [-n] [-f] [-r REV]... [DEST]'))
3593 3598 def outgoing(ui, repo, dest=None, **opts):
3594 3599 """show changesets not found in the destination
3595 3600
3596 3601 Show changesets not found in the specified destination repository
3597 3602 or the default push location. These are the changesets that would
3598 3603 be pushed if a push was requested.
3599 3604
3600 3605 See pull for details of valid destination formats.
3601 3606
3602 3607 .. container:: verbose
3603 3608
3604 3609 With -B/--bookmarks, the result of bookmark comparison between
3605 3610 local and remote repositories is displayed. With -v/--verbose,
3606 3611 status is also displayed for each bookmark like below::
3607 3612
3608 3613 BM1 01234567890a added
3609 3614 BM2 deleted
3610 3615 BM3 234567890abc advanced
3611 3616 BM4 34567890abcd diverged
3612 3617 BM5 4567890abcde changed
3613 3618
3614 3619 The action taken when pushing depends on the
3615 3620 status of each bookmark:
3616 3621
3617 3622 :``added``: push with ``-B`` will create it
3618 3623 :``deleted``: push with ``-B`` will delete it
3619 3624 :``advanced``: push will update it
3620 3625 :``diverged``: push with ``-B`` will update it
3621 3626 :``changed``: push with ``-B`` will update it
3622 3627
3623 3628 From the point of view of pushing behavior, bookmarks
3624 3629 existing only in the remote repository are treated as
3625 3630 ``deleted``, even if it is in fact added remotely.
3626 3631
3627 3632 Returns 0 if there are outgoing changes, 1 otherwise.
3628 3633 """
3629 3634 opts = pycompat.byteskwargs(opts)
3630 3635 if opts.get('graph'):
3631 3636 cmdutil.checkunsupportedgraphflags([], opts)
3632 3637 o, other = hg._outgoing(ui, repo, dest, opts)
3633 3638 if not o:
3634 3639 cmdutil.outgoinghooks(ui, repo, other, opts, o)
3635 3640 return
3636 3641
3637 3642 revdag = cmdutil.graphrevs(repo, o, opts)
3638 3643 ui.pager('outgoing')
3639 3644 displayer = cmdutil.show_changeset(ui, repo, opts, buffered=True)
3640 3645 cmdutil.displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges)
3641 3646 cmdutil.outgoinghooks(ui, repo, other, opts, o)
3642 3647 return 0
3643 3648
3644 3649 if opts.get('bookmarks'):
3645 3650 dest = ui.expandpath(dest or 'default-push', dest or 'default')
3646 3651 dest, branches = hg.parseurl(dest, opts.get('branch'))
3647 3652 other = hg.peer(repo, opts, dest)
3648 3653 if 'bookmarks' not in other.listkeys('namespaces'):
3649 3654 ui.warn(_("remote doesn't support bookmarks\n"))
3650 3655 return 0
3651 3656 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
3652 3657 ui.pager('outgoing')
3653 3658 return bookmarks.outgoing(ui, repo, other)
3654 3659
3655 3660 repo._subtoppath = ui.expandpath(dest or 'default-push', dest or 'default')
3656 3661 try:
3657 3662 return hg.outgoing(ui, repo, dest, opts)
3658 3663 finally:
3659 3664 del repo._subtoppath
3660 3665
3661 3666 @command('parents',
3662 3667 [('r', 'rev', '', _('show parents of the specified revision'), _('REV')),
3663 3668 ] + templateopts,
3664 3669 _('[-r REV] [FILE]'),
3665 3670 inferrepo=True)
3666 3671 def parents(ui, repo, file_=None, **opts):
3667 3672 """show the parents of the working directory or revision (DEPRECATED)
3668 3673
3669 3674 Print the working directory's parent revisions. If a revision is
3670 3675 given via -r/--rev, the parent of that revision will be printed.
3671 3676 If a file argument is given, the revision in which the file was
3672 3677 last changed (before the working directory revision or the
3673 3678 argument to --rev if given) is printed.
3674 3679
3675 3680 This command is equivalent to::
3676 3681
3677 3682 hg log -r "p1()+p2()" or
3678 3683 hg log -r "p1(REV)+p2(REV)" or
3679 3684 hg log -r "max(::p1() and file(FILE))+max(::p2() and file(FILE))" or
3680 3685 hg log -r "max(::p1(REV) and file(FILE))+max(::p2(REV) and file(FILE))"
3681 3686
3682 3687 See :hg:`summary` and :hg:`help revsets` for related information.
3683 3688
3684 3689 Returns 0 on success.
3685 3690 """
3686 3691
3687 3692 opts = pycompat.byteskwargs(opts)
3688 3693 ctx = scmutil.revsingle(repo, opts.get('rev'), None)
3689 3694
3690 3695 if file_:
3691 3696 m = scmutil.match(ctx, (file_,), opts)
3692 3697 if m.anypats() or len(m.files()) != 1:
3693 3698 raise error.Abort(_('can only specify an explicit filename'))
3694 3699 file_ = m.files()[0]
3695 3700 filenodes = []
3696 3701 for cp in ctx.parents():
3697 3702 if not cp:
3698 3703 continue
3699 3704 try:
3700 3705 filenodes.append(cp.filenode(file_))
3701 3706 except error.LookupError:
3702 3707 pass
3703 3708 if not filenodes:
3704 3709 raise error.Abort(_("'%s' not found in manifest!") % file_)
3705 3710 p = []
3706 3711 for fn in filenodes:
3707 3712 fctx = repo.filectx(file_, fileid=fn)
3708 3713 p.append(fctx.node())
3709 3714 else:
3710 3715 p = [cp.node() for cp in ctx.parents()]
3711 3716
3712 3717 displayer = cmdutil.show_changeset(ui, repo, opts)
3713 3718 for n in p:
3714 3719 if n != nullid:
3715 3720 displayer.show(repo[n])
3716 3721 displayer.close()
3717 3722
3718 3723 @command('paths', formatteropts, _('[NAME]'), optionalrepo=True)
3719 3724 def paths(ui, repo, search=None, **opts):
3720 3725 """show aliases for remote repositories
3721 3726
3722 3727 Show definition of symbolic path name NAME. If no name is given,
3723 3728 show definition of all available names.
3724 3729
3725 3730 Option -q/--quiet suppresses all output when searching for NAME
3726 3731 and shows only the path names when listing all definitions.
3727 3732
3728 3733 Path names are defined in the [paths] section of your
3729 3734 configuration file and in ``/etc/mercurial/hgrc``. If run inside a
3730 3735 repository, ``.hg/hgrc`` is used, too.
3731 3736
3732 3737 The path names ``default`` and ``default-push`` have a special
3733 3738 meaning. When performing a push or pull operation, they are used
3734 3739 as fallbacks if no location is specified on the command-line.
3735 3740 When ``default-push`` is set, it will be used for push and
3736 3741 ``default`` will be used for pull; otherwise ``default`` is used
3737 3742 as the fallback for both. When cloning a repository, the clone
3738 3743 source is written as ``default`` in ``.hg/hgrc``.
3739 3744
3740 3745 .. note::
3741 3746
3742 3747 ``default`` and ``default-push`` apply to all inbound (e.g.
3743 3748 :hg:`incoming`) and outbound (e.g. :hg:`outgoing`, :hg:`email`
3744 3749 and :hg:`bundle`) operations.
3745 3750
3746 3751 See :hg:`help urls` for more information.
3747 3752
3748 3753 Returns 0 on success.
3749 3754 """
3750 3755
3751 3756 opts = pycompat.byteskwargs(opts)
3752 3757 ui.pager('paths')
3753 3758 if search:
3754 3759 pathitems = [(name, path) for name, path in ui.paths.iteritems()
3755 3760 if name == search]
3756 3761 else:
3757 3762 pathitems = sorted(ui.paths.iteritems())
3758 3763
3759 3764 fm = ui.formatter('paths', opts)
3760 3765 if fm.isplain():
3761 3766 hidepassword = util.hidepassword
3762 3767 else:
3763 3768 hidepassword = str
3764 3769 if ui.quiet:
3765 3770 namefmt = '%s\n'
3766 3771 else:
3767 3772 namefmt = '%s = '
3768 3773 showsubopts = not search and not ui.quiet
3769 3774
3770 3775 for name, path in pathitems:
3771 3776 fm.startitem()
3772 3777 fm.condwrite(not search, 'name', namefmt, name)
3773 3778 fm.condwrite(not ui.quiet, 'url', '%s\n', hidepassword(path.rawloc))
3774 3779 for subopt, value in sorted(path.suboptions.items()):
3775 3780 assert subopt not in ('name', 'url')
3776 3781 if showsubopts:
3777 3782 fm.plain('%s:%s = ' % (name, subopt))
3778 3783 fm.condwrite(showsubopts, subopt, '%s\n', value)
3779 3784
3780 3785 fm.end()
3781 3786
3782 3787 if search and not pathitems:
3783 3788 if not ui.quiet:
3784 3789 ui.warn(_("not found!\n"))
3785 3790 return 1
3786 3791 else:
3787 3792 return 0
3788 3793
3789 3794 @command('phase',
3790 3795 [('p', 'public', False, _('set changeset phase to public')),
3791 3796 ('d', 'draft', False, _('set changeset phase to draft')),
3792 3797 ('s', 'secret', False, _('set changeset phase to secret')),
3793 3798 ('f', 'force', False, _('allow to move boundary backward')),
3794 3799 ('r', 'rev', [], _('target revision'), _('REV')),
3795 3800 ],
3796 3801 _('[-p|-d|-s] [-f] [-r] [REV...]'))
3797 3802 def phase(ui, repo, *revs, **opts):
3798 3803 """set or show the current phase name
3799 3804
3800 3805 With no argument, show the phase name of the current revision(s).
3801 3806
3802 3807 With one of -p/--public, -d/--draft or -s/--secret, change the
3803 3808 phase value of the specified revisions.
3804 3809
3805 3810 Unless -f/--force is specified, :hg:`phase` won't move changeset from a
3806 3811 lower phase to an higher phase. Phases are ordered as follows::
3807 3812
3808 3813 public < draft < secret
3809 3814
3810 3815 Returns 0 on success, 1 if some phases could not be changed.
3811 3816
3812 3817 (For more information about the phases concept, see :hg:`help phases`.)
3813 3818 """
3814 3819 opts = pycompat.byteskwargs(opts)
3815 3820 # search for a unique phase argument
3816 3821 targetphase = None
3817 3822 for idx, name in enumerate(phases.phasenames):
3818 3823 if opts[name]:
3819 3824 if targetphase is not None:
3820 3825 raise error.Abort(_('only one phase can be specified'))
3821 3826 targetphase = idx
3822 3827
3823 3828 # look for specified revision
3824 3829 revs = list(revs)
3825 3830 revs.extend(opts['rev'])
3826 3831 if not revs:
3827 3832 # display both parents as the second parent phase can influence
3828 3833 # the phase of a merge commit
3829 3834 revs = [c.rev() for c in repo[None].parents()]
3830 3835
3831 3836 revs = scmutil.revrange(repo, revs)
3832 3837
3833 3838 lock = None
3834 3839 ret = 0
3835 3840 if targetphase is None:
3836 3841 # display
3837 3842 for r in revs:
3838 3843 ctx = repo[r]
3839 3844 ui.write('%i: %s\n' % (ctx.rev(), ctx.phasestr()))
3840 3845 else:
3841 3846 tr = None
3842 3847 lock = repo.lock()
3843 3848 try:
3844 3849 tr = repo.transaction("phase")
3845 3850 # set phase
3846 3851 if not revs:
3847 3852 raise error.Abort(_('empty revision set'))
3848 3853 nodes = [repo[r].node() for r in revs]
3849 3854 # moving revision from public to draft may hide them
3850 3855 # We have to check result on an unfiltered repository
3851 3856 unfi = repo.unfiltered()
3852 3857 getphase = unfi._phasecache.phase
3853 3858 olddata = [getphase(unfi, r) for r in unfi]
3854 3859 phases.advanceboundary(repo, tr, targetphase, nodes)
3855 3860 if opts['force']:
3856 3861 phases.retractboundary(repo, tr, targetphase, nodes)
3857 3862 tr.close()
3858 3863 finally:
3859 3864 if tr is not None:
3860 3865 tr.release()
3861 3866 lock.release()
3862 3867 getphase = unfi._phasecache.phase
3863 3868 newdata = [getphase(unfi, r) for r in unfi]
3864 3869 changes = sum(newdata[r] != olddata[r] for r in unfi)
3865 3870 cl = unfi.changelog
3866 3871 rejected = [n for n in nodes
3867 3872 if newdata[cl.rev(n)] < targetphase]
3868 3873 if rejected:
3869 3874 ui.warn(_('cannot move %i changesets to a higher '
3870 3875 'phase, use --force\n') % len(rejected))
3871 3876 ret = 1
3872 3877 if changes:
3873 3878 msg = _('phase changed for %i changesets\n') % changes
3874 3879 if ret:
3875 3880 ui.status(msg)
3876 3881 else:
3877 3882 ui.note(msg)
3878 3883 else:
3879 3884 ui.warn(_('no phases changed\n'))
3880 3885 return ret
3881 3886
3882 3887 def postincoming(ui, repo, modheads, optupdate, checkout, brev):
3883 3888 """Run after a changegroup has been added via pull/unbundle
3884 3889
3885 3890 This takes arguments below:
3886 3891
3887 3892 :modheads: change of heads by pull/unbundle
3888 3893 :optupdate: updating working directory is needed or not
3889 3894 :checkout: update destination revision (or None to default destination)
3890 3895 :brev: a name, which might be a bookmark to be activated after updating
3891 3896 """
3892 3897 if modheads == 0:
3893 3898 return
3894 3899 if optupdate:
3895 3900 try:
3896 3901 return hg.updatetotally(ui, repo, checkout, brev)
3897 3902 except error.UpdateAbort as inst:
3898 3903 msg = _("not updating: %s") % str(inst)
3899 3904 hint = inst.hint
3900 3905 raise error.UpdateAbort(msg, hint=hint)
3901 3906 if modheads > 1:
3902 3907 currentbranchheads = len(repo.branchheads())
3903 3908 if currentbranchheads == modheads:
3904 3909 ui.status(_("(run 'hg heads' to see heads, 'hg merge' to merge)\n"))
3905 3910 elif currentbranchheads > 1:
3906 3911 ui.status(_("(run 'hg heads .' to see heads, 'hg merge' to "
3907 3912 "merge)\n"))
3908 3913 else:
3909 3914 ui.status(_("(run 'hg heads' to see heads)\n"))
3910 3915 else:
3911 3916 ui.status(_("(run 'hg update' to get a working copy)\n"))
3912 3917
3913 3918 @command('^pull',
3914 3919 [('u', 'update', None,
3915 3920 _('update to new branch head if changesets were pulled')),
3916 3921 ('f', 'force', None, _('run even when remote repository is unrelated')),
3917 3922 ('r', 'rev', [], _('a remote changeset intended to be added'), _('REV')),
3918 3923 ('B', 'bookmark', [], _("bookmark to pull"), _('BOOKMARK')),
3919 3924 ('b', 'branch', [], _('a specific branch you would like to pull'),
3920 3925 _('BRANCH')),
3921 3926 ] + remoteopts,
3922 3927 _('[-u] [-f] [-r REV]... [-e CMD] [--remotecmd CMD] [SOURCE]'))
3923 3928 def pull(ui, repo, source="default", **opts):
3924 3929 """pull changes from the specified source
3925 3930
3926 3931 Pull changes from a remote repository to a local one.
3927 3932
3928 3933 This finds all changes from the repository at the specified path
3929 3934 or URL and adds them to a local repository (the current one unless
3930 3935 -R is specified). By default, this does not update the copy of the
3931 3936 project in the working directory.
3932 3937
3933 3938 Use :hg:`incoming` if you want to see what would have been added
3934 3939 by a pull at the time you issued this command. If you then decide
3935 3940 to add those changes to the repository, you should use :hg:`pull
3936 3941 -r X` where ``X`` is the last changeset listed by :hg:`incoming`.
3937 3942
3938 3943 If SOURCE is omitted, the 'default' path will be used.
3939 3944 See :hg:`help urls` for more information.
3940 3945
3941 3946 Specifying bookmark as ``.`` is equivalent to specifying the active
3942 3947 bookmark's name.
3943 3948
3944 3949 Returns 0 on success, 1 if an update had unresolved files.
3945 3950 """
3946 3951
3947 3952 opts = pycompat.byteskwargs(opts)
3948 3953 if ui.configbool('commands', 'update.requiredest') and opts.get('update'):
3949 3954 msg = _('update destination required by configuration')
3950 3955 hint = _('use hg pull followed by hg update DEST')
3951 3956 raise error.Abort(msg, hint=hint)
3952 3957
3953 3958 source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch'))
3954 3959 ui.status(_('pulling from %s\n') % util.hidepassword(source))
3955 3960 other = hg.peer(repo, opts, source)
3956 3961 try:
3957 3962 revs, checkout = hg.addbranchrevs(repo, other, branches,
3958 3963 opts.get('rev'))
3959 3964
3960 3965
3961 3966 pullopargs = {}
3962 3967 if opts.get('bookmark'):
3963 3968 if not revs:
3964 3969 revs = []
3965 3970 # The list of bookmark used here is not the one used to actually
3966 3971 # update the bookmark name. This can result in the revision pulled
3967 3972 # not ending up with the name of the bookmark because of a race
3968 3973 # condition on the server. (See issue 4689 for details)
3969 3974 remotebookmarks = other.listkeys('bookmarks')
3970 3975 pullopargs['remotebookmarks'] = remotebookmarks
3971 3976 for b in opts['bookmark']:
3972 3977 b = repo._bookmarks.expandname(b)
3973 3978 if b not in remotebookmarks:
3974 3979 raise error.Abort(_('remote bookmark %s not found!') % b)
3975 3980 revs.append(remotebookmarks[b])
3976 3981
3977 3982 if revs:
3978 3983 try:
3979 3984 # When 'rev' is a bookmark name, we cannot guarantee that it
3980 3985 # will be updated with that name because of a race condition
3981 3986 # server side. (See issue 4689 for details)
3982 3987 oldrevs = revs
3983 3988 revs = [] # actually, nodes
3984 3989 for r in oldrevs:
3985 3990 node = other.lookup(r)
3986 3991 revs.append(node)
3987 3992 if r == checkout:
3988 3993 checkout = node
3989 3994 except error.CapabilityError:
3990 3995 err = _("other repository doesn't support revision lookup, "
3991 3996 "so a rev cannot be specified.")
3992 3997 raise error.Abort(err)
3993 3998
3994 3999 pullopargs.update(opts.get('opargs', {}))
3995 4000 modheads = exchange.pull(repo, other, heads=revs,
3996 4001 force=opts.get('force'),
3997 4002 bookmarks=opts.get('bookmark', ()),
3998 4003 opargs=pullopargs).cgresult
3999 4004
4000 4005 # brev is a name, which might be a bookmark to be activated at
4001 4006 # the end of the update. In other words, it is an explicit
4002 4007 # destination of the update
4003 4008 brev = None
4004 4009
4005 4010 if checkout:
4006 4011 checkout = str(repo.changelog.rev(checkout))
4007 4012
4008 4013 # order below depends on implementation of
4009 4014 # hg.addbranchrevs(). opts['bookmark'] is ignored,
4010 4015 # because 'checkout' is determined without it.
4011 4016 if opts.get('rev'):
4012 4017 brev = opts['rev'][0]
4013 4018 elif opts.get('branch'):
4014 4019 brev = opts['branch'][0]
4015 4020 else:
4016 4021 brev = branches[0]
4017 4022 repo._subtoppath = source
4018 4023 try:
4019 4024 ret = postincoming(ui, repo, modheads, opts.get('update'),
4020 4025 checkout, brev)
4021 4026
4022 4027 finally:
4023 4028 del repo._subtoppath
4024 4029
4025 4030 finally:
4026 4031 other.close()
4027 4032 return ret
4028 4033
4029 4034 @command('^push',
4030 4035 [('f', 'force', None, _('force push')),
4031 4036 ('r', 'rev', [],
4032 4037 _('a changeset intended to be included in the destination'),
4033 4038 _('REV')),
4034 4039 ('B', 'bookmark', [], _("bookmark to push"), _('BOOKMARK')),
4035 4040 ('b', 'branch', [],
4036 4041 _('a specific branch you would like to push'), _('BRANCH')),
4037 4042 ('', 'new-branch', False, _('allow pushing a new branch')),
4038 4043 ] + remoteopts,
4039 4044 _('[-f] [-r REV]... [-e CMD] [--remotecmd CMD] [DEST]'))
4040 4045 def push(ui, repo, dest=None, **opts):
4041 4046 """push changes to the specified destination
4042 4047
4043 4048 Push changesets from the local repository to the specified
4044 4049 destination.
4045 4050
4046 4051 This operation is symmetrical to pull: it is identical to a pull
4047 4052 in the destination repository from the current one.
4048 4053
4049 4054 By default, push will not allow creation of new heads at the
4050 4055 destination, since multiple heads would make it unclear which head
4051 4056 to use. In this situation, it is recommended to pull and merge
4052 4057 before pushing.
4053 4058
4054 4059 Use --new-branch if you want to allow push to create a new named
4055 4060 branch that is not present at the destination. This allows you to
4056 4061 only create a new branch without forcing other changes.
4057 4062
4058 4063 .. note::
4059 4064
4060 4065 Extra care should be taken with the -f/--force option,
4061 4066 which will push all new heads on all branches, an action which will
4062 4067 almost always cause confusion for collaborators.
4063 4068
4064 4069 If -r/--rev is used, the specified revision and all its ancestors
4065 4070 will be pushed to the remote repository.
4066 4071
4067 4072 If -B/--bookmark is used, the specified bookmarked revision, its
4068 4073 ancestors, and the bookmark will be pushed to the remote
4069 4074 repository. Specifying ``.`` is equivalent to specifying the active
4070 4075 bookmark's name.
4071 4076
4072 4077 Please see :hg:`help urls` for important details about ``ssh://``
4073 4078 URLs. If DESTINATION is omitted, a default path will be used.
4074 4079
4075 4080 Returns 0 if push was successful, 1 if nothing to push.
4076 4081 """
4077 4082
4078 4083 opts = pycompat.byteskwargs(opts)
4079 4084 if opts.get('bookmark'):
4080 4085 ui.setconfig('bookmarks', 'pushing', opts['bookmark'], 'push')
4081 4086 for b in opts['bookmark']:
4082 4087 # translate -B options to -r so changesets get pushed
4083 4088 b = repo._bookmarks.expandname(b)
4084 4089 if b in repo._bookmarks:
4085 4090 opts.setdefault('rev', []).append(b)
4086 4091 else:
4087 4092 # if we try to push a deleted bookmark, translate it to null
4088 4093 # this lets simultaneous -r, -b options continue working
4089 4094 opts.setdefault('rev', []).append("null")
4090 4095
4091 4096 path = ui.paths.getpath(dest, default=('default-push', 'default'))
4092 4097 if not path:
4093 4098 raise error.Abort(_('default repository not configured!'),
4094 4099 hint=_("see 'hg help config.paths'"))
4095 4100 dest = path.pushloc or path.loc
4096 4101 branches = (path.branch, opts.get('branch') or [])
4097 4102 ui.status(_('pushing to %s\n') % util.hidepassword(dest))
4098 4103 revs, checkout = hg.addbranchrevs(repo, repo, branches, opts.get('rev'))
4099 4104 other = hg.peer(repo, opts, dest)
4100 4105
4101 4106 if revs:
4102 4107 revs = [repo.lookup(r) for r in scmutil.revrange(repo, revs)]
4103 4108 if not revs:
4104 4109 raise error.Abort(_("specified revisions evaluate to an empty set"),
4105 4110 hint=_("use different revision arguments"))
4106 4111 elif path.pushrev:
4107 4112 # It doesn't make any sense to specify ancestor revisions. So limit
4108 4113 # to DAG heads to make discovery simpler.
4109 4114 expr = revsetlang.formatspec('heads(%r)', path.pushrev)
4110 4115 revs = scmutil.revrange(repo, [expr])
4111 4116 revs = [repo[rev].node() for rev in revs]
4112 4117 if not revs:
4113 4118 raise error.Abort(_('default push revset for path evaluates to an '
4114 4119 'empty set'))
4115 4120
4116 4121 repo._subtoppath = dest
4117 4122 try:
4118 4123 # push subrepos depth-first for coherent ordering
4119 4124 c = repo['']
4120 4125 subs = c.substate # only repos that are committed
4121 4126 for s in sorted(subs):
4122 4127 result = c.sub(s).push(opts)
4123 4128 if result == 0:
4124 4129 return not result
4125 4130 finally:
4126 4131 del repo._subtoppath
4127 4132 pushop = exchange.push(repo, other, opts.get('force'), revs=revs,
4128 4133 newbranch=opts.get('new_branch'),
4129 4134 bookmarks=opts.get('bookmark', ()),
4130 4135 opargs=opts.get('opargs'))
4131 4136
4132 4137 result = not pushop.cgresult
4133 4138
4134 4139 if pushop.bkresult is not None:
4135 4140 if pushop.bkresult == 2:
4136 4141 result = 2
4137 4142 elif not result and pushop.bkresult:
4138 4143 result = 2
4139 4144
4140 4145 return result
4141 4146
4142 4147 @command('recover', [])
4143 4148 def recover(ui, repo):
4144 4149 """roll back an interrupted transaction
4145 4150
4146 4151 Recover from an interrupted commit or pull.
4147 4152
4148 4153 This command tries to fix the repository status after an
4149 4154 interrupted operation. It should only be necessary when Mercurial
4150 4155 suggests it.
4151 4156
4152 4157 Returns 0 if successful, 1 if nothing to recover or verify fails.
4153 4158 """
4154 4159 if repo.recover():
4155 4160 return hg.verify(repo)
4156 4161 return 1
4157 4162
4158 4163 @command('^remove|rm',
4159 4164 [('A', 'after', None, _('record delete for missing files')),
4160 4165 ('f', 'force', None,
4161 4166 _('forget added files, delete modified files')),
4162 4167 ] + subrepoopts + walkopts,
4163 4168 _('[OPTION]... FILE...'),
4164 4169 inferrepo=True)
4165 4170 def remove(ui, repo, *pats, **opts):
4166 4171 """remove the specified files on the next commit
4167 4172
4168 4173 Schedule the indicated files for removal from the current branch.
4169 4174
4170 4175 This command schedules the files to be removed at the next commit.
4171 4176 To undo a remove before that, see :hg:`revert`. To undo added
4172 4177 files, see :hg:`forget`.
4173 4178
4174 4179 .. container:: verbose
4175 4180
4176 4181 -A/--after can be used to remove only files that have already
4177 4182 been deleted, -f/--force can be used to force deletion, and -Af
4178 4183 can be used to remove files from the next revision without
4179 4184 deleting them from the working directory.
4180 4185
4181 4186 The following table details the behavior of remove for different
4182 4187 file states (columns) and option combinations (rows). The file
4183 4188 states are Added [A], Clean [C], Modified [M] and Missing [!]
4184 4189 (as reported by :hg:`status`). The actions are Warn, Remove
4185 4190 (from branch) and Delete (from disk):
4186 4191
4187 4192 ========= == == == ==
4188 4193 opt/state A C M !
4189 4194 ========= == == == ==
4190 4195 none W RD W R
4191 4196 -f R RD RD R
4192 4197 -A W W W R
4193 4198 -Af R R R R
4194 4199 ========= == == == ==
4195 4200
4196 4201 .. note::
4197 4202
4198 4203 :hg:`remove` never deletes files in Added [A] state from the
4199 4204 working directory, not even if ``--force`` is specified.
4200 4205
4201 4206 Returns 0 on success, 1 if any warnings encountered.
4202 4207 """
4203 4208
4204 4209 opts = pycompat.byteskwargs(opts)
4205 4210 after, force = opts.get('after'), opts.get('force')
4206 4211 if not pats and not after:
4207 4212 raise error.Abort(_('no files specified'))
4208 4213
4209 4214 m = scmutil.match(repo[None], pats, opts)
4210 4215 subrepos = opts.get('subrepos')
4211 4216 return cmdutil.remove(ui, repo, m, "", after, force, subrepos)
4212 4217
4213 4218 @command('rename|move|mv',
4214 4219 [('A', 'after', None, _('record a rename that has already occurred')),
4215 4220 ('f', 'force', None, _('forcibly copy over an existing managed file')),
4216 4221 ] + walkopts + dryrunopts,
4217 4222 _('[OPTION]... SOURCE... DEST'))
4218 4223 def rename(ui, repo, *pats, **opts):
4219 4224 """rename files; equivalent of copy + remove
4220 4225
4221 4226 Mark dest as copies of sources; mark sources for deletion. If dest
4222 4227 is a directory, copies are put in that directory. If dest is a
4223 4228 file, there can only be one source.
4224 4229
4225 4230 By default, this command copies the contents of files as they
4226 4231 exist in the working directory. If invoked with -A/--after, the
4227 4232 operation is recorded, but no copying is performed.
4228 4233
4229 4234 This command takes effect at the next commit. To undo a rename
4230 4235 before that, see :hg:`revert`.
4231 4236
4232 4237 Returns 0 on success, 1 if errors are encountered.
4233 4238 """
4234 4239 opts = pycompat.byteskwargs(opts)
4235 4240 with repo.wlock(False):
4236 4241 return cmdutil.copy(ui, repo, pats, opts, rename=True)
4237 4242
4238 4243 @command('resolve',
4239 4244 [('a', 'all', None, _('select all unresolved files')),
4240 4245 ('l', 'list', None, _('list state of files needing merge')),
4241 4246 ('m', 'mark', None, _('mark files as resolved')),
4242 4247 ('u', 'unmark', None, _('mark files as unresolved')),
4243 4248 ('n', 'no-status', None, _('hide status prefix'))]
4244 4249 + mergetoolopts + walkopts + formatteropts,
4245 4250 _('[OPTION]... [FILE]...'),
4246 4251 inferrepo=True)
4247 4252 def resolve(ui, repo, *pats, **opts):
4248 4253 """redo merges or set/view the merge status of files
4249 4254
4250 4255 Merges with unresolved conflicts are often the result of
4251 4256 non-interactive merging using the ``internal:merge`` configuration
4252 4257 setting, or a command-line merge tool like ``diff3``. The resolve
4253 4258 command is used to manage the files involved in a merge, after
4254 4259 :hg:`merge` has been run, and before :hg:`commit` is run (i.e. the
4255 4260 working directory must have two parents). See :hg:`help
4256 4261 merge-tools` for information on configuring merge tools.
4257 4262
4258 4263 The resolve command can be used in the following ways:
4259 4264
4260 4265 - :hg:`resolve [--tool TOOL] FILE...`: attempt to re-merge the specified
4261 4266 files, discarding any previous merge attempts. Re-merging is not
4262 4267 performed for files already marked as resolved. Use ``--all/-a``
4263 4268 to select all unresolved files. ``--tool`` can be used to specify
4264 4269 the merge tool used for the given files. It overrides the HGMERGE
4265 4270 environment variable and your configuration files. Previous file
4266 4271 contents are saved with a ``.orig`` suffix.
4267 4272
4268 4273 - :hg:`resolve -m [FILE]`: mark a file as having been resolved
4269 4274 (e.g. after having manually fixed-up the files). The default is
4270 4275 to mark all unresolved files.
4271 4276
4272 4277 - :hg:`resolve -u [FILE]...`: mark a file as unresolved. The
4273 4278 default is to mark all resolved files.
4274 4279
4275 4280 - :hg:`resolve -l`: list files which had or still have conflicts.
4276 4281 In the printed list, ``U`` = unresolved and ``R`` = resolved.
4277 4282 You can use ``set:unresolved()`` or ``set:resolved()`` to filter
4278 4283 the list. See :hg:`help filesets` for details.
4279 4284
4280 4285 .. note::
4281 4286
4282 4287 Mercurial will not let you commit files with unresolved merge
4283 4288 conflicts. You must use :hg:`resolve -m ...` before you can
4284 4289 commit after a conflicting merge.
4285 4290
4286 4291 Returns 0 on success, 1 if any files fail a resolve attempt.
4287 4292 """
4288 4293
4289 4294 opts = pycompat.byteskwargs(opts)
4290 4295 flaglist = 'all mark unmark list no_status'.split()
4291 4296 all, mark, unmark, show, nostatus = \
4292 4297 [opts.get(o) for o in flaglist]
4293 4298
4294 4299 if (show and (mark or unmark)) or (mark and unmark):
4295 4300 raise error.Abort(_("too many options specified"))
4296 4301 if pats and all:
4297 4302 raise error.Abort(_("can't specify --all and patterns"))
4298 4303 if not (all or pats or show or mark or unmark):
4299 4304 raise error.Abort(_('no files or directories specified'),
4300 4305 hint=('use --all to re-merge all unresolved files'))
4301 4306
4302 4307 if show:
4303 4308 ui.pager('resolve')
4304 4309 fm = ui.formatter('resolve', opts)
4305 4310 ms = mergemod.mergestate.read(repo)
4306 4311 m = scmutil.match(repo[None], pats, opts)
4307 4312 for f in ms:
4308 4313 if not m(f):
4309 4314 continue
4310 4315 l = 'resolve.' + {'u': 'unresolved', 'r': 'resolved',
4311 4316 'd': 'driverresolved'}[ms[f]]
4312 4317 fm.startitem()
4313 4318 fm.condwrite(not nostatus, 'status', '%s ', ms[f].upper(), label=l)
4314 4319 fm.write('path', '%s\n', f, label=l)
4315 4320 fm.end()
4316 4321 return 0
4317 4322
4318 4323 with repo.wlock():
4319 4324 ms = mergemod.mergestate.read(repo)
4320 4325
4321 4326 if not (ms.active() or repo.dirstate.p2() != nullid):
4322 4327 raise error.Abort(
4323 4328 _('resolve command not applicable when not merging'))
4324 4329
4325 4330 wctx = repo[None]
4326 4331
4327 4332 if ms.mergedriver and ms.mdstate() == 'u':
4328 4333 proceed = mergemod.driverpreprocess(repo, ms, wctx)
4329 4334 ms.commit()
4330 4335 # allow mark and unmark to go through
4331 4336 if not mark and not unmark and not proceed:
4332 4337 return 1
4333 4338
4334 4339 m = scmutil.match(wctx, pats, opts)
4335 4340 ret = 0
4336 4341 didwork = False
4337 4342 runconclude = False
4338 4343
4339 4344 tocomplete = []
4340 4345 for f in ms:
4341 4346 if not m(f):
4342 4347 continue
4343 4348
4344 4349 didwork = True
4345 4350
4346 4351 # don't let driver-resolved files be marked, and run the conclude
4347 4352 # step if asked to resolve
4348 4353 if ms[f] == "d":
4349 4354 exact = m.exact(f)
4350 4355 if mark:
4351 4356 if exact:
4352 4357 ui.warn(_('not marking %s as it is driver-resolved\n')
4353 4358 % f)
4354 4359 elif unmark:
4355 4360 if exact:
4356 4361 ui.warn(_('not unmarking %s as it is driver-resolved\n')
4357 4362 % f)
4358 4363 else:
4359 4364 runconclude = True
4360 4365 continue
4361 4366
4362 4367 if mark:
4363 4368 ms.mark(f, "r")
4364 4369 elif unmark:
4365 4370 ms.mark(f, "u")
4366 4371 else:
4367 4372 # backup pre-resolve (merge uses .orig for its own purposes)
4368 4373 a = repo.wjoin(f)
4369 4374 try:
4370 4375 util.copyfile(a, a + ".resolve")
4371 4376 except (IOError, OSError) as inst:
4372 4377 if inst.errno != errno.ENOENT:
4373 4378 raise
4374 4379
4375 4380 try:
4376 4381 # preresolve file
4377 4382 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
4378 4383 'resolve')
4379 4384 complete, r = ms.preresolve(f, wctx)
4380 4385 if not complete:
4381 4386 tocomplete.append(f)
4382 4387 elif r:
4383 4388 ret = 1
4384 4389 finally:
4385 4390 ui.setconfig('ui', 'forcemerge', '', 'resolve')
4386 4391 ms.commit()
4387 4392
4388 4393 # replace filemerge's .orig file with our resolve file, but only
4389 4394 # for merges that are complete
4390 4395 if complete:
4391 4396 try:
4392 4397 util.rename(a + ".resolve",
4393 4398 scmutil.origpath(ui, repo, a))
4394 4399 except OSError as inst:
4395 4400 if inst.errno != errno.ENOENT:
4396 4401 raise
4397 4402
4398 4403 for f in tocomplete:
4399 4404 try:
4400 4405 # resolve file
4401 4406 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
4402 4407 'resolve')
4403 4408 r = ms.resolve(f, wctx)
4404 4409 if r:
4405 4410 ret = 1
4406 4411 finally:
4407 4412 ui.setconfig('ui', 'forcemerge', '', 'resolve')
4408 4413 ms.commit()
4409 4414
4410 4415 # replace filemerge's .orig file with our resolve file
4411 4416 a = repo.wjoin(f)
4412 4417 try:
4413 4418 util.rename(a + ".resolve", scmutil.origpath(ui, repo, a))
4414 4419 except OSError as inst:
4415 4420 if inst.errno != errno.ENOENT:
4416 4421 raise
4417 4422
4418 4423 ms.commit()
4419 4424 ms.recordactions()
4420 4425
4421 4426 if not didwork and pats:
4422 4427 hint = None
4423 4428 if not any([p for p in pats if p.find(':') >= 0]):
4424 4429 pats = ['path:%s' % p for p in pats]
4425 4430 m = scmutil.match(wctx, pats, opts)
4426 4431 for f in ms:
4427 4432 if not m(f):
4428 4433 continue
4429 4434 flags = ''.join(['-%s ' % o[0] for o in flaglist
4430 4435 if opts.get(o)])
4431 4436 hint = _("(try: hg resolve %s%s)\n") % (
4432 4437 flags,
4433 4438 ' '.join(pats))
4434 4439 break
4435 4440 ui.warn(_("arguments do not match paths that need resolving\n"))
4436 4441 if hint:
4437 4442 ui.warn(hint)
4438 4443 elif ms.mergedriver and ms.mdstate() != 's':
4439 4444 # run conclude step when either a driver-resolved file is requested
4440 4445 # or there are no driver-resolved files
4441 4446 # we can't use 'ret' to determine whether any files are unresolved
4442 4447 # because we might not have tried to resolve some
4443 4448 if ((runconclude or not list(ms.driverresolved()))
4444 4449 and not list(ms.unresolved())):
4445 4450 proceed = mergemod.driverconclude(repo, ms, wctx)
4446 4451 ms.commit()
4447 4452 if not proceed:
4448 4453 return 1
4449 4454
4450 4455 # Nudge users into finishing an unfinished operation
4451 4456 unresolvedf = list(ms.unresolved())
4452 4457 driverresolvedf = list(ms.driverresolved())
4453 4458 if not unresolvedf and not driverresolvedf:
4454 4459 ui.status(_('(no more unresolved files)\n'))
4455 4460 cmdutil.checkafterresolved(repo)
4456 4461 elif not unresolvedf:
4457 4462 ui.status(_('(no more unresolved files -- '
4458 4463 'run "hg resolve --all" to conclude)\n'))
4459 4464
4460 4465 return ret
4461 4466
4462 4467 @command('revert',
4463 4468 [('a', 'all', None, _('revert all changes when no arguments given')),
4464 4469 ('d', 'date', '', _('tipmost revision matching date'), _('DATE')),
4465 4470 ('r', 'rev', '', _('revert to the specified revision'), _('REV')),
4466 4471 ('C', 'no-backup', None, _('do not save backup copies of files')),
4467 4472 ('i', 'interactive', None,
4468 4473 _('interactively select the changes (EXPERIMENTAL)')),
4469 4474 ] + walkopts + dryrunopts,
4470 4475 _('[OPTION]... [-r REV] [NAME]...'))
4471 4476 def revert(ui, repo, *pats, **opts):
4472 4477 """restore files to their checkout state
4473 4478
4474 4479 .. note::
4475 4480
4476 4481 To check out earlier revisions, you should use :hg:`update REV`.
4477 4482 To cancel an uncommitted merge (and lose your changes),
4478 4483 use :hg:`update --clean .`.
4479 4484
4480 4485 With no revision specified, revert the specified files or directories
4481 4486 to the contents they had in the parent of the working directory.
4482 4487 This restores the contents of files to an unmodified
4483 4488 state and unschedules adds, removes, copies, and renames. If the
4484 4489 working directory has two parents, you must explicitly specify a
4485 4490 revision.
4486 4491
4487 4492 Using the -r/--rev or -d/--date options, revert the given files or
4488 4493 directories to their states as of a specific revision. Because
4489 4494 revert does not change the working directory parents, this will
4490 4495 cause these files to appear modified. This can be helpful to "back
4491 4496 out" some or all of an earlier change. See :hg:`backout` for a
4492 4497 related method.
4493 4498
4494 4499 Modified files are saved with a .orig suffix before reverting.
4495 4500 To disable these backups, use --no-backup. It is possible to store
4496 4501 the backup files in a custom directory relative to the root of the
4497 4502 repository by setting the ``ui.origbackuppath`` configuration
4498 4503 option.
4499 4504
4500 4505 See :hg:`help dates` for a list of formats valid for -d/--date.
4501 4506
4502 4507 See :hg:`help backout` for a way to reverse the effect of an
4503 4508 earlier changeset.
4504 4509
4505 4510 Returns 0 on success.
4506 4511 """
4507 4512
4508 4513 if opts.get("date"):
4509 4514 if opts.get("rev"):
4510 4515 raise error.Abort(_("you can't specify a revision and a date"))
4511 4516 opts["rev"] = cmdutil.finddate(ui, repo, opts["date"])
4512 4517
4513 4518 parent, p2 = repo.dirstate.parents()
4514 4519 if not opts.get('rev') and p2 != nullid:
4515 4520 # revert after merge is a trap for new users (issue2915)
4516 4521 raise error.Abort(_('uncommitted merge with no revision specified'),
4517 4522 hint=_("use 'hg update' or see 'hg help revert'"))
4518 4523
4519 4524 ctx = scmutil.revsingle(repo, opts.get('rev'))
4520 4525
4521 4526 if (not (pats or opts.get('include') or opts.get('exclude') or
4522 4527 opts.get('all') or opts.get('interactive'))):
4523 4528 msg = _("no files or directories specified")
4524 4529 if p2 != nullid:
4525 4530 hint = _("uncommitted merge, use --all to discard all changes,"
4526 4531 " or 'hg update -C .' to abort the merge")
4527 4532 raise error.Abort(msg, hint=hint)
4528 4533 dirty = any(repo.status())
4529 4534 node = ctx.node()
4530 4535 if node != parent:
4531 4536 if dirty:
4532 4537 hint = _("uncommitted changes, use --all to discard all"
4533 4538 " changes, or 'hg update %s' to update") % ctx.rev()
4534 4539 else:
4535 4540 hint = _("use --all to revert all files,"
4536 4541 " or 'hg update %s' to update") % ctx.rev()
4537 4542 elif dirty:
4538 4543 hint = _("uncommitted changes, use --all to discard all changes")
4539 4544 else:
4540 4545 hint = _("use --all to revert all files")
4541 4546 raise error.Abort(msg, hint=hint)
4542 4547
4543 4548 return cmdutil.revert(ui, repo, ctx, (parent, p2), *pats, **opts)
4544 4549
4545 4550 @command('rollback', dryrunopts +
4546 4551 [('f', 'force', False, _('ignore safety measures'))])
4547 4552 def rollback(ui, repo, **opts):
4548 4553 """roll back the last transaction (DANGEROUS) (DEPRECATED)
4549 4554
4550 4555 Please use :hg:`commit --amend` instead of rollback to correct
4551 4556 mistakes in the last commit.
4552 4557
4553 4558 This command should be used with care. There is only one level of
4554 4559 rollback, and there is no way to undo a rollback. It will also
4555 4560 restore the dirstate at the time of the last transaction, losing
4556 4561 any dirstate changes since that time. This command does not alter
4557 4562 the working directory.
4558 4563
4559 4564 Transactions are used to encapsulate the effects of all commands
4560 4565 that create new changesets or propagate existing changesets into a
4561 4566 repository.
4562 4567
4563 4568 .. container:: verbose
4564 4569
4565 4570 For example, the following commands are transactional, and their
4566 4571 effects can be rolled back:
4567 4572
4568 4573 - commit
4569 4574 - import
4570 4575 - pull
4571 4576 - push (with this repository as the destination)
4572 4577 - unbundle
4573 4578
4574 4579 To avoid permanent data loss, rollback will refuse to rollback a
4575 4580 commit transaction if it isn't checked out. Use --force to
4576 4581 override this protection.
4577 4582
4578 4583 The rollback command can be entirely disabled by setting the
4579 4584 ``ui.rollback`` configuration setting to false. If you're here
4580 4585 because you want to use rollback and it's disabled, you can
4581 4586 re-enable the command by setting ``ui.rollback`` to true.
4582 4587
4583 4588 This command is not intended for use on public repositories. Once
4584 4589 changes are visible for pull by other users, rolling a transaction
4585 4590 back locally is ineffective (someone else may already have pulled
4586 4591 the changes). Furthermore, a race is possible with readers of the
4587 4592 repository; for example an in-progress pull from the repository
4588 4593 may fail if a rollback is performed.
4589 4594
4590 4595 Returns 0 on success, 1 if no rollback data is available.
4591 4596 """
4592 4597 if not ui.configbool('ui', 'rollback', True):
4593 4598 raise error.Abort(_('rollback is disabled because it is unsafe'),
4594 4599 hint=('see `hg help -v rollback` for information'))
4595 4600 return repo.rollback(dryrun=opts.get(r'dry_run'),
4596 4601 force=opts.get(r'force'))
4597 4602
4598 4603 @command('root', [])
4599 4604 def root(ui, repo):
4600 4605 """print the root (top) of the current working directory
4601 4606
4602 4607 Print the root directory of the current repository.
4603 4608
4604 4609 Returns 0 on success.
4605 4610 """
4606 4611 ui.write(repo.root + "\n")
4607 4612
4608 4613 @command('^serve',
4609 4614 [('A', 'accesslog', '', _('name of access log file to write to'),
4610 4615 _('FILE')),
4611 4616 ('d', 'daemon', None, _('run server in background')),
4612 4617 ('', 'daemon-postexec', [], _('used internally by daemon mode')),
4613 4618 ('E', 'errorlog', '', _('name of error log file to write to'), _('FILE')),
4614 4619 # use string type, then we can check if something was passed
4615 4620 ('p', 'port', '', _('port to listen on (default: 8000)'), _('PORT')),
4616 4621 ('a', 'address', '', _('address to listen on (default: all interfaces)'),
4617 4622 _('ADDR')),
4618 4623 ('', 'prefix', '', _('prefix path to serve from (default: server root)'),
4619 4624 _('PREFIX')),
4620 4625 ('n', 'name', '',
4621 4626 _('name to show in web pages (default: working directory)'), _('NAME')),
4622 4627 ('', 'web-conf', '',
4623 4628 _("name of the hgweb config file (see 'hg help hgweb')"), _('FILE')),
4624 4629 ('', 'webdir-conf', '', _('name of the hgweb config file (DEPRECATED)'),
4625 4630 _('FILE')),
4626 4631 ('', 'pid-file', '', _('name of file to write process ID to'), _('FILE')),
4627 4632 ('', 'stdio', None, _('for remote clients (ADVANCED)')),
4628 4633 ('', 'cmdserver', '', _('for remote clients (ADVANCED)'), _('MODE')),
4629 4634 ('t', 'templates', '', _('web templates to use'), _('TEMPLATE')),
4630 4635 ('', 'style', '', _('template style to use'), _('STYLE')),
4631 4636 ('6', 'ipv6', None, _('use IPv6 in addition to IPv4')),
4632 4637 ('', 'certificate', '', _('SSL certificate file'), _('FILE'))]
4633 4638 + subrepoopts,
4634 4639 _('[OPTION]...'),
4635 4640 optionalrepo=True)
4636 4641 def serve(ui, repo, **opts):
4637 4642 """start stand-alone webserver
4638 4643
4639 4644 Start a local HTTP repository browser and pull server. You can use
4640 4645 this for ad-hoc sharing and browsing of repositories. It is
4641 4646 recommended to use a real web server to serve a repository for
4642 4647 longer periods of time.
4643 4648
4644 4649 Please note that the server does not implement access control.
4645 4650 This means that, by default, anybody can read from the server and
4646 4651 nobody can write to it by default. Set the ``web.allow_push``
4647 4652 option to ``*`` to allow everybody to push to the server. You
4648 4653 should use a real web server if you need to authenticate users.
4649 4654
4650 4655 By default, the server logs accesses to stdout and errors to
4651 4656 stderr. Use the -A/--accesslog and -E/--errorlog options to log to
4652 4657 files.
4653 4658
4654 4659 To have the server choose a free port number to listen on, specify
4655 4660 a port number of 0; in this case, the server will print the port
4656 4661 number it uses.
4657 4662
4658 4663 Returns 0 on success.
4659 4664 """
4660 4665
4661 4666 opts = pycompat.byteskwargs(opts)
4662 4667 if opts["stdio"] and opts["cmdserver"]:
4663 4668 raise error.Abort(_("cannot use --stdio with --cmdserver"))
4664 4669
4665 4670 if opts["stdio"]:
4666 4671 if repo is None:
4667 4672 raise error.RepoError(_("there is no Mercurial repository here"
4668 4673 " (.hg not found)"))
4669 4674 s = sshserver.sshserver(ui, repo)
4670 4675 s.serve_forever()
4671 4676
4672 4677 service = server.createservice(ui, repo, opts)
4673 4678 return server.runservice(opts, initfn=service.init, runfn=service.run)
4674 4679
4675 4680 @command('^status|st',
4676 4681 [('A', 'all', None, _('show status of all files')),
4677 4682 ('m', 'modified', None, _('show only modified files')),
4678 4683 ('a', 'added', None, _('show only added files')),
4679 4684 ('r', 'removed', None, _('show only removed files')),
4680 4685 ('d', 'deleted', None, _('show only deleted (but tracked) files')),
4681 4686 ('c', 'clean', None, _('show only files without changes')),
4682 4687 ('u', 'unknown', None, _('show only unknown (not tracked) files')),
4683 4688 ('i', 'ignored', None, _('show only ignored files')),
4684 4689 ('n', 'no-status', None, _('hide status prefix')),
4685 4690 ('C', 'copies', None, _('show source of copied files')),
4686 4691 ('0', 'print0', None, _('end filenames with NUL, for use with xargs')),
4687 4692 ('', 'rev', [], _('show difference from revision'), _('REV')),
4688 4693 ('', 'change', '', _('list the changed files of a revision'), _('REV')),
4689 4694 ] + walkopts + subrepoopts + formatteropts,
4690 4695 _('[OPTION]... [FILE]...'),
4691 4696 inferrepo=True)
4692 4697 def status(ui, repo, *pats, **opts):
4693 4698 """show changed files in the working directory
4694 4699
4695 4700 Show status of files in the repository. If names are given, only
4696 4701 files that match are shown. Files that are clean or ignored or
4697 4702 the source of a copy/move operation, are not listed unless
4698 4703 -c/--clean, -i/--ignored, -C/--copies or -A/--all are given.
4699 4704 Unless options described with "show only ..." are given, the
4700 4705 options -mardu are used.
4701 4706
4702 4707 Option -q/--quiet hides untracked (unknown and ignored) files
4703 4708 unless explicitly requested with -u/--unknown or -i/--ignored.
4704 4709
4705 4710 .. note::
4706 4711
4707 4712 :hg:`status` may appear to disagree with diff if permissions have
4708 4713 changed or a merge has occurred. The standard diff format does
4709 4714 not report permission changes and diff only reports changes
4710 4715 relative to one merge parent.
4711 4716
4712 4717 If one revision is given, it is used as the base revision.
4713 4718 If two revisions are given, the differences between them are
4714 4719 shown. The --change option can also be used as a shortcut to list
4715 4720 the changed files of a revision from its first parent.
4716 4721
4717 4722 The codes used to show the status of files are::
4718 4723
4719 4724 M = modified
4720 4725 A = added
4721 4726 R = removed
4722 4727 C = clean
4723 4728 ! = missing (deleted by non-hg command, but still tracked)
4724 4729 ? = not tracked
4725 4730 I = ignored
4726 4731 = origin of the previous file (with --copies)
4727 4732
4728 4733 .. container:: verbose
4729 4734
4730 4735 Examples:
4731 4736
4732 4737 - show changes in the working directory relative to a
4733 4738 changeset::
4734 4739
4735 4740 hg status --rev 9353
4736 4741
4737 4742 - show changes in the working directory relative to the
4738 4743 current directory (see :hg:`help patterns` for more information)::
4739 4744
4740 4745 hg status re:
4741 4746
4742 4747 - show all changes including copies in an existing changeset::
4743 4748
4744 4749 hg status --copies --change 9353
4745 4750
4746 4751 - get a NUL separated list of added files, suitable for xargs::
4747 4752
4748 4753 hg status -an0
4749 4754
4750 4755 Returns 0 on success.
4751 4756 """
4752 4757
4753 4758 opts = pycompat.byteskwargs(opts)
4754 4759 revs = opts.get('rev')
4755 4760 change = opts.get('change')
4756 4761
4757 4762 if revs and change:
4758 4763 msg = _('cannot specify --rev and --change at the same time')
4759 4764 raise error.Abort(msg)
4760 4765 elif change:
4761 4766 node2 = scmutil.revsingle(repo, change, None).node()
4762 4767 node1 = repo[node2].p1().node()
4763 4768 else:
4764 4769 node1, node2 = scmutil.revpair(repo, revs)
4765 4770
4766 4771 if pats or ui.configbool('commands', 'status.relative'):
4767 4772 cwd = repo.getcwd()
4768 4773 else:
4769 4774 cwd = ''
4770 4775
4771 4776 if opts.get('print0'):
4772 4777 end = '\0'
4773 4778 else:
4774 4779 end = '\n'
4775 4780 copy = {}
4776 4781 states = 'modified added removed deleted unknown ignored clean'.split()
4777 4782 show = [k for k in states if opts.get(k)]
4778 4783 if opts.get('all'):
4779 4784 show += ui.quiet and (states[:4] + ['clean']) or states
4780 4785 if not show:
4781 4786 if ui.quiet:
4782 4787 show = states[:4]
4783 4788 else:
4784 4789 show = states[:5]
4785 4790
4786 4791 m = scmutil.match(repo[node2], pats, opts)
4787 4792 stat = repo.status(node1, node2, m,
4788 4793 'ignored' in show, 'clean' in show, 'unknown' in show,
4789 4794 opts.get('subrepos'))
4790 4795 changestates = zip(states, pycompat.iterbytestr('MAR!?IC'), stat)
4791 4796
4792 4797 if (opts.get('all') or opts.get('copies')
4793 4798 or ui.configbool('ui', 'statuscopies')) and not opts.get('no_status'):
4794 4799 copy = copies.pathcopies(repo[node1], repo[node2], m)
4795 4800
4796 4801 ui.pager('status')
4797 4802 fm = ui.formatter('status', opts)
4798 4803 fmt = '%s' + end
4799 4804 showchar = not opts.get('no_status')
4800 4805
4801 4806 for state, char, files in changestates:
4802 4807 if state in show:
4803 4808 label = 'status.' + state
4804 4809 for f in files:
4805 4810 fm.startitem()
4806 4811 fm.condwrite(showchar, 'status', '%s ', char, label=label)
4807 4812 fm.write('path', fmt, repo.pathto(f, cwd), label=label)
4808 4813 if f in copy:
4809 4814 fm.write("copy", ' %s' + end, repo.pathto(copy[f], cwd),
4810 4815 label='status.copied')
4811 4816 fm.end()
4812 4817
4813 4818 @command('^summary|sum',
4814 4819 [('', 'remote', None, _('check for push and pull'))], '[--remote]')
4815 4820 def summary(ui, repo, **opts):
4816 4821 """summarize working directory state
4817 4822
4818 4823 This generates a brief summary of the working directory state,
4819 4824 including parents, branch, commit status, phase and available updates.
4820 4825
4821 4826 With the --remote option, this will check the default paths for
4822 4827 incoming and outgoing changes. This can be time-consuming.
4823 4828
4824 4829 Returns 0 on success.
4825 4830 """
4826 4831
4827 4832 opts = pycompat.byteskwargs(opts)
4828 4833 ui.pager('summary')
4829 4834 ctx = repo[None]
4830 4835 parents = ctx.parents()
4831 4836 pnode = parents[0].node()
4832 4837 marks = []
4833 4838
4834 4839 ms = None
4835 4840 try:
4836 4841 ms = mergemod.mergestate.read(repo)
4837 4842 except error.UnsupportedMergeRecords as e:
4838 4843 s = ' '.join(e.recordtypes)
4839 4844 ui.warn(
4840 4845 _('warning: merge state has unsupported record types: %s\n') % s)
4841 4846 unresolved = 0
4842 4847 else:
4843 4848 unresolved = [f for f in ms if ms[f] == 'u']
4844 4849
4845 4850 for p in parents:
4846 4851 # label with log.changeset (instead of log.parent) since this
4847 4852 # shows a working directory parent *changeset*:
4848 4853 # i18n: column positioning for "hg summary"
4849 4854 ui.write(_('parent: %d:%s ') % (p.rev(), p),
4850 4855 label=cmdutil._changesetlabels(p))
4851 4856 ui.write(' '.join(p.tags()), label='log.tag')
4852 4857 if p.bookmarks():
4853 4858 marks.extend(p.bookmarks())
4854 4859 if p.rev() == -1:
4855 4860 if not len(repo):
4856 4861 ui.write(_(' (empty repository)'))
4857 4862 else:
4858 4863 ui.write(_(' (no revision checked out)'))
4859 4864 if p.obsolete():
4860 4865 ui.write(_(' (obsolete)'))
4861 4866 if p.troubled():
4862 4867 ui.write(' ('
4863 4868 + ', '.join(ui.label(trouble, 'trouble.%s' % trouble)
4864 4869 for trouble in p.troubles())
4865 4870 + ')')
4866 4871 ui.write('\n')
4867 4872 if p.description():
4868 4873 ui.status(' ' + p.description().splitlines()[0].strip() + '\n',
4869 4874 label='log.summary')
4870 4875
4871 4876 branch = ctx.branch()
4872 4877 bheads = repo.branchheads(branch)
4873 4878 # i18n: column positioning for "hg summary"
4874 4879 m = _('branch: %s\n') % branch
4875 4880 if branch != 'default':
4876 4881 ui.write(m, label='log.branch')
4877 4882 else:
4878 4883 ui.status(m, label='log.branch')
4879 4884
4880 4885 if marks:
4881 4886 active = repo._activebookmark
4882 4887 # i18n: column positioning for "hg summary"
4883 4888 ui.write(_('bookmarks:'), label='log.bookmark')
4884 4889 if active is not None:
4885 4890 if active in marks:
4886 4891 ui.write(' *' + active, label=activebookmarklabel)
4887 4892 marks.remove(active)
4888 4893 else:
4889 4894 ui.write(' [%s]' % active, label=activebookmarklabel)
4890 4895 for m in marks:
4891 4896 ui.write(' ' + m, label='log.bookmark')
4892 4897 ui.write('\n', label='log.bookmark')
4893 4898
4894 4899 status = repo.status(unknown=True)
4895 4900
4896 4901 c = repo.dirstate.copies()
4897 4902 copied, renamed = [], []
4898 4903 for d, s in c.iteritems():
4899 4904 if s in status.removed:
4900 4905 status.removed.remove(s)
4901 4906 renamed.append(d)
4902 4907 else:
4903 4908 copied.append(d)
4904 4909 if d in status.added:
4905 4910 status.added.remove(d)
4906 4911
4907 4912 subs = [s for s in ctx.substate if ctx.sub(s).dirty()]
4908 4913
4909 4914 labels = [(ui.label(_('%d modified'), 'status.modified'), status.modified),
4910 4915 (ui.label(_('%d added'), 'status.added'), status.added),
4911 4916 (ui.label(_('%d removed'), 'status.removed'), status.removed),
4912 4917 (ui.label(_('%d renamed'), 'status.copied'), renamed),
4913 4918 (ui.label(_('%d copied'), 'status.copied'), copied),
4914 4919 (ui.label(_('%d deleted'), 'status.deleted'), status.deleted),
4915 4920 (ui.label(_('%d unknown'), 'status.unknown'), status.unknown),
4916 4921 (ui.label(_('%d unresolved'), 'resolve.unresolved'), unresolved),
4917 4922 (ui.label(_('%d subrepos'), 'status.modified'), subs)]
4918 4923 t = []
4919 4924 for l, s in labels:
4920 4925 if s:
4921 4926 t.append(l % len(s))
4922 4927
4923 4928 t = ', '.join(t)
4924 4929 cleanworkdir = False
4925 4930
4926 4931 if repo.vfs.exists('graftstate'):
4927 4932 t += _(' (graft in progress)')
4928 4933 if repo.vfs.exists('updatestate'):
4929 4934 t += _(' (interrupted update)')
4930 4935 elif len(parents) > 1:
4931 4936 t += _(' (merge)')
4932 4937 elif branch != parents[0].branch():
4933 4938 t += _(' (new branch)')
4934 4939 elif (parents[0].closesbranch() and
4935 4940 pnode in repo.branchheads(branch, closed=True)):
4936 4941 t += _(' (head closed)')
4937 4942 elif not (status.modified or status.added or status.removed or renamed or
4938 4943 copied or subs):
4939 4944 t += _(' (clean)')
4940 4945 cleanworkdir = True
4941 4946 elif pnode not in bheads:
4942 4947 t += _(' (new branch head)')
4943 4948
4944 4949 if parents:
4945 4950 pendingphase = max(p.phase() for p in parents)
4946 4951 else:
4947 4952 pendingphase = phases.public
4948 4953
4949 4954 if pendingphase > phases.newcommitphase(ui):
4950 4955 t += ' (%s)' % phases.phasenames[pendingphase]
4951 4956
4952 4957 if cleanworkdir:
4953 4958 # i18n: column positioning for "hg summary"
4954 4959 ui.status(_('commit: %s\n') % t.strip())
4955 4960 else:
4956 4961 # i18n: column positioning for "hg summary"
4957 4962 ui.write(_('commit: %s\n') % t.strip())
4958 4963
4959 4964 # all ancestors of branch heads - all ancestors of parent = new csets
4960 4965 new = len(repo.changelog.findmissing([pctx.node() for pctx in parents],
4961 4966 bheads))
4962 4967
4963 4968 if new == 0:
4964 4969 # i18n: column positioning for "hg summary"
4965 4970 ui.status(_('update: (current)\n'))
4966 4971 elif pnode not in bheads:
4967 4972 # i18n: column positioning for "hg summary"
4968 4973 ui.write(_('update: %d new changesets (update)\n') % new)
4969 4974 else:
4970 4975 # i18n: column positioning for "hg summary"
4971 4976 ui.write(_('update: %d new changesets, %d branch heads (merge)\n') %
4972 4977 (new, len(bheads)))
4973 4978
4974 4979 t = []
4975 4980 draft = len(repo.revs('draft()'))
4976 4981 if draft:
4977 4982 t.append(_('%d draft') % draft)
4978 4983 secret = len(repo.revs('secret()'))
4979 4984 if secret:
4980 4985 t.append(_('%d secret') % secret)
4981 4986
4982 4987 if draft or secret:
4983 4988 ui.status(_('phases: %s\n') % ', '.join(t))
4984 4989
4985 4990 if obsolete.isenabled(repo, obsolete.createmarkersopt):
4986 4991 for trouble in ("unstable", "divergent", "bumped"):
4987 4992 numtrouble = len(repo.revs(trouble + "()"))
4988 4993 # We write all the possibilities to ease translation
4989 4994 troublemsg = {
4990 4995 "unstable": _("unstable: %d changesets"),
4991 4996 "divergent": _("divergent: %d changesets"),
4992 4997 "bumped": _("bumped: %d changesets"),
4993 4998 }
4994 4999 if numtrouble > 0:
4995 5000 ui.status(troublemsg[trouble] % numtrouble + "\n")
4996 5001
4997 5002 cmdutil.summaryhooks(ui, repo)
4998 5003
4999 5004 if opts.get('remote'):
5000 5005 needsincoming, needsoutgoing = True, True
5001 5006 else:
5002 5007 needsincoming, needsoutgoing = False, False
5003 5008 for i, o in cmdutil.summaryremotehooks(ui, repo, opts, None):
5004 5009 if i:
5005 5010 needsincoming = True
5006 5011 if o:
5007 5012 needsoutgoing = True
5008 5013 if not needsincoming and not needsoutgoing:
5009 5014 return
5010 5015
5011 5016 def getincoming():
5012 5017 source, branches = hg.parseurl(ui.expandpath('default'))
5013 5018 sbranch = branches[0]
5014 5019 try:
5015 5020 other = hg.peer(repo, {}, source)
5016 5021 except error.RepoError:
5017 5022 if opts.get('remote'):
5018 5023 raise
5019 5024 return source, sbranch, None, None, None
5020 5025 revs, checkout = hg.addbranchrevs(repo, other, branches, None)
5021 5026 if revs:
5022 5027 revs = [other.lookup(rev) for rev in revs]
5023 5028 ui.debug('comparing with %s\n' % util.hidepassword(source))
5024 5029 repo.ui.pushbuffer()
5025 5030 commoninc = discovery.findcommonincoming(repo, other, heads=revs)
5026 5031 repo.ui.popbuffer()
5027 5032 return source, sbranch, other, commoninc, commoninc[1]
5028 5033
5029 5034 if needsincoming:
5030 5035 source, sbranch, sother, commoninc, incoming = getincoming()
5031 5036 else:
5032 5037 source = sbranch = sother = commoninc = incoming = None
5033 5038
5034 5039 def getoutgoing():
5035 5040 dest, branches = hg.parseurl(ui.expandpath('default-push', 'default'))
5036 5041 dbranch = branches[0]
5037 5042 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
5038 5043 if source != dest:
5039 5044 try:
5040 5045 dother = hg.peer(repo, {}, dest)
5041 5046 except error.RepoError:
5042 5047 if opts.get('remote'):
5043 5048 raise
5044 5049 return dest, dbranch, None, None
5045 5050 ui.debug('comparing with %s\n' % util.hidepassword(dest))
5046 5051 elif sother is None:
5047 5052 # there is no explicit destination peer, but source one is invalid
5048 5053 return dest, dbranch, None, None
5049 5054 else:
5050 5055 dother = sother
5051 5056 if (source != dest or (sbranch is not None and sbranch != dbranch)):
5052 5057 common = None
5053 5058 else:
5054 5059 common = commoninc
5055 5060 if revs:
5056 5061 revs = [repo.lookup(rev) for rev in revs]
5057 5062 repo.ui.pushbuffer()
5058 5063 outgoing = discovery.findcommonoutgoing(repo, dother, onlyheads=revs,
5059 5064 commoninc=common)
5060 5065 repo.ui.popbuffer()
5061 5066 return dest, dbranch, dother, outgoing
5062 5067
5063 5068 if needsoutgoing:
5064 5069 dest, dbranch, dother, outgoing = getoutgoing()
5065 5070 else:
5066 5071 dest = dbranch = dother = outgoing = None
5067 5072
5068 5073 if opts.get('remote'):
5069 5074 t = []
5070 5075 if incoming:
5071 5076 t.append(_('1 or more incoming'))
5072 5077 o = outgoing.missing
5073 5078 if o:
5074 5079 t.append(_('%d outgoing') % len(o))
5075 5080 other = dother or sother
5076 5081 if 'bookmarks' in other.listkeys('namespaces'):
5077 5082 counts = bookmarks.summary(repo, other)
5078 5083 if counts[0] > 0:
5079 5084 t.append(_('%d incoming bookmarks') % counts[0])
5080 5085 if counts[1] > 0:
5081 5086 t.append(_('%d outgoing bookmarks') % counts[1])
5082 5087
5083 5088 if t:
5084 5089 # i18n: column positioning for "hg summary"
5085 5090 ui.write(_('remote: %s\n') % (', '.join(t)))
5086 5091 else:
5087 5092 # i18n: column positioning for "hg summary"
5088 5093 ui.status(_('remote: (synced)\n'))
5089 5094
5090 5095 cmdutil.summaryremotehooks(ui, repo, opts,
5091 5096 ((source, sbranch, sother, commoninc),
5092 5097 (dest, dbranch, dother, outgoing)))
5093 5098
5094 5099 @command('tag',
5095 5100 [('f', 'force', None, _('force tag')),
5096 5101 ('l', 'local', None, _('make the tag local')),
5097 5102 ('r', 'rev', '', _('revision to tag'), _('REV')),
5098 5103 ('', 'remove', None, _('remove a tag')),
5099 5104 # -l/--local is already there, commitopts cannot be used
5100 5105 ('e', 'edit', None, _('invoke editor on commit messages')),
5101 5106 ('m', 'message', '', _('use text as commit message'), _('TEXT')),
5102 5107 ] + commitopts2,
5103 5108 _('[-f] [-l] [-m TEXT] [-d DATE] [-u USER] [-r REV] NAME...'))
5104 5109 def tag(ui, repo, name1, *names, **opts):
5105 5110 """add one or more tags for the current or given revision
5106 5111
5107 5112 Name a particular revision using <name>.
5108 5113
5109 5114 Tags are used to name particular revisions of the repository and are
5110 5115 very useful to compare different revisions, to go back to significant
5111 5116 earlier versions or to mark branch points as releases, etc. Changing
5112 5117 an existing tag is normally disallowed; use -f/--force to override.
5113 5118
5114 5119 If no revision is given, the parent of the working directory is
5115 5120 used.
5116 5121
5117 5122 To facilitate version control, distribution, and merging of tags,
5118 5123 they are stored as a file named ".hgtags" which is managed similarly
5119 5124 to other project files and can be hand-edited if necessary. This
5120 5125 also means that tagging creates a new commit. The file
5121 5126 ".hg/localtags" is used for local tags (not shared among
5122 5127 repositories).
5123 5128
5124 5129 Tag commits are usually made at the head of a branch. If the parent
5125 5130 of the working directory is not a branch head, :hg:`tag` aborts; use
5126 5131 -f/--force to force the tag commit to be based on a non-head
5127 5132 changeset.
5128 5133
5129 5134 See :hg:`help dates` for a list of formats valid for -d/--date.
5130 5135
5131 5136 Since tag names have priority over branch names during revision
5132 5137 lookup, using an existing branch name as a tag name is discouraged.
5133 5138
5134 5139 Returns 0 on success.
5135 5140 """
5136 5141 opts = pycompat.byteskwargs(opts)
5137 5142 wlock = lock = None
5138 5143 try:
5139 5144 wlock = repo.wlock()
5140 5145 lock = repo.lock()
5141 5146 rev_ = "."
5142 5147 names = [t.strip() for t in (name1,) + names]
5143 5148 if len(names) != len(set(names)):
5144 5149 raise error.Abort(_('tag names must be unique'))
5145 5150 for n in names:
5146 5151 scmutil.checknewlabel(repo, n, 'tag')
5147 5152 if not n:
5148 5153 raise error.Abort(_('tag names cannot consist entirely of '
5149 5154 'whitespace'))
5150 5155 if opts.get('rev') and opts.get('remove'):
5151 5156 raise error.Abort(_("--rev and --remove are incompatible"))
5152 5157 if opts.get('rev'):
5153 5158 rev_ = opts['rev']
5154 5159 message = opts.get('message')
5155 5160 if opts.get('remove'):
5156 5161 if opts.get('local'):
5157 5162 expectedtype = 'local'
5158 5163 else:
5159 5164 expectedtype = 'global'
5160 5165
5161 5166 for n in names:
5162 5167 if not repo.tagtype(n):
5163 5168 raise error.Abort(_("tag '%s' does not exist") % n)
5164 5169 if repo.tagtype(n) != expectedtype:
5165 5170 if expectedtype == 'global':
5166 5171 raise error.Abort(_("tag '%s' is not a global tag") % n)
5167 5172 else:
5168 5173 raise error.Abort(_("tag '%s' is not a local tag") % n)
5169 5174 rev_ = 'null'
5170 5175 if not message:
5171 5176 # we don't translate commit messages
5172 5177 message = 'Removed tag %s' % ', '.join(names)
5173 5178 elif not opts.get('force'):
5174 5179 for n in names:
5175 5180 if n in repo.tags():
5176 5181 raise error.Abort(_("tag '%s' already exists "
5177 5182 "(use -f to force)") % n)
5178 5183 if not opts.get('local'):
5179 5184 p1, p2 = repo.dirstate.parents()
5180 5185 if p2 != nullid:
5181 5186 raise error.Abort(_('uncommitted merge'))
5182 5187 bheads = repo.branchheads()
5183 5188 if not opts.get('force') and bheads and p1 not in bheads:
5184 5189 raise error.Abort(_('working directory is not at a branch head '
5185 5190 '(use -f to force)'))
5186 5191 r = scmutil.revsingle(repo, rev_).node()
5187 5192
5188 5193 if not message:
5189 5194 # we don't translate commit messages
5190 5195 message = ('Added tag %s for changeset %s' %
5191 5196 (', '.join(names), short(r)))
5192 5197
5193 5198 date = opts.get('date')
5194 5199 if date:
5195 5200 date = util.parsedate(date)
5196 5201
5197 5202 if opts.get('remove'):
5198 5203 editform = 'tag.remove'
5199 5204 else:
5200 5205 editform = 'tag.add'
5201 5206 editor = cmdutil.getcommiteditor(editform=editform,
5202 5207 **pycompat.strkwargs(opts))
5203 5208
5204 5209 # don't allow tagging the null rev
5205 5210 if (not opts.get('remove') and
5206 5211 scmutil.revsingle(repo, rev_).rev() == nullrev):
5207 5212 raise error.Abort(_("cannot tag null revision"))
5208 5213
5209 5214 tagsmod.tag(repo, names, r, message, opts.get('local'),
5210 5215 opts.get('user'), date, editor=editor)
5211 5216 finally:
5212 5217 release(lock, wlock)
5213 5218
5214 5219 @command('tags', formatteropts, '')
5215 5220 def tags(ui, repo, **opts):
5216 5221 """list repository tags
5217 5222
5218 5223 This lists both regular and local tags. When the -v/--verbose
5219 5224 switch is used, a third column "local" is printed for local tags.
5220 5225 When the -q/--quiet switch is used, only the tag name is printed.
5221 5226
5222 5227 Returns 0 on success.
5223 5228 """
5224 5229
5225 5230 opts = pycompat.byteskwargs(opts)
5226 5231 ui.pager('tags')
5227 5232 fm = ui.formatter('tags', opts)
5228 5233 hexfunc = fm.hexfunc
5229 5234 tagtype = ""
5230 5235
5231 5236 for t, n in reversed(repo.tagslist()):
5232 5237 hn = hexfunc(n)
5233 5238 label = 'tags.normal'
5234 5239 tagtype = ''
5235 5240 if repo.tagtype(t) == 'local':
5236 5241 label = 'tags.local'
5237 5242 tagtype = 'local'
5238 5243
5239 5244 fm.startitem()
5240 5245 fm.write('tag', '%s', t, label=label)
5241 5246 fmt = " " * (30 - encoding.colwidth(t)) + ' %5d:%s'
5242 5247 fm.condwrite(not ui.quiet, 'rev node', fmt,
5243 5248 repo.changelog.rev(n), hn, label=label)
5244 5249 fm.condwrite(ui.verbose and tagtype, 'type', ' %s',
5245 5250 tagtype, label=label)
5246 5251 fm.plain('\n')
5247 5252 fm.end()
5248 5253
5249 5254 @command('tip',
5250 5255 [('p', 'patch', None, _('show patch')),
5251 5256 ('g', 'git', None, _('use git extended diff format')),
5252 5257 ] + templateopts,
5253 5258 _('[-p] [-g]'))
5254 5259 def tip(ui, repo, **opts):
5255 5260 """show the tip revision (DEPRECATED)
5256 5261
5257 5262 The tip revision (usually just called the tip) is the changeset
5258 5263 most recently added to the repository (and therefore the most
5259 5264 recently changed head).
5260 5265
5261 5266 If you have just made a commit, that commit will be the tip. If
5262 5267 you have just pulled changes from another repository, the tip of
5263 5268 that repository becomes the current tip. The "tip" tag is special
5264 5269 and cannot be renamed or assigned to a different changeset.
5265 5270
5266 5271 This command is deprecated, please use :hg:`heads` instead.
5267 5272
5268 5273 Returns 0 on success.
5269 5274 """
5270 5275 opts = pycompat.byteskwargs(opts)
5271 5276 displayer = cmdutil.show_changeset(ui, repo, opts)
5272 5277 displayer.show(repo['tip'])
5273 5278 displayer.close()
5274 5279
5275 5280 @command('unbundle',
5276 5281 [('u', 'update', None,
5277 5282 _('update to new branch head if changesets were unbundled'))],
5278 5283 _('[-u] FILE...'))
5279 5284 def unbundle(ui, repo, fname1, *fnames, **opts):
5280 5285 """apply one or more bundle files
5281 5286
5282 5287 Apply one or more bundle files generated by :hg:`bundle`.
5283 5288
5284 5289 Returns 0 on success, 1 if an update has unresolved files.
5285 5290 """
5286 5291 fnames = (fname1,) + fnames
5287 5292
5288 5293 with repo.lock():
5289 5294 for fname in fnames:
5290 5295 f = hg.openpath(ui, fname)
5291 5296 gen = exchange.readbundle(ui, f, fname)
5292 5297 if isinstance(gen, bundle2.unbundle20):
5293 5298 tr = repo.transaction('unbundle')
5294 5299 try:
5295 5300 op = bundle2.applybundle(repo, gen, tr, source='unbundle',
5296 5301 url='bundle:' + fname)
5297 5302 tr.close()
5298 5303 except error.BundleUnknownFeatureError as exc:
5299 5304 raise error.Abort(_('%s: unknown bundle feature, %s')
5300 5305 % (fname, exc),
5301 5306 hint=_("see https://mercurial-scm.org/"
5302 5307 "wiki/BundleFeature for more "
5303 5308 "information"))
5304 5309 finally:
5305 5310 if tr:
5306 5311 tr.release()
5307 5312 changes = [r.get('return', 0)
5308 5313 for r in op.records['changegroup']]
5309 5314 modheads = changegroup.combineresults(changes)
5310 5315 elif isinstance(gen, streamclone.streamcloneapplier):
5311 5316 raise error.Abort(
5312 5317 _('packed bundles cannot be applied with '
5313 5318 '"hg unbundle"'),
5314 5319 hint=_('use "hg debugapplystreamclonebundle"'))
5315 5320 else:
5316 5321 modheads = gen.apply(repo, 'unbundle', 'bundle:' + fname)
5317 5322
5318 5323 return postincoming(ui, repo, modheads, opts.get(r'update'), None, None)
5319 5324
5320 5325 @command('^update|up|checkout|co',
5321 5326 [('C', 'clean', None, _('discard uncommitted changes (no backup)')),
5322 5327 ('c', 'check', None, _('require clean working directory')),
5323 5328 ('m', 'merge', None, _('merge uncommitted changes')),
5324 5329 ('d', 'date', '', _('tipmost revision matching date'), _('DATE')),
5325 5330 ('r', 'rev', '', _('revision'), _('REV'))
5326 5331 ] + mergetoolopts,
5327 5332 _('[-C|-c|-m] [-d DATE] [[-r] REV]'))
5328 5333 def update(ui, repo, node=None, rev=None, clean=False, date=None, check=False,
5329 5334 merge=None, tool=None):
5330 5335 """update working directory (or switch revisions)
5331 5336
5332 5337 Update the repository's working directory to the specified
5333 5338 changeset. If no changeset is specified, update to the tip of the
5334 5339 current named branch and move the active bookmark (see :hg:`help
5335 5340 bookmarks`).
5336 5341
5337 5342 Update sets the working directory's parent revision to the specified
5338 5343 changeset (see :hg:`help parents`).
5339 5344
5340 5345 If the changeset is not a descendant or ancestor of the working
5341 5346 directory's parent and there are uncommitted changes, the update is
5342 5347 aborted. With the -c/--check option, the working directory is checked
5343 5348 for uncommitted changes; if none are found, the working directory is
5344 5349 updated to the specified changeset.
5345 5350
5346 5351 .. container:: verbose
5347 5352
5348 5353 The -C/--clean, -c/--check, and -m/--merge options control what
5349 5354 happens if the working directory contains uncommitted changes.
5350 5355 At most of one of them can be specified.
5351 5356
5352 5357 1. If no option is specified, and if
5353 5358 the requested changeset is an ancestor or descendant of
5354 5359 the working directory's parent, the uncommitted changes
5355 5360 are merged into the requested changeset and the merged
5356 5361 result is left uncommitted. If the requested changeset is
5357 5362 not an ancestor or descendant (that is, it is on another
5358 5363 branch), the update is aborted and the uncommitted changes
5359 5364 are preserved.
5360 5365
5361 5366 2. With the -m/--merge option, the update is allowed even if the
5362 5367 requested changeset is not an ancestor or descendant of
5363 5368 the working directory's parent.
5364 5369
5365 5370 3. With the -c/--check option, the update is aborted and the
5366 5371 uncommitted changes are preserved.
5367 5372
5368 5373 4. With the -C/--clean option, uncommitted changes are discarded and
5369 5374 the working directory is updated to the requested changeset.
5370 5375
5371 5376 To cancel an uncommitted merge (and lose your changes), use
5372 5377 :hg:`update --clean .`.
5373 5378
5374 5379 Use null as the changeset to remove the working directory (like
5375 5380 :hg:`clone -U`).
5376 5381
5377 5382 If you want to revert just one file to an older revision, use
5378 5383 :hg:`revert [-r REV] NAME`.
5379 5384
5380 5385 See :hg:`help dates` for a list of formats valid for -d/--date.
5381 5386
5382 5387 Returns 0 on success, 1 if there are unresolved files.
5383 5388 """
5384 5389 if rev and node:
5385 5390 raise error.Abort(_("please specify just one revision"))
5386 5391
5387 5392 if ui.configbool('commands', 'update.requiredest'):
5388 5393 if not node and not rev and not date:
5389 5394 raise error.Abort(_('you must specify a destination'),
5390 5395 hint=_('for example: hg update ".::"'))
5391 5396
5392 5397 if rev is None or rev == '':
5393 5398 rev = node
5394 5399
5395 5400 if date and rev is not None:
5396 5401 raise error.Abort(_("you can't specify a revision and a date"))
5397 5402
5398 5403 if len([x for x in (clean, check, merge) if x]) > 1:
5399 5404 raise error.Abort(_("can only specify one of -C/--clean, -c/--check, "
5400 5405 "or -m/merge"))
5401 5406
5402 5407 updatecheck = None
5403 5408 if check:
5404 5409 updatecheck = 'abort'
5405 5410 elif merge:
5406 5411 updatecheck = 'none'
5407 5412
5408 5413 with repo.wlock():
5409 5414 cmdutil.clearunfinished(repo)
5410 5415
5411 5416 if date:
5412 5417 rev = cmdutil.finddate(ui, repo, date)
5413 5418
5414 5419 # if we defined a bookmark, we have to remember the original name
5415 5420 brev = rev
5416 5421 rev = scmutil.revsingle(repo, rev, rev).rev()
5417 5422
5418 5423 repo.ui.setconfig('ui', 'forcemerge', tool, 'update')
5419 5424
5420 5425 return hg.updatetotally(ui, repo, rev, brev, clean=clean,
5421 5426 updatecheck=updatecheck)
5422 5427
5423 5428 @command('verify', [])
5424 5429 def verify(ui, repo):
5425 5430 """verify the integrity of the repository
5426 5431
5427 5432 Verify the integrity of the current repository.
5428 5433
5429 5434 This will perform an extensive check of the repository's
5430 5435 integrity, validating the hashes and checksums of each entry in
5431 5436 the changelog, manifest, and tracked files, as well as the
5432 5437 integrity of their crosslinks and indices.
5433 5438
5434 5439 Please see https://mercurial-scm.org/wiki/RepositoryCorruption
5435 5440 for more information about recovery from corruption of the
5436 5441 repository.
5437 5442
5438 5443 Returns 0 on success, 1 if errors are encountered.
5439 5444 """
5440 5445 return hg.verify(repo)
5441 5446
5442 5447 @command('version', [] + formatteropts, norepo=True)
5443 5448 def version_(ui, **opts):
5444 5449 """output version and copyright information"""
5445 5450 opts = pycompat.byteskwargs(opts)
5446 5451 if ui.verbose:
5447 5452 ui.pager('version')
5448 5453 fm = ui.formatter("version", opts)
5449 5454 fm.startitem()
5450 5455 fm.write("ver", _("Mercurial Distributed SCM (version %s)\n"),
5451 5456 util.version())
5452 5457 license = _(
5453 5458 "(see https://mercurial-scm.org for more information)\n"
5454 5459 "\nCopyright (C) 2005-2017 Matt Mackall and others\n"
5455 5460 "This is free software; see the source for copying conditions. "
5456 5461 "There is NO\nwarranty; "
5457 5462 "not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"
5458 5463 )
5459 5464 if not ui.quiet:
5460 5465 fm.plain(license)
5461 5466
5462 5467 if ui.verbose:
5463 5468 fm.plain(_("\nEnabled extensions:\n\n"))
5464 5469 # format names and versions into columns
5465 5470 names = []
5466 5471 vers = []
5467 5472 isinternals = []
5468 5473 for name, module in extensions.extensions():
5469 5474 names.append(name)
5470 5475 vers.append(extensions.moduleversion(module) or None)
5471 5476 isinternals.append(extensions.ismoduleinternal(module))
5472 5477 fn = fm.nested("extensions")
5473 5478 if names:
5474 5479 namefmt = " %%-%ds " % max(len(n) for n in names)
5475 5480 places = [_("external"), _("internal")]
5476 5481 for n, v, p in zip(names, vers, isinternals):
5477 5482 fn.startitem()
5478 5483 fn.condwrite(ui.verbose, "name", namefmt, n)
5479 5484 if ui.verbose:
5480 5485 fn.plain("%s " % places[p])
5481 5486 fn.data(bundled=p)
5482 5487 fn.condwrite(ui.verbose and v, "ver", "%s", v)
5483 5488 if ui.verbose:
5484 5489 fn.plain("\n")
5485 5490 fn.end()
5486 5491 fm.end()
5487 5492
5488 5493 def loadcmdtable(ui, name, cmdtable):
5489 5494 """Load command functions from specified cmdtable
5490 5495 """
5491 5496 overrides = [cmd for cmd in cmdtable if cmd in table]
5492 5497 if overrides:
5493 5498 ui.warn(_("extension '%s' overrides commands: %s\n")
5494 5499 % (name, " ".join(overrides)))
5495 5500 table.update(cmdtable)
@@ -1,1988 +1,1989 b''
1 1 # subrepo.py - sub-repository handling for Mercurial
2 2 #
3 3 # Copyright 2009-2010 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 copy
11 11 import errno
12 12 import hashlib
13 13 import os
14 14 import posixpath
15 15 import re
16 16 import stat
17 17 import subprocess
18 18 import sys
19 19 import tarfile
20 20 import xml.dom.minidom
21 21
22 22
23 23 from .i18n import _
24 24 from . import (
25 25 cmdutil,
26 26 config,
27 27 encoding,
28 28 error,
29 29 exchange,
30 30 filemerge,
31 31 match as matchmod,
32 32 node,
33 33 pathutil,
34 34 phases,
35 35 pycompat,
36 36 scmutil,
37 37 util,
38 38 vfs as vfsmod,
39 39 )
40 40
41 41 hg = None
42 42 propertycache = util.propertycache
43 43
44 44 nullstate = ('', '', 'empty')
45 45
46 46 def _expandedabspath(path):
47 47 '''
48 48 get a path or url and if it is a path expand it and return an absolute path
49 49 '''
50 50 expandedpath = util.urllocalpath(util.expandpath(path))
51 51 u = util.url(expandedpath)
52 52 if not u.scheme:
53 53 path = util.normpath(os.path.abspath(u.path))
54 54 return path
55 55
56 56 def _getstorehashcachename(remotepath):
57 57 '''get a unique filename for the store hash cache of a remote repository'''
58 58 return hashlib.sha1(_expandedabspath(remotepath)).hexdigest()[0:12]
59 59
60 60 class SubrepoAbort(error.Abort):
61 61 """Exception class used to avoid handling a subrepo error more than once"""
62 62 def __init__(self, *args, **kw):
63 63 self.subrepo = kw.pop('subrepo', None)
64 64 self.cause = kw.pop('cause', None)
65 65 error.Abort.__init__(self, *args, **kw)
66 66
67 67 def annotatesubrepoerror(func):
68 68 def decoratedmethod(self, *args, **kargs):
69 69 try:
70 70 res = func(self, *args, **kargs)
71 71 except SubrepoAbort as ex:
72 72 # This exception has already been handled
73 73 raise ex
74 74 except error.Abort as ex:
75 75 subrepo = subrelpath(self)
76 76 errormsg = str(ex) + ' ' + _('(in subrepo %s)') % subrepo
77 77 # avoid handling this exception by raising a SubrepoAbort exception
78 78 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
79 79 cause=sys.exc_info())
80 80 return res
81 81 return decoratedmethod
82 82
83 83 def state(ctx, ui):
84 84 """return a state dict, mapping subrepo paths configured in .hgsub
85 85 to tuple: (source from .hgsub, revision from .hgsubstate, kind
86 86 (key in types dict))
87 87 """
88 88 p = config.config()
89 89 repo = ctx.repo()
90 90 def read(f, sections=None, remap=None):
91 91 if f in ctx:
92 92 try:
93 93 data = ctx[f].data()
94 94 except IOError as err:
95 95 if err.errno != errno.ENOENT:
96 96 raise
97 97 # handle missing subrepo spec files as removed
98 98 ui.warn(_("warning: subrepo spec file \'%s\' not found\n") %
99 99 repo.pathto(f))
100 100 return
101 101 p.parse(f, data, sections, remap, read)
102 102 else:
103 103 raise error.Abort(_("subrepo spec file \'%s\' not found") %
104 104 repo.pathto(f))
105 105 if '.hgsub' in ctx:
106 106 read('.hgsub')
107 107
108 108 for path, src in ui.configitems('subpaths'):
109 109 p.set('subpaths', path, src, ui.configsource('subpaths', path))
110 110
111 111 rev = {}
112 112 if '.hgsubstate' in ctx:
113 113 try:
114 114 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
115 115 l = l.lstrip()
116 116 if not l:
117 117 continue
118 118 try:
119 119 revision, path = l.split(" ", 1)
120 120 except ValueError:
121 121 raise error.Abort(_("invalid subrepository revision "
122 122 "specifier in \'%s\' line %d")
123 123 % (repo.pathto('.hgsubstate'), (i + 1)))
124 124 rev[path] = revision
125 125 except IOError as err:
126 126 if err.errno != errno.ENOENT:
127 127 raise
128 128
129 129 def remap(src):
130 130 for pattern, repl in p.items('subpaths'):
131 131 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
132 132 # does a string decode.
133 133 repl = util.escapestr(repl)
134 134 # However, we still want to allow back references to go
135 135 # through unharmed, so we turn r'\\1' into r'\1'. Again,
136 136 # extra escapes are needed because re.sub string decodes.
137 137 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
138 138 try:
139 139 src = re.sub(pattern, repl, src, 1)
140 140 except re.error as e:
141 141 raise error.Abort(_("bad subrepository pattern in %s: %s")
142 142 % (p.source('subpaths', pattern), e))
143 143 return src
144 144
145 145 state = {}
146 146 for path, src in p[''].items():
147 147 kind = 'hg'
148 148 if src.startswith('['):
149 149 if ']' not in src:
150 150 raise error.Abort(_('missing ] in subrepo source'))
151 151 kind, src = src.split(']', 1)
152 152 kind = kind[1:]
153 153 src = src.lstrip() # strip any extra whitespace after ']'
154 154
155 155 if not util.url(src).isabs():
156 156 parent = _abssource(repo, abort=False)
157 157 if parent:
158 158 parent = util.url(parent)
159 159 parent.path = posixpath.join(parent.path or '', src)
160 160 parent.path = posixpath.normpath(parent.path)
161 161 joined = str(parent)
162 162 # Remap the full joined path and use it if it changes,
163 163 # else remap the original source.
164 164 remapped = remap(joined)
165 165 if remapped == joined:
166 166 src = remap(src)
167 167 else:
168 168 src = remapped
169 169
170 170 src = remap(src)
171 171 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
172 172
173 173 return state
174 174
175 175 def writestate(repo, state):
176 176 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
177 177 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)
178 178 if state[s][1] != nullstate[1]]
179 179 repo.wwrite('.hgsubstate', ''.join(lines), '')
180 180
181 181 def submerge(repo, wctx, mctx, actx, overwrite, labels=None):
182 182 """delegated from merge.applyupdates: merging of .hgsubstate file
183 183 in working context, merging context and ancestor context"""
184 184 if mctx == actx: # backwards?
185 185 actx = wctx.p1()
186 186 s1 = wctx.substate
187 187 s2 = mctx.substate
188 188 sa = actx.substate
189 189 sm = {}
190 190
191 191 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
192 192
193 193 def debug(s, msg, r=""):
194 194 if r:
195 195 r = "%s:%s:%s" % r
196 196 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
197 197
198 198 promptssrc = filemerge.partextras(labels)
199 199 for s, l in sorted(s1.iteritems()):
200 200 prompts = None
201 201 a = sa.get(s, nullstate)
202 202 ld = l # local state with possible dirty flag for compares
203 203 if wctx.sub(s).dirty():
204 204 ld = (l[0], l[1] + "+")
205 205 if wctx == actx: # overwrite
206 206 a = ld
207 207
208 208 prompts = promptssrc.copy()
209 209 prompts['s'] = s
210 210 if s in s2:
211 211 r = s2[s]
212 212 if ld == r or r == a: # no change or local is newer
213 213 sm[s] = l
214 214 continue
215 215 elif ld == a: # other side changed
216 216 debug(s, "other changed, get", r)
217 217 wctx.sub(s).get(r, overwrite)
218 218 sm[s] = r
219 219 elif ld[0] != r[0]: # sources differ
220 220 prompts['lo'] = l[0]
221 221 prompts['ro'] = r[0]
222 222 if repo.ui.promptchoice(
223 223 _(' subrepository sources for %(s)s differ\n'
224 224 'use (l)ocal%(l)s source (%(lo)s)'
225 225 ' or (r)emote%(o)s source (%(ro)s)?'
226 226 '$$ &Local $$ &Remote') % prompts, 0):
227 227 debug(s, "prompt changed, get", r)
228 228 wctx.sub(s).get(r, overwrite)
229 229 sm[s] = r
230 230 elif ld[1] == a[1]: # local side is unchanged
231 231 debug(s, "other side changed, get", r)
232 232 wctx.sub(s).get(r, overwrite)
233 233 sm[s] = r
234 234 else:
235 235 debug(s, "both sides changed")
236 236 srepo = wctx.sub(s)
237 237 prompts['sl'] = srepo.shortid(l[1])
238 238 prompts['sr'] = srepo.shortid(r[1])
239 239 option = repo.ui.promptchoice(
240 240 _(' subrepository %(s)s diverged (local revision: %(sl)s, '
241 241 'remote revision: %(sr)s)\n'
242 242 '(M)erge, keep (l)ocal%(l)s or keep (r)emote%(o)s?'
243 243 '$$ &Merge $$ &Local $$ &Remote')
244 244 % prompts, 0)
245 245 if option == 0:
246 246 wctx.sub(s).merge(r)
247 247 sm[s] = l
248 248 debug(s, "merge with", r)
249 249 elif option == 1:
250 250 sm[s] = l
251 251 debug(s, "keep local subrepo revision", l)
252 252 else:
253 253 wctx.sub(s).get(r, overwrite)
254 254 sm[s] = r
255 255 debug(s, "get remote subrepo revision", r)
256 256 elif ld == a: # remote removed, local unchanged
257 257 debug(s, "remote removed, remove")
258 258 wctx.sub(s).remove()
259 259 elif a == nullstate: # not present in remote or ancestor
260 260 debug(s, "local added, keep")
261 261 sm[s] = l
262 262 continue
263 263 else:
264 264 if repo.ui.promptchoice(
265 265 _(' local%(l)s changed subrepository %(s)s'
266 266 ' which remote%(o)s removed\n'
267 267 'use (c)hanged version or (d)elete?'
268 268 '$$ &Changed $$ &Delete') % prompts, 0):
269 269 debug(s, "prompt remove")
270 270 wctx.sub(s).remove()
271 271
272 272 for s, r in sorted(s2.items()):
273 273 prompts = None
274 274 if s in s1:
275 275 continue
276 276 elif s not in sa:
277 277 debug(s, "remote added, get", r)
278 278 mctx.sub(s).get(r)
279 279 sm[s] = r
280 280 elif r != sa[s]:
281 281 prompts = promptssrc.copy()
282 282 prompts['s'] = s
283 283 if repo.ui.promptchoice(
284 284 _(' remote%(o)s changed subrepository %(s)s'
285 285 ' which local%(l)s removed\n'
286 286 'use (c)hanged version or (d)elete?'
287 287 '$$ &Changed $$ &Delete') % prompts, 0) == 0:
288 288 debug(s, "prompt recreate", r)
289 289 mctx.sub(s).get(r)
290 290 sm[s] = r
291 291
292 292 # record merged .hgsubstate
293 293 writestate(repo, sm)
294 294 return sm
295 295
296 296 def _updateprompt(ui, sub, dirty, local, remote):
297 297 if dirty:
298 298 msg = (_(' subrepository sources for %s differ\n'
299 299 'use (l)ocal source (%s) or (r)emote source (%s)?'
300 300 '$$ &Local $$ &Remote')
301 301 % (subrelpath(sub), local, remote))
302 302 else:
303 303 msg = (_(' subrepository sources for %s differ (in checked out '
304 304 'version)\n'
305 305 'use (l)ocal source (%s) or (r)emote source (%s)?'
306 306 '$$ &Local $$ &Remote')
307 307 % (subrelpath(sub), local, remote))
308 308 return ui.promptchoice(msg, 0)
309 309
310 310 def reporelpath(repo):
311 311 """return path to this (sub)repo as seen from outermost repo"""
312 312 parent = repo
313 313 while util.safehasattr(parent, '_subparent'):
314 314 parent = parent._subparent
315 315 return repo.root[len(pathutil.normasprefix(parent.root)):]
316 316
317 317 def subrelpath(sub):
318 318 """return path to this subrepo as seen from outermost repo"""
319 319 return sub._relpath
320 320
321 321 def _abssource(repo, push=False, abort=True):
322 322 """return pull/push path of repo - either based on parent repo .hgsub info
323 323 or on the top repo config. Abort or return None if no source found."""
324 324 if util.safehasattr(repo, '_subparent'):
325 325 source = util.url(repo._subsource)
326 326 if source.isabs():
327 327 return str(source)
328 328 source.path = posixpath.normpath(source.path)
329 329 parent = _abssource(repo._subparent, push, abort=False)
330 330 if parent:
331 331 parent = util.url(util.pconvert(parent))
332 332 parent.path = posixpath.join(parent.path or '', source.path)
333 333 parent.path = posixpath.normpath(parent.path)
334 334 return str(parent)
335 335 else: # recursion reached top repo
336 336 if util.safehasattr(repo, '_subtoppath'):
337 337 return repo._subtoppath
338 338 if push and repo.ui.config('paths', 'default-push'):
339 339 return repo.ui.config('paths', 'default-push')
340 340 if repo.ui.config('paths', 'default'):
341 341 return repo.ui.config('paths', 'default')
342 342 if repo.shared():
343 343 # chop off the .hg component to get the default path form
344 344 return os.path.dirname(repo.sharedpath)
345 345 if abort:
346 346 raise error.Abort(_("default path for subrepository not found"))
347 347
348 348 def _sanitize(ui, vfs, ignore):
349 349 for dirname, dirs, names in vfs.walk():
350 350 for i, d in enumerate(dirs):
351 351 if d.lower() == ignore:
352 352 del dirs[i]
353 353 break
354 354 if vfs.basename(dirname).lower() != '.hg':
355 355 continue
356 356 for f in names:
357 357 if f.lower() == 'hgrc':
358 358 ui.warn(_("warning: removing potentially hostile 'hgrc' "
359 359 "in '%s'\n") % vfs.join(dirname))
360 360 vfs.unlink(vfs.reljoin(dirname, f))
361 361
362 362 def subrepo(ctx, path, allowwdir=False, allowcreate=True):
363 363 """return instance of the right subrepo class for subrepo in path"""
364 364 # subrepo inherently violates our import layering rules
365 365 # because it wants to make repo objects from deep inside the stack
366 366 # so we manually delay the circular imports to not break
367 367 # scripts that don't use our demand-loading
368 368 global hg
369 369 from . import hg as h
370 370 hg = h
371 371
372 372 pathutil.pathauditor(ctx.repo().root)(path)
373 373 state = ctx.substate[path]
374 374 if state[2] not in types:
375 375 raise error.Abort(_('unknown subrepo type %s') % state[2])
376 376 if allowwdir:
377 377 state = (state[0], ctx.subrev(path), state[2])
378 378 return types[state[2]](ctx, path, state[:2], allowcreate)
379 379
380 380 def nullsubrepo(ctx, path, pctx):
381 381 """return an empty subrepo in pctx for the extant subrepo in ctx"""
382 382 # subrepo inherently violates our import layering rules
383 383 # because it wants to make repo objects from deep inside the stack
384 384 # so we manually delay the circular imports to not break
385 385 # scripts that don't use our demand-loading
386 386 global hg
387 387 from . import hg as h
388 388 hg = h
389 389
390 390 pathutil.pathauditor(ctx.repo().root)(path)
391 391 state = ctx.substate[path]
392 392 if state[2] not in types:
393 393 raise error.Abort(_('unknown subrepo type %s') % state[2])
394 394 subrev = ''
395 395 if state[2] == 'hg':
396 396 subrev = "0" * 40
397 397 return types[state[2]](pctx, path, (state[0], subrev), True)
398 398
399 399 def newcommitphase(ui, ctx):
400 400 commitphase = phases.newcommitphase(ui)
401 401 substate = getattr(ctx, "substate", None)
402 402 if not substate:
403 403 return commitphase
404 404 check = ui.config('phases', 'checksubrepos', 'follow')
405 405 if check not in ('ignore', 'follow', 'abort'):
406 406 raise error.Abort(_('invalid phases.checksubrepos configuration: %s')
407 407 % (check))
408 408 if check == 'ignore':
409 409 return commitphase
410 410 maxphase = phases.public
411 411 maxsub = None
412 412 for s in sorted(substate):
413 413 sub = ctx.sub(s)
414 414 subphase = sub.phase(substate[s][1])
415 415 if maxphase < subphase:
416 416 maxphase = subphase
417 417 maxsub = s
418 418 if commitphase < maxphase:
419 419 if check == 'abort':
420 420 raise error.Abort(_("can't commit in %s phase"
421 421 " conflicting %s from subrepository %s") %
422 422 (phases.phasenames[commitphase],
423 423 phases.phasenames[maxphase], maxsub))
424 424 ui.warn(_("warning: changes are committed in"
425 425 " %s phase from subrepository %s\n") %
426 426 (phases.phasenames[maxphase], maxsub))
427 427 return maxphase
428 428 return commitphase
429 429
430 430 # subrepo classes need to implement the following abstract class:
431 431
432 432 class abstractsubrepo(object):
433 433
434 434 def __init__(self, ctx, path):
435 435 """Initialize abstractsubrepo part
436 436
437 437 ``ctx`` is the context referring this subrepository in the
438 438 parent repository.
439 439
440 440 ``path`` is the path to this subrepository as seen from
441 441 innermost repository.
442 442 """
443 443 self.ui = ctx.repo().ui
444 444 self._ctx = ctx
445 445 self._path = path
446 446
447 447 def addwebdirpath(self, serverpath, webconf):
448 448 """Add the hgwebdir entries for this subrepo, and any of its subrepos.
449 449
450 450 ``serverpath`` is the path component of the URL for this repo.
451 451
452 452 ``webconf`` is the dictionary of hgwebdir entries.
453 453 """
454 454 pass
455 455
456 456 def storeclean(self, path):
457 457 """
458 458 returns true if the repository has not changed since it was last
459 459 cloned from or pushed to a given repository.
460 460 """
461 461 return False
462 462
463 463 def dirty(self, ignoreupdate=False):
464 464 """returns true if the dirstate of the subrepo is dirty or does not
465 465 match current stored state. If ignoreupdate is true, only check
466 466 whether the subrepo has uncommitted changes in its dirstate.
467 467 """
468 468 raise NotImplementedError
469 469
470 470 def dirtyreason(self, ignoreupdate=False):
471 471 """return reason string if it is ``dirty()``
472 472
473 473 Returned string should have enough information for the message
474 474 of exception.
475 475
476 476 This returns None, otherwise.
477 477 """
478 478 if self.dirty(ignoreupdate=ignoreupdate):
479 479 return _("uncommitted changes in subrepository '%s'"
480 480 ) % subrelpath(self)
481 481
482 482 def bailifchanged(self, ignoreupdate=False, hint=None):
483 483 """raise Abort if subrepository is ``dirty()``
484 484 """
485 485 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate)
486 486 if dirtyreason:
487 487 raise error.Abort(dirtyreason, hint=hint)
488 488
489 489 def basestate(self):
490 490 """current working directory base state, disregarding .hgsubstate
491 491 state and working directory modifications"""
492 492 raise NotImplementedError
493 493
494 494 def checknested(self, path):
495 495 """check if path is a subrepository within this repository"""
496 496 return False
497 497
498 498 def commit(self, text, user, date):
499 499 """commit the current changes to the subrepo with the given
500 500 log message. Use given user and date if possible. Return the
501 501 new state of the subrepo.
502 502 """
503 503 raise NotImplementedError
504 504
505 505 def phase(self, state):
506 506 """returns phase of specified state in the subrepository.
507 507 """
508 508 return phases.public
509 509
510 510 def remove(self):
511 511 """remove the subrepo
512 512
513 513 (should verify the dirstate is not dirty first)
514 514 """
515 515 raise NotImplementedError
516 516
517 517 def get(self, state, overwrite=False):
518 518 """run whatever commands are needed to put the subrepo into
519 519 this state
520 520 """
521 521 raise NotImplementedError
522 522
523 523 def merge(self, state):
524 524 """merge currently-saved state with the new state."""
525 525 raise NotImplementedError
526 526
527 527 def push(self, opts):
528 528 """perform whatever action is analogous to 'hg push'
529 529
530 530 This may be a no-op on some systems.
531 531 """
532 532 raise NotImplementedError
533 533
534 534 def add(self, ui, match, prefix, explicitonly, **opts):
535 535 return []
536 536
537 537 def addremove(self, matcher, prefix, opts, dry_run, similarity):
538 538 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
539 539 return 1
540 540
541 def cat(self, match, fntemplate, prefix, **opts):
541 def cat(self, match, fm, fntemplate, prefix, **opts):
542 542 return 1
543 543
544 544 def status(self, rev2, **opts):
545 545 return scmutil.status([], [], [], [], [], [], [])
546 546
547 547 def diff(self, ui, diffopts, node2, match, prefix, **opts):
548 548 pass
549 549
550 550 def outgoing(self, ui, dest, opts):
551 551 return 1
552 552
553 553 def incoming(self, ui, source, opts):
554 554 return 1
555 555
556 556 def files(self):
557 557 """return filename iterator"""
558 558 raise NotImplementedError
559 559
560 560 def filedata(self, name, decode):
561 561 """return file data, optionally passed through repo decoders"""
562 562 raise NotImplementedError
563 563
564 564 def fileflags(self, name):
565 565 """return file flags"""
566 566 return ''
567 567
568 568 def getfileset(self, expr):
569 569 """Resolve the fileset expression for this repo"""
570 570 return set()
571 571
572 572 def printfiles(self, ui, m, fm, fmt, subrepos):
573 573 """handle the files command for this subrepo"""
574 574 return 1
575 575
576 576 def archive(self, archiver, prefix, match=None, decode=True):
577 577 if match is not None:
578 578 files = [f for f in self.files() if match(f)]
579 579 else:
580 580 files = self.files()
581 581 total = len(files)
582 582 relpath = subrelpath(self)
583 583 self.ui.progress(_('archiving (%s)') % relpath, 0,
584 584 unit=_('files'), total=total)
585 585 for i, name in enumerate(files):
586 586 flags = self.fileflags(name)
587 587 mode = 'x' in flags and 0o755 or 0o644
588 588 symlink = 'l' in flags
589 589 archiver.addfile(prefix + self._path + '/' + name,
590 590 mode, symlink, self.filedata(name, decode))
591 591 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
592 592 unit=_('files'), total=total)
593 593 self.ui.progress(_('archiving (%s)') % relpath, None)
594 594 return total
595 595
596 596 def walk(self, match):
597 597 '''
598 598 walk recursively through the directory tree, finding all files
599 599 matched by the match function
600 600 '''
601 601 pass
602 602
603 603 def forget(self, match, prefix):
604 604 return ([], [])
605 605
606 606 def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
607 607 """remove the matched files from the subrepository and the filesystem,
608 608 possibly by force and/or after the file has been removed from the
609 609 filesystem. Return 0 on success, 1 on any warning.
610 610 """
611 611 warnings.append(_("warning: removefiles not implemented (%s)")
612 612 % self._path)
613 613 return 1
614 614
615 615 def revert(self, substate, *pats, **opts):
616 616 self.ui.warn(_('%s: reverting %s subrepos is unsupported\n') \
617 617 % (substate[0], substate[2]))
618 618 return []
619 619
620 620 def shortid(self, revid):
621 621 return revid
622 622
623 623 def verify(self):
624 624 '''verify the integrity of the repository. Return 0 on success or
625 625 warning, 1 on any error.
626 626 '''
627 627 return 0
628 628
629 629 @propertycache
630 630 def wvfs(self):
631 631 """return vfs to access the working directory of this subrepository
632 632 """
633 633 return vfsmod.vfs(self._ctx.repo().wvfs.join(self._path))
634 634
635 635 @propertycache
636 636 def _relpath(self):
637 637 """return path to this subrepository as seen from outermost repository
638 638 """
639 639 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
640 640
641 641 class hgsubrepo(abstractsubrepo):
642 642 def __init__(self, ctx, path, state, allowcreate):
643 643 super(hgsubrepo, self).__init__(ctx, path)
644 644 self._state = state
645 645 r = ctx.repo()
646 646 root = r.wjoin(path)
647 647 create = allowcreate and not r.wvfs.exists('%s/.hg' % path)
648 648 self._repo = hg.repository(r.baseui, root, create=create)
649 649
650 650 # Propagate the parent's --hidden option
651 651 if r is r.unfiltered():
652 652 self._repo = self._repo.unfiltered()
653 653
654 654 self.ui = self._repo.ui
655 655 for s, k in [('ui', 'commitsubrepos')]:
656 656 v = r.ui.config(s, k)
657 657 if v:
658 658 self.ui.setconfig(s, k, v, 'subrepo')
659 659 # internal config: ui._usedassubrepo
660 660 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
661 661 self._initrepo(r, state[0], create)
662 662
663 663 @annotatesubrepoerror
664 664 def addwebdirpath(self, serverpath, webconf):
665 665 cmdutil.addwebdirpath(self._repo, subrelpath(self), webconf)
666 666
667 667 def storeclean(self, path):
668 668 with self._repo.lock():
669 669 return self._storeclean(path)
670 670
671 671 def _storeclean(self, path):
672 672 clean = True
673 673 itercache = self._calcstorehash(path)
674 674 for filehash in self._readstorehashcache(path):
675 675 if filehash != next(itercache, None):
676 676 clean = False
677 677 break
678 678 if clean:
679 679 # if not empty:
680 680 # the cached and current pull states have a different size
681 681 clean = next(itercache, None) is None
682 682 return clean
683 683
684 684 def _calcstorehash(self, remotepath):
685 685 '''calculate a unique "store hash"
686 686
687 687 This method is used to to detect when there are changes that may
688 688 require a push to a given remote path.'''
689 689 # sort the files that will be hashed in increasing (likely) file size
690 690 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
691 691 yield '# %s\n' % _expandedabspath(remotepath)
692 692 vfs = self._repo.vfs
693 693 for relname in filelist:
694 694 filehash = hashlib.sha1(vfs.tryread(relname)).hexdigest()
695 695 yield '%s = %s\n' % (relname, filehash)
696 696
697 697 @propertycache
698 698 def _cachestorehashvfs(self):
699 699 return vfsmod.vfs(self._repo.vfs.join('cache/storehash'))
700 700
701 701 def _readstorehashcache(self, remotepath):
702 702 '''read the store hash cache for a given remote repository'''
703 703 cachefile = _getstorehashcachename(remotepath)
704 704 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
705 705
706 706 def _cachestorehash(self, remotepath):
707 707 '''cache the current store hash
708 708
709 709 Each remote repo requires its own store hash cache, because a subrepo
710 710 store may be "clean" versus a given remote repo, but not versus another
711 711 '''
712 712 cachefile = _getstorehashcachename(remotepath)
713 713 with self._repo.lock():
714 714 storehash = list(self._calcstorehash(remotepath))
715 715 vfs = self._cachestorehashvfs
716 716 vfs.writelines(cachefile, storehash, mode='w', notindexed=True)
717 717
718 718 def _getctx(self):
719 719 '''fetch the context for this subrepo revision, possibly a workingctx
720 720 '''
721 721 if self._ctx.rev() is None:
722 722 return self._repo[None] # workingctx if parent is workingctx
723 723 else:
724 724 rev = self._state[1]
725 725 return self._repo[rev]
726 726
727 727 @annotatesubrepoerror
728 728 def _initrepo(self, parentrepo, source, create):
729 729 self._repo._subparent = parentrepo
730 730 self._repo._subsource = source
731 731
732 732 if create:
733 733 lines = ['[paths]\n']
734 734
735 735 def addpathconfig(key, value):
736 736 if value:
737 737 lines.append('%s = %s\n' % (key, value))
738 738 self.ui.setconfig('paths', key, value, 'subrepo')
739 739
740 740 defpath = _abssource(self._repo, abort=False)
741 741 defpushpath = _abssource(self._repo, True, abort=False)
742 742 addpathconfig('default', defpath)
743 743 if defpath != defpushpath:
744 744 addpathconfig('default-push', defpushpath)
745 745
746 746 fp = self._repo.vfs("hgrc", "w", text=True)
747 747 try:
748 748 fp.write(''.join(lines))
749 749 finally:
750 750 fp.close()
751 751
752 752 @annotatesubrepoerror
753 753 def add(self, ui, match, prefix, explicitonly, **opts):
754 754 return cmdutil.add(ui, self._repo, match,
755 755 self.wvfs.reljoin(prefix, self._path),
756 756 explicitonly, **opts)
757 757
758 758 @annotatesubrepoerror
759 759 def addremove(self, m, prefix, opts, dry_run, similarity):
760 760 # In the same way as sub directories are processed, once in a subrepo,
761 761 # always entry any of its subrepos. Don't corrupt the options that will
762 762 # be used to process sibling subrepos however.
763 763 opts = copy.copy(opts)
764 764 opts['subrepos'] = True
765 765 return scmutil.addremove(self._repo, m,
766 766 self.wvfs.reljoin(prefix, self._path), opts,
767 767 dry_run, similarity)
768 768
769 769 @annotatesubrepoerror
770 def cat(self, match, fntemplate, prefix, **opts):
770 def cat(self, match, fm, fntemplate, prefix, **opts):
771 771 rev = self._state[1]
772 772 ctx = self._repo[rev]
773 return cmdutil.cat(self.ui, self._repo, ctx, match, fntemplate, prefix,
774 **opts)
773 return cmdutil.cat(self.ui, self._repo, ctx, match, fm, fntemplate,
774 prefix, **opts)
775 775
776 776 @annotatesubrepoerror
777 777 def status(self, rev2, **opts):
778 778 try:
779 779 rev1 = self._state[1]
780 780 ctx1 = self._repo[rev1]
781 781 ctx2 = self._repo[rev2]
782 782 return self._repo.status(ctx1, ctx2, **opts)
783 783 except error.RepoLookupError as inst:
784 784 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
785 785 % (inst, subrelpath(self)))
786 786 return scmutil.status([], [], [], [], [], [], [])
787 787
788 788 @annotatesubrepoerror
789 789 def diff(self, ui, diffopts, node2, match, prefix, **opts):
790 790 try:
791 791 node1 = node.bin(self._state[1])
792 792 # We currently expect node2 to come from substate and be
793 793 # in hex format
794 794 if node2 is not None:
795 795 node2 = node.bin(node2)
796 796 cmdutil.diffordiffstat(ui, self._repo, diffopts,
797 797 node1, node2, match,
798 798 prefix=posixpath.join(prefix, self._path),
799 799 listsubrepos=True, **opts)
800 800 except error.RepoLookupError as inst:
801 801 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
802 802 % (inst, subrelpath(self)))
803 803
804 804 @annotatesubrepoerror
805 805 def archive(self, archiver, prefix, match=None, decode=True):
806 806 self._get(self._state + ('hg',))
807 807 total = abstractsubrepo.archive(self, archiver, prefix, match)
808 808 rev = self._state[1]
809 809 ctx = self._repo[rev]
810 810 for subpath in ctx.substate:
811 811 s = subrepo(ctx, subpath, True)
812 812 submatch = matchmod.subdirmatcher(subpath, match)
813 813 total += s.archive(archiver, prefix + self._path + '/', submatch,
814 814 decode)
815 815 return total
816 816
817 817 @annotatesubrepoerror
818 818 def dirty(self, ignoreupdate=False):
819 819 r = self._state[1]
820 820 if r == '' and not ignoreupdate: # no state recorded
821 821 return True
822 822 w = self._repo[None]
823 823 if r != w.p1().hex() and not ignoreupdate:
824 824 # different version checked out
825 825 return True
826 826 return w.dirty() # working directory changed
827 827
828 828 def basestate(self):
829 829 return self._repo['.'].hex()
830 830
831 831 def checknested(self, path):
832 832 return self._repo._checknested(self._repo.wjoin(path))
833 833
834 834 @annotatesubrepoerror
835 835 def commit(self, text, user, date):
836 836 # don't bother committing in the subrepo if it's only been
837 837 # updated
838 838 if not self.dirty(True):
839 839 return self._repo['.'].hex()
840 840 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
841 841 n = self._repo.commit(text, user, date)
842 842 if not n:
843 843 return self._repo['.'].hex() # different version checked out
844 844 return node.hex(n)
845 845
846 846 @annotatesubrepoerror
847 847 def phase(self, state):
848 848 return self._repo[state].phase()
849 849
850 850 @annotatesubrepoerror
851 851 def remove(self):
852 852 # we can't fully delete the repository as it may contain
853 853 # local-only history
854 854 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
855 855 hg.clean(self._repo, node.nullid, False)
856 856
857 857 def _get(self, state):
858 858 source, revision, kind = state
859 859 if revision in self._repo.unfiltered():
860 860 return True
861 861 self._repo._subsource = source
862 862 srcurl = _abssource(self._repo)
863 863 other = hg.peer(self._repo, {}, srcurl)
864 864 if len(self._repo) == 0:
865 865 self.ui.status(_('cloning subrepo %s from %s\n')
866 866 % (subrelpath(self), srcurl))
867 867 parentrepo = self._repo._subparent
868 868 # use self._repo.vfs instead of self.wvfs to remove .hg only
869 869 self._repo.vfs.rmtree()
870 870 other, cloned = hg.clone(self._repo._subparent.baseui, {},
871 871 other, self._repo.root,
872 872 update=False)
873 873 self._repo = cloned.local()
874 874 self._initrepo(parentrepo, source, create=True)
875 875 self._cachestorehash(srcurl)
876 876 else:
877 877 self.ui.status(_('pulling subrepo %s from %s\n')
878 878 % (subrelpath(self), srcurl))
879 879 cleansub = self.storeclean(srcurl)
880 880 exchange.pull(self._repo, other)
881 881 if cleansub:
882 882 # keep the repo clean after pull
883 883 self._cachestorehash(srcurl)
884 884 return False
885 885
886 886 @annotatesubrepoerror
887 887 def get(self, state, overwrite=False):
888 888 inrepo = self._get(state)
889 889 source, revision, kind = state
890 890 repo = self._repo
891 891 repo.ui.debug("getting subrepo %s\n" % self._path)
892 892 if inrepo:
893 893 urepo = repo.unfiltered()
894 894 ctx = urepo[revision]
895 895 if ctx.hidden():
896 896 urepo.ui.warn(
897 897 _('revision %s in subrepo %s is hidden\n') \
898 898 % (revision[0:12], self._path))
899 899 repo = urepo
900 900 hg.updaterepo(repo, revision, overwrite)
901 901
902 902 @annotatesubrepoerror
903 903 def merge(self, state):
904 904 self._get(state)
905 905 cur = self._repo['.']
906 906 dst = self._repo[state[1]]
907 907 anc = dst.ancestor(cur)
908 908
909 909 def mergefunc():
910 910 if anc == cur and dst.branch() == cur.branch():
911 911 self.ui.debug("updating subrepo %s\n" % subrelpath(self))
912 912 hg.update(self._repo, state[1])
913 913 elif anc == dst:
914 914 self.ui.debug("skipping subrepo %s\n" % subrelpath(self))
915 915 else:
916 916 self.ui.debug("merging subrepo %s\n" % subrelpath(self))
917 917 hg.merge(self._repo, state[1], remind=False)
918 918
919 919 wctx = self._repo[None]
920 920 if self.dirty():
921 921 if anc != dst:
922 922 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
923 923 mergefunc()
924 924 else:
925 925 mergefunc()
926 926 else:
927 927 mergefunc()
928 928
929 929 @annotatesubrepoerror
930 930 def push(self, opts):
931 931 force = opts.get('force')
932 932 newbranch = opts.get('new_branch')
933 933 ssh = opts.get('ssh')
934 934
935 935 # push subrepos depth-first for coherent ordering
936 936 c = self._repo['']
937 937 subs = c.substate # only repos that are committed
938 938 for s in sorted(subs):
939 939 if c.sub(s).push(opts) == 0:
940 940 return False
941 941
942 942 dsturl = _abssource(self._repo, True)
943 943 if not force:
944 944 if self.storeclean(dsturl):
945 945 self.ui.status(
946 946 _('no changes made to subrepo %s since last push to %s\n')
947 947 % (subrelpath(self), dsturl))
948 948 return None
949 949 self.ui.status(_('pushing subrepo %s to %s\n') %
950 950 (subrelpath(self), dsturl))
951 951 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
952 952 res = exchange.push(self._repo, other, force, newbranch=newbranch)
953 953
954 954 # the repo is now clean
955 955 self._cachestorehash(dsturl)
956 956 return res.cgresult
957 957
958 958 @annotatesubrepoerror
959 959 def outgoing(self, ui, dest, opts):
960 960 if 'rev' in opts or 'branch' in opts:
961 961 opts = copy.copy(opts)
962 962 opts.pop('rev', None)
963 963 opts.pop('branch', None)
964 964 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
965 965
966 966 @annotatesubrepoerror
967 967 def incoming(self, ui, source, opts):
968 968 if 'rev' in opts or 'branch' in opts:
969 969 opts = copy.copy(opts)
970 970 opts.pop('rev', None)
971 971 opts.pop('branch', None)
972 972 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
973 973
974 974 @annotatesubrepoerror
975 975 def files(self):
976 976 rev = self._state[1]
977 977 ctx = self._repo[rev]
978 978 return ctx.manifest().keys()
979 979
980 980 def filedata(self, name, decode):
981 981 rev = self._state[1]
982 982 data = self._repo[rev][name].data()
983 983 if decode:
984 984 data = self._repo.wwritedata(name, data)
985 985 return data
986 986
987 987 def fileflags(self, name):
988 988 rev = self._state[1]
989 989 ctx = self._repo[rev]
990 990 return ctx.flags(name)
991 991
992 992 @annotatesubrepoerror
993 993 def printfiles(self, ui, m, fm, fmt, subrepos):
994 994 # If the parent context is a workingctx, use the workingctx here for
995 995 # consistency.
996 996 if self._ctx.rev() is None:
997 997 ctx = self._repo[None]
998 998 else:
999 999 rev = self._state[1]
1000 1000 ctx = self._repo[rev]
1001 1001 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
1002 1002
1003 1003 @annotatesubrepoerror
1004 1004 def getfileset(self, expr):
1005 1005 if self._ctx.rev() is None:
1006 1006 ctx = self._repo[None]
1007 1007 else:
1008 1008 rev = self._state[1]
1009 1009 ctx = self._repo[rev]
1010 1010
1011 1011 files = ctx.getfileset(expr)
1012 1012
1013 1013 for subpath in ctx.substate:
1014 1014 sub = ctx.sub(subpath)
1015 1015
1016 1016 try:
1017 1017 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
1018 1018 except error.LookupError:
1019 1019 self.ui.status(_("skipping missing subrepository: %s\n")
1020 1020 % self.wvfs.reljoin(reporelpath(self), subpath))
1021 1021 return files
1022 1022
1023 1023 def walk(self, match):
1024 1024 ctx = self._repo[None]
1025 1025 return ctx.walk(match)
1026 1026
1027 1027 @annotatesubrepoerror
1028 1028 def forget(self, match, prefix):
1029 1029 return cmdutil.forget(self.ui, self._repo, match,
1030 1030 self.wvfs.reljoin(prefix, self._path), True)
1031 1031
1032 1032 @annotatesubrepoerror
1033 1033 def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
1034 1034 return cmdutil.remove(self.ui, self._repo, matcher,
1035 1035 self.wvfs.reljoin(prefix, self._path),
1036 1036 after, force, subrepos)
1037 1037
1038 1038 @annotatesubrepoerror
1039 1039 def revert(self, substate, *pats, **opts):
1040 1040 # reverting a subrepo is a 2 step process:
1041 1041 # 1. if the no_backup is not set, revert all modified
1042 1042 # files inside the subrepo
1043 1043 # 2. update the subrepo to the revision specified in
1044 1044 # the corresponding substate dictionary
1045 1045 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1046 1046 if not opts.get('no_backup'):
1047 1047 # Revert all files on the subrepo, creating backups
1048 1048 # Note that this will not recursively revert subrepos
1049 1049 # We could do it if there was a set:subrepos() predicate
1050 1050 opts = opts.copy()
1051 1051 opts['date'] = None
1052 1052 opts['rev'] = substate[1]
1053 1053
1054 1054 self.filerevert(*pats, **opts)
1055 1055
1056 1056 # Update the repo to the revision specified in the given substate
1057 1057 if not opts.get('dry_run'):
1058 1058 self.get(substate, overwrite=True)
1059 1059
1060 1060 def filerevert(self, *pats, **opts):
1061 1061 ctx = self._repo[opts['rev']]
1062 1062 parents = self._repo.dirstate.parents()
1063 1063 if opts.get('all'):
1064 1064 pats = ['set:modified()']
1065 1065 else:
1066 1066 pats = []
1067 1067 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
1068 1068
1069 1069 def shortid(self, revid):
1070 1070 return revid[:12]
1071 1071
1072 1072 def verify(self):
1073 1073 try:
1074 1074 rev = self._state[1]
1075 1075 ctx = self._repo.unfiltered()[rev]
1076 1076 if ctx.hidden():
1077 1077 # Since hidden revisions aren't pushed/pulled, it seems worth an
1078 1078 # explicit warning.
1079 1079 ui = self._repo.ui
1080 1080 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
1081 1081 (self._relpath, node.short(self._ctx.node())))
1082 1082 return 0
1083 1083 except error.RepoLookupError:
1084 1084 # A missing subrepo revision may be a case of needing to pull it, so
1085 1085 # don't treat this as an error.
1086 1086 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
1087 1087 (self._relpath, node.short(self._ctx.node())))
1088 1088 return 0
1089 1089
1090 1090 @propertycache
1091 1091 def wvfs(self):
1092 1092 """return own wvfs for efficiency and consistency
1093 1093 """
1094 1094 return self._repo.wvfs
1095 1095
1096 1096 @propertycache
1097 1097 def _relpath(self):
1098 1098 """return path to this subrepository as seen from outermost repository
1099 1099 """
1100 1100 # Keep consistent dir separators by avoiding vfs.join(self._path)
1101 1101 return reporelpath(self._repo)
1102 1102
1103 1103 class svnsubrepo(abstractsubrepo):
1104 1104 def __init__(self, ctx, path, state, allowcreate):
1105 1105 super(svnsubrepo, self).__init__(ctx, path)
1106 1106 self._state = state
1107 1107 self._exe = util.findexe('svn')
1108 1108 if not self._exe:
1109 1109 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
1110 1110 % self._path)
1111 1111
1112 1112 def _svncommand(self, commands, filename='', failok=False):
1113 1113 cmd = [self._exe]
1114 1114 extrakw = {}
1115 1115 if not self.ui.interactive():
1116 1116 # Making stdin be a pipe should prevent svn from behaving
1117 1117 # interactively even if we can't pass --non-interactive.
1118 1118 extrakw['stdin'] = subprocess.PIPE
1119 1119 # Starting in svn 1.5 --non-interactive is a global flag
1120 1120 # instead of being per-command, but we need to support 1.4 so
1121 1121 # we have to be intelligent about what commands take
1122 1122 # --non-interactive.
1123 1123 if commands[0] in ('update', 'checkout', 'commit'):
1124 1124 cmd.append('--non-interactive')
1125 1125 cmd.extend(commands)
1126 1126 if filename is not None:
1127 1127 path = self.wvfs.reljoin(self._ctx.repo().origroot,
1128 1128 self._path, filename)
1129 1129 cmd.append(path)
1130 1130 env = dict(encoding.environ)
1131 1131 # Avoid localized output, preserve current locale for everything else.
1132 1132 lc_all = env.get('LC_ALL')
1133 1133 if lc_all:
1134 1134 env['LANG'] = lc_all
1135 1135 del env['LC_ALL']
1136 1136 env['LC_MESSAGES'] = 'C'
1137 1137 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
1138 1138 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1139 1139 universal_newlines=True, env=env, **extrakw)
1140 1140 stdout, stderr = p.communicate()
1141 1141 stderr = stderr.strip()
1142 1142 if not failok:
1143 1143 if p.returncode:
1144 1144 raise error.Abort(stderr or 'exited with code %d'
1145 1145 % p.returncode)
1146 1146 if stderr:
1147 1147 self.ui.warn(stderr + '\n')
1148 1148 return stdout, stderr
1149 1149
1150 1150 @propertycache
1151 1151 def _svnversion(self):
1152 1152 output, err = self._svncommand(['--version', '--quiet'], filename=None)
1153 1153 m = re.search(r'^(\d+)\.(\d+)', output)
1154 1154 if not m:
1155 1155 raise error.Abort(_('cannot retrieve svn tool version'))
1156 1156 return (int(m.group(1)), int(m.group(2)))
1157 1157
1158 1158 def _wcrevs(self):
1159 1159 # Get the working directory revision as well as the last
1160 1160 # commit revision so we can compare the subrepo state with
1161 1161 # both. We used to store the working directory one.
1162 1162 output, err = self._svncommand(['info', '--xml'])
1163 1163 doc = xml.dom.minidom.parseString(output)
1164 1164 entries = doc.getElementsByTagName('entry')
1165 1165 lastrev, rev = '0', '0'
1166 1166 if entries:
1167 1167 rev = str(entries[0].getAttribute('revision')) or '0'
1168 1168 commits = entries[0].getElementsByTagName('commit')
1169 1169 if commits:
1170 1170 lastrev = str(commits[0].getAttribute('revision')) or '0'
1171 1171 return (lastrev, rev)
1172 1172
1173 1173 def _wcrev(self):
1174 1174 return self._wcrevs()[0]
1175 1175
1176 1176 def _wcchanged(self):
1177 1177 """Return (changes, extchanges, missing) where changes is True
1178 1178 if the working directory was changed, extchanges is
1179 1179 True if any of these changes concern an external entry and missing
1180 1180 is True if any change is a missing entry.
1181 1181 """
1182 1182 output, err = self._svncommand(['status', '--xml'])
1183 1183 externals, changes, missing = [], [], []
1184 1184 doc = xml.dom.minidom.parseString(output)
1185 1185 for e in doc.getElementsByTagName('entry'):
1186 1186 s = e.getElementsByTagName('wc-status')
1187 1187 if not s:
1188 1188 continue
1189 1189 item = s[0].getAttribute('item')
1190 1190 props = s[0].getAttribute('props')
1191 1191 path = e.getAttribute('path')
1192 1192 if item == 'external':
1193 1193 externals.append(path)
1194 1194 elif item == 'missing':
1195 1195 missing.append(path)
1196 1196 if (item not in ('', 'normal', 'unversioned', 'external')
1197 1197 or props not in ('', 'none', 'normal')):
1198 1198 changes.append(path)
1199 1199 for path in changes:
1200 1200 for ext in externals:
1201 1201 if path == ext or path.startswith(ext + pycompat.ossep):
1202 1202 return True, True, bool(missing)
1203 1203 return bool(changes), False, bool(missing)
1204 1204
1205 1205 def dirty(self, ignoreupdate=False):
1206 1206 if not self._wcchanged()[0]:
1207 1207 if self._state[1] in self._wcrevs() or ignoreupdate:
1208 1208 return False
1209 1209 return True
1210 1210
1211 1211 def basestate(self):
1212 1212 lastrev, rev = self._wcrevs()
1213 1213 if lastrev != rev:
1214 1214 # Last committed rev is not the same than rev. We would
1215 1215 # like to take lastrev but we do not know if the subrepo
1216 1216 # URL exists at lastrev. Test it and fallback to rev it
1217 1217 # is not there.
1218 1218 try:
1219 1219 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1220 1220 return lastrev
1221 1221 except error.Abort:
1222 1222 pass
1223 1223 return rev
1224 1224
1225 1225 @annotatesubrepoerror
1226 1226 def commit(self, text, user, date):
1227 1227 # user and date are out of our hands since svn is centralized
1228 1228 changed, extchanged, missing = self._wcchanged()
1229 1229 if not changed:
1230 1230 return self.basestate()
1231 1231 if extchanged:
1232 1232 # Do not try to commit externals
1233 1233 raise error.Abort(_('cannot commit svn externals'))
1234 1234 if missing:
1235 1235 # svn can commit with missing entries but aborting like hg
1236 1236 # seems a better approach.
1237 1237 raise error.Abort(_('cannot commit missing svn entries'))
1238 1238 commitinfo, err = self._svncommand(['commit', '-m', text])
1239 1239 self.ui.status(commitinfo)
1240 1240 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1241 1241 if not newrev:
1242 1242 if not commitinfo.strip():
1243 1243 # Sometimes, our definition of "changed" differs from
1244 1244 # svn one. For instance, svn ignores missing files
1245 1245 # when committing. If there are only missing files, no
1246 1246 # commit is made, no output and no error code.
1247 1247 raise error.Abort(_('failed to commit svn changes'))
1248 1248 raise error.Abort(commitinfo.splitlines()[-1])
1249 1249 newrev = newrev.groups()[0]
1250 1250 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1251 1251 return newrev
1252 1252
1253 1253 @annotatesubrepoerror
1254 1254 def remove(self):
1255 1255 if self.dirty():
1256 1256 self.ui.warn(_('not removing repo %s because '
1257 1257 'it has changes.\n') % self._path)
1258 1258 return
1259 1259 self.ui.note(_('removing subrepo %s\n') % self._path)
1260 1260
1261 1261 self.wvfs.rmtree(forcibly=True)
1262 1262 try:
1263 1263 pwvfs = self._ctx.repo().wvfs
1264 1264 pwvfs.removedirs(pwvfs.dirname(self._path))
1265 1265 except OSError:
1266 1266 pass
1267 1267
1268 1268 @annotatesubrepoerror
1269 1269 def get(self, state, overwrite=False):
1270 1270 if overwrite:
1271 1271 self._svncommand(['revert', '--recursive'])
1272 1272 args = ['checkout']
1273 1273 if self._svnversion >= (1, 5):
1274 1274 args.append('--force')
1275 1275 # The revision must be specified at the end of the URL to properly
1276 1276 # update to a directory which has since been deleted and recreated.
1277 1277 args.append('%s@%s' % (state[0], state[1]))
1278 1278 status, err = self._svncommand(args, failok=True)
1279 1279 _sanitize(self.ui, self.wvfs, '.svn')
1280 1280 if not re.search('Checked out revision [0-9]+.', status):
1281 1281 if ('is already a working copy for a different URL' in err
1282 1282 and (self._wcchanged()[:2] == (False, False))):
1283 1283 # obstructed but clean working copy, so just blow it away.
1284 1284 self.remove()
1285 1285 self.get(state, overwrite=False)
1286 1286 return
1287 1287 raise error.Abort((status or err).splitlines()[-1])
1288 1288 self.ui.status(status)
1289 1289
1290 1290 @annotatesubrepoerror
1291 1291 def merge(self, state):
1292 1292 old = self._state[1]
1293 1293 new = state[1]
1294 1294 wcrev = self._wcrev()
1295 1295 if new != wcrev:
1296 1296 dirty = old == wcrev or self._wcchanged()[0]
1297 1297 if _updateprompt(self.ui, self, dirty, wcrev, new):
1298 1298 self.get(state, False)
1299 1299
1300 1300 def push(self, opts):
1301 1301 # push is a no-op for SVN
1302 1302 return True
1303 1303
1304 1304 @annotatesubrepoerror
1305 1305 def files(self):
1306 1306 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1307 1307 doc = xml.dom.minidom.parseString(output)
1308 1308 paths = []
1309 1309 for e in doc.getElementsByTagName('entry'):
1310 1310 kind = str(e.getAttribute('kind'))
1311 1311 if kind != 'file':
1312 1312 continue
1313 1313 name = ''.join(c.data for c
1314 1314 in e.getElementsByTagName('name')[0].childNodes
1315 1315 if c.nodeType == c.TEXT_NODE)
1316 1316 paths.append(name.encode('utf-8'))
1317 1317 return paths
1318 1318
1319 1319 def filedata(self, name, decode):
1320 1320 return self._svncommand(['cat'], name)[0]
1321 1321
1322 1322
1323 1323 class gitsubrepo(abstractsubrepo):
1324 1324 def __init__(self, ctx, path, state, allowcreate):
1325 1325 super(gitsubrepo, self).__init__(ctx, path)
1326 1326 self._state = state
1327 1327 self._abspath = ctx.repo().wjoin(path)
1328 1328 self._subparent = ctx.repo()
1329 1329 self._ensuregit()
1330 1330
1331 1331 def _ensuregit(self):
1332 1332 try:
1333 1333 self._gitexecutable = 'git'
1334 1334 out, err = self._gitnodir(['--version'])
1335 1335 except OSError as e:
1336 1336 genericerror = _("error executing git for subrepo '%s': %s")
1337 1337 notfoundhint = _("check git is installed and in your PATH")
1338 1338 if e.errno != errno.ENOENT:
1339 1339 raise error.Abort(genericerror % (self._path, e.strerror))
1340 1340 elif pycompat.osname == 'nt':
1341 1341 try:
1342 1342 self._gitexecutable = 'git.cmd'
1343 1343 out, err = self._gitnodir(['--version'])
1344 1344 except OSError as e2:
1345 1345 if e2.errno == errno.ENOENT:
1346 1346 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1347 1347 " for subrepo '%s'") % self._path,
1348 1348 hint=notfoundhint)
1349 1349 else:
1350 1350 raise error.Abort(genericerror % (self._path,
1351 1351 e2.strerror))
1352 1352 else:
1353 1353 raise error.Abort(_("couldn't find git for subrepo '%s'")
1354 1354 % self._path, hint=notfoundhint)
1355 1355 versionstatus = self._checkversion(out)
1356 1356 if versionstatus == 'unknown':
1357 1357 self.ui.warn(_('cannot retrieve git version\n'))
1358 1358 elif versionstatus == 'abort':
1359 1359 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1360 1360 elif versionstatus == 'warning':
1361 1361 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1362 1362
1363 1363 @staticmethod
1364 1364 def _gitversion(out):
1365 1365 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
1366 1366 if m:
1367 1367 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1368 1368
1369 1369 m = re.search(r'^git version (\d+)\.(\d+)', out)
1370 1370 if m:
1371 1371 return (int(m.group(1)), int(m.group(2)), 0)
1372 1372
1373 1373 return -1
1374 1374
1375 1375 @staticmethod
1376 1376 def _checkversion(out):
1377 1377 '''ensure git version is new enough
1378 1378
1379 1379 >>> _checkversion = gitsubrepo._checkversion
1380 1380 >>> _checkversion('git version 1.6.0')
1381 1381 'ok'
1382 1382 >>> _checkversion('git version 1.8.5')
1383 1383 'ok'
1384 1384 >>> _checkversion('git version 1.4.0')
1385 1385 'abort'
1386 1386 >>> _checkversion('git version 1.5.0')
1387 1387 'warning'
1388 1388 >>> _checkversion('git version 1.9-rc0')
1389 1389 'ok'
1390 1390 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1391 1391 'ok'
1392 1392 >>> _checkversion('git version 1.9.0.GIT')
1393 1393 'ok'
1394 1394 >>> _checkversion('git version 12345')
1395 1395 'unknown'
1396 1396 >>> _checkversion('no')
1397 1397 'unknown'
1398 1398 '''
1399 1399 version = gitsubrepo._gitversion(out)
1400 1400 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1401 1401 # despite the docstring comment. For now, error on 1.4.0, warn on
1402 1402 # 1.5.0 but attempt to continue.
1403 1403 if version == -1:
1404 1404 return 'unknown'
1405 1405 if version < (1, 5, 0):
1406 1406 return 'abort'
1407 1407 elif version < (1, 6, 0):
1408 1408 return 'warning'
1409 1409 return 'ok'
1410 1410
1411 1411 def _gitcommand(self, commands, env=None, stream=False):
1412 1412 return self._gitdir(commands, env=env, stream=stream)[0]
1413 1413
1414 1414 def _gitdir(self, commands, env=None, stream=False):
1415 1415 return self._gitnodir(commands, env=env, stream=stream,
1416 1416 cwd=self._abspath)
1417 1417
1418 1418 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1419 1419 """Calls the git command
1420 1420
1421 1421 The methods tries to call the git command. versions prior to 1.6.0
1422 1422 are not supported and very probably fail.
1423 1423 """
1424 1424 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1425 1425 if env is None:
1426 1426 env = encoding.environ.copy()
1427 1427 # disable localization for Git output (issue5176)
1428 1428 env['LC_ALL'] = 'C'
1429 1429 # fix for Git CVE-2015-7545
1430 1430 if 'GIT_ALLOW_PROTOCOL' not in env:
1431 1431 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1432 1432 # unless ui.quiet is set, print git's stderr,
1433 1433 # which is mostly progress and useful info
1434 1434 errpipe = None
1435 1435 if self.ui.quiet:
1436 1436 errpipe = open(os.devnull, 'w')
1437 1437 if self.ui._colormode and len(commands) and commands[0] == "diff":
1438 1438 # insert the argument in the front,
1439 1439 # the end of git diff arguments is used for paths
1440 1440 commands.insert(1, '--color')
1441 1441 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1442 1442 cwd=cwd, env=env, close_fds=util.closefds,
1443 1443 stdout=subprocess.PIPE, stderr=errpipe)
1444 1444 if stream:
1445 1445 return p.stdout, None
1446 1446
1447 1447 retdata = p.stdout.read().strip()
1448 1448 # wait for the child to exit to avoid race condition.
1449 1449 p.wait()
1450 1450
1451 1451 if p.returncode != 0 and p.returncode != 1:
1452 1452 # there are certain error codes that are ok
1453 1453 command = commands[0]
1454 1454 if command in ('cat-file', 'symbolic-ref'):
1455 1455 return retdata, p.returncode
1456 1456 # for all others, abort
1457 1457 raise error.Abort(_('git %s error %d in %s') %
1458 1458 (command, p.returncode, self._relpath))
1459 1459
1460 1460 return retdata, p.returncode
1461 1461
1462 1462 def _gitmissing(self):
1463 1463 return not self.wvfs.exists('.git')
1464 1464
1465 1465 def _gitstate(self):
1466 1466 return self._gitcommand(['rev-parse', 'HEAD'])
1467 1467
1468 1468 def _gitcurrentbranch(self):
1469 1469 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1470 1470 if err:
1471 1471 current = None
1472 1472 return current
1473 1473
1474 1474 def _gitremote(self, remote):
1475 1475 out = self._gitcommand(['remote', 'show', '-n', remote])
1476 1476 line = out.split('\n')[1]
1477 1477 i = line.index('URL: ') + len('URL: ')
1478 1478 return line[i:]
1479 1479
1480 1480 def _githavelocally(self, revision):
1481 1481 out, code = self._gitdir(['cat-file', '-e', revision])
1482 1482 return code == 0
1483 1483
1484 1484 def _gitisancestor(self, r1, r2):
1485 1485 base = self._gitcommand(['merge-base', r1, r2])
1486 1486 return base == r1
1487 1487
1488 1488 def _gitisbare(self):
1489 1489 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1490 1490
1491 1491 def _gitupdatestat(self):
1492 1492 """This must be run before git diff-index.
1493 1493 diff-index only looks at changes to file stat;
1494 1494 this command looks at file contents and updates the stat."""
1495 1495 self._gitcommand(['update-index', '-q', '--refresh'])
1496 1496
1497 1497 def _gitbranchmap(self):
1498 1498 '''returns 2 things:
1499 1499 a map from git branch to revision
1500 1500 a map from revision to branches'''
1501 1501 branch2rev = {}
1502 1502 rev2branch = {}
1503 1503
1504 1504 out = self._gitcommand(['for-each-ref', '--format',
1505 1505 '%(objectname) %(refname)'])
1506 1506 for line in out.split('\n'):
1507 1507 revision, ref = line.split(' ')
1508 1508 if (not ref.startswith('refs/heads/') and
1509 1509 not ref.startswith('refs/remotes/')):
1510 1510 continue
1511 1511 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1512 1512 continue # ignore remote/HEAD redirects
1513 1513 branch2rev[ref] = revision
1514 1514 rev2branch.setdefault(revision, []).append(ref)
1515 1515 return branch2rev, rev2branch
1516 1516
1517 1517 def _gittracking(self, branches):
1518 1518 'return map of remote branch to local tracking branch'
1519 1519 # assumes no more than one local tracking branch for each remote
1520 1520 tracking = {}
1521 1521 for b in branches:
1522 1522 if b.startswith('refs/remotes/'):
1523 1523 continue
1524 1524 bname = b.split('/', 2)[2]
1525 1525 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1526 1526 if remote:
1527 1527 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1528 1528 tracking['refs/remotes/%s/%s' %
1529 1529 (remote, ref.split('/', 2)[2])] = b
1530 1530 return tracking
1531 1531
1532 1532 def _abssource(self, source):
1533 1533 if '://' not in source:
1534 1534 # recognize the scp syntax as an absolute source
1535 1535 colon = source.find(':')
1536 1536 if colon != -1 and '/' not in source[:colon]:
1537 1537 return source
1538 1538 self._subsource = source
1539 1539 return _abssource(self)
1540 1540
1541 1541 def _fetch(self, source, revision):
1542 1542 if self._gitmissing():
1543 1543 source = self._abssource(source)
1544 1544 self.ui.status(_('cloning subrepo %s from %s\n') %
1545 1545 (self._relpath, source))
1546 1546 self._gitnodir(['clone', source, self._abspath])
1547 1547 if self._githavelocally(revision):
1548 1548 return
1549 1549 self.ui.status(_('pulling subrepo %s from %s\n') %
1550 1550 (self._relpath, self._gitremote('origin')))
1551 1551 # try only origin: the originally cloned repo
1552 1552 self._gitcommand(['fetch'])
1553 1553 if not self._githavelocally(revision):
1554 1554 raise error.Abort(_("revision %s does not exist in subrepo %s\n") %
1555 1555 (revision, self._relpath))
1556 1556
1557 1557 @annotatesubrepoerror
1558 1558 def dirty(self, ignoreupdate=False):
1559 1559 if self._gitmissing():
1560 1560 return self._state[1] != ''
1561 1561 if self._gitisbare():
1562 1562 return True
1563 1563 if not ignoreupdate and self._state[1] != self._gitstate():
1564 1564 # different version checked out
1565 1565 return True
1566 1566 # check for staged changes or modified files; ignore untracked files
1567 1567 self._gitupdatestat()
1568 1568 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1569 1569 return code == 1
1570 1570
1571 1571 def basestate(self):
1572 1572 return self._gitstate()
1573 1573
1574 1574 @annotatesubrepoerror
1575 1575 def get(self, state, overwrite=False):
1576 1576 source, revision, kind = state
1577 1577 if not revision:
1578 1578 self.remove()
1579 1579 return
1580 1580 self._fetch(source, revision)
1581 1581 # if the repo was set to be bare, unbare it
1582 1582 if self._gitisbare():
1583 1583 self._gitcommand(['config', 'core.bare', 'false'])
1584 1584 if self._gitstate() == revision:
1585 1585 self._gitcommand(['reset', '--hard', 'HEAD'])
1586 1586 return
1587 1587 elif self._gitstate() == revision:
1588 1588 if overwrite:
1589 1589 # first reset the index to unmark new files for commit, because
1590 1590 # reset --hard will otherwise throw away files added for commit,
1591 1591 # not just unmark them.
1592 1592 self._gitcommand(['reset', 'HEAD'])
1593 1593 self._gitcommand(['reset', '--hard', 'HEAD'])
1594 1594 return
1595 1595 branch2rev, rev2branch = self._gitbranchmap()
1596 1596
1597 1597 def checkout(args):
1598 1598 cmd = ['checkout']
1599 1599 if overwrite:
1600 1600 # first reset the index to unmark new files for commit, because
1601 1601 # the -f option will otherwise throw away files added for
1602 1602 # commit, not just unmark them.
1603 1603 self._gitcommand(['reset', 'HEAD'])
1604 1604 cmd.append('-f')
1605 1605 self._gitcommand(cmd + args)
1606 1606 _sanitize(self.ui, self.wvfs, '.git')
1607 1607
1608 1608 def rawcheckout():
1609 1609 # no branch to checkout, check it out with no branch
1610 1610 self.ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1611 1611 self._relpath)
1612 1612 self.ui.warn(_('check out a git branch if you intend '
1613 1613 'to make changes\n'))
1614 1614 checkout(['-q', revision])
1615 1615
1616 1616 if revision not in rev2branch:
1617 1617 rawcheckout()
1618 1618 return
1619 1619 branches = rev2branch[revision]
1620 1620 firstlocalbranch = None
1621 1621 for b in branches:
1622 1622 if b == 'refs/heads/master':
1623 1623 # master trumps all other branches
1624 1624 checkout(['refs/heads/master'])
1625 1625 return
1626 1626 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1627 1627 firstlocalbranch = b
1628 1628 if firstlocalbranch:
1629 1629 checkout([firstlocalbranch])
1630 1630 return
1631 1631
1632 1632 tracking = self._gittracking(branch2rev.keys())
1633 1633 # choose a remote branch already tracked if possible
1634 1634 remote = branches[0]
1635 1635 if remote not in tracking:
1636 1636 for b in branches:
1637 1637 if b in tracking:
1638 1638 remote = b
1639 1639 break
1640 1640
1641 1641 if remote not in tracking:
1642 1642 # create a new local tracking branch
1643 1643 local = remote.split('/', 3)[3]
1644 1644 checkout(['-b', local, remote])
1645 1645 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1646 1646 # When updating to a tracked remote branch,
1647 1647 # if the local tracking branch is downstream of it,
1648 1648 # a normal `git pull` would have performed a "fast-forward merge"
1649 1649 # which is equivalent to updating the local branch to the remote.
1650 1650 # Since we are only looking at branching at update, we need to
1651 1651 # detect this situation and perform this action lazily.
1652 1652 if tracking[remote] != self._gitcurrentbranch():
1653 1653 checkout([tracking[remote]])
1654 1654 self._gitcommand(['merge', '--ff', remote])
1655 1655 _sanitize(self.ui, self.wvfs, '.git')
1656 1656 else:
1657 1657 # a real merge would be required, just checkout the revision
1658 1658 rawcheckout()
1659 1659
1660 1660 @annotatesubrepoerror
1661 1661 def commit(self, text, user, date):
1662 1662 if self._gitmissing():
1663 1663 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1664 1664 cmd = ['commit', '-a', '-m', text]
1665 1665 env = encoding.environ.copy()
1666 1666 if user:
1667 1667 cmd += ['--author', user]
1668 1668 if date:
1669 1669 # git's date parser silently ignores when seconds < 1e9
1670 1670 # convert to ISO8601
1671 1671 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1672 1672 '%Y-%m-%dT%H:%M:%S %1%2')
1673 1673 self._gitcommand(cmd, env=env)
1674 1674 # make sure commit works otherwise HEAD might not exist under certain
1675 1675 # circumstances
1676 1676 return self._gitstate()
1677 1677
1678 1678 @annotatesubrepoerror
1679 1679 def merge(self, state):
1680 1680 source, revision, kind = state
1681 1681 self._fetch(source, revision)
1682 1682 base = self._gitcommand(['merge-base', revision, self._state[1]])
1683 1683 self._gitupdatestat()
1684 1684 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1685 1685
1686 1686 def mergefunc():
1687 1687 if base == revision:
1688 1688 self.get(state) # fast forward merge
1689 1689 elif base != self._state[1]:
1690 1690 self._gitcommand(['merge', '--no-commit', revision])
1691 1691 _sanitize(self.ui, self.wvfs, '.git')
1692 1692
1693 1693 if self.dirty():
1694 1694 if self._gitstate() != revision:
1695 1695 dirty = self._gitstate() == self._state[1] or code != 0
1696 1696 if _updateprompt(self.ui, self, dirty,
1697 1697 self._state[1][:7], revision[:7]):
1698 1698 mergefunc()
1699 1699 else:
1700 1700 mergefunc()
1701 1701
1702 1702 @annotatesubrepoerror
1703 1703 def push(self, opts):
1704 1704 force = opts.get('force')
1705 1705
1706 1706 if not self._state[1]:
1707 1707 return True
1708 1708 if self._gitmissing():
1709 1709 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1710 1710 # if a branch in origin contains the revision, nothing to do
1711 1711 branch2rev, rev2branch = self._gitbranchmap()
1712 1712 if self._state[1] in rev2branch:
1713 1713 for b in rev2branch[self._state[1]]:
1714 1714 if b.startswith('refs/remotes/origin/'):
1715 1715 return True
1716 1716 for b, revision in branch2rev.iteritems():
1717 1717 if b.startswith('refs/remotes/origin/'):
1718 1718 if self._gitisancestor(self._state[1], revision):
1719 1719 return True
1720 1720 # otherwise, try to push the currently checked out branch
1721 1721 cmd = ['push']
1722 1722 if force:
1723 1723 cmd.append('--force')
1724 1724
1725 1725 current = self._gitcurrentbranch()
1726 1726 if current:
1727 1727 # determine if the current branch is even useful
1728 1728 if not self._gitisancestor(self._state[1], current):
1729 1729 self.ui.warn(_('unrelated git branch checked out '
1730 1730 'in subrepo %s\n') % self._relpath)
1731 1731 return False
1732 1732 self.ui.status(_('pushing branch %s of subrepo %s\n') %
1733 1733 (current.split('/', 2)[2], self._relpath))
1734 1734 ret = self._gitdir(cmd + ['origin', current])
1735 1735 return ret[1] == 0
1736 1736 else:
1737 1737 self.ui.warn(_('no branch checked out in subrepo %s\n'
1738 1738 'cannot push revision %s\n') %
1739 1739 (self._relpath, self._state[1]))
1740 1740 return False
1741 1741
1742 1742 @annotatesubrepoerror
1743 1743 def add(self, ui, match, prefix, explicitonly, **opts):
1744 1744 if self._gitmissing():
1745 1745 return []
1746 1746
1747 1747 (modified, added, removed,
1748 1748 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1749 1749 clean=True)
1750 1750
1751 1751 tracked = set()
1752 1752 # dirstates 'amn' warn, 'r' is added again
1753 1753 for l in (modified, added, deleted, clean):
1754 1754 tracked.update(l)
1755 1755
1756 1756 # Unknown files not of interest will be rejected by the matcher
1757 1757 files = unknown
1758 1758 files.extend(match.files())
1759 1759
1760 1760 rejected = []
1761 1761
1762 1762 files = [f for f in sorted(set(files)) if match(f)]
1763 1763 for f in files:
1764 1764 exact = match.exact(f)
1765 1765 command = ["add"]
1766 1766 if exact:
1767 1767 command.append("-f") #should be added, even if ignored
1768 1768 if ui.verbose or not exact:
1769 1769 ui.status(_('adding %s\n') % match.rel(f))
1770 1770
1771 1771 if f in tracked: # hg prints 'adding' even if already tracked
1772 1772 if exact:
1773 1773 rejected.append(f)
1774 1774 continue
1775 1775 if not opts.get(r'dry_run'):
1776 1776 self._gitcommand(command + [f])
1777 1777
1778 1778 for f in rejected:
1779 1779 ui.warn(_("%s already tracked!\n") % match.abs(f))
1780 1780
1781 1781 return rejected
1782 1782
1783 1783 @annotatesubrepoerror
1784 1784 def remove(self):
1785 1785 if self._gitmissing():
1786 1786 return
1787 1787 if self.dirty():
1788 1788 self.ui.warn(_('not removing repo %s because '
1789 1789 'it has changes.\n') % self._relpath)
1790 1790 return
1791 1791 # we can't fully delete the repository as it may contain
1792 1792 # local-only history
1793 1793 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1794 1794 self._gitcommand(['config', 'core.bare', 'true'])
1795 1795 for f, kind in self.wvfs.readdir():
1796 1796 if f == '.git':
1797 1797 continue
1798 1798 if kind == stat.S_IFDIR:
1799 1799 self.wvfs.rmtree(f)
1800 1800 else:
1801 1801 self.wvfs.unlink(f)
1802 1802
1803 1803 def archive(self, archiver, prefix, match=None, decode=True):
1804 1804 total = 0
1805 1805 source, revision = self._state
1806 1806 if not revision:
1807 1807 return total
1808 1808 self._fetch(source, revision)
1809 1809
1810 1810 # Parse git's native archive command.
1811 1811 # This should be much faster than manually traversing the trees
1812 1812 # and objects with many subprocess calls.
1813 1813 tarstream = self._gitcommand(['archive', revision], stream=True)
1814 1814 tar = tarfile.open(fileobj=tarstream, mode='r|')
1815 1815 relpath = subrelpath(self)
1816 1816 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1817 1817 for i, info in enumerate(tar):
1818 1818 if info.isdir():
1819 1819 continue
1820 1820 if match and not match(info.name):
1821 1821 continue
1822 1822 if info.issym():
1823 1823 data = info.linkname
1824 1824 else:
1825 1825 data = tar.extractfile(info).read()
1826 1826 archiver.addfile(prefix + self._path + '/' + info.name,
1827 1827 info.mode, info.issym(), data)
1828 1828 total += 1
1829 1829 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1830 1830 unit=_('files'))
1831 1831 self.ui.progress(_('archiving (%s)') % relpath, None)
1832 1832 return total
1833 1833
1834 1834
1835 1835 @annotatesubrepoerror
1836 def cat(self, match, fntemplate, prefix, **opts):
1836 def cat(self, match, fm, fntemplate, prefix, **opts):
1837 1837 rev = self._state[1]
1838 1838 if match.anypats():
1839 1839 return 1 #No support for include/exclude yet
1840 1840
1841 1841 if not match.files():
1842 1842 return 1
1843 1843
1844 # TODO: add support for non-plain formatter (see cmdutil.cat())
1844 1845 for f in match.files():
1845 1846 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1846 1847 fp = cmdutil.makefileobj(self._subparent, fntemplate,
1847 1848 self._ctx.node(),
1848 1849 pathname=self.wvfs.reljoin(prefix, f))
1849 1850 fp.write(output)
1850 1851 fp.close()
1851 1852 return 0
1852 1853
1853 1854
1854 1855 @annotatesubrepoerror
1855 1856 def status(self, rev2, **opts):
1856 1857 rev1 = self._state[1]
1857 1858 if self._gitmissing() or not rev1:
1858 1859 # if the repo is missing, return no results
1859 1860 return scmutil.status([], [], [], [], [], [], [])
1860 1861 modified, added, removed = [], [], []
1861 1862 self._gitupdatestat()
1862 1863 if rev2:
1863 1864 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
1864 1865 else:
1865 1866 command = ['diff-index', '--no-renames', rev1]
1866 1867 out = self._gitcommand(command)
1867 1868 for line in out.split('\n'):
1868 1869 tab = line.find('\t')
1869 1870 if tab == -1:
1870 1871 continue
1871 1872 status, f = line[tab - 1], line[tab + 1:]
1872 1873 if status == 'M':
1873 1874 modified.append(f)
1874 1875 elif status == 'A':
1875 1876 added.append(f)
1876 1877 elif status == 'D':
1877 1878 removed.append(f)
1878 1879
1879 1880 deleted, unknown, ignored, clean = [], [], [], []
1880 1881
1881 1882 command = ['status', '--porcelain', '-z']
1882 1883 if opts.get(r'unknown'):
1883 1884 command += ['--untracked-files=all']
1884 1885 if opts.get(r'ignored'):
1885 1886 command += ['--ignored']
1886 1887 out = self._gitcommand(command)
1887 1888
1888 1889 changedfiles = set()
1889 1890 changedfiles.update(modified)
1890 1891 changedfiles.update(added)
1891 1892 changedfiles.update(removed)
1892 1893 for line in out.split('\0'):
1893 1894 if not line:
1894 1895 continue
1895 1896 st = line[0:2]
1896 1897 #moves and copies show 2 files on one line
1897 1898 if line.find('\0') >= 0:
1898 1899 filename1, filename2 = line[3:].split('\0')
1899 1900 else:
1900 1901 filename1 = line[3:]
1901 1902 filename2 = None
1902 1903
1903 1904 changedfiles.add(filename1)
1904 1905 if filename2:
1905 1906 changedfiles.add(filename2)
1906 1907
1907 1908 if st == '??':
1908 1909 unknown.append(filename1)
1909 1910 elif st == '!!':
1910 1911 ignored.append(filename1)
1911 1912
1912 1913 if opts.get(r'clean'):
1913 1914 out = self._gitcommand(['ls-files'])
1914 1915 for f in out.split('\n'):
1915 1916 if not f in changedfiles:
1916 1917 clean.append(f)
1917 1918
1918 1919 return scmutil.status(modified, added, removed, deleted,
1919 1920 unknown, ignored, clean)
1920 1921
1921 1922 @annotatesubrepoerror
1922 1923 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1923 1924 node1 = self._state[1]
1924 1925 cmd = ['diff', '--no-renames']
1925 1926 if opts[r'stat']:
1926 1927 cmd.append('--stat')
1927 1928 else:
1928 1929 # for Git, this also implies '-p'
1929 1930 cmd.append('-U%d' % diffopts.context)
1930 1931
1931 1932 gitprefix = self.wvfs.reljoin(prefix, self._path)
1932 1933
1933 1934 if diffopts.noprefix:
1934 1935 cmd.extend(['--src-prefix=%s/' % gitprefix,
1935 1936 '--dst-prefix=%s/' % gitprefix])
1936 1937 else:
1937 1938 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1938 1939 '--dst-prefix=b/%s/' % gitprefix])
1939 1940
1940 1941 if diffopts.ignorews:
1941 1942 cmd.append('--ignore-all-space')
1942 1943 if diffopts.ignorewsamount:
1943 1944 cmd.append('--ignore-space-change')
1944 1945 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1945 1946 and diffopts.ignoreblanklines:
1946 1947 cmd.append('--ignore-blank-lines')
1947 1948
1948 1949 cmd.append(node1)
1949 1950 if node2:
1950 1951 cmd.append(node2)
1951 1952
1952 1953 output = ""
1953 1954 if match.always():
1954 1955 output += self._gitcommand(cmd) + '\n'
1955 1956 else:
1956 1957 st = self.status(node2)[:3]
1957 1958 files = [f for sublist in st for f in sublist]
1958 1959 for f in files:
1959 1960 if match(f):
1960 1961 output += self._gitcommand(cmd + ['--', f]) + '\n'
1961 1962
1962 1963 if output.strip():
1963 1964 ui.write(output)
1964 1965
1965 1966 @annotatesubrepoerror
1966 1967 def revert(self, substate, *pats, **opts):
1967 1968 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1968 1969 if not opts.get(r'no_backup'):
1969 1970 status = self.status(None)
1970 1971 names = status.modified
1971 1972 for name in names:
1972 1973 bakname = scmutil.origpath(self.ui, self._subparent, name)
1973 1974 self.ui.note(_('saving current version of %s as %s\n') %
1974 1975 (name, bakname))
1975 1976 self.wvfs.rename(name, bakname)
1976 1977
1977 1978 if not opts.get(r'dry_run'):
1978 1979 self.get(substate, overwrite=True)
1979 1980 return []
1980 1981
1981 1982 def shortid(self, revid):
1982 1983 return revid[:7]
1983 1984
1984 1985 types = {
1985 1986 'hg': hgsubrepo,
1986 1987 'svn': svnsubrepo,
1987 1988 'git': gitsubrepo,
1988 1989 }
@@ -1,81 +1,121 b''
1 1 $ hg init
2 2 $ echo 0 > a
3 3 $ echo 0 > b
4 4 $ hg ci -A -m m
5 5 adding a
6 6 adding b
7 7 $ hg rm a
8 8 $ hg cat a
9 9 0
10 10 $ hg cat --decode a # more tests in test-encode
11 11 0
12 12 $ echo 1 > b
13 13 $ hg ci -m m
14 14 $ echo 2 > b
15 15 $ hg cat -r 0 a
16 16 0
17 17 $ hg cat -r 0 b
18 18 0
19 19 $ hg cat -r 1 a
20 20 a: no such file in rev 7040230c159c
21 21 [1]
22 22 $ hg cat -r 1 b
23 23 1
24 24
25 25 Test multiple files
26 26
27 27 $ echo 3 > c
28 28 $ hg ci -Am addmore c
29 29 $ hg cat b c
30 30 1
31 31 3
32 32 $ hg cat .
33 33 1
34 34 3
35 35 $ hg cat . c
36 36 1
37 37 3
38 38
39 39 Test fileset
40 40
41 41 $ hg cat 'set:not(b) or a'
42 42 3
43 43 $ hg cat 'set:c or b'
44 44 1
45 45 3
46 46
47 47 $ mkdir tmp
48 48 $ hg cat --output tmp/HH_%H c
49 49 $ hg cat --output tmp/RR_%R c
50 50 $ hg cat --output tmp/h_%h c
51 51 $ hg cat --output tmp/r_%r c
52 52 $ hg cat --output tmp/%s_s c
53 53 $ hg cat --output tmp/%d%%_d c
54 54 $ hg cat --output tmp/%p_p c
55 55 $ hg log -r . --template "{rev}: {node|short}\n"
56 56 2: 45116003780e
57 57 $ find tmp -type f | sort
58 58 tmp/.%_d
59 59 tmp/HH_45116003780e3678b333fb2c99fa7d559c8457e9
60 60 tmp/RR_2
61 61 tmp/c_p
62 62 tmp/c_s
63 63 tmp/h_45116003780e
64 64 tmp/r_2
65 65
66 Test template output
67
68 $ hg --cwd tmp cat ../b ../c -T '== {path} ({abspath}) ==\n{data}'
69 == ../b (b) == (glob)
70 1
71 == ../c (c) == (glob)
72 3
73
74 $ hg cat b c -Tjson --output -
75 [
76 {
77 "abspath": "b",
78 "data": "1\n",
79 "path": "b"
80 },
81 {
82 "abspath": "c",
83 "data": "3\n",
84 "path": "c"
85 }
86 ]
87
88 $ hg cat b c -Tjson --output 'tmp/%p.json'
89 $ cat tmp/b.json
90 [
91 {
92 "abspath": "b",
93 "data": "1\n",
94 "path": "b"
95 }
96 ]
97 $ cat tmp/c.json
98 [
99 {
100 "abspath": "c",
101 "data": "3\n",
102 "path": "c"
103 }
104 ]
105
66 106 Test working directory
67 107
68 108 $ echo b-wdir > b
69 109 $ hg cat -r 'wdir()' b
70 110 b-wdir
71 111
72 112 Environment variables are not visible by default
73 113
74 114 $ PATTERN='t4' hg log -r '.' -T "{ifcontains('PATTERN', envvars, 'yes', 'no')}\n"
75 115 no
76 116
77 117 Environment variable visibility can be explicit
78 118
79 119 $ PATTERN='t4' hg log -r '.' -T "{envvars % '{key} -> {value}\n'}" \
80 120 > --config "experimental.exportableenviron=PATTERN"
81 121 PATTERN -> t4
@@ -1,381 +1,381 b''
1 1 Show all commands except debug commands
2 2 $ hg debugcomplete
3 3 add
4 4 addremove
5 5 annotate
6 6 archive
7 7 backout
8 8 bisect
9 9 bookmarks
10 10 branch
11 11 branches
12 12 bundle
13 13 cat
14 14 clone
15 15 commit
16 16 config
17 17 copy
18 18 diff
19 19 export
20 20 files
21 21 forget
22 22 graft
23 23 grep
24 24 heads
25 25 help
26 26 identify
27 27 import
28 28 incoming
29 29 init
30 30 locate
31 31 log
32 32 manifest
33 33 merge
34 34 outgoing
35 35 parents
36 36 paths
37 37 phase
38 38 pull
39 39 push
40 40 recover
41 41 remove
42 42 rename
43 43 resolve
44 44 revert
45 45 rollback
46 46 root
47 47 serve
48 48 status
49 49 summary
50 50 tag
51 51 tags
52 52 tip
53 53 unbundle
54 54 update
55 55 verify
56 56 version
57 57
58 58 Show all commands that start with "a"
59 59 $ hg debugcomplete a
60 60 add
61 61 addremove
62 62 annotate
63 63 archive
64 64
65 65 Do not show debug commands if there are other candidates
66 66 $ hg debugcomplete d
67 67 diff
68 68
69 69 Show debug commands if there are no other candidates
70 70 $ hg debugcomplete debug
71 71 debugancestor
72 72 debugapplystreamclonebundle
73 73 debugbuilddag
74 74 debugbundle
75 75 debugcheckstate
76 76 debugcolor
77 77 debugcommands
78 78 debugcomplete
79 79 debugconfig
80 80 debugcreatestreamclonebundle
81 81 debugdag
82 82 debugdata
83 83 debugdate
84 84 debugdeltachain
85 85 debugdirstate
86 86 debugdiscovery
87 87 debugextensions
88 88 debugfileset
89 89 debugfsinfo
90 90 debuggetbundle
91 91 debugignore
92 92 debugindex
93 93 debugindexdot
94 94 debuginstall
95 95 debugknown
96 96 debuglabelcomplete
97 97 debuglocks
98 98 debugmergestate
99 99 debugnamecomplete
100 100 debugobsolete
101 101 debugpathcomplete
102 102 debugpickmergetool
103 103 debugpushkey
104 104 debugpvec
105 105 debugrebuilddirstate
106 106 debugrebuildfncache
107 107 debugrename
108 108 debugrevlog
109 109 debugrevspec
110 110 debugsetparents
111 111 debugsub
112 112 debugsuccessorssets
113 113 debugtemplate
114 114 debugupdatecaches
115 115 debugupgraderepo
116 116 debugwalk
117 117 debugwireargs
118 118
119 119 Do not show the alias of a debug command if there are other candidates
120 120 (this should hide rawcommit)
121 121 $ hg debugcomplete r
122 122 recover
123 123 remove
124 124 rename
125 125 resolve
126 126 revert
127 127 rollback
128 128 root
129 129 Show the alias of a debug command if there are no other candidates
130 130 $ hg debugcomplete rawc
131 131
132 132
133 133 Show the global options
134 134 $ hg debugcomplete --options | sort
135 135 --color
136 136 --config
137 137 --cwd
138 138 --debug
139 139 --debugger
140 140 --encoding
141 141 --encodingmode
142 142 --help
143 143 --hidden
144 144 --noninteractive
145 145 --pager
146 146 --profile
147 147 --quiet
148 148 --repository
149 149 --time
150 150 --traceback
151 151 --verbose
152 152 --version
153 153 -R
154 154 -h
155 155 -q
156 156 -v
157 157 -y
158 158
159 159 Show the options for the "serve" command
160 160 $ hg debugcomplete --options serve | sort
161 161 --accesslog
162 162 --address
163 163 --certificate
164 164 --cmdserver
165 165 --color
166 166 --config
167 167 --cwd
168 168 --daemon
169 169 --daemon-postexec
170 170 --debug
171 171 --debugger
172 172 --encoding
173 173 --encodingmode
174 174 --errorlog
175 175 --help
176 176 --hidden
177 177 --ipv6
178 178 --name
179 179 --noninteractive
180 180 --pager
181 181 --pid-file
182 182 --port
183 183 --prefix
184 184 --profile
185 185 --quiet
186 186 --repository
187 187 --stdio
188 188 --style
189 189 --subrepos
190 190 --templates
191 191 --time
192 192 --traceback
193 193 --verbose
194 194 --version
195 195 --web-conf
196 196 -6
197 197 -A
198 198 -E
199 199 -R
200 200 -S
201 201 -a
202 202 -d
203 203 -h
204 204 -n
205 205 -p
206 206 -q
207 207 -t
208 208 -v
209 209 -y
210 210
211 211 Show an error if we use --options with an ambiguous abbreviation
212 212 $ hg debugcomplete --options s
213 213 hg: command 's' is ambiguous:
214 214 serve showconfig status summary
215 215 [255]
216 216
217 217 Show all commands + options
218 218 $ hg debugcommands
219 219 add: include, exclude, subrepos, dry-run
220 220 annotate: rev, follow, no-follow, text, user, file, date, number, changeset, line-number, skip, ignore-all-space, ignore-space-change, ignore-blank-lines, include, exclude, template
221 221 clone: noupdate, updaterev, rev, branch, pull, uncompressed, ssh, remotecmd, insecure
222 222 commit: addremove, close-branch, amend, secret, edit, interactive, include, exclude, message, logfile, date, user, subrepos
223 223 diff: rev, change, text, git, binary, nodates, noprefix, show-function, reverse, ignore-all-space, ignore-space-change, ignore-blank-lines, unified, stat, root, include, exclude, subrepos
224 224 export: output, switch-parent, rev, text, git, binary, nodates
225 225 forget: include, exclude
226 226 init: ssh, remotecmd, insecure
227 227 log: follow, follow-first, date, copies, keyword, rev, removed, only-merges, user, only-branch, branch, prune, patch, git, limit, no-merges, stat, graph, style, template, include, exclude
228 228 merge: force, rev, preview, tool
229 229 pull: update, force, rev, bookmark, branch, ssh, remotecmd, insecure
230 230 push: force, rev, bookmark, branch, new-branch, ssh, remotecmd, insecure
231 231 remove: after, force, subrepos, include, exclude
232 232 serve: accesslog, daemon, daemon-postexec, errorlog, port, address, prefix, name, web-conf, webdir-conf, pid-file, stdio, cmdserver, templates, style, ipv6, certificate, subrepos
233 233 status: all, modified, added, removed, deleted, clean, unknown, ignored, no-status, copies, print0, rev, change, include, exclude, subrepos, template
234 234 summary: remote
235 235 update: clean, check, merge, date, rev, tool
236 236 addremove: similarity, subrepos, include, exclude, dry-run
237 237 archive: no-decode, prefix, rev, type, subrepos, include, exclude
238 238 backout: merge, commit, no-commit, parent, rev, edit, tool, include, exclude, message, logfile, date, user
239 239 bisect: reset, good, bad, skip, extend, command, noupdate
240 240 bookmarks: force, rev, delete, rename, inactive, template
241 241 branch: force, clean
242 242 branches: active, closed, template
243 243 bundle: force, rev, branch, base, all, type, ssh, remotecmd, insecure
244 cat: output, rev, decode, include, exclude
244 cat: output, rev, decode, include, exclude, template
245 245 config: untrusted, edit, local, global, template
246 246 copy: after, force, include, exclude, dry-run
247 247 debugancestor:
248 248 debugapplystreamclonebundle:
249 249 debugbuilddag: mergeable-file, overwritten-file, new-file
250 250 debugbundle: all, spec
251 251 debugcheckstate:
252 252 debugcolor: style
253 253 debugcommands:
254 254 debugcomplete: options
255 255 debugcreatestreamclonebundle:
256 256 debugdag: tags, branches, dots, spaces
257 257 debugdata: changelog, manifest, dir
258 258 debugdate: extended
259 259 debugdeltachain: changelog, manifest, dir, template
260 260 debugdirstate: nodates, datesort
261 261 debugdiscovery: old, nonheads, ssh, remotecmd, insecure
262 262 debugextensions: template
263 263 debugfileset: rev
264 264 debugfsinfo:
265 265 debuggetbundle: head, common, type
266 266 debugignore:
267 267 debugindex: changelog, manifest, dir, format
268 268 debugindexdot: changelog, manifest, dir
269 269 debuginstall: template
270 270 debugknown:
271 271 debuglabelcomplete:
272 272 debuglocks: force-lock, force-wlock
273 273 debugmergestate:
274 274 debugnamecomplete:
275 275 debugobsolete: flags, record-parents, rev, index, delete, date, user, template
276 276 debugpathcomplete: full, normal, added, removed
277 277 debugpickmergetool: rev, changedelete, include, exclude, tool
278 278 debugpushkey:
279 279 debugpvec:
280 280 debugrebuilddirstate: rev, minimal
281 281 debugrebuildfncache:
282 282 debugrename: rev
283 283 debugrevlog: changelog, manifest, dir, dump
284 284 debugrevspec: optimize, show-stage, no-optimized, verify-optimized
285 285 debugsetparents:
286 286 debugsub: rev
287 287 debugsuccessorssets:
288 288 debugtemplate: rev, define
289 289 debugupdatecaches:
290 290 debugupgraderepo: optimize, run
291 291 debugwalk: include, exclude
292 292 debugwireargs: three, four, five, ssh, remotecmd, insecure
293 293 files: rev, print0, include, exclude, template, subrepos
294 294 graft: rev, continue, edit, log, force, currentdate, currentuser, date, user, tool, dry-run
295 295 grep: print0, all, text, follow, ignore-case, files-with-matches, line-number, rev, user, date, template, include, exclude
296 296 heads: rev, topo, active, closed, style, template
297 297 help: extension, command, keyword, system
298 298 identify: rev, num, id, branch, tags, bookmarks, ssh, remotecmd, insecure
299 299 import: strip, base, edit, force, no-commit, bypass, partial, exact, prefix, import-branch, message, logfile, date, user, similarity
300 300 incoming: force, newest-first, bundle, rev, bookmarks, branch, patch, git, limit, no-merges, stat, graph, style, template, ssh, remotecmd, insecure, subrepos
301 301 locate: rev, print0, fullpath, include, exclude
302 302 manifest: rev, all, template
303 303 outgoing: force, rev, newest-first, bookmarks, branch, patch, git, limit, no-merges, stat, graph, style, template, ssh, remotecmd, insecure, subrepos
304 304 parents: rev, style, template
305 305 paths: template
306 306 phase: public, draft, secret, force, rev
307 307 recover:
308 308 rename: after, force, include, exclude, dry-run
309 309 resolve: all, list, mark, unmark, no-status, tool, include, exclude, template
310 310 revert: all, date, rev, no-backup, interactive, include, exclude, dry-run
311 311 rollback: dry-run, force
312 312 root:
313 313 tag: force, local, rev, remove, edit, message, date, user
314 314 tags: template
315 315 tip: patch, git, style, template
316 316 unbundle: update
317 317 verify:
318 318 version: template
319 319
320 320 $ hg init a
321 321 $ cd a
322 322 $ echo fee > fee
323 323 $ hg ci -q -Amfee
324 324 $ hg tag fee
325 325 $ mkdir fie
326 326 $ echo dead > fie/dead
327 327 $ echo live > fie/live
328 328 $ hg bookmark fo
329 329 $ hg branch -q fie
330 330 $ hg ci -q -Amfie
331 331 $ echo fo > fo
332 332 $ hg branch -qf default
333 333 $ hg ci -q -Amfo
334 334 $ echo Fum > Fum
335 335 $ hg ci -q -AmFum
336 336 $ hg bookmark Fum
337 337
338 338 Test debugpathcomplete
339 339
340 340 $ hg debugpathcomplete f
341 341 fee
342 342 fie
343 343 fo
344 344 $ hg debugpathcomplete -f f
345 345 fee
346 346 fie/dead
347 347 fie/live
348 348 fo
349 349
350 350 $ hg rm Fum
351 351 $ hg debugpathcomplete -r F
352 352 Fum
353 353
354 354 Test debugnamecomplete
355 355
356 356 $ hg debugnamecomplete
357 357 Fum
358 358 default
359 359 fee
360 360 fie
361 361 fo
362 362 tip
363 363 $ hg debugnamecomplete f
364 364 fee
365 365 fie
366 366 fo
367 367
368 368 Test debuglabelcomplete, a deprecated name for debugnamecomplete that is still
369 369 used for completions in some shells.
370 370
371 371 $ hg debuglabelcomplete
372 372 Fum
373 373 default
374 374 fee
375 375 fie
376 376 fo
377 377 tip
378 378 $ hg debuglabelcomplete f
379 379 fee
380 380 fie
381 381 fo
@@ -1,936 +1,936 b''
1 1 commit hooks can see env vars
2 2 (and post-transaction one are run unlocked)
3 3
4 4
5 5 $ cat > $TESTTMP/txnabort.checkargs.py <<EOF
6 6 > def showargs(ui, repo, hooktype, **kwargs):
7 7 > ui.write('%s python hook: %s\n' % (hooktype, ','.join(sorted(kwargs))))
8 8 > EOF
9 9
10 10 $ hg init a
11 11 $ cd a
12 12 $ cat > .hg/hgrc <<EOF
13 13 > [hooks]
14 14 > commit = sh -c "HG_LOCAL= HG_TAG= printenv.py commit"
15 15 > commit.b = sh -c "HG_LOCAL= HG_TAG= printenv.py commit.b"
16 16 > precommit = sh -c "HG_LOCAL= HG_NODE= HG_TAG= printenv.py precommit"
17 17 > pretxncommit = sh -c "HG_LOCAL= HG_TAG= printenv.py pretxncommit"
18 18 > pretxncommit.tip = hg -q tip
19 19 > pre-identify = sh -c "printenv.py pre-identify 1"
20 20 > pre-cat = sh -c "printenv.py pre-cat"
21 21 > post-cat = sh -c "printenv.py post-cat"
22 22 > pretxnopen = sh -c "HG_LOCAL= HG_TAG= printenv.py pretxnopen"
23 23 > pretxnclose = sh -c "HG_LOCAL= HG_TAG= printenv.py pretxnclose"
24 24 > txnclose = sh -c "HG_LOCAL= HG_TAG= printenv.py txnclose"
25 25 > txnabort.0 = python:$TESTTMP/txnabort.checkargs.py:showargs
26 26 > txnabort.1 = sh -c "HG_LOCAL= HG_TAG= printenv.py txnabort"
27 27 > txnclose.checklock = sh -c "hg debuglock > /dev/null"
28 28 > EOF
29 29 $ echo a > a
30 30 $ hg add a
31 31 $ hg commit -m a
32 32 precommit hook: HG_HOOKNAME=precommit HG_HOOKTYPE=precommit HG_PARENT1=0000000000000000000000000000000000000000
33 33 pretxnopen hook: HG_HOOKNAME=pretxnopen HG_HOOKTYPE=pretxnopen HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
34 34 pretxncommit hook: HG_HOOKNAME=pretxncommit HG_HOOKTYPE=pretxncommit HG_NODE=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b HG_PARENT1=0000000000000000000000000000000000000000 HG_PENDING=$TESTTMP/a
35 35 0:cb9a9f314b8b
36 36 pretxnclose hook: HG_HOOKNAME=pretxnclose HG_HOOKTYPE=pretxnclose HG_PENDING=$TESTTMP/a HG_PHASES_MOVED=1 HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
37 37 txnclose hook: HG_HOOKNAME=txnclose HG_HOOKTYPE=txnclose HG_PHASES_MOVED=1 HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
38 38 commit hook: HG_HOOKNAME=commit HG_HOOKTYPE=commit HG_NODE=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b HG_PARENT1=0000000000000000000000000000000000000000
39 39 commit.b hook: HG_HOOKNAME=commit.b HG_HOOKTYPE=commit HG_NODE=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b HG_PARENT1=0000000000000000000000000000000000000000
40 40
41 41 $ hg clone . ../b
42 42 updating to branch default
43 43 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
44 44 $ cd ../b
45 45
46 46 changegroup hooks can see env vars
47 47
48 48 $ cat > .hg/hgrc <<EOF
49 49 > [hooks]
50 50 > prechangegroup = sh -c "printenv.py prechangegroup"
51 51 > changegroup = sh -c "printenv.py changegroup"
52 52 > incoming = sh -c "printenv.py incoming"
53 53 > EOF
54 54
55 55 pretxncommit and commit hooks can see both parents of merge
56 56
57 57 $ cd ../a
58 58 $ echo b >> a
59 59 $ hg commit -m a1 -d "1 0"
60 60 precommit hook: HG_HOOKNAME=precommit HG_HOOKTYPE=precommit HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
61 61 pretxnopen hook: HG_HOOKNAME=pretxnopen HG_HOOKTYPE=pretxnopen HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
62 62 pretxncommit hook: HG_HOOKNAME=pretxncommit HG_HOOKTYPE=pretxncommit HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b HG_PENDING=$TESTTMP/a
63 63 1:ab228980c14d
64 64 pretxnclose hook: HG_HOOKNAME=pretxnclose HG_HOOKTYPE=pretxnclose HG_PENDING=$TESTTMP/a HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
65 65 txnclose hook: HG_HOOKNAME=txnclose HG_HOOKTYPE=txnclose HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
66 66 commit hook: HG_HOOKNAME=commit HG_HOOKTYPE=commit HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
67 67 commit.b hook: HG_HOOKNAME=commit.b HG_HOOKTYPE=commit HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
68 68 $ hg update -C 0
69 69 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
70 70 $ echo b > b
71 71 $ hg add b
72 72 $ hg commit -m b -d '1 0'
73 73 precommit hook: HG_HOOKNAME=precommit HG_HOOKTYPE=precommit HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
74 74 pretxnopen hook: HG_HOOKNAME=pretxnopen HG_HOOKTYPE=pretxnopen HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
75 75 pretxncommit hook: HG_HOOKNAME=pretxncommit HG_HOOKTYPE=pretxncommit HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b HG_PENDING=$TESTTMP/a
76 76 2:ee9deb46ab31
77 77 pretxnclose hook: HG_HOOKNAME=pretxnclose HG_HOOKTYPE=pretxnclose HG_PENDING=$TESTTMP/a HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
78 78 created new head
79 79 txnclose hook: HG_HOOKNAME=txnclose HG_HOOKTYPE=txnclose HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
80 80 commit hook: HG_HOOKNAME=commit HG_HOOKTYPE=commit HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
81 81 commit.b hook: HG_HOOKNAME=commit.b HG_HOOKTYPE=commit HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
82 82 $ hg merge 1
83 83 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
84 84 (branch merge, don't forget to commit)
85 85 $ hg commit -m merge -d '2 0'
86 86 precommit hook: HG_HOOKNAME=precommit HG_HOOKTYPE=precommit HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd
87 87 pretxnopen hook: HG_HOOKNAME=pretxnopen HG_HOOKTYPE=pretxnopen HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
88 88 pretxncommit hook: HG_HOOKNAME=pretxncommit HG_HOOKTYPE=pretxncommit HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd HG_PENDING=$TESTTMP/a
89 89 3:07f3376c1e65
90 90 pretxnclose hook: HG_HOOKNAME=pretxnclose HG_HOOKTYPE=pretxnclose HG_PENDING=$TESTTMP/a HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
91 91 txnclose hook: HG_HOOKNAME=txnclose HG_HOOKTYPE=txnclose HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
92 92 commit hook: HG_HOOKNAME=commit HG_HOOKTYPE=commit HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd
93 93 commit.b hook: HG_HOOKNAME=commit.b HG_HOOKTYPE=commit HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd
94 94
95 95 test generic hooks
96 96
97 97 $ hg id
98 98 pre-identify hook: HG_ARGS=id HG_HOOKNAME=pre-identify HG_HOOKTYPE=pre-identify HG_OPTS={'bookmarks': None, 'branch': None, 'id': None, 'insecure': None, 'num': None, 'remotecmd': '', 'rev': '', 'ssh': '', 'tags': None} HG_PATS=[]
99 99 abort: pre-identify hook exited with status 1
100 100 [255]
101 101 $ hg cat b
102 pre-cat hook: HG_ARGS=cat b HG_HOOKNAME=pre-cat HG_HOOKTYPE=pre-cat HG_OPTS={'decode': None, 'exclude': [], 'include': [], 'output': '', 'rev': ''} HG_PATS=['b']
102 pre-cat hook: HG_ARGS=cat b HG_HOOKNAME=pre-cat HG_HOOKTYPE=pre-cat HG_OPTS={'decode': None, 'exclude': [], 'include': [], 'output': '', 'rev': '', 'template': ''} HG_PATS=['b']
103 103 b
104 post-cat hook: HG_ARGS=cat b HG_HOOKNAME=post-cat HG_HOOKTYPE=post-cat HG_OPTS={'decode': None, 'exclude': [], 'include': [], 'output': '', 'rev': ''} HG_PATS=['b'] HG_RESULT=0
104 post-cat hook: HG_ARGS=cat b HG_HOOKNAME=post-cat HG_HOOKTYPE=post-cat HG_OPTS={'decode': None, 'exclude': [], 'include': [], 'output': '', 'rev': '', 'template': ''} HG_PATS=['b'] HG_RESULT=0
105 105
106 106 $ cd ../b
107 107 $ hg pull ../a
108 108 pulling from ../a
109 109 searching for changes
110 110 prechangegroup hook: HG_HOOKNAME=prechangegroup HG_HOOKTYPE=prechangegroup HG_SOURCE=pull HG_TXNID=TXN:$ID$ HG_URL=file:$TESTTMP/a
111 111 adding changesets
112 112 adding manifests
113 113 adding file changes
114 114 added 3 changesets with 2 changes to 2 files
115 115 changegroup hook: HG_HOOKNAME=changegroup HG_HOOKTYPE=changegroup HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd HG_NODE_LAST=07f3376c1e655977439df2a814e3cc14b27abac2 HG_SOURCE=pull HG_TXNID=TXN:$ID$ HG_URL=file:$TESTTMP/a
116 116 incoming hook: HG_HOOKNAME=incoming HG_HOOKTYPE=incoming HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd HG_SOURCE=pull HG_TXNID=TXN:$ID$ HG_URL=file:$TESTTMP/a
117 117 incoming hook: HG_HOOKNAME=incoming HG_HOOKTYPE=incoming HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_SOURCE=pull HG_TXNID=TXN:$ID$ HG_URL=file:$TESTTMP/a
118 118 incoming hook: HG_HOOKNAME=incoming HG_HOOKTYPE=incoming HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_SOURCE=pull HG_TXNID=TXN:$ID$ HG_URL=file:$TESTTMP/a
119 119 (run 'hg update' to get a working copy)
120 120
121 121 tag hooks can see env vars
122 122
123 123 $ cd ../a
124 124 $ cat >> .hg/hgrc <<EOF
125 125 > pretag = sh -c "printenv.py pretag"
126 126 > tag = sh -c "HG_PARENT1= HG_PARENT2= printenv.py tag"
127 127 > EOF
128 128 $ hg tag -d '3 0' a
129 129 pretag hook: HG_HOOKNAME=pretag HG_HOOKTYPE=pretag HG_LOCAL=0 HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_TAG=a
130 130 precommit hook: HG_HOOKNAME=precommit HG_HOOKTYPE=precommit HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2
131 131 pretxnopen hook: HG_HOOKNAME=pretxnopen HG_HOOKTYPE=pretxnopen HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
132 132 pretxncommit hook: HG_HOOKNAME=pretxncommit HG_HOOKTYPE=pretxncommit HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2 HG_PENDING=$TESTTMP/a
133 133 4:539e4b31b6dc
134 134 pretxnclose hook: HG_HOOKNAME=pretxnclose HG_HOOKTYPE=pretxnclose HG_PENDING=$TESTTMP/a HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
135 135 tag hook: HG_HOOKNAME=tag HG_HOOKTYPE=tag HG_LOCAL=0 HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_TAG=a
136 136 txnclose hook: HG_HOOKNAME=txnclose HG_HOOKTYPE=txnclose HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
137 137 commit hook: HG_HOOKNAME=commit HG_HOOKTYPE=commit HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2
138 138 commit.b hook: HG_HOOKNAME=commit.b HG_HOOKTYPE=commit HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2
139 139 $ hg tag -l la
140 140 pretag hook: HG_HOOKNAME=pretag HG_HOOKTYPE=pretag HG_LOCAL=1 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_TAG=la
141 141 tag hook: HG_HOOKNAME=tag HG_HOOKTYPE=tag HG_LOCAL=1 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_TAG=la
142 142
143 143 pretag hook can forbid tagging
144 144
145 145 $ cat >> .hg/hgrc <<EOF
146 146 > pretag.forbid = sh -c "printenv.py pretag.forbid 1"
147 147 > EOF
148 148 $ hg tag -d '4 0' fa
149 149 pretag hook: HG_HOOKNAME=pretag HG_HOOKTYPE=pretag HG_LOCAL=0 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_TAG=fa
150 150 pretag.forbid hook: HG_HOOKNAME=pretag.forbid HG_HOOKTYPE=pretag HG_LOCAL=0 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_TAG=fa
151 151 abort: pretag.forbid hook exited with status 1
152 152 [255]
153 153 $ hg tag -l fla
154 154 pretag hook: HG_HOOKNAME=pretag HG_HOOKTYPE=pretag HG_LOCAL=1 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_TAG=fla
155 155 pretag.forbid hook: HG_HOOKNAME=pretag.forbid HG_HOOKTYPE=pretag HG_LOCAL=1 HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_TAG=fla
156 156 abort: pretag.forbid hook exited with status 1
157 157 [255]
158 158
159 159 pretxncommit hook can see changeset, can roll back txn, changeset no
160 160 more there after
161 161
162 162 $ cat >> .hg/hgrc <<EOF
163 163 > pretxncommit.forbid0 = sh -c "hg tip -q"
164 164 > pretxncommit.forbid1 = sh -c "printenv.py pretxncommit.forbid 1"
165 165 > EOF
166 166 $ echo z > z
167 167 $ hg add z
168 168 $ hg -q tip
169 169 4:539e4b31b6dc
170 170 $ hg commit -m 'fail' -d '4 0'
171 171 precommit hook: HG_HOOKNAME=precommit HG_HOOKTYPE=precommit HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
172 172 pretxnopen hook: HG_HOOKNAME=pretxnopen HG_HOOKTYPE=pretxnopen HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
173 173 pretxncommit hook: HG_HOOKNAME=pretxncommit HG_HOOKTYPE=pretxncommit HG_NODE=6f611f8018c10e827fee6bd2bc807f937e761567 HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PENDING=$TESTTMP/a
174 174 5:6f611f8018c1
175 175 5:6f611f8018c1
176 176 pretxncommit.forbid hook: HG_HOOKNAME=pretxncommit.forbid1 HG_HOOKTYPE=pretxncommit HG_NODE=6f611f8018c10e827fee6bd2bc807f937e761567 HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PENDING=$TESTTMP/a
177 177 transaction abort!
178 178 txnabort python hook: txnid,txnname
179 179 txnabort hook: HG_HOOKNAME=txnabort.1 HG_HOOKTYPE=txnabort HG_TXNID=TXN:$ID$ HG_TXNNAME=commit
180 180 rollback completed
181 181 abort: pretxncommit.forbid1 hook exited with status 1
182 182 [255]
183 183 $ hg -q tip
184 184 4:539e4b31b6dc
185 185
186 186 (Check that no 'changelog.i.a' file were left behind)
187 187
188 188 $ ls -1 .hg/store/
189 189 00changelog.i
190 190 00manifest.i
191 191 data
192 192 fncache
193 193 journal.phaseroots
194 194 phaseroots
195 195 undo
196 196 undo.backup.fncache
197 197 undo.backupfiles
198 198 undo.phaseroots
199 199
200 200
201 201 precommit hook can prevent commit
202 202
203 203 $ cat >> .hg/hgrc <<EOF
204 204 > precommit.forbid = sh -c "printenv.py precommit.forbid 1"
205 205 > EOF
206 206 $ hg commit -m 'fail' -d '4 0'
207 207 precommit hook: HG_HOOKNAME=precommit HG_HOOKTYPE=precommit HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
208 208 precommit.forbid hook: HG_HOOKNAME=precommit.forbid HG_HOOKTYPE=precommit HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10
209 209 abort: precommit.forbid hook exited with status 1
210 210 [255]
211 211 $ hg -q tip
212 212 4:539e4b31b6dc
213 213
214 214 preupdate hook can prevent update
215 215
216 216 $ cat >> .hg/hgrc <<EOF
217 217 > preupdate = sh -c "printenv.py preupdate"
218 218 > EOF
219 219 $ hg update 1
220 220 preupdate hook: HG_HOOKNAME=preupdate HG_HOOKTYPE=preupdate HG_PARENT1=ab228980c14d
221 221 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
222 222
223 223 update hook
224 224
225 225 $ cat >> .hg/hgrc <<EOF
226 226 > update = sh -c "printenv.py update"
227 227 > EOF
228 228 $ hg update
229 229 preupdate hook: HG_HOOKNAME=preupdate HG_HOOKTYPE=preupdate HG_PARENT1=539e4b31b6dc
230 230 update hook: HG_ERROR=0 HG_HOOKNAME=update HG_HOOKTYPE=update HG_PARENT1=539e4b31b6dc
231 231 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
232 232
233 233 pushkey hook
234 234
235 235 $ cat >> .hg/hgrc <<EOF
236 236 > pushkey = sh -c "printenv.py pushkey"
237 237 > EOF
238 238 $ cd ../b
239 239 $ hg bookmark -r null foo
240 240 $ hg push -B foo ../a
241 241 pushing to ../a
242 242 searching for changes
243 243 no changes found
244 244 pretxnopen hook: HG_HOOKNAME=pretxnopen HG_HOOKTYPE=pretxnopen HG_TXNID=TXN:$ID$ HG_TXNNAME=push
245 245 pretxnclose hook: HG_BOOKMARK_MOVED=1 HG_BUNDLE2=1 HG_HOOKNAME=pretxnclose HG_HOOKTYPE=pretxnclose HG_PENDING=$TESTTMP/a HG_SOURCE=push HG_TXNID=TXN:$ID$ HG_TXNNAME=push HG_URL=file:$TESTTMP/a
246 246 pushkey hook: HG_HOOKNAME=pushkey HG_HOOKTYPE=pushkey HG_KEY=foo HG_NAMESPACE=bookmarks HG_NEW=0000000000000000000000000000000000000000 HG_RET=1
247 247 txnclose hook: HG_BOOKMARK_MOVED=1 HG_BUNDLE2=1 HG_HOOKNAME=txnclose HG_HOOKTYPE=txnclose HG_SOURCE=push HG_TXNID=TXN:$ID$ HG_TXNNAME=push HG_URL=file:$TESTTMP/a
248 248 exporting bookmark foo
249 249 [1]
250 250 $ cd ../a
251 251
252 252 listkeys hook
253 253
254 254 $ cat >> .hg/hgrc <<EOF
255 255 > listkeys = sh -c "printenv.py listkeys"
256 256 > EOF
257 257 $ hg bookmark -r null bar
258 258 pretxnopen hook: HG_HOOKNAME=pretxnopen HG_HOOKTYPE=pretxnopen HG_TXNID=TXN:$ID$ HG_TXNNAME=bookmark
259 259 pretxnclose hook: HG_BOOKMARK_MOVED=1 HG_HOOKNAME=pretxnclose HG_HOOKTYPE=pretxnclose HG_PENDING=$TESTTMP/a HG_TXNID=TXN:$ID$ HG_TXNNAME=bookmark
260 260 txnclose hook: HG_BOOKMARK_MOVED=1 HG_HOOKNAME=txnclose HG_HOOKTYPE=txnclose HG_TXNID=TXN:$ID$ HG_TXNNAME=bookmark
261 261 $ cd ../b
262 262 $ hg pull -B bar ../a
263 263 pulling from ../a
264 264 listkeys hook: HG_HOOKNAME=listkeys HG_HOOKTYPE=listkeys HG_NAMESPACE=bookmarks HG_VALUES={'bar': '0000000000000000000000000000000000000000', 'foo': '0000000000000000000000000000000000000000'}
265 265 no changes found
266 266 listkeys hook: HG_HOOKNAME=listkeys HG_HOOKTYPE=listkeys HG_NAMESPACE=phases HG_VALUES={'cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b': '1', 'publishing': 'True'}
267 267 adding remote bookmark bar
268 268 $ cd ../a
269 269
270 270 test that prepushkey can prevent incoming keys
271 271
272 272 $ cat >> .hg/hgrc <<EOF
273 273 > prepushkey = sh -c "printenv.py prepushkey.forbid 1"
274 274 > EOF
275 275 $ cd ../b
276 276 $ hg bookmark -r null baz
277 277 $ hg push -B baz ../a
278 278 pushing to ../a
279 279 searching for changes
280 280 listkeys hook: HG_HOOKNAME=listkeys HG_HOOKTYPE=listkeys HG_NAMESPACE=phases HG_VALUES={'cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b': '1', 'publishing': 'True'}
281 281 listkeys hook: HG_HOOKNAME=listkeys HG_HOOKTYPE=listkeys HG_NAMESPACE=bookmarks HG_VALUES={'bar': '0000000000000000000000000000000000000000', 'foo': '0000000000000000000000000000000000000000'}
282 282 no changes found
283 283 pretxnopen hook: HG_HOOKNAME=pretxnopen HG_HOOKTYPE=pretxnopen HG_TXNID=TXN:$ID$ HG_TXNNAME=push
284 284 prepushkey.forbid hook: HG_BUNDLE2=1 HG_HOOKNAME=prepushkey HG_HOOKTYPE=prepushkey HG_KEY=baz HG_NAMESPACE=bookmarks HG_NEW=0000000000000000000000000000000000000000 HG_SOURCE=push HG_TXNID=TXN:$ID$ HG_URL=file:$TESTTMP/a
285 285 pushkey-abort: prepushkey hook exited with status 1
286 286 abort: exporting bookmark baz failed!
287 287 [255]
288 288 $ cd ../a
289 289
290 290 test that prelistkeys can prevent listing keys
291 291
292 292 $ cat >> .hg/hgrc <<EOF
293 293 > prelistkeys = sh -c "printenv.py prelistkeys.forbid 1"
294 294 > EOF
295 295 $ hg bookmark -r null quux
296 296 pretxnopen hook: HG_HOOKNAME=pretxnopen HG_HOOKTYPE=pretxnopen HG_TXNID=TXN:$ID$ HG_TXNNAME=bookmark
297 297 pretxnclose hook: HG_BOOKMARK_MOVED=1 HG_HOOKNAME=pretxnclose HG_HOOKTYPE=pretxnclose HG_PENDING=$TESTTMP/a HG_TXNID=TXN:$ID$ HG_TXNNAME=bookmark
298 298 txnclose hook: HG_BOOKMARK_MOVED=1 HG_HOOKNAME=txnclose HG_HOOKTYPE=txnclose HG_TXNID=TXN:$ID$ HG_TXNNAME=bookmark
299 299 $ cd ../b
300 300 $ hg pull -B quux ../a
301 301 pulling from ../a
302 302 prelistkeys.forbid hook: HG_HOOKNAME=prelistkeys HG_HOOKTYPE=prelistkeys HG_NAMESPACE=bookmarks
303 303 abort: prelistkeys hook exited with status 1
304 304 [255]
305 305 $ cd ../a
306 306 $ rm .hg/hgrc
307 307
308 308 prechangegroup hook can prevent incoming changes
309 309
310 310 $ cd ../b
311 311 $ hg -q tip
312 312 3:07f3376c1e65
313 313 $ cat > .hg/hgrc <<EOF
314 314 > [hooks]
315 315 > prechangegroup.forbid = sh -c "printenv.py prechangegroup.forbid 1"
316 316 > EOF
317 317 $ hg pull ../a
318 318 pulling from ../a
319 319 searching for changes
320 320 prechangegroup.forbid hook: HG_HOOKNAME=prechangegroup.forbid HG_HOOKTYPE=prechangegroup HG_SOURCE=pull HG_TXNID=TXN:$ID$ HG_URL=file:$TESTTMP/a
321 321 abort: prechangegroup.forbid hook exited with status 1
322 322 [255]
323 323
324 324 pretxnchangegroup hook can see incoming changes, can roll back txn,
325 325 incoming changes no longer there after
326 326
327 327 $ cat > .hg/hgrc <<EOF
328 328 > [hooks]
329 329 > pretxnchangegroup.forbid0 = hg tip -q
330 330 > pretxnchangegroup.forbid1 = sh -c "printenv.py pretxnchangegroup.forbid 1"
331 331 > EOF
332 332 $ hg pull ../a
333 333 pulling from ../a
334 334 searching for changes
335 335 adding changesets
336 336 adding manifests
337 337 adding file changes
338 338 added 1 changesets with 1 changes to 1 files
339 339 4:539e4b31b6dc
340 340 pretxnchangegroup.forbid hook: HG_HOOKNAME=pretxnchangegroup.forbid1 HG_HOOKTYPE=pretxnchangegroup HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_NODE_LAST=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PENDING=$TESTTMP/b HG_SOURCE=pull HG_TXNID=TXN:$ID$ HG_URL=file:$TESTTMP/a
341 341 transaction abort!
342 342 rollback completed
343 343 abort: pretxnchangegroup.forbid1 hook exited with status 1
344 344 [255]
345 345 $ hg -q tip
346 346 3:07f3376c1e65
347 347
348 348 outgoing hooks can see env vars
349 349
350 350 $ rm .hg/hgrc
351 351 $ cat > ../a/.hg/hgrc <<EOF
352 352 > [hooks]
353 353 > preoutgoing = sh -c "printenv.py preoutgoing"
354 354 > outgoing = sh -c "printenv.py outgoing"
355 355 > EOF
356 356 $ hg pull ../a
357 357 pulling from ../a
358 358 searching for changes
359 359 preoutgoing hook: HG_HOOKNAME=preoutgoing HG_HOOKTYPE=preoutgoing HG_SOURCE=pull
360 360 outgoing hook: HG_HOOKNAME=outgoing HG_HOOKTYPE=outgoing HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_SOURCE=pull
361 361 adding changesets
362 362 adding manifests
363 363 adding file changes
364 364 added 1 changesets with 1 changes to 1 files
365 365 adding remote bookmark quux
366 366 (run 'hg update' to get a working copy)
367 367 $ hg rollback
368 368 repository tip rolled back to revision 3 (undo pull)
369 369
370 370 preoutgoing hook can prevent outgoing changes
371 371
372 372 $ cat >> ../a/.hg/hgrc <<EOF
373 373 > preoutgoing.forbid = sh -c "printenv.py preoutgoing.forbid 1"
374 374 > EOF
375 375 $ hg pull ../a
376 376 pulling from ../a
377 377 searching for changes
378 378 preoutgoing hook: HG_HOOKNAME=preoutgoing HG_HOOKTYPE=preoutgoing HG_SOURCE=pull
379 379 preoutgoing.forbid hook: HG_HOOKNAME=preoutgoing.forbid HG_HOOKTYPE=preoutgoing HG_SOURCE=pull
380 380 abort: preoutgoing.forbid hook exited with status 1
381 381 [255]
382 382
383 383 outgoing hooks work for local clones
384 384
385 385 $ cd ..
386 386 $ cat > a/.hg/hgrc <<EOF
387 387 > [hooks]
388 388 > preoutgoing = sh -c "printenv.py preoutgoing"
389 389 > outgoing = sh -c "printenv.py outgoing"
390 390 > EOF
391 391 $ hg clone a c
392 392 preoutgoing hook: HG_HOOKNAME=preoutgoing HG_HOOKTYPE=preoutgoing HG_SOURCE=clone
393 393 outgoing hook: HG_HOOKNAME=outgoing HG_HOOKTYPE=outgoing HG_NODE=0000000000000000000000000000000000000000 HG_SOURCE=clone
394 394 updating to branch default
395 395 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
396 396 $ rm -rf c
397 397
398 398 preoutgoing hook can prevent outgoing changes for local clones
399 399
400 400 $ cat >> a/.hg/hgrc <<EOF
401 401 > preoutgoing.forbid = sh -c "printenv.py preoutgoing.forbid 1"
402 402 > EOF
403 403 $ hg clone a zzz
404 404 preoutgoing hook: HG_HOOKNAME=preoutgoing HG_HOOKTYPE=preoutgoing HG_SOURCE=clone
405 405 preoutgoing.forbid hook: HG_HOOKNAME=preoutgoing.forbid HG_HOOKTYPE=preoutgoing HG_SOURCE=clone
406 406 abort: preoutgoing.forbid hook exited with status 1
407 407 [255]
408 408
409 409 $ cd "$TESTTMP/b"
410 410
411 411 $ cat > hooktests.py <<EOF
412 412 > from mercurial import error
413 413 >
414 414 > uncallable = 0
415 415 >
416 416 > def printargs(args):
417 417 > args.pop('ui', None)
418 418 > args.pop('repo', None)
419 419 > a = list(args.items())
420 420 > a.sort()
421 421 > print 'hook args:'
422 422 > for k, v in a:
423 423 > print ' ', k, v
424 424 >
425 425 > def passhook(**args):
426 426 > printargs(args)
427 427 >
428 428 > def failhook(**args):
429 429 > printargs(args)
430 430 > return True
431 431 >
432 432 > class LocalException(Exception):
433 433 > pass
434 434 >
435 435 > def raisehook(**args):
436 436 > raise LocalException('exception from hook')
437 437 >
438 438 > def aborthook(**args):
439 439 > raise error.Abort('raise abort from hook')
440 440 >
441 441 > def brokenhook(**args):
442 442 > return 1 + {}
443 443 >
444 444 > def verbosehook(ui, **args):
445 445 > ui.note('verbose output from hook\n')
446 446 >
447 447 > def printtags(ui, repo, **args):
448 448 > print sorted(repo.tags())
449 449 >
450 450 > class container:
451 451 > unreachable = 1
452 452 > EOF
453 453
454 454 $ cat > syntaxerror.py << EOF
455 455 > (foo
456 456 > EOF
457 457
458 458 test python hooks
459 459
460 460 #if windows
461 461 $ PYTHONPATH="$TESTTMP/b;$PYTHONPATH"
462 462 #else
463 463 $ PYTHONPATH="$TESTTMP/b:$PYTHONPATH"
464 464 #endif
465 465 $ export PYTHONPATH
466 466
467 467 $ echo '[hooks]' > ../a/.hg/hgrc
468 468 $ echo 'preoutgoing.broken = python:hooktests.brokenhook' >> ../a/.hg/hgrc
469 469 $ hg pull ../a 2>&1 | grep 'raised an exception'
470 470 error: preoutgoing.broken hook raised an exception: unsupported operand type(s) for +: 'int' and 'dict'
471 471
472 472 $ echo '[hooks]' > ../a/.hg/hgrc
473 473 $ echo 'preoutgoing.raise = python:hooktests.raisehook' >> ../a/.hg/hgrc
474 474 $ hg pull ../a 2>&1 | grep 'raised an exception'
475 475 error: preoutgoing.raise hook raised an exception: exception from hook
476 476
477 477 $ echo '[hooks]' > ../a/.hg/hgrc
478 478 $ echo 'preoutgoing.abort = python:hooktests.aborthook' >> ../a/.hg/hgrc
479 479 $ hg pull ../a
480 480 pulling from ../a
481 481 searching for changes
482 482 error: preoutgoing.abort hook failed: raise abort from hook
483 483 abort: raise abort from hook
484 484 [255]
485 485
486 486 $ echo '[hooks]' > ../a/.hg/hgrc
487 487 $ echo 'preoutgoing.fail = python:hooktests.failhook' >> ../a/.hg/hgrc
488 488 $ hg pull ../a
489 489 pulling from ../a
490 490 searching for changes
491 491 hook args:
492 492 hooktype preoutgoing
493 493 source pull
494 494 abort: preoutgoing.fail hook failed
495 495 [255]
496 496
497 497 $ echo '[hooks]' > ../a/.hg/hgrc
498 498 $ echo 'preoutgoing.uncallable = python:hooktests.uncallable' >> ../a/.hg/hgrc
499 499 $ hg pull ../a
500 500 pulling from ../a
501 501 searching for changes
502 502 abort: preoutgoing.uncallable hook is invalid: "hooktests.uncallable" is not callable
503 503 [255]
504 504
505 505 $ echo '[hooks]' > ../a/.hg/hgrc
506 506 $ echo 'preoutgoing.nohook = python:hooktests.nohook' >> ../a/.hg/hgrc
507 507 $ hg pull ../a
508 508 pulling from ../a
509 509 searching for changes
510 510 abort: preoutgoing.nohook hook is invalid: "hooktests.nohook" is not defined
511 511 [255]
512 512
513 513 $ echo '[hooks]' > ../a/.hg/hgrc
514 514 $ echo 'preoutgoing.nomodule = python:nomodule' >> ../a/.hg/hgrc
515 515 $ hg pull ../a
516 516 pulling from ../a
517 517 searching for changes
518 518 abort: preoutgoing.nomodule hook is invalid: "nomodule" not in a module
519 519 [255]
520 520
521 521 $ echo '[hooks]' > ../a/.hg/hgrc
522 522 $ echo 'preoutgoing.badmodule = python:nomodule.nowhere' >> ../a/.hg/hgrc
523 523 $ hg pull ../a
524 524 pulling from ../a
525 525 searching for changes
526 526 abort: preoutgoing.badmodule hook is invalid: import of "nomodule" failed
527 527 (run with --traceback for stack trace)
528 528 [255]
529 529
530 530 $ echo '[hooks]' > ../a/.hg/hgrc
531 531 $ echo 'preoutgoing.unreachable = python:hooktests.container.unreachable' >> ../a/.hg/hgrc
532 532 $ hg pull ../a
533 533 pulling from ../a
534 534 searching for changes
535 535 abort: preoutgoing.unreachable hook is invalid: import of "hooktests.container" failed
536 536 (run with --traceback for stack trace)
537 537 [255]
538 538
539 539 $ echo '[hooks]' > ../a/.hg/hgrc
540 540 $ echo 'preoutgoing.syntaxerror = python:syntaxerror.syntaxerror' >> ../a/.hg/hgrc
541 541 $ hg pull ../a
542 542 pulling from ../a
543 543 searching for changes
544 544 abort: preoutgoing.syntaxerror hook is invalid: import of "syntaxerror" failed
545 545 (run with --traceback for stack trace)
546 546 [255]
547 547
548 548 The second egrep is to filter out lines like ' ^', which are slightly
549 549 different between Python 2.6 and Python 2.7.
550 550 $ hg pull ../a --traceback 2>&1 | egrep -v '^( +File| [_a-zA-Z*(])' | egrep -v '^( )+(\^)?$'
551 551 pulling from ../a
552 552 searching for changes
553 553 exception from first failed import attempt:
554 554 Traceback (most recent call last):
555 555 SyntaxError: * (glob)
556 556 exception from second failed import attempt:
557 557 Traceback (most recent call last):
558 558 ImportError: No module named hgext_syntaxerror
559 559 Traceback (most recent call last):
560 560 HookLoadError: preoutgoing.syntaxerror hook is invalid: import of "syntaxerror" failed
561 561 abort: preoutgoing.syntaxerror hook is invalid: import of "syntaxerror" failed
562 562
563 563 $ echo '[hooks]' > ../a/.hg/hgrc
564 564 $ echo 'preoutgoing.pass = python:hooktests.passhook' >> ../a/.hg/hgrc
565 565 $ hg pull ../a
566 566 pulling from ../a
567 567 searching for changes
568 568 hook args:
569 569 hooktype preoutgoing
570 570 source pull
571 571 adding changesets
572 572 adding manifests
573 573 adding file changes
574 574 added 1 changesets with 1 changes to 1 files
575 575 adding remote bookmark quux
576 576 (run 'hg update' to get a working copy)
577 577
578 578 post- python hooks that fail to *run* don't cause an abort
579 579 $ rm ../a/.hg/hgrc
580 580 $ echo '[hooks]' > .hg/hgrc
581 581 $ echo 'post-pull.broken = python:hooktests.brokenhook' >> .hg/hgrc
582 582 $ hg pull ../a
583 583 pulling from ../a
584 584 searching for changes
585 585 no changes found
586 586 error: post-pull.broken hook raised an exception: unsupported operand type(s) for +: 'int' and 'dict'
587 587 (run with --traceback for stack trace)
588 588
589 589 but post- python hooks that fail to *load* do
590 590 $ echo '[hooks]' > .hg/hgrc
591 591 $ echo 'post-pull.nomodule = python:nomodule' >> .hg/hgrc
592 592 $ hg pull ../a
593 593 pulling from ../a
594 594 searching for changes
595 595 no changes found
596 596 abort: post-pull.nomodule hook is invalid: "nomodule" not in a module
597 597 [255]
598 598
599 599 $ echo '[hooks]' > .hg/hgrc
600 600 $ echo 'post-pull.badmodule = python:nomodule.nowhere' >> .hg/hgrc
601 601 $ hg pull ../a
602 602 pulling from ../a
603 603 searching for changes
604 604 no changes found
605 605 abort: post-pull.badmodule hook is invalid: import of "nomodule" failed
606 606 (run with --traceback for stack trace)
607 607 [255]
608 608
609 609 $ echo '[hooks]' > .hg/hgrc
610 610 $ echo 'post-pull.nohook = python:hooktests.nohook' >> .hg/hgrc
611 611 $ hg pull ../a
612 612 pulling from ../a
613 613 searching for changes
614 614 no changes found
615 615 abort: post-pull.nohook hook is invalid: "hooktests.nohook" is not defined
616 616 [255]
617 617
618 618 make sure --traceback works
619 619
620 620 $ echo '[hooks]' > .hg/hgrc
621 621 $ echo 'commit.abort = python:hooktests.aborthook' >> .hg/hgrc
622 622
623 623 $ echo aa > a
624 624 $ hg --traceback commit -d '0 0' -ma 2>&1 | grep '^Traceback'
625 625 Traceback (most recent call last):
626 626
627 627 $ cd ..
628 628 $ hg init c
629 629 $ cd c
630 630
631 631 $ cat > hookext.py <<EOF
632 632 > def autohook(**args):
633 633 > print "Automatically installed hook"
634 634 >
635 635 > def reposetup(ui, repo):
636 636 > repo.ui.setconfig("hooks", "commit.auto", autohook)
637 637 > EOF
638 638 $ echo '[extensions]' >> .hg/hgrc
639 639 $ echo 'hookext = hookext.py' >> .hg/hgrc
640 640
641 641 $ touch foo
642 642 $ hg add foo
643 643 $ hg ci -d '0 0' -m 'add foo'
644 644 Automatically installed hook
645 645 $ echo >> foo
646 646 $ hg ci --debug -d '0 0' -m 'change foo'
647 647 committing files:
648 648 foo
649 649 committing manifest
650 650 committing changelog
651 651 updating the branch cache
652 652 committed changeset 1:52998019f6252a2b893452765fcb0a47351a5708
653 653 calling hook commit.auto: hgext_hookext.autohook
654 654 Automatically installed hook
655 655
656 656 $ hg showconfig hooks
657 657 hooks.commit.auto=<function autohook at *> (glob)
658 658
659 659 test python hook configured with python:[file]:[hook] syntax
660 660
661 661 $ cd ..
662 662 $ mkdir d
663 663 $ cd d
664 664 $ hg init repo
665 665 $ mkdir hooks
666 666
667 667 $ cd hooks
668 668 $ cat > testhooks.py <<EOF
669 669 > def testhook(**args):
670 670 > print 'hook works'
671 671 > EOF
672 672 $ echo '[hooks]' > ../repo/.hg/hgrc
673 673 $ echo "pre-commit.test = python:`pwd`/testhooks.py:testhook" >> ../repo/.hg/hgrc
674 674
675 675 $ cd ../repo
676 676 $ hg commit -d '0 0'
677 677 hook works
678 678 nothing changed
679 679 [1]
680 680
681 681 $ echo '[hooks]' > .hg/hgrc
682 682 $ echo "update.ne = python:`pwd`/nonexistent.py:testhook" >> .hg/hgrc
683 683 $ echo "pre-identify.npmd = python:`pwd`/:no_python_module_dir" >> .hg/hgrc
684 684
685 685 $ hg up null
686 686 loading update.ne hook failed:
687 687 abort: No such file or directory: $TESTTMP/d/repo/nonexistent.py
688 688 [255]
689 689
690 690 $ hg id
691 691 loading pre-identify.npmd hook failed:
692 692 abort: No module named repo!
693 693 [255]
694 694
695 695 $ cd ../../b
696 696
697 697 make sure --traceback works on hook import failure
698 698
699 699 $ cat > importfail.py <<EOF
700 700 > import somebogusmodule
701 701 > # dereference something in the module to force demandimport to load it
702 702 > somebogusmodule.whatever
703 703 > EOF
704 704
705 705 $ echo '[hooks]' > .hg/hgrc
706 706 $ echo 'precommit.importfail = python:importfail.whatever' >> .hg/hgrc
707 707
708 708 $ echo a >> a
709 709 $ hg --traceback commit -ma 2>&1 | egrep -v '^( +File| [a-zA-Z(])'
710 710 exception from first failed import attempt:
711 711 Traceback (most recent call last):
712 712 ImportError: No module named somebogusmodule
713 713 exception from second failed import attempt:
714 714 Traceback (most recent call last):
715 715 ImportError: No module named hgext_importfail
716 716 Traceback (most recent call last):
717 717 HookLoadError: precommit.importfail hook is invalid: import of "importfail" failed
718 718 abort: precommit.importfail hook is invalid: import of "importfail" failed
719 719
720 720 Issue1827: Hooks Update & Commit not completely post operation
721 721
722 722 commit and update hooks should run after command completion. The largefiles
723 723 use demonstrates a recursive wlock, showing the hook doesn't run until the
724 724 final release (and dirstate flush).
725 725
726 726 $ echo '[hooks]' > .hg/hgrc
727 727 $ echo 'commit = hg id' >> .hg/hgrc
728 728 $ echo 'update = hg id' >> .hg/hgrc
729 729 $ echo bb > a
730 730 $ hg ci -ma
731 731 223eafe2750c tip
732 732 $ hg up 0 --config extensions.largefiles=
733 733 cb9a9f314b8b
734 734 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
735 735
736 736 make sure --verbose (and --quiet/--debug etc.) are propagated to the local ui
737 737 that is passed to pre/post hooks
738 738
739 739 $ echo '[hooks]' > .hg/hgrc
740 740 $ echo 'pre-identify = python:hooktests.verbosehook' >> .hg/hgrc
741 741 $ hg id
742 742 cb9a9f314b8b
743 743 $ hg id --verbose
744 744 calling hook pre-identify: hooktests.verbosehook
745 745 verbose output from hook
746 746 cb9a9f314b8b
747 747
748 748 Ensure hooks can be prioritized
749 749
750 750 $ echo '[hooks]' > .hg/hgrc
751 751 $ echo 'pre-identify.a = python:hooktests.verbosehook' >> .hg/hgrc
752 752 $ echo 'pre-identify.b = python:hooktests.verbosehook' >> .hg/hgrc
753 753 $ echo 'priority.pre-identify.b = 1' >> .hg/hgrc
754 754 $ echo 'pre-identify.c = python:hooktests.verbosehook' >> .hg/hgrc
755 755 $ hg id --verbose
756 756 calling hook pre-identify.b: hooktests.verbosehook
757 757 verbose output from hook
758 758 calling hook pre-identify.a: hooktests.verbosehook
759 759 verbose output from hook
760 760 calling hook pre-identify.c: hooktests.verbosehook
761 761 verbose output from hook
762 762 cb9a9f314b8b
763 763
764 764 new tags must be visible in pretxncommit (issue3210)
765 765
766 766 $ echo 'pretxncommit.printtags = python:hooktests.printtags' >> .hg/hgrc
767 767 $ hg tag -f foo
768 768 ['a', 'foo', 'tip']
769 769
770 770 post-init hooks must not crash (issue4983)
771 771 This also creates the `to` repo for the next test block.
772 772
773 773 $ cd ..
774 774 $ cat << EOF >> hgrc-with-post-init-hook
775 775 > [hooks]
776 776 > post-init = sh -c "printenv.py post-init"
777 777 > EOF
778 778 $ HGRCPATH=hgrc-with-post-init-hook hg init to
779 779 post-init hook: HG_ARGS=init to HG_HOOKNAME=post-init HG_HOOKTYPE=post-init HG_OPTS={'insecure': None, 'remotecmd': '', 'ssh': ''} HG_PATS=['to'] HG_RESULT=0
780 780
781 781 new commits must be visible in pretxnchangegroup (issue3428)
782 782
783 783 $ echo '[hooks]' >> to/.hg/hgrc
784 784 $ echo 'prechangegroup = hg --traceback tip' >> to/.hg/hgrc
785 785 $ echo 'pretxnchangegroup = hg --traceback tip' >> to/.hg/hgrc
786 786 $ echo a >> to/a
787 787 $ hg --cwd to ci -Ama
788 788 adding a
789 789 $ hg clone to from
790 790 updating to branch default
791 791 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
792 792 $ echo aa >> from/a
793 793 $ hg --cwd from ci -mb
794 794 $ hg --cwd from push
795 795 pushing to $TESTTMP/to (glob)
796 796 searching for changes
797 797 changeset: 0:cb9a9f314b8b
798 798 tag: tip
799 799 user: test
800 800 date: Thu Jan 01 00:00:00 1970 +0000
801 801 summary: a
802 802
803 803 adding changesets
804 804 adding manifests
805 805 adding file changes
806 806 added 1 changesets with 1 changes to 1 files
807 807 changeset: 1:9836a07b9b9d
808 808 tag: tip
809 809 user: test
810 810 date: Thu Jan 01 00:00:00 1970 +0000
811 811 summary: b
812 812
813 813
814 814 pretxnclose hook failure should abort the transaction
815 815
816 816 $ hg init txnfailure
817 817 $ cd txnfailure
818 818 $ touch a && hg commit -Aqm a
819 819 $ cat >> .hg/hgrc <<EOF
820 820 > [hooks]
821 821 > pretxnclose.error = exit 1
822 822 > EOF
823 823 $ hg strip -r 0 --config extensions.strip=
824 824 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
825 825 saved backup bundle to * (glob)
826 826 transaction abort!
827 827 rollback completed
828 828 strip failed, backup bundle stored in * (glob)
829 829 abort: pretxnclose.error hook exited with status 1
830 830 [255]
831 831 $ hg recover
832 832 no interrupted transaction available
833 833 [1]
834 834 $ cd ..
835 835
836 836 check whether HG_PENDING makes pending changes only in related
837 837 repositories visible to an external hook.
838 838
839 839 (emulate a transaction running concurrently by copied
840 840 .hg/store/00changelog.i.a in subsequent test)
841 841
842 842 $ cat > $TESTTMP/savepending.sh <<EOF
843 843 > cp .hg/store/00changelog.i.a .hg/store/00changelog.i.a.saved
844 844 > exit 1 # to avoid adding new revision for subsequent tests
845 845 > EOF
846 846 $ cd a
847 847 $ hg tip -q
848 848 4:539e4b31b6dc
849 849 $ hg --config hooks.pretxnclose="sh $TESTTMP/savepending.sh" commit -m "invisible"
850 850 transaction abort!
851 851 rollback completed
852 852 abort: pretxnclose hook exited with status 1
853 853 [255]
854 854 $ cp .hg/store/00changelog.i.a.saved .hg/store/00changelog.i.a
855 855
856 856 (check (in)visibility of new changeset while transaction running in
857 857 repo)
858 858
859 859 $ cat > $TESTTMP/checkpending.sh <<EOF
860 860 > echo '@a'
861 861 > hg -R "$TESTTMP/a" tip -q
862 862 > echo '@a/nested'
863 863 > hg -R "$TESTTMP/a/nested" tip -q
864 864 > exit 1 # to avoid adding new revision for subsequent tests
865 865 > EOF
866 866 $ hg init nested
867 867 $ cd nested
868 868 $ echo a > a
869 869 $ hg add a
870 870 $ hg --config hooks.pretxnclose="sh $TESTTMP/checkpending.sh" commit -m '#0'
871 871 @a
872 872 4:539e4b31b6dc
873 873 @a/nested
874 874 0:bf5e395ced2c
875 875 transaction abort!
876 876 rollback completed
877 877 abort: pretxnclose hook exited with status 1
878 878 [255]
879 879
880 880 Hook from untrusted hgrc are reported as failure
881 881 ================================================
882 882
883 883 $ cat << EOF > $TESTTMP/untrusted.py
884 884 > from mercurial import scmutil, util
885 885 > def uisetup(ui):
886 886 > class untrustedui(ui.__class__):
887 887 > def _trusted(self, fp, f):
888 888 > if util.normpath(fp.name).endswith('untrusted/.hg/hgrc'):
889 889 > return False
890 890 > return super(untrustedui, self)._trusted(fp, f)
891 891 > ui.__class__ = untrustedui
892 892 > EOF
893 893 $ cat << EOF >> $HGRCPATH
894 894 > [extensions]
895 895 > untrusted=$TESTTMP/untrusted.py
896 896 > EOF
897 897 $ hg init untrusted
898 898 $ cd untrusted
899 899
900 900 Non-blocking hook
901 901 -----------------
902 902
903 903 $ cat << EOF >> .hg/hgrc
904 904 > [hooks]
905 905 > txnclose.testing=echo txnclose hook called
906 906 > EOF
907 907 $ touch a && hg commit -Aqm a
908 908 warning: untrusted hook txnclose.testing not executed
909 909 $ hg log
910 910 changeset: 0:3903775176ed
911 911 tag: tip
912 912 user: test
913 913 date: Thu Jan 01 00:00:00 1970 +0000
914 914 summary: a
915 915
916 916
917 917 Non-blocking hook
918 918 -----------------
919 919
920 920 $ cat << EOF >> .hg/hgrc
921 921 > [hooks]
922 922 > pretxnclose.testing=echo pre-txnclose hook called
923 923 > EOF
924 924 $ touch b && hg commit -Aqm a
925 925 transaction abort!
926 926 rollback completed
927 927 abort: untrusted hook pretxnclose.testing not executed
928 928 (see 'hg help config.trusted')
929 929 [255]
930 930 $ hg log
931 931 changeset: 0:3903775176ed
932 932 tag: tip
933 933 user: test
934 934 date: Thu Jan 01 00:00:00 1970 +0000
935 935 summary: a
936 936
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now