##// END OF EJS Templates
patch: make extract() a context manager (API)...
Gregory Szorc -
r37639:5537d8f5 default
parent child Browse files
Show More
@@ -1,3234 +1,3232 b''
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import os
12 12 import re
13 13 import tempfile
14 14
15 15 from .i18n import _
16 16 from .node import (
17 17 hex,
18 18 nullid,
19 19 nullrev,
20 20 short,
21 21 )
22 22
23 23 from . import (
24 24 bookmarks,
25 25 changelog,
26 26 copies,
27 27 crecord as crecordmod,
28 28 dirstateguard,
29 29 encoding,
30 30 error,
31 31 formatter,
32 32 logcmdutil,
33 33 match as matchmod,
34 34 merge as mergemod,
35 35 mergeutil,
36 36 obsolete,
37 37 patch,
38 38 pathutil,
39 39 pycompat,
40 40 registrar,
41 41 revlog,
42 42 rewriteutil,
43 43 scmutil,
44 44 smartset,
45 45 subrepoutil,
46 46 templatekw,
47 47 templater,
48 48 util,
49 49 vfs as vfsmod,
50 50 )
51 51
52 52 from .utils import (
53 53 dateutil,
54 54 stringutil,
55 55 )
56 56
57 57 stringio = util.stringio
58 58
59 59 # templates of common command options
60 60
61 61 dryrunopts = [
62 62 ('n', 'dry-run', None,
63 63 _('do not perform actions, just print output')),
64 64 ]
65 65
66 66 remoteopts = [
67 67 ('e', 'ssh', '',
68 68 _('specify ssh command to use'), _('CMD')),
69 69 ('', 'remotecmd', '',
70 70 _('specify hg command to run on the remote side'), _('CMD')),
71 71 ('', 'insecure', None,
72 72 _('do not verify server certificate (ignoring web.cacerts config)')),
73 73 ]
74 74
75 75 walkopts = [
76 76 ('I', 'include', [],
77 77 _('include names matching the given patterns'), _('PATTERN')),
78 78 ('X', 'exclude', [],
79 79 _('exclude names matching the given patterns'), _('PATTERN')),
80 80 ]
81 81
82 82 commitopts = [
83 83 ('m', 'message', '',
84 84 _('use text as commit message'), _('TEXT')),
85 85 ('l', 'logfile', '',
86 86 _('read commit message from file'), _('FILE')),
87 87 ]
88 88
89 89 commitopts2 = [
90 90 ('d', 'date', '',
91 91 _('record the specified date as commit date'), _('DATE')),
92 92 ('u', 'user', '',
93 93 _('record the specified user as committer'), _('USER')),
94 94 ]
95 95
96 96 # hidden for now
97 97 formatteropts = [
98 98 ('T', 'template', '',
99 99 _('display with template (EXPERIMENTAL)'), _('TEMPLATE')),
100 100 ]
101 101
102 102 templateopts = [
103 103 ('', 'style', '',
104 104 _('display using template map file (DEPRECATED)'), _('STYLE')),
105 105 ('T', 'template', '',
106 106 _('display with template'), _('TEMPLATE')),
107 107 ]
108 108
109 109 logopts = [
110 110 ('p', 'patch', None, _('show patch')),
111 111 ('g', 'git', None, _('use git extended diff format')),
112 112 ('l', 'limit', '',
113 113 _('limit number of changes displayed'), _('NUM')),
114 114 ('M', 'no-merges', None, _('do not show merges')),
115 115 ('', 'stat', None, _('output diffstat-style summary of changes')),
116 116 ('G', 'graph', None, _("show the revision DAG")),
117 117 ] + templateopts
118 118
119 119 diffopts = [
120 120 ('a', 'text', None, _('treat all files as text')),
121 121 ('g', 'git', None, _('use git extended diff format')),
122 122 ('', 'binary', None, _('generate binary diffs in git mode (default)')),
123 123 ('', 'nodates', None, _('omit dates from diff headers'))
124 124 ]
125 125
126 126 diffwsopts = [
127 127 ('w', 'ignore-all-space', None,
128 128 _('ignore white space when comparing lines')),
129 129 ('b', 'ignore-space-change', None,
130 130 _('ignore changes in the amount of white space')),
131 131 ('B', 'ignore-blank-lines', None,
132 132 _('ignore changes whose lines are all blank')),
133 133 ('Z', 'ignore-space-at-eol', None,
134 134 _('ignore changes in whitespace at EOL')),
135 135 ]
136 136
137 137 diffopts2 = [
138 138 ('', 'noprefix', None, _('omit a/ and b/ prefixes from filenames')),
139 139 ('p', 'show-function', None, _('show which function each change is in')),
140 140 ('', 'reverse', None, _('produce a diff that undoes the changes')),
141 141 ] + diffwsopts + [
142 142 ('U', 'unified', '',
143 143 _('number of lines of context to show'), _('NUM')),
144 144 ('', 'stat', None, _('output diffstat-style summary of changes')),
145 145 ('', 'root', '', _('produce diffs relative to subdirectory'), _('DIR')),
146 146 ]
147 147
148 148 mergetoolopts = [
149 149 ('t', 'tool', '', _('specify merge tool')),
150 150 ]
151 151
152 152 similarityopts = [
153 153 ('s', 'similarity', '',
154 154 _('guess renamed files by similarity (0<=s<=100)'), _('SIMILARITY'))
155 155 ]
156 156
157 157 subrepoopts = [
158 158 ('S', 'subrepos', None,
159 159 _('recurse into subrepositories'))
160 160 ]
161 161
162 162 debugrevlogopts = [
163 163 ('c', 'changelog', False, _('open changelog')),
164 164 ('m', 'manifest', False, _('open manifest')),
165 165 ('', 'dir', '', _('open directory manifest')),
166 166 ]
167 167
168 168 # special string such that everything below this line will be ingored in the
169 169 # editor text
170 170 _linebelow = "^HG: ------------------------ >8 ------------------------$"
171 171
172 172 def ishunk(x):
173 173 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
174 174 return isinstance(x, hunkclasses)
175 175
176 176 def newandmodified(chunks, originalchunks):
177 177 newlyaddedandmodifiedfiles = set()
178 178 for chunk in chunks:
179 179 if ishunk(chunk) and chunk.header.isnewfile() and chunk not in \
180 180 originalchunks:
181 181 newlyaddedandmodifiedfiles.add(chunk.header.filename())
182 182 return newlyaddedandmodifiedfiles
183 183
184 184 def parsealiases(cmd):
185 185 return cmd.lstrip("^").split("|")
186 186
187 187 def setupwrapcolorwrite(ui):
188 188 # wrap ui.write so diff output can be labeled/colorized
189 189 def wrapwrite(orig, *args, **kw):
190 190 label = kw.pop(r'label', '')
191 191 for chunk, l in patch.difflabel(lambda: args):
192 192 orig(chunk, label=label + l)
193 193
194 194 oldwrite = ui.write
195 195 def wrap(*args, **kwargs):
196 196 return wrapwrite(oldwrite, *args, **kwargs)
197 197 setattr(ui, 'write', wrap)
198 198 return oldwrite
199 199
200 200 def filterchunks(ui, originalhunks, usecurses, testfile, operation=None):
201 201 if usecurses:
202 202 if testfile:
203 203 recordfn = crecordmod.testdecorator(testfile,
204 204 crecordmod.testchunkselector)
205 205 else:
206 206 recordfn = crecordmod.chunkselector
207 207
208 208 return crecordmod.filterpatch(ui, originalhunks, recordfn, operation)
209 209
210 210 else:
211 211 return patch.filterpatch(ui, originalhunks, operation)
212 212
213 213 def recordfilter(ui, originalhunks, operation=None):
214 214 """ Prompts the user to filter the originalhunks and return a list of
215 215 selected hunks.
216 216 *operation* is used for to build ui messages to indicate the user what
217 217 kind of filtering they are doing: reverting, committing, shelving, etc.
218 218 (see patch.filterpatch).
219 219 """
220 220 usecurses = crecordmod.checkcurses(ui)
221 221 testfile = ui.config('experimental', 'crecordtest')
222 222 oldwrite = setupwrapcolorwrite(ui)
223 223 try:
224 224 newchunks, newopts = filterchunks(ui, originalhunks, usecurses,
225 225 testfile, operation)
226 226 finally:
227 227 ui.write = oldwrite
228 228 return newchunks, newopts
229 229
230 230 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
231 231 filterfn, *pats, **opts):
232 232 opts = pycompat.byteskwargs(opts)
233 233 if not ui.interactive():
234 234 if cmdsuggest:
235 235 msg = _('running non-interactively, use %s instead') % cmdsuggest
236 236 else:
237 237 msg = _('running non-interactively')
238 238 raise error.Abort(msg)
239 239
240 240 # make sure username is set before going interactive
241 241 if not opts.get('user'):
242 242 ui.username() # raise exception, username not provided
243 243
244 244 def recordfunc(ui, repo, message, match, opts):
245 245 """This is generic record driver.
246 246
247 247 Its job is to interactively filter local changes, and
248 248 accordingly prepare working directory into a state in which the
249 249 job can be delegated to a non-interactive commit command such as
250 250 'commit' or 'qrefresh'.
251 251
252 252 After the actual job is done by non-interactive command, the
253 253 working directory is restored to its original state.
254 254
255 255 In the end we'll record interesting changes, and everything else
256 256 will be left in place, so the user can continue working.
257 257 """
258 258
259 259 checkunfinished(repo, commit=True)
260 260 wctx = repo[None]
261 261 merge = len(wctx.parents()) > 1
262 262 if merge:
263 263 raise error.Abort(_('cannot partially commit a merge '
264 264 '(use "hg commit" instead)'))
265 265
266 266 def fail(f, msg):
267 267 raise error.Abort('%s: %s' % (f, msg))
268 268
269 269 force = opts.get('force')
270 270 if not force:
271 271 vdirs = []
272 272 match.explicitdir = vdirs.append
273 273 match.bad = fail
274 274
275 275 status = repo.status(match=match)
276 276 if not force:
277 277 repo.checkcommitpatterns(wctx, vdirs, match, status, fail)
278 278 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
279 279 diffopts.nodates = True
280 280 diffopts.git = True
281 281 diffopts.showfunc = True
282 282 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
283 283 originalchunks = patch.parsepatch(originaldiff)
284 284
285 285 # 1. filter patch, since we are intending to apply subset of it
286 286 try:
287 287 chunks, newopts = filterfn(ui, originalchunks)
288 288 except error.PatchError as err:
289 289 raise error.Abort(_('error parsing patch: %s') % err)
290 290 opts.update(newopts)
291 291
292 292 # We need to keep a backup of files that have been newly added and
293 293 # modified during the recording process because there is a previous
294 294 # version without the edit in the workdir
295 295 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
296 296 contenders = set()
297 297 for h in chunks:
298 298 try:
299 299 contenders.update(set(h.files()))
300 300 except AttributeError:
301 301 pass
302 302
303 303 changed = status.modified + status.added + status.removed
304 304 newfiles = [f for f in changed if f in contenders]
305 305 if not newfiles:
306 306 ui.status(_('no changes to record\n'))
307 307 return 0
308 308
309 309 modified = set(status.modified)
310 310
311 311 # 2. backup changed files, so we can restore them in the end
312 312
313 313 if backupall:
314 314 tobackup = changed
315 315 else:
316 316 tobackup = [f for f in newfiles if f in modified or f in \
317 317 newlyaddedandmodifiedfiles]
318 318 backups = {}
319 319 if tobackup:
320 320 backupdir = repo.vfs.join('record-backups')
321 321 try:
322 322 os.mkdir(backupdir)
323 323 except OSError as err:
324 324 if err.errno != errno.EEXIST:
325 325 raise
326 326 try:
327 327 # backup continues
328 328 for f in tobackup:
329 329 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
330 330 dir=backupdir)
331 331 os.close(fd)
332 332 ui.debug('backup %r as %r\n' % (f, tmpname))
333 333 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
334 334 backups[f] = tmpname
335 335
336 336 fp = stringio()
337 337 for c in chunks:
338 338 fname = c.filename()
339 339 if fname in backups:
340 340 c.write(fp)
341 341 dopatch = fp.tell()
342 342 fp.seek(0)
343 343
344 344 # 2.5 optionally review / modify patch in text editor
345 345 if opts.get('review', False):
346 346 patchtext = (crecordmod.diffhelptext
347 347 + crecordmod.patchhelptext
348 348 + fp.read())
349 349 reviewedpatch = ui.edit(patchtext, "",
350 350 action="diff",
351 351 repopath=repo.path)
352 352 fp.truncate(0)
353 353 fp.write(reviewedpatch)
354 354 fp.seek(0)
355 355
356 356 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
357 357 # 3a. apply filtered patch to clean repo (clean)
358 358 if backups:
359 359 # Equivalent to hg.revert
360 360 m = scmutil.matchfiles(repo, backups.keys())
361 361 mergemod.update(repo, repo.dirstate.p1(),
362 362 False, True, matcher=m)
363 363
364 364 # 3b. (apply)
365 365 if dopatch:
366 366 try:
367 367 ui.debug('applying patch\n')
368 368 ui.debug(fp.getvalue())
369 369 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
370 370 except error.PatchError as err:
371 371 raise error.Abort(pycompat.bytestr(err))
372 372 del fp
373 373
374 374 # 4. We prepared working directory according to filtered
375 375 # patch. Now is the time to delegate the job to
376 376 # commit/qrefresh or the like!
377 377
378 378 # Make all of the pathnames absolute.
379 379 newfiles = [repo.wjoin(nf) for nf in newfiles]
380 380 return commitfunc(ui, repo, *newfiles, **pycompat.strkwargs(opts))
381 381 finally:
382 382 # 5. finally restore backed-up files
383 383 try:
384 384 dirstate = repo.dirstate
385 385 for realname, tmpname in backups.iteritems():
386 386 ui.debug('restoring %r to %r\n' % (tmpname, realname))
387 387
388 388 if dirstate[realname] == 'n':
389 389 # without normallookup, restoring timestamp
390 390 # may cause partially committed files
391 391 # to be treated as unmodified
392 392 dirstate.normallookup(realname)
393 393
394 394 # copystat=True here and above are a hack to trick any
395 395 # editors that have f open that we haven't modified them.
396 396 #
397 397 # Also note that this racy as an editor could notice the
398 398 # file's mtime before we've finished writing it.
399 399 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
400 400 os.unlink(tmpname)
401 401 if tobackup:
402 402 os.rmdir(backupdir)
403 403 except OSError:
404 404 pass
405 405
406 406 def recordinwlock(ui, repo, message, match, opts):
407 407 with repo.wlock():
408 408 return recordfunc(ui, repo, message, match, opts)
409 409
410 410 return commit(ui, repo, recordinwlock, pats, opts)
411 411
412 412 class dirnode(object):
413 413 """
414 414 Represent a directory in user working copy with information required for
415 415 the purpose of tersing its status.
416 416
417 417 path is the path to the directory
418 418
419 419 statuses is a set of statuses of all files in this directory (this includes
420 420 all the files in all the subdirectories too)
421 421
422 422 files is a list of files which are direct child of this directory
423 423
424 424 subdirs is a dictionary of sub-directory name as the key and it's own
425 425 dirnode object as the value
426 426 """
427 427
428 428 def __init__(self, dirpath):
429 429 self.path = dirpath
430 430 self.statuses = set([])
431 431 self.files = []
432 432 self.subdirs = {}
433 433
434 434 def _addfileindir(self, filename, status):
435 435 """Add a file in this directory as a direct child."""
436 436 self.files.append((filename, status))
437 437
438 438 def addfile(self, filename, status):
439 439 """
440 440 Add a file to this directory or to its direct parent directory.
441 441
442 442 If the file is not direct child of this directory, we traverse to the
443 443 directory of which this file is a direct child of and add the file
444 444 there.
445 445 """
446 446
447 447 # the filename contains a path separator, it means it's not the direct
448 448 # child of this directory
449 449 if '/' in filename:
450 450 subdir, filep = filename.split('/', 1)
451 451
452 452 # does the dirnode object for subdir exists
453 453 if subdir not in self.subdirs:
454 454 subdirpath = os.path.join(self.path, subdir)
455 455 self.subdirs[subdir] = dirnode(subdirpath)
456 456
457 457 # try adding the file in subdir
458 458 self.subdirs[subdir].addfile(filep, status)
459 459
460 460 else:
461 461 self._addfileindir(filename, status)
462 462
463 463 if status not in self.statuses:
464 464 self.statuses.add(status)
465 465
466 466 def iterfilepaths(self):
467 467 """Yield (status, path) for files directly under this directory."""
468 468 for f, st in self.files:
469 469 yield st, os.path.join(self.path, f)
470 470
471 471 def tersewalk(self, terseargs):
472 472 """
473 473 Yield (status, path) obtained by processing the status of this
474 474 dirnode.
475 475
476 476 terseargs is the string of arguments passed by the user with `--terse`
477 477 flag.
478 478
479 479 Following are the cases which can happen:
480 480
481 481 1) All the files in the directory (including all the files in its
482 482 subdirectories) share the same status and the user has asked us to terse
483 483 that status. -> yield (status, dirpath)
484 484
485 485 2) Otherwise, we do following:
486 486
487 487 a) Yield (status, filepath) for all the files which are in this
488 488 directory (only the ones in this directory, not the subdirs)
489 489
490 490 b) Recurse the function on all the subdirectories of this
491 491 directory
492 492 """
493 493
494 494 if len(self.statuses) == 1:
495 495 onlyst = self.statuses.pop()
496 496
497 497 # Making sure we terse only when the status abbreviation is
498 498 # passed as terse argument
499 499 if onlyst in terseargs:
500 500 yield onlyst, self.path + pycompat.ossep
501 501 return
502 502
503 503 # add the files to status list
504 504 for st, fpath in self.iterfilepaths():
505 505 yield st, fpath
506 506
507 507 #recurse on the subdirs
508 508 for dirobj in self.subdirs.values():
509 509 for st, fpath in dirobj.tersewalk(terseargs):
510 510 yield st, fpath
511 511
512 512 def tersedir(statuslist, terseargs):
513 513 """
514 514 Terse the status if all the files in a directory shares the same status.
515 515
516 516 statuslist is scmutil.status() object which contains a list of files for
517 517 each status.
518 518 terseargs is string which is passed by the user as the argument to `--terse`
519 519 flag.
520 520
521 521 The function makes a tree of objects of dirnode class, and at each node it
522 522 stores the information required to know whether we can terse a certain
523 523 directory or not.
524 524 """
525 525 # the order matters here as that is used to produce final list
526 526 allst = ('m', 'a', 'r', 'd', 'u', 'i', 'c')
527 527
528 528 # checking the argument validity
529 529 for s in pycompat.bytestr(terseargs):
530 530 if s not in allst:
531 531 raise error.Abort(_("'%s' not recognized") % s)
532 532
533 533 # creating a dirnode object for the root of the repo
534 534 rootobj = dirnode('')
535 535 pstatus = ('modified', 'added', 'deleted', 'clean', 'unknown',
536 536 'ignored', 'removed')
537 537
538 538 tersedict = {}
539 539 for attrname in pstatus:
540 540 statuschar = attrname[0:1]
541 541 for f in getattr(statuslist, attrname):
542 542 rootobj.addfile(f, statuschar)
543 543 tersedict[statuschar] = []
544 544
545 545 # we won't be tersing the root dir, so add files in it
546 546 for st, fpath in rootobj.iterfilepaths():
547 547 tersedict[st].append(fpath)
548 548
549 549 # process each sub-directory and build tersedict
550 550 for subdir in rootobj.subdirs.values():
551 551 for st, f in subdir.tersewalk(terseargs):
552 552 tersedict[st].append(f)
553 553
554 554 tersedlist = []
555 555 for st in allst:
556 556 tersedict[st].sort()
557 557 tersedlist.append(tersedict[st])
558 558
559 559 return tersedlist
560 560
561 561 def _commentlines(raw):
562 562 '''Surround lineswith a comment char and a new line'''
563 563 lines = raw.splitlines()
564 564 commentedlines = ['# %s' % line for line in lines]
565 565 return '\n'.join(commentedlines) + '\n'
566 566
567 567 def _conflictsmsg(repo):
568 568 mergestate = mergemod.mergestate.read(repo)
569 569 if not mergestate.active():
570 570 return
571 571
572 572 m = scmutil.match(repo[None])
573 573 unresolvedlist = [f for f in mergestate.unresolved() if m(f)]
574 574 if unresolvedlist:
575 575 mergeliststr = '\n'.join(
576 576 [' %s' % util.pathto(repo.root, pycompat.getcwd(), path)
577 577 for path in unresolvedlist])
578 578 msg = _('''Unresolved merge conflicts:
579 579
580 580 %s
581 581
582 582 To mark files as resolved: hg resolve --mark FILE''') % mergeliststr
583 583 else:
584 584 msg = _('No unresolved merge conflicts.')
585 585
586 586 return _commentlines(msg)
587 587
588 588 def _helpmessage(continuecmd, abortcmd):
589 589 msg = _('To continue: %s\n'
590 590 'To abort: %s') % (continuecmd, abortcmd)
591 591 return _commentlines(msg)
592 592
593 593 def _rebasemsg():
594 594 return _helpmessage('hg rebase --continue', 'hg rebase --abort')
595 595
596 596 def _histeditmsg():
597 597 return _helpmessage('hg histedit --continue', 'hg histedit --abort')
598 598
599 599 def _unshelvemsg():
600 600 return _helpmessage('hg unshelve --continue', 'hg unshelve --abort')
601 601
602 602 def _updatecleanmsg(dest=None):
603 603 warning = _('warning: this will discard uncommitted changes')
604 604 return 'hg update --clean %s (%s)' % (dest or '.', warning)
605 605
606 606 def _graftmsg():
607 607 # tweakdefaults requires `update` to have a rev hence the `.`
608 608 return _helpmessage('hg graft --continue', _updatecleanmsg())
609 609
610 610 def _mergemsg():
611 611 # tweakdefaults requires `update` to have a rev hence the `.`
612 612 return _helpmessage('hg commit', _updatecleanmsg())
613 613
614 614 def _bisectmsg():
615 615 msg = _('To mark the changeset good: hg bisect --good\n'
616 616 'To mark the changeset bad: hg bisect --bad\n'
617 617 'To abort: hg bisect --reset\n')
618 618 return _commentlines(msg)
619 619
620 620 def fileexistspredicate(filename):
621 621 return lambda repo: repo.vfs.exists(filename)
622 622
623 623 def _mergepredicate(repo):
624 624 return len(repo[None].parents()) > 1
625 625
626 626 STATES = (
627 627 # (state, predicate to detect states, helpful message function)
628 628 ('histedit', fileexistspredicate('histedit-state'), _histeditmsg),
629 629 ('bisect', fileexistspredicate('bisect.state'), _bisectmsg),
630 630 ('graft', fileexistspredicate('graftstate'), _graftmsg),
631 631 ('unshelve', fileexistspredicate('unshelverebasestate'), _unshelvemsg),
632 632 ('rebase', fileexistspredicate('rebasestate'), _rebasemsg),
633 633 # The merge state is part of a list that will be iterated over.
634 634 # They need to be last because some of the other unfinished states may also
635 635 # be in a merge or update state (eg. rebase, histedit, graft, etc).
636 636 # We want those to have priority.
637 637 ('merge', _mergepredicate, _mergemsg),
638 638 )
639 639
640 640 def _getrepostate(repo):
641 641 # experimental config: commands.status.skipstates
642 642 skip = set(repo.ui.configlist('commands', 'status.skipstates'))
643 643 for state, statedetectionpredicate, msgfn in STATES:
644 644 if state in skip:
645 645 continue
646 646 if statedetectionpredicate(repo):
647 647 return (state, statedetectionpredicate, msgfn)
648 648
649 649 def morestatus(repo, fm):
650 650 statetuple = _getrepostate(repo)
651 651 label = 'status.morestatus'
652 652 if statetuple:
653 653 fm.startitem()
654 654 state, statedetectionpredicate, helpfulmsg = statetuple
655 655 statemsg = _('The repository is in an unfinished *%s* state.') % state
656 656 fm.write('statemsg', '%s\n', _commentlines(statemsg), label=label)
657 657 conmsg = _conflictsmsg(repo)
658 658 if conmsg:
659 659 fm.write('conflictsmsg', '%s\n', conmsg, label=label)
660 660 if helpfulmsg:
661 661 helpmsg = helpfulmsg()
662 662 fm.write('helpmsg', '%s\n', helpmsg, label=label)
663 663
664 664 def findpossible(cmd, table, strict=False):
665 665 """
666 666 Return cmd -> (aliases, command table entry)
667 667 for each matching command.
668 668 Return debug commands (or their aliases) only if no normal command matches.
669 669 """
670 670 choice = {}
671 671 debugchoice = {}
672 672
673 673 if cmd in table:
674 674 # short-circuit exact matches, "log" alias beats "^log|history"
675 675 keys = [cmd]
676 676 else:
677 677 keys = table.keys()
678 678
679 679 allcmds = []
680 680 for e in keys:
681 681 aliases = parsealiases(e)
682 682 allcmds.extend(aliases)
683 683 found = None
684 684 if cmd in aliases:
685 685 found = cmd
686 686 elif not strict:
687 687 for a in aliases:
688 688 if a.startswith(cmd):
689 689 found = a
690 690 break
691 691 if found is not None:
692 692 if aliases[0].startswith("debug") or found.startswith("debug"):
693 693 debugchoice[found] = (aliases, table[e])
694 694 else:
695 695 choice[found] = (aliases, table[e])
696 696
697 697 if not choice and debugchoice:
698 698 choice = debugchoice
699 699
700 700 return choice, allcmds
701 701
702 702 def findcmd(cmd, table, strict=True):
703 703 """Return (aliases, command table entry) for command string."""
704 704 choice, allcmds = findpossible(cmd, table, strict)
705 705
706 706 if cmd in choice:
707 707 return choice[cmd]
708 708
709 709 if len(choice) > 1:
710 710 clist = sorted(choice)
711 711 raise error.AmbiguousCommand(cmd, clist)
712 712
713 713 if choice:
714 714 return list(choice.values())[0]
715 715
716 716 raise error.UnknownCommand(cmd, allcmds)
717 717
718 718 def changebranch(ui, repo, revs, label):
719 719 """ Change the branch name of given revs to label """
720 720
721 721 with repo.wlock(), repo.lock(), repo.transaction('branches'):
722 722 # abort in case of uncommitted merge or dirty wdir
723 723 bailifchanged(repo)
724 724 revs = scmutil.revrange(repo, revs)
725 725 if not revs:
726 726 raise error.Abort("empty revision set")
727 727 roots = repo.revs('roots(%ld)', revs)
728 728 if len(roots) > 1:
729 729 raise error.Abort(_("cannot change branch of non-linear revisions"))
730 730 rewriteutil.precheck(repo, revs, 'change branch of')
731 731
732 732 root = repo[roots.first()]
733 733 if not root.p1().branch() == label and label in repo.branchmap():
734 734 raise error.Abort(_("a branch of the same name already exists"))
735 735
736 736 if repo.revs('merge() and %ld', revs):
737 737 raise error.Abort(_("cannot change branch of a merge commit"))
738 738 if repo.revs('obsolete() and %ld', revs):
739 739 raise error.Abort(_("cannot change branch of a obsolete changeset"))
740 740
741 741 # make sure only topological heads
742 742 if repo.revs('heads(%ld) - head()', revs):
743 743 raise error.Abort(_("cannot change branch in middle of a stack"))
744 744
745 745 replacements = {}
746 746 # avoid import cycle mercurial.cmdutil -> mercurial.context ->
747 747 # mercurial.subrepo -> mercurial.cmdutil
748 748 from . import context
749 749 for rev in revs:
750 750 ctx = repo[rev]
751 751 oldbranch = ctx.branch()
752 752 # check if ctx has same branch
753 753 if oldbranch == label:
754 754 continue
755 755
756 756 def filectxfn(repo, newctx, path):
757 757 try:
758 758 return ctx[path]
759 759 except error.ManifestLookupError:
760 760 return None
761 761
762 762 ui.debug("changing branch of '%s' from '%s' to '%s'\n"
763 763 % (hex(ctx.node()), oldbranch, label))
764 764 extra = ctx.extra()
765 765 extra['branch_change'] = hex(ctx.node())
766 766 # While changing branch of set of linear commits, make sure that
767 767 # we base our commits on new parent rather than old parent which
768 768 # was obsoleted while changing the branch
769 769 p1 = ctx.p1().node()
770 770 p2 = ctx.p2().node()
771 771 if p1 in replacements:
772 772 p1 = replacements[p1][0]
773 773 if p2 in replacements:
774 774 p2 = replacements[p2][0]
775 775
776 776 mc = context.memctx(repo, (p1, p2),
777 777 ctx.description(),
778 778 ctx.files(),
779 779 filectxfn,
780 780 user=ctx.user(),
781 781 date=ctx.date(),
782 782 extra=extra,
783 783 branch=label)
784 784
785 785 commitphase = ctx.phase()
786 786 overrides = {('phases', 'new-commit'): commitphase}
787 787 with repo.ui.configoverride(overrides, 'branch-change'):
788 788 newnode = repo.commitctx(mc)
789 789
790 790 replacements[ctx.node()] = (newnode,)
791 791 ui.debug('new node id is %s\n' % hex(newnode))
792 792
793 793 # create obsmarkers and move bookmarks
794 794 scmutil.cleanupnodes(repo, replacements, 'branch-change')
795 795
796 796 # move the working copy too
797 797 wctx = repo[None]
798 798 # in-progress merge is a bit too complex for now.
799 799 if len(wctx.parents()) == 1:
800 800 newid = replacements.get(wctx.p1().node())
801 801 if newid is not None:
802 802 # avoid import cycle mercurial.cmdutil -> mercurial.hg ->
803 803 # mercurial.cmdutil
804 804 from . import hg
805 805 hg.update(repo, newid[0], quietempty=True)
806 806
807 807 ui.status(_("changed branch on %d changesets\n") % len(replacements))
808 808
809 809 def findrepo(p):
810 810 while not os.path.isdir(os.path.join(p, ".hg")):
811 811 oldp, p = p, os.path.dirname(p)
812 812 if p == oldp:
813 813 return None
814 814
815 815 return p
816 816
817 817 def bailifchanged(repo, merge=True, hint=None):
818 818 """ enforce the precondition that working directory must be clean.
819 819
820 820 'merge' can be set to false if a pending uncommitted merge should be
821 821 ignored (such as when 'update --check' runs).
822 822
823 823 'hint' is the usual hint given to Abort exception.
824 824 """
825 825
826 826 if merge and repo.dirstate.p2() != nullid:
827 827 raise error.Abort(_('outstanding uncommitted merge'), hint=hint)
828 828 modified, added, removed, deleted = repo.status()[:4]
829 829 if modified or added or removed or deleted:
830 830 raise error.Abort(_('uncommitted changes'), hint=hint)
831 831 ctx = repo[None]
832 832 for s in sorted(ctx.substate):
833 833 ctx.sub(s).bailifchanged(hint=hint)
834 834
835 835 def logmessage(ui, opts):
836 836 """ get the log message according to -m and -l option """
837 837 message = opts.get('message')
838 838 logfile = opts.get('logfile')
839 839
840 840 if message and logfile:
841 841 raise error.Abort(_('options --message and --logfile are mutually '
842 842 'exclusive'))
843 843 if not message and logfile:
844 844 try:
845 845 if isstdiofilename(logfile):
846 846 message = ui.fin.read()
847 847 else:
848 848 message = '\n'.join(util.readfile(logfile).splitlines())
849 849 except IOError as inst:
850 850 raise error.Abort(_("can't read commit message '%s': %s") %
851 851 (logfile, encoding.strtolocal(inst.strerror)))
852 852 return message
853 853
854 854 def mergeeditform(ctxorbool, baseformname):
855 855 """return appropriate editform name (referencing a committemplate)
856 856
857 857 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
858 858 merging is committed.
859 859
860 860 This returns baseformname with '.merge' appended if it is a merge,
861 861 otherwise '.normal' is appended.
862 862 """
863 863 if isinstance(ctxorbool, bool):
864 864 if ctxorbool:
865 865 return baseformname + ".merge"
866 866 elif 1 < len(ctxorbool.parents()):
867 867 return baseformname + ".merge"
868 868
869 869 return baseformname + ".normal"
870 870
871 871 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
872 872 editform='', **opts):
873 873 """get appropriate commit message editor according to '--edit' option
874 874
875 875 'finishdesc' is a function to be called with edited commit message
876 876 (= 'description' of the new changeset) just after editing, but
877 877 before checking empty-ness. It should return actual text to be
878 878 stored into history. This allows to change description before
879 879 storing.
880 880
881 881 'extramsg' is a extra message to be shown in the editor instead of
882 882 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
883 883 is automatically added.
884 884
885 885 'editform' is a dot-separated list of names, to distinguish
886 886 the purpose of commit text editing.
887 887
888 888 'getcommiteditor' returns 'commitforceeditor' regardless of
889 889 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
890 890 they are specific for usage in MQ.
891 891 """
892 892 if edit or finishdesc or extramsg:
893 893 return lambda r, c, s: commitforceeditor(r, c, s,
894 894 finishdesc=finishdesc,
895 895 extramsg=extramsg,
896 896 editform=editform)
897 897 elif editform:
898 898 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
899 899 else:
900 900 return commiteditor
901 901
902 902 def rendertemplate(ctx, tmpl, props=None):
903 903 """Expand a literal template 'tmpl' byte-string against one changeset
904 904
905 905 Each props item must be a stringify-able value or a callable returning
906 906 such value, i.e. no bare list nor dict should be passed.
907 907 """
908 908 repo = ctx.repo()
909 909 tres = formatter.templateresources(repo.ui, repo)
910 910 t = formatter.maketemplater(repo.ui, tmpl, defaults=templatekw.keywords,
911 911 resources=tres)
912 912 mapping = {'ctx': ctx}
913 913 if props:
914 914 mapping.update(props)
915 915 return t.renderdefault(mapping)
916 916
917 917 def _buildfntemplate(pat, total=None, seqno=None, revwidth=None, pathname=None):
918 918 r"""Convert old-style filename format string to template string
919 919
920 920 >>> _buildfntemplate(b'foo-%b-%n.patch', seqno=0)
921 921 'foo-{reporoot|basename}-{seqno}.patch'
922 922 >>> _buildfntemplate(b'%R{tags % "{tag}"}%H')
923 923 '{rev}{tags % "{tag}"}{node}'
924 924
925 925 '\' in outermost strings has to be escaped because it is a directory
926 926 separator on Windows:
927 927
928 928 >>> _buildfntemplate(b'c:\\tmp\\%R\\%n.patch', seqno=0)
929 929 'c:\\\\tmp\\\\{rev}\\\\{seqno}.patch'
930 930 >>> _buildfntemplate(b'\\\\foo\\bar.patch')
931 931 '\\\\\\\\foo\\\\bar.patch'
932 932 >>> _buildfntemplate(b'\\{tags % "{tag}"}')
933 933 '\\\\{tags % "{tag}"}'
934 934
935 935 but inner strings follow the template rules (i.e. '\' is taken as an
936 936 escape character):
937 937
938 938 >>> _buildfntemplate(br'{"c:\tmp"}', seqno=0)
939 939 '{"c:\\tmp"}'
940 940 """
941 941 expander = {
942 942 b'H': b'{node}',
943 943 b'R': b'{rev}',
944 944 b'h': b'{node|short}',
945 945 b'm': br'{sub(r"[^\w]", "_", desc|firstline)}',
946 946 b'r': b'{if(revwidth, pad(rev, revwidth, "0", left=True), rev)}',
947 947 b'%': b'%',
948 948 b'b': b'{reporoot|basename}',
949 949 }
950 950 if total is not None:
951 951 expander[b'N'] = b'{total}'
952 952 if seqno is not None:
953 953 expander[b'n'] = b'{seqno}'
954 954 if total is not None and seqno is not None:
955 955 expander[b'n'] = b'{pad(seqno, total|stringify|count, "0", left=True)}'
956 956 if pathname is not None:
957 957 expander[b's'] = b'{pathname|basename}'
958 958 expander[b'd'] = b'{if(pathname|dirname, pathname|dirname, ".")}'
959 959 expander[b'p'] = b'{pathname}'
960 960
961 961 newname = []
962 962 for typ, start, end in templater.scantemplate(pat, raw=True):
963 963 if typ != b'string':
964 964 newname.append(pat[start:end])
965 965 continue
966 966 i = start
967 967 while i < end:
968 968 n = pat.find(b'%', i, end)
969 969 if n < 0:
970 970 newname.append(stringutil.escapestr(pat[i:end]))
971 971 break
972 972 newname.append(stringutil.escapestr(pat[i:n]))
973 973 if n + 2 > end:
974 974 raise error.Abort(_("incomplete format spec in output "
975 975 "filename"))
976 976 c = pat[n + 1:n + 2]
977 977 i = n + 2
978 978 try:
979 979 newname.append(expander[c])
980 980 except KeyError:
981 981 raise error.Abort(_("invalid format spec '%%%s' in output "
982 982 "filename") % c)
983 983 return ''.join(newname)
984 984
985 985 def makefilename(ctx, pat, **props):
986 986 if not pat:
987 987 return pat
988 988 tmpl = _buildfntemplate(pat, **props)
989 989 # BUG: alias expansion shouldn't be made against template fragments
990 990 # rewritten from %-format strings, but we have no easy way to partially
991 991 # disable the expansion.
992 992 return rendertemplate(ctx, tmpl, pycompat.byteskwargs(props))
993 993
994 994 def isstdiofilename(pat):
995 995 """True if the given pat looks like a filename denoting stdin/stdout"""
996 996 return not pat or pat == '-'
997 997
998 998 class _unclosablefile(object):
999 999 def __init__(self, fp):
1000 1000 self._fp = fp
1001 1001
1002 1002 def close(self):
1003 1003 pass
1004 1004
1005 1005 def __iter__(self):
1006 1006 return iter(self._fp)
1007 1007
1008 1008 def __getattr__(self, attr):
1009 1009 return getattr(self._fp, attr)
1010 1010
1011 1011 def __enter__(self):
1012 1012 return self
1013 1013
1014 1014 def __exit__(self, exc_type, exc_value, exc_tb):
1015 1015 pass
1016 1016
1017 1017 def makefileobj(ctx, pat, mode='wb', **props):
1018 1018 writable = mode not in ('r', 'rb')
1019 1019
1020 1020 if isstdiofilename(pat):
1021 1021 repo = ctx.repo()
1022 1022 if writable:
1023 1023 fp = repo.ui.fout
1024 1024 else:
1025 1025 fp = repo.ui.fin
1026 1026 return _unclosablefile(fp)
1027 1027 fn = makefilename(ctx, pat, **props)
1028 1028 return open(fn, mode)
1029 1029
1030 1030 def openrevlog(repo, cmd, file_, opts):
1031 1031 """opens the changelog, manifest, a filelog or a given revlog"""
1032 1032 cl = opts['changelog']
1033 1033 mf = opts['manifest']
1034 1034 dir = opts['dir']
1035 1035 msg = None
1036 1036 if cl and mf:
1037 1037 msg = _('cannot specify --changelog and --manifest at the same time')
1038 1038 elif cl and dir:
1039 1039 msg = _('cannot specify --changelog and --dir at the same time')
1040 1040 elif cl or mf or dir:
1041 1041 if file_:
1042 1042 msg = _('cannot specify filename with --changelog or --manifest')
1043 1043 elif not repo:
1044 1044 msg = _('cannot specify --changelog or --manifest or --dir '
1045 1045 'without a repository')
1046 1046 if msg:
1047 1047 raise error.Abort(msg)
1048 1048
1049 1049 r = None
1050 1050 if repo:
1051 1051 if cl:
1052 1052 r = repo.unfiltered().changelog
1053 1053 elif dir:
1054 1054 if 'treemanifest' not in repo.requirements:
1055 1055 raise error.Abort(_("--dir can only be used on repos with "
1056 1056 "treemanifest enabled"))
1057 1057 if not dir.endswith('/'):
1058 1058 dir = dir + '/'
1059 1059 dirlog = repo.manifestlog._revlog.dirlog(dir)
1060 1060 if len(dirlog):
1061 1061 r = dirlog
1062 1062 elif mf:
1063 1063 r = repo.manifestlog._revlog
1064 1064 elif file_:
1065 1065 filelog = repo.file(file_)
1066 1066 if len(filelog):
1067 1067 r = filelog
1068 1068 if not r:
1069 1069 if not file_:
1070 1070 raise error.CommandError(cmd, _('invalid arguments'))
1071 1071 if not os.path.isfile(file_):
1072 1072 raise error.Abort(_("revlog '%s' not found") % file_)
1073 1073 r = revlog.revlog(vfsmod.vfs(pycompat.getcwd(), audit=False),
1074 1074 file_[:-2] + ".i")
1075 1075 return r
1076 1076
1077 1077 def copy(ui, repo, pats, opts, rename=False):
1078 1078 # called with the repo lock held
1079 1079 #
1080 1080 # hgsep => pathname that uses "/" to separate directories
1081 1081 # ossep => pathname that uses os.sep to separate directories
1082 1082 cwd = repo.getcwd()
1083 1083 targets = {}
1084 1084 after = opts.get("after")
1085 1085 dryrun = opts.get("dry_run")
1086 1086 wctx = repo[None]
1087 1087
1088 1088 def walkpat(pat):
1089 1089 srcs = []
1090 1090 if after:
1091 1091 badstates = '?'
1092 1092 else:
1093 1093 badstates = '?r'
1094 1094 m = scmutil.match(wctx, [pat], opts, globbed=True)
1095 1095 for abs in wctx.walk(m):
1096 1096 state = repo.dirstate[abs]
1097 1097 rel = m.rel(abs)
1098 1098 exact = m.exact(abs)
1099 1099 if state in badstates:
1100 1100 if exact and state == '?':
1101 1101 ui.warn(_('%s: not copying - file is not managed\n') % rel)
1102 1102 if exact and state == 'r':
1103 1103 ui.warn(_('%s: not copying - file has been marked for'
1104 1104 ' remove\n') % rel)
1105 1105 continue
1106 1106 # abs: hgsep
1107 1107 # rel: ossep
1108 1108 srcs.append((abs, rel, exact))
1109 1109 return srcs
1110 1110
1111 1111 # abssrc: hgsep
1112 1112 # relsrc: ossep
1113 1113 # otarget: ossep
1114 1114 def copyfile(abssrc, relsrc, otarget, exact):
1115 1115 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
1116 1116 if '/' in abstarget:
1117 1117 # We cannot normalize abstarget itself, this would prevent
1118 1118 # case only renames, like a => A.
1119 1119 abspath, absname = abstarget.rsplit('/', 1)
1120 1120 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
1121 1121 reltarget = repo.pathto(abstarget, cwd)
1122 1122 target = repo.wjoin(abstarget)
1123 1123 src = repo.wjoin(abssrc)
1124 1124 state = repo.dirstate[abstarget]
1125 1125
1126 1126 scmutil.checkportable(ui, abstarget)
1127 1127
1128 1128 # check for collisions
1129 1129 prevsrc = targets.get(abstarget)
1130 1130 if prevsrc is not None:
1131 1131 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
1132 1132 (reltarget, repo.pathto(abssrc, cwd),
1133 1133 repo.pathto(prevsrc, cwd)))
1134 1134 return
1135 1135
1136 1136 # check for overwrites
1137 1137 exists = os.path.lexists(target)
1138 1138 samefile = False
1139 1139 if exists and abssrc != abstarget:
1140 1140 if (repo.dirstate.normalize(abssrc) ==
1141 1141 repo.dirstate.normalize(abstarget)):
1142 1142 if not rename:
1143 1143 ui.warn(_("%s: can't copy - same file\n") % reltarget)
1144 1144 return
1145 1145 exists = False
1146 1146 samefile = True
1147 1147
1148 1148 if not after and exists or after and state in 'mn':
1149 1149 if not opts['force']:
1150 1150 if state in 'mn':
1151 1151 msg = _('%s: not overwriting - file already committed\n')
1152 1152 if after:
1153 1153 flags = '--after --force'
1154 1154 else:
1155 1155 flags = '--force'
1156 1156 if rename:
1157 1157 hint = _('(hg rename %s to replace the file by '
1158 1158 'recording a rename)\n') % flags
1159 1159 else:
1160 1160 hint = _('(hg copy %s to replace the file by '
1161 1161 'recording a copy)\n') % flags
1162 1162 else:
1163 1163 msg = _('%s: not overwriting - file exists\n')
1164 1164 if rename:
1165 1165 hint = _('(hg rename --after to record the rename)\n')
1166 1166 else:
1167 1167 hint = _('(hg copy --after to record the copy)\n')
1168 1168 ui.warn(msg % reltarget)
1169 1169 ui.warn(hint)
1170 1170 return
1171 1171
1172 1172 if after:
1173 1173 if not exists:
1174 1174 if rename:
1175 1175 ui.warn(_('%s: not recording move - %s does not exist\n') %
1176 1176 (relsrc, reltarget))
1177 1177 else:
1178 1178 ui.warn(_('%s: not recording copy - %s does not exist\n') %
1179 1179 (relsrc, reltarget))
1180 1180 return
1181 1181 elif not dryrun:
1182 1182 try:
1183 1183 if exists:
1184 1184 os.unlink(target)
1185 1185 targetdir = os.path.dirname(target) or '.'
1186 1186 if not os.path.isdir(targetdir):
1187 1187 os.makedirs(targetdir)
1188 1188 if samefile:
1189 1189 tmp = target + "~hgrename"
1190 1190 os.rename(src, tmp)
1191 1191 os.rename(tmp, target)
1192 1192 else:
1193 1193 # Preserve stat info on renames, not on copies; this matches
1194 1194 # Linux CLI behavior.
1195 1195 util.copyfile(src, target, copystat=rename)
1196 1196 srcexists = True
1197 1197 except IOError as inst:
1198 1198 if inst.errno == errno.ENOENT:
1199 1199 ui.warn(_('%s: deleted in working directory\n') % relsrc)
1200 1200 srcexists = False
1201 1201 else:
1202 1202 ui.warn(_('%s: cannot copy - %s\n') %
1203 1203 (relsrc, encoding.strtolocal(inst.strerror)))
1204 1204 return True # report a failure
1205 1205
1206 1206 if ui.verbose or not exact:
1207 1207 if rename:
1208 1208 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
1209 1209 else:
1210 1210 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
1211 1211
1212 1212 targets[abstarget] = abssrc
1213 1213
1214 1214 # fix up dirstate
1215 1215 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
1216 1216 dryrun=dryrun, cwd=cwd)
1217 1217 if rename and not dryrun:
1218 1218 if not after and srcexists and not samefile:
1219 1219 repo.wvfs.unlinkpath(abssrc)
1220 1220 wctx.forget([abssrc])
1221 1221
1222 1222 # pat: ossep
1223 1223 # dest ossep
1224 1224 # srcs: list of (hgsep, hgsep, ossep, bool)
1225 1225 # return: function that takes hgsep and returns ossep
1226 1226 def targetpathfn(pat, dest, srcs):
1227 1227 if os.path.isdir(pat):
1228 1228 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1229 1229 abspfx = util.localpath(abspfx)
1230 1230 if destdirexists:
1231 1231 striplen = len(os.path.split(abspfx)[0])
1232 1232 else:
1233 1233 striplen = len(abspfx)
1234 1234 if striplen:
1235 1235 striplen += len(pycompat.ossep)
1236 1236 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1237 1237 elif destdirexists:
1238 1238 res = lambda p: os.path.join(dest,
1239 1239 os.path.basename(util.localpath(p)))
1240 1240 else:
1241 1241 res = lambda p: dest
1242 1242 return res
1243 1243
1244 1244 # pat: ossep
1245 1245 # dest ossep
1246 1246 # srcs: list of (hgsep, hgsep, ossep, bool)
1247 1247 # return: function that takes hgsep and returns ossep
1248 1248 def targetpathafterfn(pat, dest, srcs):
1249 1249 if matchmod.patkind(pat):
1250 1250 # a mercurial pattern
1251 1251 res = lambda p: os.path.join(dest,
1252 1252 os.path.basename(util.localpath(p)))
1253 1253 else:
1254 1254 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1255 1255 if len(abspfx) < len(srcs[0][0]):
1256 1256 # A directory. Either the target path contains the last
1257 1257 # component of the source path or it does not.
1258 1258 def evalpath(striplen):
1259 1259 score = 0
1260 1260 for s in srcs:
1261 1261 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1262 1262 if os.path.lexists(t):
1263 1263 score += 1
1264 1264 return score
1265 1265
1266 1266 abspfx = util.localpath(abspfx)
1267 1267 striplen = len(abspfx)
1268 1268 if striplen:
1269 1269 striplen += len(pycompat.ossep)
1270 1270 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1271 1271 score = evalpath(striplen)
1272 1272 striplen1 = len(os.path.split(abspfx)[0])
1273 1273 if striplen1:
1274 1274 striplen1 += len(pycompat.ossep)
1275 1275 if evalpath(striplen1) > score:
1276 1276 striplen = striplen1
1277 1277 res = lambda p: os.path.join(dest,
1278 1278 util.localpath(p)[striplen:])
1279 1279 else:
1280 1280 # a file
1281 1281 if destdirexists:
1282 1282 res = lambda p: os.path.join(dest,
1283 1283 os.path.basename(util.localpath(p)))
1284 1284 else:
1285 1285 res = lambda p: dest
1286 1286 return res
1287 1287
1288 1288 pats = scmutil.expandpats(pats)
1289 1289 if not pats:
1290 1290 raise error.Abort(_('no source or destination specified'))
1291 1291 if len(pats) == 1:
1292 1292 raise error.Abort(_('no destination specified'))
1293 1293 dest = pats.pop()
1294 1294 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1295 1295 if not destdirexists:
1296 1296 if len(pats) > 1 or matchmod.patkind(pats[0]):
1297 1297 raise error.Abort(_('with multiple sources, destination must be an '
1298 1298 'existing directory'))
1299 1299 if util.endswithsep(dest):
1300 1300 raise error.Abort(_('destination %s is not a directory') % dest)
1301 1301
1302 1302 tfn = targetpathfn
1303 1303 if after:
1304 1304 tfn = targetpathafterfn
1305 1305 copylist = []
1306 1306 for pat in pats:
1307 1307 srcs = walkpat(pat)
1308 1308 if not srcs:
1309 1309 continue
1310 1310 copylist.append((tfn(pat, dest, srcs), srcs))
1311 1311 if not copylist:
1312 1312 raise error.Abort(_('no files to copy'))
1313 1313
1314 1314 errors = 0
1315 1315 for targetpath, srcs in copylist:
1316 1316 for abssrc, relsrc, exact in srcs:
1317 1317 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1318 1318 errors += 1
1319 1319
1320 1320 if errors:
1321 1321 ui.warn(_('(consider using --after)\n'))
1322 1322
1323 1323 return errors != 0
1324 1324
1325 1325 ## facility to let extension process additional data into an import patch
1326 1326 # list of identifier to be executed in order
1327 1327 extrapreimport = [] # run before commit
1328 1328 extrapostimport = [] # run after commit
1329 1329 # mapping from identifier to actual import function
1330 1330 #
1331 1331 # 'preimport' are run before the commit is made and are provided the following
1332 1332 # arguments:
1333 1333 # - repo: the localrepository instance,
1334 1334 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1335 1335 # - extra: the future extra dictionary of the changeset, please mutate it,
1336 1336 # - opts: the import options.
1337 1337 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
1338 1338 # mutation of in memory commit and more. Feel free to rework the code to get
1339 1339 # there.
1340 1340 extrapreimportmap = {}
1341 1341 # 'postimport' are run after the commit is made and are provided the following
1342 1342 # argument:
1343 1343 # - ctx: the changectx created by import.
1344 1344 extrapostimportmap = {}
1345 1345
1346 1346 def tryimportone(ui, repo, patchdata, parents, opts, msgs, updatefunc):
1347 1347 """Utility function used by commands.import to import a single patch
1348 1348
1349 1349 This function is explicitly defined here to help the evolve extension to
1350 1350 wrap this part of the import logic.
1351 1351
1352 1352 The API is currently a bit ugly because it a simple code translation from
1353 1353 the import command. Feel free to make it better.
1354 1354
1355 1355 :patchdata: a dictionary containing parsed patch data (such as from
1356 1356 ``patch.extract()``)
1357 1357 :parents: nodes that will be parent of the created commit
1358 1358 :opts: the full dict of option passed to the import command
1359 1359 :msgs: list to save commit message to.
1360 1360 (used in case we need to save it when failing)
1361 1361 :updatefunc: a function that update a repo to a given node
1362 1362 updatefunc(<repo>, <node>)
1363 1363 """
1364 1364 # avoid cycle context -> subrepo -> cmdutil
1365 1365 from . import context
1366 1366
1367 1367 tmpname = patchdata.get('filename')
1368 1368 message = patchdata.get('message')
1369 1369 user = opts.get('user') or patchdata.get('user')
1370 1370 date = opts.get('date') or patchdata.get('date')
1371 1371 branch = patchdata.get('branch')
1372 1372 nodeid = patchdata.get('nodeid')
1373 1373 p1 = patchdata.get('p1')
1374 1374 p2 = patchdata.get('p2')
1375 1375
1376 1376 nocommit = opts.get('no_commit')
1377 1377 importbranch = opts.get('import_branch')
1378 1378 update = not opts.get('bypass')
1379 1379 strip = opts["strip"]
1380 1380 prefix = opts["prefix"]
1381 1381 sim = float(opts.get('similarity') or 0)
1382
1382 1383 if not tmpname:
1383 return (None, None, False)
1384 return None, None, False
1384 1385
1385 1386 rejects = False
1386 1387
1387 try:
1388 cmdline_message = logmessage(ui, opts)
1389 if cmdline_message:
1390 # pickup the cmdline msg
1391 message = cmdline_message
1392 elif message:
1393 # pickup the patch msg
1394 message = message.strip()
1395 else:
1396 # launch the editor
1397 message = None
1398 ui.debug('message:\n%s\n' % (message or ''))
1399
1400 if len(parents) == 1:
1401 parents.append(repo[nullid])
1402 if opts.get('exact'):
1403 if not nodeid or not p1:
1404 raise error.Abort(_('not a Mercurial patch'))
1388 cmdline_message = logmessage(ui, opts)
1389 if cmdline_message:
1390 # pickup the cmdline msg
1391 message = cmdline_message
1392 elif message:
1393 # pickup the patch msg
1394 message = message.strip()
1395 else:
1396 # launch the editor
1397 message = None
1398 ui.debug('message:\n%s\n' % (message or ''))
1399
1400 if len(parents) == 1:
1401 parents.append(repo[nullid])
1402 if opts.get('exact'):
1403 if not nodeid or not p1:
1404 raise error.Abort(_('not a Mercurial patch'))
1405 p1 = repo[p1]
1406 p2 = repo[p2 or nullid]
1407 elif p2:
1408 try:
1405 1409 p1 = repo[p1]
1406 p2 = repo[p2 or nullid]
1407 elif p2:
1408 try:
1409 p1 = repo[p1]
1410 p2 = repo[p2]
1411 # Without any options, consider p2 only if the
1412 # patch is being applied on top of the recorded
1413 # first parent.
1414 if p1 != parents[0]:
1415 p1 = parents[0]
1416 p2 = repo[nullid]
1417 except error.RepoError:
1418 p1, p2 = parents
1419 if p2.node() == nullid:
1420 ui.warn(_("warning: import the patch as a normal revision\n"
1421 "(use --exact to import the patch as a merge)\n"))
1410 p2 = repo[p2]
1411 # Without any options, consider p2 only if the
1412 # patch is being applied on top of the recorded
1413 # first parent.
1414 if p1 != parents[0]:
1415 p1 = parents[0]
1416 p2 = repo[nullid]
1417 except error.RepoError:
1418 p1, p2 = parents
1419 if p2.node() == nullid:
1420 ui.warn(_("warning: import the patch as a normal revision\n"
1421 "(use --exact to import the patch as a merge)\n"))
1422 else:
1423 p1, p2 = parents
1424
1425 n = None
1426 if update:
1427 if p1 != parents[0]:
1428 updatefunc(repo, p1.node())
1429 if p2 != parents[1]:
1430 repo.setparents(p1.node(), p2.node())
1431
1432 if opts.get('exact') or importbranch:
1433 repo.dirstate.setbranch(branch or 'default')
1434
1435 partial = opts.get('partial', False)
1436 files = set()
1437 try:
1438 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
1439 files=files, eolmode=None, similarity=sim / 100.0)
1440 except error.PatchError as e:
1441 if not partial:
1442 raise error.Abort(pycompat.bytestr(e))
1443 if partial:
1444 rejects = True
1445
1446 files = list(files)
1447 if nocommit:
1448 if message:
1449 msgs.append(message)
1422 1450 else:
1423 p1, p2 = parents
1424
1425 n = None
1426 if update:
1427 if p1 != parents[0]:
1428 updatefunc(repo, p1.node())
1429 if p2 != parents[1]:
1430 repo.setparents(p1.node(), p2.node())
1431
1432 if opts.get('exact') or importbranch:
1433 repo.dirstate.setbranch(branch or 'default')
1434
1435 partial = opts.get('partial', False)
1451 if opts.get('exact') or p2:
1452 # If you got here, you either use --force and know what
1453 # you are doing or used --exact or a merge patch while
1454 # being updated to its first parent.
1455 m = None
1456 else:
1457 m = scmutil.matchfiles(repo, files or [])
1458 editform = mergeeditform(repo[None], 'import.normal')
1459 if opts.get('exact'):
1460 editor = None
1461 else:
1462 editor = getcommiteditor(editform=editform,
1463 **pycompat.strkwargs(opts))
1464 extra = {}
1465 for idfunc in extrapreimport:
1466 extrapreimportmap[idfunc](repo, patchdata, extra, opts)
1467 overrides = {}
1468 if partial:
1469 overrides[('ui', 'allowemptycommit')] = True
1470 with repo.ui.configoverride(overrides, 'import'):
1471 n = repo.commit(message, user,
1472 date, match=m,
1473 editor=editor, extra=extra)
1474 for idfunc in extrapostimport:
1475 extrapostimportmap[idfunc](repo[n])
1476 else:
1477 if opts.get('exact') or importbranch:
1478 branch = branch or 'default'
1479 else:
1480 branch = p1.branch()
1481 store = patch.filestore()
1482 try:
1436 1483 files = set()
1437 1484 try:
1438 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
1439 files=files, eolmode=None, similarity=sim / 100.0)
1485 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
1486 files, eolmode=None)
1440 1487 except error.PatchError as e:
1441 if not partial:
1442 raise error.Abort(pycompat.bytestr(e))
1443 if partial:
1444 rejects = True
1445
1446 files = list(files)
1447 if nocommit:
1448 if message:
1449 msgs.append(message)
1488 raise error.Abort(stringutil.forcebytestr(e))
1489 if opts.get('exact'):
1490 editor = None
1450 1491 else:
1451 if opts.get('exact') or p2:
1452 # If you got here, you either use --force and know what
1453 # you are doing or used --exact or a merge patch while
1454 # being updated to its first parent.
1455 m = None
1456 else:
1457 m = scmutil.matchfiles(repo, files or [])
1458 editform = mergeeditform(repo[None], 'import.normal')
1459 if opts.get('exact'):
1460 editor = None
1461 else:
1462 editor = getcommiteditor(editform=editform,
1463 **pycompat.strkwargs(opts))
1464 extra = {}
1465 for idfunc in extrapreimport:
1466 extrapreimportmap[idfunc](repo, patchdata, extra, opts)
1467 overrides = {}
1468 if partial:
1469 overrides[('ui', 'allowemptycommit')] = True
1470 with repo.ui.configoverride(overrides, 'import'):
1471 n = repo.commit(message, user,
1472 date, match=m,
1473 editor=editor, extra=extra)
1474 for idfunc in extrapostimport:
1475 extrapostimportmap[idfunc](repo[n])
1476 else:
1477 if opts.get('exact') or importbranch:
1478 branch = branch or 'default'
1479 else:
1480 branch = p1.branch()
1481 store = patch.filestore()
1482 try:
1483 files = set()
1484 try:
1485 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
1486 files, eolmode=None)
1487 except error.PatchError as e:
1488 raise error.Abort(stringutil.forcebytestr(e))
1489 if opts.get('exact'):
1490 editor = None
1491 else:
1492 editor = getcommiteditor(editform='import.bypass')
1493 memctx = context.memctx(repo, (p1.node(), p2.node()),
1494 message,
1495 files=files,
1496 filectxfn=store,
1497 user=user,
1498 date=date,
1499 branch=branch,
1500 editor=editor)
1501 n = memctx.commit()
1502 finally:
1503 store.close()
1504 if opts.get('exact') and nocommit:
1505 # --exact with --no-commit is still useful in that it does merge
1506 # and branch bits
1507 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1508 elif opts.get('exact') and hex(n) != nodeid:
1509 raise error.Abort(_('patch is damaged or loses information'))
1510 msg = _('applied to working directory')
1511 if n:
1512 # i18n: refers to a short changeset id
1513 msg = _('created %s') % short(n)
1514 return (msg, n, rejects)
1515 finally:
1516 os.unlink(tmpname)
1492 editor = getcommiteditor(editform='import.bypass')
1493 memctx = context.memctx(repo, (p1.node(), p2.node()),
1494 message,
1495 files=files,
1496 filectxfn=store,
1497 user=user,
1498 date=date,
1499 branch=branch,
1500 editor=editor)
1501 n = memctx.commit()
1502 finally:
1503 store.close()
1504 if opts.get('exact') and nocommit:
1505 # --exact with --no-commit is still useful in that it does merge
1506 # and branch bits
1507 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1508 elif opts.get('exact') and hex(n) != nodeid:
1509 raise error.Abort(_('patch is damaged or loses information'))
1510 msg = _('applied to working directory')
1511 if n:
1512 # i18n: refers to a short changeset id
1513 msg = _('created %s') % short(n)
1514 return msg, n, rejects
1517 1515
1518 1516 # facility to let extensions include additional data in an exported patch
1519 1517 # list of identifiers to be executed in order
1520 1518 extraexport = []
1521 1519 # mapping from identifier to actual export function
1522 1520 # function as to return a string to be added to the header or None
1523 1521 # it is given two arguments (sequencenumber, changectx)
1524 1522 extraexportmap = {}
1525 1523
1526 1524 def _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts):
1527 1525 node = scmutil.binnode(ctx)
1528 1526 parents = [p.node() for p in ctx.parents() if p]
1529 1527 branch = ctx.branch()
1530 1528 if switch_parent:
1531 1529 parents.reverse()
1532 1530
1533 1531 if parents:
1534 1532 prev = parents[0]
1535 1533 else:
1536 1534 prev = nullid
1537 1535
1538 1536 fm.context(ctx=ctx)
1539 1537 fm.plain('# HG changeset patch\n')
1540 1538 fm.write('user', '# User %s\n', ctx.user())
1541 1539 fm.plain('# Date %d %d\n' % ctx.date())
1542 1540 fm.write('date', '# %s\n', fm.formatdate(ctx.date()))
1543 1541 fm.condwrite(branch and branch != 'default',
1544 1542 'branch', '# Branch %s\n', branch)
1545 1543 fm.write('node', '# Node ID %s\n', hex(node))
1546 1544 fm.plain('# Parent %s\n' % hex(prev))
1547 1545 if len(parents) > 1:
1548 1546 fm.plain('# Parent %s\n' % hex(parents[1]))
1549 1547 fm.data(parents=fm.formatlist(pycompat.maplist(hex, parents), name='node'))
1550 1548
1551 1549 # TODO: redesign extraexportmap function to support formatter
1552 1550 for headerid in extraexport:
1553 1551 header = extraexportmap[headerid](seqno, ctx)
1554 1552 if header is not None:
1555 1553 fm.plain('# %s\n' % header)
1556 1554
1557 1555 fm.write('desc', '%s\n', ctx.description().rstrip())
1558 1556 fm.plain('\n')
1559 1557
1560 1558 if fm.isplain():
1561 1559 chunkiter = patch.diffui(repo, prev, node, match, opts=diffopts)
1562 1560 for chunk, label in chunkiter:
1563 1561 fm.plain(chunk, label=label)
1564 1562 else:
1565 1563 chunkiter = patch.diff(repo, prev, node, match, opts=diffopts)
1566 1564 # TODO: make it structured?
1567 1565 fm.data(diff=b''.join(chunkiter))
1568 1566
1569 1567 def _exportfile(repo, revs, fm, dest, switch_parent, diffopts, match):
1570 1568 """Export changesets to stdout or a single file"""
1571 1569 for seqno, rev in enumerate(revs, 1):
1572 1570 ctx = repo[rev]
1573 1571 if not dest.startswith('<'):
1574 1572 repo.ui.note("%s\n" % dest)
1575 1573 fm.startitem()
1576 1574 _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts)
1577 1575
1578 1576 def _exportfntemplate(repo, revs, basefm, fntemplate, switch_parent, diffopts,
1579 1577 match):
1580 1578 """Export changesets to possibly multiple files"""
1581 1579 total = len(revs)
1582 1580 revwidth = max(len(str(rev)) for rev in revs)
1583 1581 filemap = util.sortdict() # filename: [(seqno, rev), ...]
1584 1582
1585 1583 for seqno, rev in enumerate(revs, 1):
1586 1584 ctx = repo[rev]
1587 1585 dest = makefilename(ctx, fntemplate,
1588 1586 total=total, seqno=seqno, revwidth=revwidth)
1589 1587 filemap.setdefault(dest, []).append((seqno, rev))
1590 1588
1591 1589 for dest in filemap:
1592 1590 with formatter.maybereopen(basefm, dest) as fm:
1593 1591 repo.ui.note("%s\n" % dest)
1594 1592 for seqno, rev in filemap[dest]:
1595 1593 fm.startitem()
1596 1594 ctx = repo[rev]
1597 1595 _exportsingle(repo, ctx, fm, match, switch_parent, seqno,
1598 1596 diffopts)
1599 1597
1600 1598 def export(repo, revs, basefm, fntemplate='hg-%h.patch', switch_parent=False,
1601 1599 opts=None, match=None):
1602 1600 '''export changesets as hg patches
1603 1601
1604 1602 Args:
1605 1603 repo: The repository from which we're exporting revisions.
1606 1604 revs: A list of revisions to export as revision numbers.
1607 1605 basefm: A formatter to which patches should be written.
1608 1606 fntemplate: An optional string to use for generating patch file names.
1609 1607 switch_parent: If True, show diffs against second parent when not nullid.
1610 1608 Default is false, which always shows diff against p1.
1611 1609 opts: diff options to use for generating the patch.
1612 1610 match: If specified, only export changes to files matching this matcher.
1613 1611
1614 1612 Returns:
1615 1613 Nothing.
1616 1614
1617 1615 Side Effect:
1618 1616 "HG Changeset Patch" data is emitted to one of the following
1619 1617 destinations:
1620 1618 fntemplate specified: Each rev is written to a unique file named using
1621 1619 the given template.
1622 1620 Otherwise: All revs will be written to basefm.
1623 1621 '''
1624 1622 if not fntemplate:
1625 1623 _exportfile(repo, revs, basefm, '<unnamed>', switch_parent, opts, match)
1626 1624 else:
1627 1625 _exportfntemplate(repo, revs, basefm, fntemplate, switch_parent, opts,
1628 1626 match)
1629 1627
1630 1628 def exportfile(repo, revs, fp, switch_parent=False, opts=None, match=None):
1631 1629 """Export changesets to the given file stream"""
1632 1630 dest = getattr(fp, 'name', '<unnamed>')
1633 1631 with formatter.formatter(repo.ui, fp, 'export', {}) as fm:
1634 1632 _exportfile(repo, revs, fm, dest, switch_parent, opts, match)
1635 1633
1636 1634 def showmarker(fm, marker, index=None):
1637 1635 """utility function to display obsolescence marker in a readable way
1638 1636
1639 1637 To be used by debug function."""
1640 1638 if index is not None:
1641 1639 fm.write('index', '%i ', index)
1642 1640 fm.write('prednode', '%s ', hex(marker.prednode()))
1643 1641 succs = marker.succnodes()
1644 1642 fm.condwrite(succs, 'succnodes', '%s ',
1645 1643 fm.formatlist(map(hex, succs), name='node'))
1646 1644 fm.write('flag', '%X ', marker.flags())
1647 1645 parents = marker.parentnodes()
1648 1646 if parents is not None:
1649 1647 fm.write('parentnodes', '{%s} ',
1650 1648 fm.formatlist(map(hex, parents), name='node', sep=', '))
1651 1649 fm.write('date', '(%s) ', fm.formatdate(marker.date()))
1652 1650 meta = marker.metadata().copy()
1653 1651 meta.pop('date', None)
1654 1652 smeta = util.rapply(pycompat.maybebytestr, meta)
1655 1653 fm.write('metadata', '{%s}', fm.formatdict(smeta, fmt='%r: %r', sep=', '))
1656 1654 fm.plain('\n')
1657 1655
1658 1656 def finddate(ui, repo, date):
1659 1657 """Find the tipmost changeset that matches the given date spec"""
1660 1658
1661 1659 df = dateutil.matchdate(date)
1662 1660 m = scmutil.matchall(repo)
1663 1661 results = {}
1664 1662
1665 1663 def prep(ctx, fns):
1666 1664 d = ctx.date()
1667 1665 if df(d[0]):
1668 1666 results[ctx.rev()] = d
1669 1667
1670 1668 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1671 1669 rev = ctx.rev()
1672 1670 if rev in results:
1673 1671 ui.status(_("found revision %s from %s\n") %
1674 1672 (rev, dateutil.datestr(results[rev])))
1675 1673 return '%d' % rev
1676 1674
1677 1675 raise error.Abort(_("revision matching date not found"))
1678 1676
1679 1677 def increasingwindows(windowsize=8, sizelimit=512):
1680 1678 while True:
1681 1679 yield windowsize
1682 1680 if windowsize < sizelimit:
1683 1681 windowsize *= 2
1684 1682
1685 1683 def _walkrevs(repo, opts):
1686 1684 # Default --rev value depends on --follow but --follow behavior
1687 1685 # depends on revisions resolved from --rev...
1688 1686 follow = opts.get('follow') or opts.get('follow_first')
1689 1687 if opts.get('rev'):
1690 1688 revs = scmutil.revrange(repo, opts['rev'])
1691 1689 elif follow and repo.dirstate.p1() == nullid:
1692 1690 revs = smartset.baseset()
1693 1691 elif follow:
1694 1692 revs = repo.revs('reverse(:.)')
1695 1693 else:
1696 1694 revs = smartset.spanset(repo)
1697 1695 revs.reverse()
1698 1696 return revs
1699 1697
1700 1698 class FileWalkError(Exception):
1701 1699 pass
1702 1700
1703 1701 def walkfilerevs(repo, match, follow, revs, fncache):
1704 1702 '''Walks the file history for the matched files.
1705 1703
1706 1704 Returns the changeset revs that are involved in the file history.
1707 1705
1708 1706 Throws FileWalkError if the file history can't be walked using
1709 1707 filelogs alone.
1710 1708 '''
1711 1709 wanted = set()
1712 1710 copies = []
1713 1711 minrev, maxrev = min(revs), max(revs)
1714 1712 def filerevgen(filelog, last):
1715 1713 """
1716 1714 Only files, no patterns. Check the history of each file.
1717 1715
1718 1716 Examines filelog entries within minrev, maxrev linkrev range
1719 1717 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1720 1718 tuples in backwards order
1721 1719 """
1722 1720 cl_count = len(repo)
1723 1721 revs = []
1724 1722 for j in xrange(0, last + 1):
1725 1723 linkrev = filelog.linkrev(j)
1726 1724 if linkrev < minrev:
1727 1725 continue
1728 1726 # only yield rev for which we have the changelog, it can
1729 1727 # happen while doing "hg log" during a pull or commit
1730 1728 if linkrev >= cl_count:
1731 1729 break
1732 1730
1733 1731 parentlinkrevs = []
1734 1732 for p in filelog.parentrevs(j):
1735 1733 if p != nullrev:
1736 1734 parentlinkrevs.append(filelog.linkrev(p))
1737 1735 n = filelog.node(j)
1738 1736 revs.append((linkrev, parentlinkrevs,
1739 1737 follow and filelog.renamed(n)))
1740 1738
1741 1739 return reversed(revs)
1742 1740 def iterfiles():
1743 1741 pctx = repo['.']
1744 1742 for filename in match.files():
1745 1743 if follow:
1746 1744 if filename not in pctx:
1747 1745 raise error.Abort(_('cannot follow file not in parent '
1748 1746 'revision: "%s"') % filename)
1749 1747 yield filename, pctx[filename].filenode()
1750 1748 else:
1751 1749 yield filename, None
1752 1750 for filename_node in copies:
1753 1751 yield filename_node
1754 1752
1755 1753 for file_, node in iterfiles():
1756 1754 filelog = repo.file(file_)
1757 1755 if not len(filelog):
1758 1756 if node is None:
1759 1757 # A zero count may be a directory or deleted file, so
1760 1758 # try to find matching entries on the slow path.
1761 1759 if follow:
1762 1760 raise error.Abort(
1763 1761 _('cannot follow nonexistent file: "%s"') % file_)
1764 1762 raise FileWalkError("Cannot walk via filelog")
1765 1763 else:
1766 1764 continue
1767 1765
1768 1766 if node is None:
1769 1767 last = len(filelog) - 1
1770 1768 else:
1771 1769 last = filelog.rev(node)
1772 1770
1773 1771 # keep track of all ancestors of the file
1774 1772 ancestors = {filelog.linkrev(last)}
1775 1773
1776 1774 # iterate from latest to oldest revision
1777 1775 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1778 1776 if not follow:
1779 1777 if rev > maxrev:
1780 1778 continue
1781 1779 else:
1782 1780 # Note that last might not be the first interesting
1783 1781 # rev to us:
1784 1782 # if the file has been changed after maxrev, we'll
1785 1783 # have linkrev(last) > maxrev, and we still need
1786 1784 # to explore the file graph
1787 1785 if rev not in ancestors:
1788 1786 continue
1789 1787 # XXX insert 1327 fix here
1790 1788 if flparentlinkrevs:
1791 1789 ancestors.update(flparentlinkrevs)
1792 1790
1793 1791 fncache.setdefault(rev, []).append(file_)
1794 1792 wanted.add(rev)
1795 1793 if copied:
1796 1794 copies.append(copied)
1797 1795
1798 1796 return wanted
1799 1797
1800 1798 class _followfilter(object):
1801 1799 def __init__(self, repo, onlyfirst=False):
1802 1800 self.repo = repo
1803 1801 self.startrev = nullrev
1804 1802 self.roots = set()
1805 1803 self.onlyfirst = onlyfirst
1806 1804
1807 1805 def match(self, rev):
1808 1806 def realparents(rev):
1809 1807 if self.onlyfirst:
1810 1808 return self.repo.changelog.parentrevs(rev)[0:1]
1811 1809 else:
1812 1810 return filter(lambda x: x != nullrev,
1813 1811 self.repo.changelog.parentrevs(rev))
1814 1812
1815 1813 if self.startrev == nullrev:
1816 1814 self.startrev = rev
1817 1815 return True
1818 1816
1819 1817 if rev > self.startrev:
1820 1818 # forward: all descendants
1821 1819 if not self.roots:
1822 1820 self.roots.add(self.startrev)
1823 1821 for parent in realparents(rev):
1824 1822 if parent in self.roots:
1825 1823 self.roots.add(rev)
1826 1824 return True
1827 1825 else:
1828 1826 # backwards: all parents
1829 1827 if not self.roots:
1830 1828 self.roots.update(realparents(self.startrev))
1831 1829 if rev in self.roots:
1832 1830 self.roots.remove(rev)
1833 1831 self.roots.update(realparents(rev))
1834 1832 return True
1835 1833
1836 1834 return False
1837 1835
1838 1836 def walkchangerevs(repo, match, opts, prepare):
1839 1837 '''Iterate over files and the revs in which they changed.
1840 1838
1841 1839 Callers most commonly need to iterate backwards over the history
1842 1840 in which they are interested. Doing so has awful (quadratic-looking)
1843 1841 performance, so we use iterators in a "windowed" way.
1844 1842
1845 1843 We walk a window of revisions in the desired order. Within the
1846 1844 window, we first walk forwards to gather data, then in the desired
1847 1845 order (usually backwards) to display it.
1848 1846
1849 1847 This function returns an iterator yielding contexts. Before
1850 1848 yielding each context, the iterator will first call the prepare
1851 1849 function on each context in the window in forward order.'''
1852 1850
1853 1851 follow = opts.get('follow') or opts.get('follow_first')
1854 1852 revs = _walkrevs(repo, opts)
1855 1853 if not revs:
1856 1854 return []
1857 1855 wanted = set()
1858 1856 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
1859 1857 fncache = {}
1860 1858 change = repo.__getitem__
1861 1859
1862 1860 # First step is to fill wanted, the set of revisions that we want to yield.
1863 1861 # When it does not induce extra cost, we also fill fncache for revisions in
1864 1862 # wanted: a cache of filenames that were changed (ctx.files()) and that
1865 1863 # match the file filtering conditions.
1866 1864
1867 1865 if match.always():
1868 1866 # No files, no patterns. Display all revs.
1869 1867 wanted = revs
1870 1868 elif not slowpath:
1871 1869 # We only have to read through the filelog to find wanted revisions
1872 1870
1873 1871 try:
1874 1872 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1875 1873 except FileWalkError:
1876 1874 slowpath = True
1877 1875
1878 1876 # We decided to fall back to the slowpath because at least one
1879 1877 # of the paths was not a file. Check to see if at least one of them
1880 1878 # existed in history, otherwise simply return
1881 1879 for path in match.files():
1882 1880 if path == '.' or path in repo.store:
1883 1881 break
1884 1882 else:
1885 1883 return []
1886 1884
1887 1885 if slowpath:
1888 1886 # We have to read the changelog to match filenames against
1889 1887 # changed files
1890 1888
1891 1889 if follow:
1892 1890 raise error.Abort(_('can only follow copies/renames for explicit '
1893 1891 'filenames'))
1894 1892
1895 1893 # The slow path checks files modified in every changeset.
1896 1894 # This is really slow on large repos, so compute the set lazily.
1897 1895 class lazywantedset(object):
1898 1896 def __init__(self):
1899 1897 self.set = set()
1900 1898 self.revs = set(revs)
1901 1899
1902 1900 # No need to worry about locality here because it will be accessed
1903 1901 # in the same order as the increasing window below.
1904 1902 def __contains__(self, value):
1905 1903 if value in self.set:
1906 1904 return True
1907 1905 elif not value in self.revs:
1908 1906 return False
1909 1907 else:
1910 1908 self.revs.discard(value)
1911 1909 ctx = change(value)
1912 1910 matches = [f for f in ctx.files() if match(f)]
1913 1911 if matches:
1914 1912 fncache[value] = matches
1915 1913 self.set.add(value)
1916 1914 return True
1917 1915 return False
1918 1916
1919 1917 def discard(self, value):
1920 1918 self.revs.discard(value)
1921 1919 self.set.discard(value)
1922 1920
1923 1921 wanted = lazywantedset()
1924 1922
1925 1923 # it might be worthwhile to do this in the iterator if the rev range
1926 1924 # is descending and the prune args are all within that range
1927 1925 for rev in opts.get('prune', ()):
1928 1926 rev = repo[rev].rev()
1929 1927 ff = _followfilter(repo)
1930 1928 stop = min(revs[0], revs[-1])
1931 1929 for x in xrange(rev, stop - 1, -1):
1932 1930 if ff.match(x):
1933 1931 wanted = wanted - [x]
1934 1932
1935 1933 # Now that wanted is correctly initialized, we can iterate over the
1936 1934 # revision range, yielding only revisions in wanted.
1937 1935 def iterate():
1938 1936 if follow and match.always():
1939 1937 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
1940 1938 def want(rev):
1941 1939 return ff.match(rev) and rev in wanted
1942 1940 else:
1943 1941 def want(rev):
1944 1942 return rev in wanted
1945 1943
1946 1944 it = iter(revs)
1947 1945 stopiteration = False
1948 1946 for windowsize in increasingwindows():
1949 1947 nrevs = []
1950 1948 for i in xrange(windowsize):
1951 1949 rev = next(it, None)
1952 1950 if rev is None:
1953 1951 stopiteration = True
1954 1952 break
1955 1953 elif want(rev):
1956 1954 nrevs.append(rev)
1957 1955 for rev in sorted(nrevs):
1958 1956 fns = fncache.get(rev)
1959 1957 ctx = change(rev)
1960 1958 if not fns:
1961 1959 def fns_generator():
1962 1960 for f in ctx.files():
1963 1961 if match(f):
1964 1962 yield f
1965 1963 fns = fns_generator()
1966 1964 prepare(ctx, fns)
1967 1965 for rev in nrevs:
1968 1966 yield change(rev)
1969 1967
1970 1968 if stopiteration:
1971 1969 break
1972 1970
1973 1971 return iterate()
1974 1972
1975 1973 def add(ui, repo, match, prefix, explicitonly, **opts):
1976 1974 join = lambda f: os.path.join(prefix, f)
1977 1975 bad = []
1978 1976
1979 1977 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
1980 1978 names = []
1981 1979 wctx = repo[None]
1982 1980 cca = None
1983 1981 abort, warn = scmutil.checkportabilityalert(ui)
1984 1982 if abort or warn:
1985 1983 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
1986 1984
1987 1985 badmatch = matchmod.badmatch(match, badfn)
1988 1986 dirstate = repo.dirstate
1989 1987 # We don't want to just call wctx.walk here, since it would return a lot of
1990 1988 # clean files, which we aren't interested in and takes time.
1991 1989 for f in sorted(dirstate.walk(badmatch, subrepos=sorted(wctx.substate),
1992 1990 unknown=True, ignored=False, full=False)):
1993 1991 exact = match.exact(f)
1994 1992 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
1995 1993 if cca:
1996 1994 cca(f)
1997 1995 names.append(f)
1998 1996 if ui.verbose or not exact:
1999 1997 ui.status(_('adding %s\n') % match.rel(f))
2000 1998
2001 1999 for subpath in sorted(wctx.substate):
2002 2000 sub = wctx.sub(subpath)
2003 2001 try:
2004 2002 submatch = matchmod.subdirmatcher(subpath, match)
2005 2003 if opts.get(r'subrepos'):
2006 2004 bad.extend(sub.add(ui, submatch, prefix, False, **opts))
2007 2005 else:
2008 2006 bad.extend(sub.add(ui, submatch, prefix, True, **opts))
2009 2007 except error.LookupError:
2010 2008 ui.status(_("skipping missing subrepository: %s\n")
2011 2009 % join(subpath))
2012 2010
2013 2011 if not opts.get(r'dry_run'):
2014 2012 rejected = wctx.add(names, prefix)
2015 2013 bad.extend(f for f in rejected if f in match.files())
2016 2014 return bad
2017 2015
2018 2016 def addwebdirpath(repo, serverpath, webconf):
2019 2017 webconf[serverpath] = repo.root
2020 2018 repo.ui.debug('adding %s = %s\n' % (serverpath, repo.root))
2021 2019
2022 2020 for r in repo.revs('filelog("path:.hgsub")'):
2023 2021 ctx = repo[r]
2024 2022 for subpath in ctx.substate:
2025 2023 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2026 2024
2027 2025 def forget(ui, repo, match, prefix, explicitonly, dryrun):
2028 2026 join = lambda f: os.path.join(prefix, f)
2029 2027 bad = []
2030 2028 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2031 2029 wctx = repo[None]
2032 2030 forgot = []
2033 2031
2034 2032 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2035 2033 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2036 2034 if explicitonly:
2037 2035 forget = [f for f in forget if match.exact(f)]
2038 2036
2039 2037 for subpath in sorted(wctx.substate):
2040 2038 sub = wctx.sub(subpath)
2041 2039 try:
2042 2040 submatch = matchmod.subdirmatcher(subpath, match)
2043 2041 subbad, subforgot = sub.forget(submatch, prefix, dryrun=dryrun)
2044 2042 bad.extend([subpath + '/' + f for f in subbad])
2045 2043 forgot.extend([subpath + '/' + f for f in subforgot])
2046 2044 except error.LookupError:
2047 2045 ui.status(_("skipping missing subrepository: %s\n")
2048 2046 % join(subpath))
2049 2047
2050 2048 if not explicitonly:
2051 2049 for f in match.files():
2052 2050 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2053 2051 if f not in forgot:
2054 2052 if repo.wvfs.exists(f):
2055 2053 # Don't complain if the exact case match wasn't given.
2056 2054 # But don't do this until after checking 'forgot', so
2057 2055 # that subrepo files aren't normalized, and this op is
2058 2056 # purely from data cached by the status walk above.
2059 2057 if repo.dirstate.normalize(f) in repo.dirstate:
2060 2058 continue
2061 2059 ui.warn(_('not removing %s: '
2062 2060 'file is already untracked\n')
2063 2061 % match.rel(f))
2064 2062 bad.append(f)
2065 2063
2066 2064 for f in forget:
2067 2065 if ui.verbose or not match.exact(f):
2068 2066 ui.status(_('removing %s\n') % match.rel(f))
2069 2067
2070 2068 if not dryrun:
2071 2069 rejected = wctx.forget(forget, prefix)
2072 2070 bad.extend(f for f in rejected if f in match.files())
2073 2071 forgot.extend(f for f in forget if f not in rejected)
2074 2072 return bad, forgot
2075 2073
2076 2074 def files(ui, ctx, m, fm, fmt, subrepos):
2077 2075 rev = ctx.rev()
2078 2076 ret = 1
2079 2077 ds = ctx.repo().dirstate
2080 2078
2081 2079 for f in ctx.matches(m):
2082 2080 if rev is None and ds[f] == 'r':
2083 2081 continue
2084 2082 fm.startitem()
2085 2083 if ui.verbose:
2086 2084 fc = ctx[f]
2087 2085 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2088 2086 fm.data(abspath=f)
2089 2087 fm.write('path', fmt, m.rel(f))
2090 2088 ret = 0
2091 2089
2092 2090 for subpath in sorted(ctx.substate):
2093 2091 submatch = matchmod.subdirmatcher(subpath, m)
2094 2092 if (subrepos or m.exact(subpath) or any(submatch.files())):
2095 2093 sub = ctx.sub(subpath)
2096 2094 try:
2097 2095 recurse = m.exact(subpath) or subrepos
2098 2096 if sub.printfiles(ui, submatch, fm, fmt, recurse) == 0:
2099 2097 ret = 0
2100 2098 except error.LookupError:
2101 2099 ui.status(_("skipping missing subrepository: %s\n")
2102 2100 % m.abs(subpath))
2103 2101
2104 2102 return ret
2105 2103
2106 2104 def remove(ui, repo, m, prefix, after, force, subrepos, dryrun, warnings=None):
2107 2105 join = lambda f: os.path.join(prefix, f)
2108 2106 ret = 0
2109 2107 s = repo.status(match=m, clean=True)
2110 2108 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2111 2109
2112 2110 wctx = repo[None]
2113 2111
2114 2112 if warnings is None:
2115 2113 warnings = []
2116 2114 warn = True
2117 2115 else:
2118 2116 warn = False
2119 2117
2120 2118 subs = sorted(wctx.substate)
2121 2119 total = len(subs)
2122 2120 count = 0
2123 2121 for subpath in subs:
2124 2122 count += 1
2125 2123 submatch = matchmod.subdirmatcher(subpath, m)
2126 2124 if subrepos or m.exact(subpath) or any(submatch.files()):
2127 2125 ui.progress(_('searching'), count, total=total, unit=_('subrepos'))
2128 2126 sub = wctx.sub(subpath)
2129 2127 try:
2130 2128 if sub.removefiles(submatch, prefix, after, force, subrepos,
2131 2129 dryrun, warnings):
2132 2130 ret = 1
2133 2131 except error.LookupError:
2134 2132 warnings.append(_("skipping missing subrepository: %s\n")
2135 2133 % join(subpath))
2136 2134 ui.progress(_('searching'), None)
2137 2135
2138 2136 # warn about failure to delete explicit files/dirs
2139 2137 deleteddirs = util.dirs(deleted)
2140 2138 files = m.files()
2141 2139 total = len(files)
2142 2140 count = 0
2143 2141 for f in files:
2144 2142 def insubrepo():
2145 2143 for subpath in wctx.substate:
2146 2144 if f.startswith(subpath + '/'):
2147 2145 return True
2148 2146 return False
2149 2147
2150 2148 count += 1
2151 2149 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2152 2150 isdir = f in deleteddirs or wctx.hasdir(f)
2153 2151 if (f in repo.dirstate or isdir or f == '.'
2154 2152 or insubrepo() or f in subs):
2155 2153 continue
2156 2154
2157 2155 if repo.wvfs.exists(f):
2158 2156 if repo.wvfs.isdir(f):
2159 2157 warnings.append(_('not removing %s: no tracked files\n')
2160 2158 % m.rel(f))
2161 2159 else:
2162 2160 warnings.append(_('not removing %s: file is untracked\n')
2163 2161 % m.rel(f))
2164 2162 # missing files will generate a warning elsewhere
2165 2163 ret = 1
2166 2164 ui.progress(_('deleting'), None)
2167 2165
2168 2166 if force:
2169 2167 list = modified + deleted + clean + added
2170 2168 elif after:
2171 2169 list = deleted
2172 2170 remaining = modified + added + clean
2173 2171 total = len(remaining)
2174 2172 count = 0
2175 2173 for f in remaining:
2176 2174 count += 1
2177 2175 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2178 2176 if ui.verbose or (f in files):
2179 2177 warnings.append(_('not removing %s: file still exists\n')
2180 2178 % m.rel(f))
2181 2179 ret = 1
2182 2180 ui.progress(_('skipping'), None)
2183 2181 else:
2184 2182 list = deleted + clean
2185 2183 total = len(modified) + len(added)
2186 2184 count = 0
2187 2185 for f in modified:
2188 2186 count += 1
2189 2187 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2190 2188 warnings.append(_('not removing %s: file is modified (use -f'
2191 2189 ' to force removal)\n') % m.rel(f))
2192 2190 ret = 1
2193 2191 for f in added:
2194 2192 count += 1
2195 2193 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2196 2194 warnings.append(_("not removing %s: file has been marked for add"
2197 2195 " (use 'hg forget' to undo add)\n") % m.rel(f))
2198 2196 ret = 1
2199 2197 ui.progress(_('skipping'), None)
2200 2198
2201 2199 list = sorted(list)
2202 2200 total = len(list)
2203 2201 count = 0
2204 2202 for f in list:
2205 2203 count += 1
2206 2204 if ui.verbose or not m.exact(f):
2207 2205 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2208 2206 ui.status(_('removing %s\n') % m.rel(f))
2209 2207 ui.progress(_('deleting'), None)
2210 2208
2211 2209 if not dryrun:
2212 2210 with repo.wlock():
2213 2211 if not after:
2214 2212 for f in list:
2215 2213 if f in added:
2216 2214 continue # we never unlink added files on remove
2217 2215 repo.wvfs.unlinkpath(f, ignoremissing=True)
2218 2216 repo[None].forget(list)
2219 2217
2220 2218 if warn:
2221 2219 for warning in warnings:
2222 2220 ui.warn(warning)
2223 2221
2224 2222 return ret
2225 2223
2226 2224 def _updatecatformatter(fm, ctx, matcher, path, decode):
2227 2225 """Hook for adding data to the formatter used by ``hg cat``.
2228 2226
2229 2227 Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
2230 2228 this method first."""
2231 2229 data = ctx[path].data()
2232 2230 if decode:
2233 2231 data = ctx.repo().wwritedata(path, data)
2234 2232 fm.startitem()
2235 2233 fm.write('data', '%s', data)
2236 2234 fm.data(abspath=path, path=matcher.rel(path))
2237 2235
2238 2236 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2239 2237 err = 1
2240 2238 opts = pycompat.byteskwargs(opts)
2241 2239
2242 2240 def write(path):
2243 2241 filename = None
2244 2242 if fntemplate:
2245 2243 filename = makefilename(ctx, fntemplate,
2246 2244 pathname=os.path.join(prefix, path))
2247 2245 # attempt to create the directory if it does not already exist
2248 2246 try:
2249 2247 os.makedirs(os.path.dirname(filename))
2250 2248 except OSError:
2251 2249 pass
2252 2250 with formatter.maybereopen(basefm, filename) as fm:
2253 2251 _updatecatformatter(fm, ctx, matcher, path, opts.get('decode'))
2254 2252
2255 2253 # Automation often uses hg cat on single files, so special case it
2256 2254 # for performance to avoid the cost of parsing the manifest.
2257 2255 if len(matcher.files()) == 1 and not matcher.anypats():
2258 2256 file = matcher.files()[0]
2259 2257 mfl = repo.manifestlog
2260 2258 mfnode = ctx.manifestnode()
2261 2259 try:
2262 2260 if mfnode and mfl[mfnode].find(file)[0]:
2263 2261 scmutil.fileprefetchhooks(repo, ctx, [file])
2264 2262 write(file)
2265 2263 return 0
2266 2264 except KeyError:
2267 2265 pass
2268 2266
2269 2267 files = [f for f in ctx.walk(matcher)]
2270 2268 scmutil.fileprefetchhooks(repo, ctx, files)
2271 2269
2272 2270 for abs in files:
2273 2271 write(abs)
2274 2272 err = 0
2275 2273
2276 2274 for subpath in sorted(ctx.substate):
2277 2275 sub = ctx.sub(subpath)
2278 2276 try:
2279 2277 submatch = matchmod.subdirmatcher(subpath, matcher)
2280 2278
2281 2279 if not sub.cat(submatch, basefm, fntemplate,
2282 2280 os.path.join(prefix, sub._path),
2283 2281 **pycompat.strkwargs(opts)):
2284 2282 err = 0
2285 2283 except error.RepoLookupError:
2286 2284 ui.status(_("skipping missing subrepository: %s\n")
2287 2285 % os.path.join(prefix, subpath))
2288 2286
2289 2287 return err
2290 2288
2291 2289 def commit(ui, repo, commitfunc, pats, opts):
2292 2290 '''commit the specified files or all outstanding changes'''
2293 2291 date = opts.get('date')
2294 2292 if date:
2295 2293 opts['date'] = dateutil.parsedate(date)
2296 2294 message = logmessage(ui, opts)
2297 2295 matcher = scmutil.match(repo[None], pats, opts)
2298 2296
2299 2297 dsguard = None
2300 2298 # extract addremove carefully -- this function can be called from a command
2301 2299 # that doesn't support addremove
2302 2300 if opts.get('addremove'):
2303 2301 dsguard = dirstateguard.dirstateguard(repo, 'commit')
2304 2302 with dsguard or util.nullcontextmanager():
2305 2303 if dsguard:
2306 2304 if scmutil.addremove(repo, matcher, "", opts) != 0:
2307 2305 raise error.Abort(
2308 2306 _("failed to mark all new/missing files as added/removed"))
2309 2307
2310 2308 return commitfunc(ui, repo, message, matcher, opts)
2311 2309
2312 2310 def samefile(f, ctx1, ctx2):
2313 2311 if f in ctx1.manifest():
2314 2312 a = ctx1.filectx(f)
2315 2313 if f in ctx2.manifest():
2316 2314 b = ctx2.filectx(f)
2317 2315 return (not a.cmp(b)
2318 2316 and a.flags() == b.flags())
2319 2317 else:
2320 2318 return False
2321 2319 else:
2322 2320 return f not in ctx2.manifest()
2323 2321
2324 2322 def amend(ui, repo, old, extra, pats, opts):
2325 2323 # avoid cycle context -> subrepo -> cmdutil
2326 2324 from . import context
2327 2325
2328 2326 # amend will reuse the existing user if not specified, but the obsolete
2329 2327 # marker creation requires that the current user's name is specified.
2330 2328 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2331 2329 ui.username() # raise exception if username not set
2332 2330
2333 2331 ui.note(_('amending changeset %s\n') % old)
2334 2332 base = old.p1()
2335 2333
2336 2334 with repo.wlock(), repo.lock(), repo.transaction('amend'):
2337 2335 # Participating changesets:
2338 2336 #
2339 2337 # wctx o - workingctx that contains changes from working copy
2340 2338 # | to go into amending commit
2341 2339 # |
2342 2340 # old o - changeset to amend
2343 2341 # |
2344 2342 # base o - first parent of the changeset to amend
2345 2343 wctx = repo[None]
2346 2344
2347 2345 # Copy to avoid mutating input
2348 2346 extra = extra.copy()
2349 2347 # Update extra dict from amended commit (e.g. to preserve graft
2350 2348 # source)
2351 2349 extra.update(old.extra())
2352 2350
2353 2351 # Also update it from the from the wctx
2354 2352 extra.update(wctx.extra())
2355 2353
2356 2354 user = opts.get('user') or old.user()
2357 2355 date = opts.get('date') or old.date()
2358 2356
2359 2357 # Parse the date to allow comparison between date and old.date()
2360 2358 date = dateutil.parsedate(date)
2361 2359
2362 2360 if len(old.parents()) > 1:
2363 2361 # ctx.files() isn't reliable for merges, so fall back to the
2364 2362 # slower repo.status() method
2365 2363 files = set([fn for st in repo.status(base, old)[:3]
2366 2364 for fn in st])
2367 2365 else:
2368 2366 files = set(old.files())
2369 2367
2370 2368 # add/remove the files to the working copy if the "addremove" option
2371 2369 # was specified.
2372 2370 matcher = scmutil.match(wctx, pats, opts)
2373 2371 if (opts.get('addremove')
2374 2372 and scmutil.addremove(repo, matcher, "", opts)):
2375 2373 raise error.Abort(
2376 2374 _("failed to mark all new/missing files as added/removed"))
2377 2375
2378 2376 # Check subrepos. This depends on in-place wctx._status update in
2379 2377 # subrepo.precommit(). To minimize the risk of this hack, we do
2380 2378 # nothing if .hgsub does not exist.
2381 2379 if '.hgsub' in wctx or '.hgsub' in old:
2382 2380 subs, commitsubs, newsubstate = subrepoutil.precommit(
2383 2381 ui, wctx, wctx._status, matcher)
2384 2382 # amend should abort if commitsubrepos is enabled
2385 2383 assert not commitsubs
2386 2384 if subs:
2387 2385 subrepoutil.writestate(repo, newsubstate)
2388 2386
2389 2387 ms = mergemod.mergestate.read(repo)
2390 2388 mergeutil.checkunresolved(ms)
2391 2389
2392 2390 filestoamend = set(f for f in wctx.files() if matcher(f))
2393 2391
2394 2392 changes = (len(filestoamend) > 0)
2395 2393 if changes:
2396 2394 # Recompute copies (avoid recording a -> b -> a)
2397 2395 copied = copies.pathcopies(base, wctx, matcher)
2398 2396 if old.p2:
2399 2397 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
2400 2398
2401 2399 # Prune files which were reverted by the updates: if old
2402 2400 # introduced file X and the file was renamed in the working
2403 2401 # copy, then those two files are the same and
2404 2402 # we can discard X from our list of files. Likewise if X
2405 2403 # was removed, it's no longer relevant. If X is missing (aka
2406 2404 # deleted), old X must be preserved.
2407 2405 files.update(filestoamend)
2408 2406 files = [f for f in files if (not samefile(f, wctx, base)
2409 2407 or f in wctx.deleted())]
2410 2408
2411 2409 def filectxfn(repo, ctx_, path):
2412 2410 try:
2413 2411 # If the file being considered is not amongst the files
2414 2412 # to be amended, we should return the file context from the
2415 2413 # old changeset. This avoids issues when only some files in
2416 2414 # the working copy are being amended but there are also
2417 2415 # changes to other files from the old changeset.
2418 2416 if path not in filestoamend:
2419 2417 return old.filectx(path)
2420 2418
2421 2419 # Return None for removed files.
2422 2420 if path in wctx.removed():
2423 2421 return None
2424 2422
2425 2423 fctx = wctx[path]
2426 2424 flags = fctx.flags()
2427 2425 mctx = context.memfilectx(repo, ctx_,
2428 2426 fctx.path(), fctx.data(),
2429 2427 islink='l' in flags,
2430 2428 isexec='x' in flags,
2431 2429 copied=copied.get(path))
2432 2430 return mctx
2433 2431 except KeyError:
2434 2432 return None
2435 2433 else:
2436 2434 ui.note(_('copying changeset %s to %s\n') % (old, base))
2437 2435
2438 2436 # Use version of files as in the old cset
2439 2437 def filectxfn(repo, ctx_, path):
2440 2438 try:
2441 2439 return old.filectx(path)
2442 2440 except KeyError:
2443 2441 return None
2444 2442
2445 2443 # See if we got a message from -m or -l, if not, open the editor with
2446 2444 # the message of the changeset to amend.
2447 2445 message = logmessage(ui, opts)
2448 2446
2449 2447 editform = mergeeditform(old, 'commit.amend')
2450 2448 editor = getcommiteditor(editform=editform,
2451 2449 **pycompat.strkwargs(opts))
2452 2450
2453 2451 if not message:
2454 2452 editor = getcommiteditor(edit=True, editform=editform)
2455 2453 message = old.description()
2456 2454
2457 2455 pureextra = extra.copy()
2458 2456 extra['amend_source'] = old.hex()
2459 2457
2460 2458 new = context.memctx(repo,
2461 2459 parents=[base.node(), old.p2().node()],
2462 2460 text=message,
2463 2461 files=files,
2464 2462 filectxfn=filectxfn,
2465 2463 user=user,
2466 2464 date=date,
2467 2465 extra=extra,
2468 2466 editor=editor)
2469 2467
2470 2468 newdesc = changelog.stripdesc(new.description())
2471 2469 if ((not changes)
2472 2470 and newdesc == old.description()
2473 2471 and user == old.user()
2474 2472 and date == old.date()
2475 2473 and pureextra == old.extra()):
2476 2474 # nothing changed. continuing here would create a new node
2477 2475 # anyway because of the amend_source noise.
2478 2476 #
2479 2477 # This not what we expect from amend.
2480 2478 return old.node()
2481 2479
2482 2480 if opts.get('secret'):
2483 2481 commitphase = 'secret'
2484 2482 else:
2485 2483 commitphase = old.phase()
2486 2484 overrides = {('phases', 'new-commit'): commitphase}
2487 2485 with ui.configoverride(overrides, 'amend'):
2488 2486 newid = repo.commitctx(new)
2489 2487
2490 2488 # Reroute the working copy parent to the new changeset
2491 2489 repo.setparents(newid, nullid)
2492 2490 mapping = {old.node(): (newid,)}
2493 2491 obsmetadata = None
2494 2492 if opts.get('note'):
2495 2493 obsmetadata = {'note': opts['note']}
2496 2494 scmutil.cleanupnodes(repo, mapping, 'amend', metadata=obsmetadata)
2497 2495
2498 2496 # Fixing the dirstate because localrepo.commitctx does not update
2499 2497 # it. This is rather convenient because we did not need to update
2500 2498 # the dirstate for all the files in the new commit which commitctx
2501 2499 # could have done if it updated the dirstate. Now, we can
2502 2500 # selectively update the dirstate only for the amended files.
2503 2501 dirstate = repo.dirstate
2504 2502
2505 2503 # Update the state of the files which were added and
2506 2504 # and modified in the amend to "normal" in the dirstate.
2507 2505 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
2508 2506 for f in normalfiles:
2509 2507 dirstate.normal(f)
2510 2508
2511 2509 # Update the state of files which were removed in the amend
2512 2510 # to "removed" in the dirstate.
2513 2511 removedfiles = set(wctx.removed()) & filestoamend
2514 2512 for f in removedfiles:
2515 2513 dirstate.drop(f)
2516 2514
2517 2515 return newid
2518 2516
2519 2517 def commiteditor(repo, ctx, subs, editform=''):
2520 2518 if ctx.description():
2521 2519 return ctx.description()
2522 2520 return commitforceeditor(repo, ctx, subs, editform=editform,
2523 2521 unchangedmessagedetection=True)
2524 2522
2525 2523 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2526 2524 editform='', unchangedmessagedetection=False):
2527 2525 if not extramsg:
2528 2526 extramsg = _("Leave message empty to abort commit.")
2529 2527
2530 2528 forms = [e for e in editform.split('.') if e]
2531 2529 forms.insert(0, 'changeset')
2532 2530 templatetext = None
2533 2531 while forms:
2534 2532 ref = '.'.join(forms)
2535 2533 if repo.ui.config('committemplate', ref):
2536 2534 templatetext = committext = buildcommittemplate(
2537 2535 repo, ctx, subs, extramsg, ref)
2538 2536 break
2539 2537 forms.pop()
2540 2538 else:
2541 2539 committext = buildcommittext(repo, ctx, subs, extramsg)
2542 2540
2543 2541 # run editor in the repository root
2544 2542 olddir = pycompat.getcwd()
2545 2543 os.chdir(repo.root)
2546 2544
2547 2545 # make in-memory changes visible to external process
2548 2546 tr = repo.currenttransaction()
2549 2547 repo.dirstate.write(tr)
2550 2548 pending = tr and tr.writepending() and repo.root
2551 2549
2552 2550 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
2553 2551 editform=editform, pending=pending,
2554 2552 repopath=repo.path, action='commit')
2555 2553 text = editortext
2556 2554
2557 2555 # strip away anything below this special string (used for editors that want
2558 2556 # to display the diff)
2559 2557 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
2560 2558 if stripbelow:
2561 2559 text = text[:stripbelow.start()]
2562 2560
2563 2561 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2564 2562 os.chdir(olddir)
2565 2563
2566 2564 if finishdesc:
2567 2565 text = finishdesc(text)
2568 2566 if not text.strip():
2569 2567 raise error.Abort(_("empty commit message"))
2570 2568 if unchangedmessagedetection and editortext == templatetext:
2571 2569 raise error.Abort(_("commit message unchanged"))
2572 2570
2573 2571 return text
2574 2572
2575 2573 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
2576 2574 ui = repo.ui
2577 2575 spec = formatter.templatespec(ref, None, None)
2578 2576 t = logcmdutil.changesettemplater(ui, repo, spec)
2579 2577 t.t.cache.update((k, templater.unquotestring(v))
2580 2578 for k, v in repo.ui.configitems('committemplate'))
2581 2579
2582 2580 if not extramsg:
2583 2581 extramsg = '' # ensure that extramsg is string
2584 2582
2585 2583 ui.pushbuffer()
2586 2584 t.show(ctx, extramsg=extramsg)
2587 2585 return ui.popbuffer()
2588 2586
2589 2587 def hgprefix(msg):
2590 2588 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
2591 2589
2592 2590 def buildcommittext(repo, ctx, subs, extramsg):
2593 2591 edittext = []
2594 2592 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2595 2593 if ctx.description():
2596 2594 edittext.append(ctx.description())
2597 2595 edittext.append("")
2598 2596 edittext.append("") # Empty line between message and comments.
2599 2597 edittext.append(hgprefix(_("Enter commit message."
2600 2598 " Lines beginning with 'HG:' are removed.")))
2601 2599 edittext.append(hgprefix(extramsg))
2602 2600 edittext.append("HG: --")
2603 2601 edittext.append(hgprefix(_("user: %s") % ctx.user()))
2604 2602 if ctx.p2():
2605 2603 edittext.append(hgprefix(_("branch merge")))
2606 2604 if ctx.branch():
2607 2605 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
2608 2606 if bookmarks.isactivewdirparent(repo):
2609 2607 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
2610 2608 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
2611 2609 edittext.extend([hgprefix(_("added %s") % f) for f in added])
2612 2610 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
2613 2611 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
2614 2612 if not added and not modified and not removed:
2615 2613 edittext.append(hgprefix(_("no files changed")))
2616 2614 edittext.append("")
2617 2615
2618 2616 return "\n".join(edittext)
2619 2617
2620 2618 def commitstatus(repo, node, branch, bheads=None, opts=None):
2621 2619 if opts is None:
2622 2620 opts = {}
2623 2621 ctx = repo[node]
2624 2622 parents = ctx.parents()
2625 2623
2626 2624 if (not opts.get('amend') and bheads and node not in bheads and not
2627 2625 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2628 2626 repo.ui.status(_('created new head\n'))
2629 2627 # The message is not printed for initial roots. For the other
2630 2628 # changesets, it is printed in the following situations:
2631 2629 #
2632 2630 # Par column: for the 2 parents with ...
2633 2631 # N: null or no parent
2634 2632 # B: parent is on another named branch
2635 2633 # C: parent is a regular non head changeset
2636 2634 # H: parent was a branch head of the current branch
2637 2635 # Msg column: whether we print "created new head" message
2638 2636 # In the following, it is assumed that there already exists some
2639 2637 # initial branch heads of the current branch, otherwise nothing is
2640 2638 # printed anyway.
2641 2639 #
2642 2640 # Par Msg Comment
2643 2641 # N N y additional topo root
2644 2642 #
2645 2643 # B N y additional branch root
2646 2644 # C N y additional topo head
2647 2645 # H N n usual case
2648 2646 #
2649 2647 # B B y weird additional branch root
2650 2648 # C B y branch merge
2651 2649 # H B n merge with named branch
2652 2650 #
2653 2651 # C C y additional head from merge
2654 2652 # C H n merge with a head
2655 2653 #
2656 2654 # H H n head merge: head count decreases
2657 2655
2658 2656 if not opts.get('close_branch'):
2659 2657 for r in parents:
2660 2658 if r.closesbranch() and r.branch() == branch:
2661 2659 repo.ui.status(_('reopening closed branch head %d\n') % r.rev())
2662 2660
2663 2661 if repo.ui.debugflag:
2664 2662 repo.ui.write(_('committed changeset %d:%s\n') % (ctx.rev(), ctx.hex()))
2665 2663 elif repo.ui.verbose:
2666 2664 repo.ui.write(_('committed changeset %d:%s\n') % (ctx.rev(), ctx))
2667 2665
2668 2666 def postcommitstatus(repo, pats, opts):
2669 2667 return repo.status(match=scmutil.match(repo[None], pats, opts))
2670 2668
2671 2669 def revert(ui, repo, ctx, parents, *pats, **opts):
2672 2670 opts = pycompat.byteskwargs(opts)
2673 2671 parent, p2 = parents
2674 2672 node = ctx.node()
2675 2673
2676 2674 mf = ctx.manifest()
2677 2675 if node == p2:
2678 2676 parent = p2
2679 2677
2680 2678 # need all matching names in dirstate and manifest of target rev,
2681 2679 # so have to walk both. do not print errors if files exist in one
2682 2680 # but not other. in both cases, filesets should be evaluated against
2683 2681 # workingctx to get consistent result (issue4497). this means 'set:**'
2684 2682 # cannot be used to select missing files from target rev.
2685 2683
2686 2684 # `names` is a mapping for all elements in working copy and target revision
2687 2685 # The mapping is in the form:
2688 2686 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2689 2687 names = {}
2690 2688
2691 2689 with repo.wlock():
2692 2690 ## filling of the `names` mapping
2693 2691 # walk dirstate to fill `names`
2694 2692
2695 2693 interactive = opts.get('interactive', False)
2696 2694 wctx = repo[None]
2697 2695 m = scmutil.match(wctx, pats, opts)
2698 2696
2699 2697 # we'll need this later
2700 2698 targetsubs = sorted(s for s in wctx.substate if m(s))
2701 2699
2702 2700 if not m.always():
2703 2701 matcher = matchmod.badmatch(m, lambda x, y: False)
2704 2702 for abs in wctx.walk(matcher):
2705 2703 names[abs] = m.rel(abs), m.exact(abs)
2706 2704
2707 2705 # walk target manifest to fill `names`
2708 2706
2709 2707 def badfn(path, msg):
2710 2708 if path in names:
2711 2709 return
2712 2710 if path in ctx.substate:
2713 2711 return
2714 2712 path_ = path + '/'
2715 2713 for f in names:
2716 2714 if f.startswith(path_):
2717 2715 return
2718 2716 ui.warn("%s: %s\n" % (m.rel(path), msg))
2719 2717
2720 2718 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
2721 2719 if abs not in names:
2722 2720 names[abs] = m.rel(abs), m.exact(abs)
2723 2721
2724 2722 # Find status of all file in `names`.
2725 2723 m = scmutil.matchfiles(repo, names)
2726 2724
2727 2725 changes = repo.status(node1=node, match=m,
2728 2726 unknown=True, ignored=True, clean=True)
2729 2727 else:
2730 2728 changes = repo.status(node1=node, match=m)
2731 2729 for kind in changes:
2732 2730 for abs in kind:
2733 2731 names[abs] = m.rel(abs), m.exact(abs)
2734 2732
2735 2733 m = scmutil.matchfiles(repo, names)
2736 2734
2737 2735 modified = set(changes.modified)
2738 2736 added = set(changes.added)
2739 2737 removed = set(changes.removed)
2740 2738 _deleted = set(changes.deleted)
2741 2739 unknown = set(changes.unknown)
2742 2740 unknown.update(changes.ignored)
2743 2741 clean = set(changes.clean)
2744 2742 modadded = set()
2745 2743
2746 2744 # We need to account for the state of the file in the dirstate,
2747 2745 # even when we revert against something else than parent. This will
2748 2746 # slightly alter the behavior of revert (doing back up or not, delete
2749 2747 # or just forget etc).
2750 2748 if parent == node:
2751 2749 dsmodified = modified
2752 2750 dsadded = added
2753 2751 dsremoved = removed
2754 2752 # store all local modifications, useful later for rename detection
2755 2753 localchanges = dsmodified | dsadded
2756 2754 modified, added, removed = set(), set(), set()
2757 2755 else:
2758 2756 changes = repo.status(node1=parent, match=m)
2759 2757 dsmodified = set(changes.modified)
2760 2758 dsadded = set(changes.added)
2761 2759 dsremoved = set(changes.removed)
2762 2760 # store all local modifications, useful later for rename detection
2763 2761 localchanges = dsmodified | dsadded
2764 2762
2765 2763 # only take into account for removes between wc and target
2766 2764 clean |= dsremoved - removed
2767 2765 dsremoved &= removed
2768 2766 # distinct between dirstate remove and other
2769 2767 removed -= dsremoved
2770 2768
2771 2769 modadded = added & dsmodified
2772 2770 added -= modadded
2773 2771
2774 2772 # tell newly modified apart.
2775 2773 dsmodified &= modified
2776 2774 dsmodified |= modified & dsadded # dirstate added may need backup
2777 2775 modified -= dsmodified
2778 2776
2779 2777 # We need to wait for some post-processing to update this set
2780 2778 # before making the distinction. The dirstate will be used for
2781 2779 # that purpose.
2782 2780 dsadded = added
2783 2781
2784 2782 # in case of merge, files that are actually added can be reported as
2785 2783 # modified, we need to post process the result
2786 2784 if p2 != nullid:
2787 2785 mergeadd = set(dsmodified)
2788 2786 for path in dsmodified:
2789 2787 if path in mf:
2790 2788 mergeadd.remove(path)
2791 2789 dsadded |= mergeadd
2792 2790 dsmodified -= mergeadd
2793 2791
2794 2792 # if f is a rename, update `names` to also revert the source
2795 2793 cwd = repo.getcwd()
2796 2794 for f in localchanges:
2797 2795 src = repo.dirstate.copied(f)
2798 2796 # XXX should we check for rename down to target node?
2799 2797 if src and src not in names and repo.dirstate[src] == 'r':
2800 2798 dsremoved.add(src)
2801 2799 names[src] = (repo.pathto(src, cwd), True)
2802 2800
2803 2801 # determine the exact nature of the deleted changesets
2804 2802 deladded = set(_deleted)
2805 2803 for path in _deleted:
2806 2804 if path in mf:
2807 2805 deladded.remove(path)
2808 2806 deleted = _deleted - deladded
2809 2807
2810 2808 # distinguish between file to forget and the other
2811 2809 added = set()
2812 2810 for abs in dsadded:
2813 2811 if repo.dirstate[abs] != 'a':
2814 2812 added.add(abs)
2815 2813 dsadded -= added
2816 2814
2817 2815 for abs in deladded:
2818 2816 if repo.dirstate[abs] == 'a':
2819 2817 dsadded.add(abs)
2820 2818 deladded -= dsadded
2821 2819
2822 2820 # For files marked as removed, we check if an unknown file is present at
2823 2821 # the same path. If a such file exists it may need to be backed up.
2824 2822 # Making the distinction at this stage helps have simpler backup
2825 2823 # logic.
2826 2824 removunk = set()
2827 2825 for abs in removed:
2828 2826 target = repo.wjoin(abs)
2829 2827 if os.path.lexists(target):
2830 2828 removunk.add(abs)
2831 2829 removed -= removunk
2832 2830
2833 2831 dsremovunk = set()
2834 2832 for abs in dsremoved:
2835 2833 target = repo.wjoin(abs)
2836 2834 if os.path.lexists(target):
2837 2835 dsremovunk.add(abs)
2838 2836 dsremoved -= dsremovunk
2839 2837
2840 2838 # action to be actually performed by revert
2841 2839 # (<list of file>, message>) tuple
2842 2840 actions = {'revert': ([], _('reverting %s\n')),
2843 2841 'add': ([], _('adding %s\n')),
2844 2842 'remove': ([], _('removing %s\n')),
2845 2843 'drop': ([], _('removing %s\n')),
2846 2844 'forget': ([], _('forgetting %s\n')),
2847 2845 'undelete': ([], _('undeleting %s\n')),
2848 2846 'noop': (None, _('no changes needed to %s\n')),
2849 2847 'unknown': (None, _('file not managed: %s\n')),
2850 2848 }
2851 2849
2852 2850 # "constant" that convey the backup strategy.
2853 2851 # All set to `discard` if `no-backup` is set do avoid checking
2854 2852 # no_backup lower in the code.
2855 2853 # These values are ordered for comparison purposes
2856 2854 backupinteractive = 3 # do backup if interactively modified
2857 2855 backup = 2 # unconditionally do backup
2858 2856 check = 1 # check if the existing file differs from target
2859 2857 discard = 0 # never do backup
2860 2858 if opts.get('no_backup'):
2861 2859 backupinteractive = backup = check = discard
2862 2860 if interactive:
2863 2861 dsmodifiedbackup = backupinteractive
2864 2862 else:
2865 2863 dsmodifiedbackup = backup
2866 2864 tobackup = set()
2867 2865
2868 2866 backupanddel = actions['remove']
2869 2867 if not opts.get('no_backup'):
2870 2868 backupanddel = actions['drop']
2871 2869
2872 2870 disptable = (
2873 2871 # dispatch table:
2874 2872 # file state
2875 2873 # action
2876 2874 # make backup
2877 2875
2878 2876 ## Sets that results that will change file on disk
2879 2877 # Modified compared to target, no local change
2880 2878 (modified, actions['revert'], discard),
2881 2879 # Modified compared to target, but local file is deleted
2882 2880 (deleted, actions['revert'], discard),
2883 2881 # Modified compared to target, local change
2884 2882 (dsmodified, actions['revert'], dsmodifiedbackup),
2885 2883 # Added since target
2886 2884 (added, actions['remove'], discard),
2887 2885 # Added in working directory
2888 2886 (dsadded, actions['forget'], discard),
2889 2887 # Added since target, have local modification
2890 2888 (modadded, backupanddel, backup),
2891 2889 # Added since target but file is missing in working directory
2892 2890 (deladded, actions['drop'], discard),
2893 2891 # Removed since target, before working copy parent
2894 2892 (removed, actions['add'], discard),
2895 2893 # Same as `removed` but an unknown file exists at the same path
2896 2894 (removunk, actions['add'], check),
2897 2895 # Removed since targe, marked as such in working copy parent
2898 2896 (dsremoved, actions['undelete'], discard),
2899 2897 # Same as `dsremoved` but an unknown file exists at the same path
2900 2898 (dsremovunk, actions['undelete'], check),
2901 2899 ## the following sets does not result in any file changes
2902 2900 # File with no modification
2903 2901 (clean, actions['noop'], discard),
2904 2902 # Existing file, not tracked anywhere
2905 2903 (unknown, actions['unknown'], discard),
2906 2904 )
2907 2905
2908 2906 for abs, (rel, exact) in sorted(names.items()):
2909 2907 # target file to be touch on disk (relative to cwd)
2910 2908 target = repo.wjoin(abs)
2911 2909 # search the entry in the dispatch table.
2912 2910 # if the file is in any of these sets, it was touched in the working
2913 2911 # directory parent and we are sure it needs to be reverted.
2914 2912 for table, (xlist, msg), dobackup in disptable:
2915 2913 if abs not in table:
2916 2914 continue
2917 2915 if xlist is not None:
2918 2916 xlist.append(abs)
2919 2917 if dobackup:
2920 2918 # If in interactive mode, don't automatically create
2921 2919 # .orig files (issue4793)
2922 2920 if dobackup == backupinteractive:
2923 2921 tobackup.add(abs)
2924 2922 elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
2925 2923 bakname = scmutil.origpath(ui, repo, rel)
2926 2924 ui.note(_('saving current version of %s as %s\n') %
2927 2925 (rel, bakname))
2928 2926 if not opts.get('dry_run'):
2929 2927 if interactive:
2930 2928 util.copyfile(target, bakname)
2931 2929 else:
2932 2930 util.rename(target, bakname)
2933 2931 if ui.verbose or not exact:
2934 2932 if not isinstance(msg, bytes):
2935 2933 msg = msg(abs)
2936 2934 ui.status(msg % rel)
2937 2935 elif exact:
2938 2936 ui.warn(msg % rel)
2939 2937 break
2940 2938
2941 2939 if not opts.get('dry_run'):
2942 2940 needdata = ('revert', 'add', 'undelete')
2943 2941 if _revertprefetch is not _revertprefetchstub:
2944 2942 ui.deprecwarn("'cmdutil._revertprefetch' is deprecated, "
2945 2943 "add a callback to 'scmutil.fileprefetchhooks'",
2946 2944 '4.6', stacklevel=1)
2947 2945 _revertprefetch(repo, ctx,
2948 2946 *[actions[name][0] for name in needdata])
2949 2947 oplist = [actions[name][0] for name in needdata]
2950 2948 prefetch = scmutil.fileprefetchhooks
2951 2949 prefetch(repo, ctx, [f for sublist in oplist for f in sublist])
2952 2950 _performrevert(repo, parents, ctx, actions, interactive, tobackup)
2953 2951
2954 2952 if targetsubs:
2955 2953 # Revert the subrepos on the revert list
2956 2954 for sub in targetsubs:
2957 2955 try:
2958 2956 wctx.sub(sub).revert(ctx.substate[sub], *pats,
2959 2957 **pycompat.strkwargs(opts))
2960 2958 except KeyError:
2961 2959 raise error.Abort("subrepository '%s' does not exist in %s!"
2962 2960 % (sub, short(ctx.node())))
2963 2961
2964 2962 def _revertprefetchstub(repo, ctx, *files):
2965 2963 """Stub method for detecting extension wrapping of _revertprefetch(), to
2966 2964 issue a deprecation warning."""
2967 2965
2968 2966 _revertprefetch = _revertprefetchstub
2969 2967
2970 2968 def _performrevert(repo, parents, ctx, actions, interactive=False,
2971 2969 tobackup=None):
2972 2970 """function that actually perform all the actions computed for revert
2973 2971
2974 2972 This is an independent function to let extension to plug in and react to
2975 2973 the imminent revert.
2976 2974
2977 2975 Make sure you have the working directory locked when calling this function.
2978 2976 """
2979 2977 parent, p2 = parents
2980 2978 node = ctx.node()
2981 2979 excluded_files = []
2982 2980
2983 2981 def checkout(f):
2984 2982 fc = ctx[f]
2985 2983 repo.wwrite(f, fc.data(), fc.flags())
2986 2984
2987 2985 def doremove(f):
2988 2986 try:
2989 2987 repo.wvfs.unlinkpath(f)
2990 2988 except OSError:
2991 2989 pass
2992 2990 repo.dirstate.remove(f)
2993 2991
2994 2992 audit_path = pathutil.pathauditor(repo.root, cached=True)
2995 2993 for f in actions['forget'][0]:
2996 2994 if interactive:
2997 2995 choice = repo.ui.promptchoice(
2998 2996 _("forget added file %s (Yn)?$$ &Yes $$ &No") % f)
2999 2997 if choice == 0:
3000 2998 repo.dirstate.drop(f)
3001 2999 else:
3002 3000 excluded_files.append(f)
3003 3001 else:
3004 3002 repo.dirstate.drop(f)
3005 3003 for f in actions['remove'][0]:
3006 3004 audit_path(f)
3007 3005 if interactive:
3008 3006 choice = repo.ui.promptchoice(
3009 3007 _("remove added file %s (Yn)?$$ &Yes $$ &No") % f)
3010 3008 if choice == 0:
3011 3009 doremove(f)
3012 3010 else:
3013 3011 excluded_files.append(f)
3014 3012 else:
3015 3013 doremove(f)
3016 3014 for f in actions['drop'][0]:
3017 3015 audit_path(f)
3018 3016 repo.dirstate.remove(f)
3019 3017
3020 3018 normal = None
3021 3019 if node == parent:
3022 3020 # We're reverting to our parent. If possible, we'd like status
3023 3021 # to report the file as clean. We have to use normallookup for
3024 3022 # merges to avoid losing information about merged/dirty files.
3025 3023 if p2 != nullid:
3026 3024 normal = repo.dirstate.normallookup
3027 3025 else:
3028 3026 normal = repo.dirstate.normal
3029 3027
3030 3028 newlyaddedandmodifiedfiles = set()
3031 3029 if interactive:
3032 3030 # Prompt the user for changes to revert
3033 3031 torevert = [f for f in actions['revert'][0] if f not in excluded_files]
3034 3032 m = scmutil.matchfiles(repo, torevert)
3035 3033 diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
3036 3034 diffopts.nodates = True
3037 3035 diffopts.git = True
3038 3036 operation = 'discard'
3039 3037 reversehunks = True
3040 3038 if node != parent:
3041 3039 operation = 'apply'
3042 3040 reversehunks = False
3043 3041 if reversehunks:
3044 3042 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3045 3043 else:
3046 3044 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3047 3045 originalchunks = patch.parsepatch(diff)
3048 3046
3049 3047 try:
3050 3048
3051 3049 chunks, opts = recordfilter(repo.ui, originalchunks,
3052 3050 operation=operation)
3053 3051 if reversehunks:
3054 3052 chunks = patch.reversehunks(chunks)
3055 3053
3056 3054 except error.PatchError as err:
3057 3055 raise error.Abort(_('error parsing patch: %s') % err)
3058 3056
3059 3057 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
3060 3058 if tobackup is None:
3061 3059 tobackup = set()
3062 3060 # Apply changes
3063 3061 fp = stringio()
3064 3062 for c in chunks:
3065 3063 # Create a backup file only if this hunk should be backed up
3066 3064 if ishunk(c) and c.header.filename() in tobackup:
3067 3065 abs = c.header.filename()
3068 3066 target = repo.wjoin(abs)
3069 3067 bakname = scmutil.origpath(repo.ui, repo, m.rel(abs))
3070 3068 util.copyfile(target, bakname)
3071 3069 tobackup.remove(abs)
3072 3070 c.write(fp)
3073 3071 dopatch = fp.tell()
3074 3072 fp.seek(0)
3075 3073 if dopatch:
3076 3074 try:
3077 3075 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3078 3076 except error.PatchError as err:
3079 3077 raise error.Abort(pycompat.bytestr(err))
3080 3078 del fp
3081 3079 else:
3082 3080 for f in actions['revert'][0]:
3083 3081 checkout(f)
3084 3082 if normal:
3085 3083 normal(f)
3086 3084
3087 3085 for f in actions['add'][0]:
3088 3086 # Don't checkout modified files, they are already created by the diff
3089 3087 if f not in newlyaddedandmodifiedfiles:
3090 3088 checkout(f)
3091 3089 repo.dirstate.add(f)
3092 3090
3093 3091 normal = repo.dirstate.normallookup
3094 3092 if node == parent and p2 == nullid:
3095 3093 normal = repo.dirstate.normal
3096 3094 for f in actions['undelete'][0]:
3097 3095 checkout(f)
3098 3096 normal(f)
3099 3097
3100 3098 copied = copies.pathcopies(repo[parent], ctx)
3101 3099
3102 3100 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3103 3101 if f in copied:
3104 3102 repo.dirstate.copy(copied[f], f)
3105 3103
3106 3104 class command(registrar.command):
3107 3105 """deprecated: used registrar.command instead"""
3108 3106 def _doregister(self, func, name, *args, **kwargs):
3109 3107 func._deprecatedregistrar = True # flag for deprecwarn in extensions.py
3110 3108 return super(command, self)._doregister(func, name, *args, **kwargs)
3111 3109
3112 3110 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3113 3111 # commands.outgoing. "missing" is "missing" of the result of
3114 3112 # "findcommonoutgoing()"
3115 3113 outgoinghooks = util.hooks()
3116 3114
3117 3115 # a list of (ui, repo) functions called by commands.summary
3118 3116 summaryhooks = util.hooks()
3119 3117
3120 3118 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3121 3119 #
3122 3120 # functions should return tuple of booleans below, if 'changes' is None:
3123 3121 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3124 3122 #
3125 3123 # otherwise, 'changes' is a tuple of tuples below:
3126 3124 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3127 3125 # - (desturl, destbranch, destpeer, outgoing)
3128 3126 summaryremotehooks = util.hooks()
3129 3127
3130 3128 # A list of state files kept by multistep operations like graft.
3131 3129 # Since graft cannot be aborted, it is considered 'clearable' by update.
3132 3130 # note: bisect is intentionally excluded
3133 3131 # (state file, clearable, allowcommit, error, hint)
3134 3132 unfinishedstates = [
3135 3133 ('graftstate', True, False, _('graft in progress'),
3136 3134 _("use 'hg graft --continue' or 'hg update' to abort")),
3137 3135 ('updatestate', True, False, _('last update was interrupted'),
3138 3136 _("use 'hg update' to get a consistent checkout"))
3139 3137 ]
3140 3138
3141 3139 def checkunfinished(repo, commit=False):
3142 3140 '''Look for an unfinished multistep operation, like graft, and abort
3143 3141 if found. It's probably good to check this right before
3144 3142 bailifchanged().
3145 3143 '''
3146 3144 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3147 3145 if commit and allowcommit:
3148 3146 continue
3149 3147 if repo.vfs.exists(f):
3150 3148 raise error.Abort(msg, hint=hint)
3151 3149
3152 3150 def clearunfinished(repo):
3153 3151 '''Check for unfinished operations (as above), and clear the ones
3154 3152 that are clearable.
3155 3153 '''
3156 3154 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3157 3155 if not clearable and repo.vfs.exists(f):
3158 3156 raise error.Abort(msg, hint=hint)
3159 3157 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3160 3158 if clearable and repo.vfs.exists(f):
3161 3159 util.unlink(repo.vfs.join(f))
3162 3160
3163 3161 afterresolvedstates = [
3164 3162 ('graftstate',
3165 3163 _('hg graft --continue')),
3166 3164 ]
3167 3165
3168 3166 def howtocontinue(repo):
3169 3167 '''Check for an unfinished operation and return the command to finish
3170 3168 it.
3171 3169
3172 3170 afterresolvedstates tuples define a .hg/{file} and the corresponding
3173 3171 command needed to finish it.
3174 3172
3175 3173 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3176 3174 a boolean.
3177 3175 '''
3178 3176 contmsg = _("continue: %s")
3179 3177 for f, msg in afterresolvedstates:
3180 3178 if repo.vfs.exists(f):
3181 3179 return contmsg % msg, True
3182 3180 if repo[None].dirty(missing=True, merge=False, branch=False):
3183 3181 return contmsg % _("hg commit"), False
3184 3182 return None, None
3185 3183
3186 3184 def checkafterresolved(repo):
3187 3185 '''Inform the user about the next action after completing hg resolve
3188 3186
3189 3187 If there's a matching afterresolvedstates, howtocontinue will yield
3190 3188 repo.ui.warn as the reporter.
3191 3189
3192 3190 Otherwise, it will yield repo.ui.note.
3193 3191 '''
3194 3192 msg, warning = howtocontinue(repo)
3195 3193 if msg is not None:
3196 3194 if warning:
3197 3195 repo.ui.warn("%s\n" % msg)
3198 3196 else:
3199 3197 repo.ui.note("%s\n" % msg)
3200 3198
3201 3199 def wrongtooltocontinue(repo, task):
3202 3200 '''Raise an abort suggesting how to properly continue if there is an
3203 3201 active task.
3204 3202
3205 3203 Uses howtocontinue() to find the active task.
3206 3204
3207 3205 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3208 3206 a hint.
3209 3207 '''
3210 3208 after = howtocontinue(repo)
3211 3209 hint = None
3212 3210 if after[1]:
3213 3211 hint = after[0]
3214 3212 raise error.Abort(_('no %s in progress') % task, hint=hint)
3215 3213
3216 3214 class changeset_printer(logcmdutil.changesetprinter):
3217 3215
3218 3216 def __init__(self, ui, *args, **kwargs):
3219 3217 msg = ("'cmdutil.changeset_printer' is deprecated, "
3220 3218 "use 'logcmdutil.logcmdutil'")
3221 3219 ui.deprecwarn(msg, "4.6")
3222 3220 super(changeset_printer, self).__init__(ui, *args, **kwargs)
3223 3221
3224 3222 def displaygraph(ui, *args, **kwargs):
3225 3223 msg = ("'cmdutil.displaygraph' is deprecated, "
3226 3224 "use 'logcmdutil.displaygraph'")
3227 3225 ui.deprecwarn(msg, "4.6")
3228 3226 return logcmdutil.displaygraph(ui, *args, **kwargs)
3229 3227
3230 3228 def show_changeset(ui, *args, **kwargs):
3231 3229 msg = ("'cmdutil.show_changeset' is deprecated, "
3232 3230 "use 'logcmdutil.changesetdisplayer'")
3233 3231 ui.deprecwarn(msg, "4.6")
3234 3232 return logcmdutil.changesetdisplayer(ui, *args, **kwargs)
@@ -1,5653 +1,5652 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 38 formatter,
39 39 graphmod,
40 40 hbisect,
41 41 help,
42 42 hg,
43 43 lock as lockmod,
44 44 logcmdutil,
45 45 merge as mergemod,
46 46 obsolete,
47 47 obsutil,
48 48 patch,
49 49 phases,
50 50 pycompat,
51 51 rcutil,
52 52 registrar,
53 53 revsetlang,
54 54 rewriteutil,
55 55 scmutil,
56 56 server,
57 57 streamclone,
58 58 tags as tagsmod,
59 59 templatekw,
60 60 ui as uimod,
61 61 util,
62 62 wireprotoserver,
63 63 )
64 64 from .utils import (
65 65 dateutil,
66 66 procutil,
67 67 stringutil,
68 68 )
69 69
70 70 release = lockmod.release
71 71
72 72 table = {}
73 73 table.update(debugcommandsmod.command._table)
74 74
75 75 command = registrar.command(table)
76 76 readonly = registrar.command.readonly
77 77
78 78 # common command options
79 79
80 80 globalopts = [
81 81 ('R', 'repository', '',
82 82 _('repository root directory or name of overlay bundle file'),
83 83 _('REPO')),
84 84 ('', 'cwd', '',
85 85 _('change working directory'), _('DIR')),
86 86 ('y', 'noninteractive', None,
87 87 _('do not prompt, automatically pick the first choice for all prompts')),
88 88 ('q', 'quiet', None, _('suppress output')),
89 89 ('v', 'verbose', None, _('enable additional output')),
90 90 ('', 'color', '',
91 91 # i18n: 'always', 'auto', 'never', and 'debug' are keywords
92 92 # and should not be translated
93 93 _("when to colorize (boolean, always, auto, never, or debug)"),
94 94 _('TYPE')),
95 95 ('', 'config', [],
96 96 _('set/override config option (use \'section.name=value\')'),
97 97 _('CONFIG')),
98 98 ('', 'debug', None, _('enable debugging output')),
99 99 ('', 'debugger', None, _('start debugger')),
100 100 ('', 'encoding', encoding.encoding, _('set the charset encoding'),
101 101 _('ENCODE')),
102 102 ('', 'encodingmode', encoding.encodingmode,
103 103 _('set the charset encoding mode'), _('MODE')),
104 104 ('', 'traceback', None, _('always print a traceback on exception')),
105 105 ('', 'time', None, _('time how long the command takes')),
106 106 ('', 'profile', None, _('print command execution profile')),
107 107 ('', 'version', None, _('output version information and exit')),
108 108 ('h', 'help', None, _('display help and exit')),
109 109 ('', 'hidden', False, _('consider hidden changesets')),
110 110 ('', 'pager', 'auto',
111 111 _("when to paginate (boolean, always, auto, or never)"), _('TYPE')),
112 112 ]
113 113
114 114 dryrunopts = cmdutil.dryrunopts
115 115 remoteopts = cmdutil.remoteopts
116 116 walkopts = cmdutil.walkopts
117 117 commitopts = cmdutil.commitopts
118 118 commitopts2 = cmdutil.commitopts2
119 119 formatteropts = cmdutil.formatteropts
120 120 templateopts = cmdutil.templateopts
121 121 logopts = cmdutil.logopts
122 122 diffopts = cmdutil.diffopts
123 123 diffwsopts = cmdutil.diffwsopts
124 124 diffopts2 = cmdutil.diffopts2
125 125 mergetoolopts = cmdutil.mergetoolopts
126 126 similarityopts = cmdutil.similarityopts
127 127 subrepoopts = cmdutil.subrepoopts
128 128 debugrevlogopts = cmdutil.debugrevlogopts
129 129
130 130 # Commands start here, listed alphabetically
131 131
132 132 @command('^add',
133 133 walkopts + subrepoopts + dryrunopts,
134 134 _('[OPTION]... [FILE]...'),
135 135 inferrepo=True)
136 136 def add(ui, repo, *pats, **opts):
137 137 """add the specified files on the next commit
138 138
139 139 Schedule files to be version controlled and added to the
140 140 repository.
141 141
142 142 The files will be added to the repository at the next commit. To
143 143 undo an add before that, see :hg:`forget`.
144 144
145 145 If no names are given, add all files to the repository (except
146 146 files matching ``.hgignore``).
147 147
148 148 .. container:: verbose
149 149
150 150 Examples:
151 151
152 152 - New (unknown) files are added
153 153 automatically by :hg:`add`::
154 154
155 155 $ ls
156 156 foo.c
157 157 $ hg status
158 158 ? foo.c
159 159 $ hg add
160 160 adding foo.c
161 161 $ hg status
162 162 A foo.c
163 163
164 164 - Specific files to be added can be specified::
165 165
166 166 $ ls
167 167 bar.c foo.c
168 168 $ hg status
169 169 ? bar.c
170 170 ? foo.c
171 171 $ hg add bar.c
172 172 $ hg status
173 173 A bar.c
174 174 ? foo.c
175 175
176 176 Returns 0 if all files are successfully added.
177 177 """
178 178
179 179 m = scmutil.match(repo[None], pats, pycompat.byteskwargs(opts))
180 180 rejected = cmdutil.add(ui, repo, m, "", False, **opts)
181 181 return rejected and 1 or 0
182 182
183 183 @command('addremove',
184 184 similarityopts + subrepoopts + walkopts + dryrunopts,
185 185 _('[OPTION]... [FILE]...'),
186 186 inferrepo=True)
187 187 def addremove(ui, repo, *pats, **opts):
188 188 """add all new files, delete all missing files
189 189
190 190 Add all new files and remove all missing files from the
191 191 repository.
192 192
193 193 Unless names are given, new files are ignored if they match any of
194 194 the patterns in ``.hgignore``. As with add, these changes take
195 195 effect at the next commit.
196 196
197 197 Use the -s/--similarity option to detect renamed files. This
198 198 option takes a percentage between 0 (disabled) and 100 (files must
199 199 be identical) as its parameter. With a parameter greater than 0,
200 200 this compares every removed file with every added file and records
201 201 those similar enough as renames. Detecting renamed files this way
202 202 can be expensive. After using this option, :hg:`status -C` can be
203 203 used to check which files were identified as moved or renamed. If
204 204 not specified, -s/--similarity defaults to 100 and only renames of
205 205 identical files are detected.
206 206
207 207 .. container:: verbose
208 208
209 209 Examples:
210 210
211 211 - A number of files (bar.c and foo.c) are new,
212 212 while foobar.c has been removed (without using :hg:`remove`)
213 213 from the repository::
214 214
215 215 $ ls
216 216 bar.c foo.c
217 217 $ hg status
218 218 ! foobar.c
219 219 ? bar.c
220 220 ? foo.c
221 221 $ hg addremove
222 222 adding bar.c
223 223 adding foo.c
224 224 removing foobar.c
225 225 $ hg status
226 226 A bar.c
227 227 A foo.c
228 228 R foobar.c
229 229
230 230 - A file foobar.c was moved to foo.c without using :hg:`rename`.
231 231 Afterwards, it was edited slightly::
232 232
233 233 $ ls
234 234 foo.c
235 235 $ hg status
236 236 ! foobar.c
237 237 ? foo.c
238 238 $ hg addremove --similarity 90
239 239 removing foobar.c
240 240 adding foo.c
241 241 recording removal of foobar.c as rename to foo.c (94% similar)
242 242 $ hg status -C
243 243 A foo.c
244 244 foobar.c
245 245 R foobar.c
246 246
247 247 Returns 0 if all files are successfully added.
248 248 """
249 249 opts = pycompat.byteskwargs(opts)
250 250 if not opts.get('similarity'):
251 251 opts['similarity'] = '100'
252 252 matcher = scmutil.match(repo[None], pats, opts)
253 253 return scmutil.addremove(repo, matcher, "", opts)
254 254
255 255 @command('^annotate|blame',
256 256 [('r', 'rev', '', _('annotate the specified revision'), _('REV')),
257 257 ('', 'follow', None,
258 258 _('follow copies/renames and list the filename (DEPRECATED)')),
259 259 ('', 'no-follow', None, _("don't follow copies and renames")),
260 260 ('a', 'text', None, _('treat all files as text')),
261 261 ('u', 'user', None, _('list the author (long with -v)')),
262 262 ('f', 'file', None, _('list the filename')),
263 263 ('d', 'date', None, _('list the date (short with -q)')),
264 264 ('n', 'number', None, _('list the revision number (default)')),
265 265 ('c', 'changeset', None, _('list the changeset')),
266 266 ('l', 'line-number', None, _('show line number at the first appearance')),
267 267 ('', 'skip', [], _('revision to not display (EXPERIMENTAL)'), _('REV')),
268 268 ] + diffwsopts + walkopts + formatteropts,
269 269 _('[-r REV] [-f] [-a] [-u] [-d] [-n] [-c] [-l] FILE...'),
270 270 inferrepo=True)
271 271 def annotate(ui, repo, *pats, **opts):
272 272 """show changeset information by line for each file
273 273
274 274 List changes in files, showing the revision id responsible for
275 275 each line.
276 276
277 277 This command is useful for discovering when a change was made and
278 278 by whom.
279 279
280 280 If you include --file, --user, or --date, the revision number is
281 281 suppressed unless you also include --number.
282 282
283 283 Without the -a/--text option, annotate will avoid processing files
284 284 it detects as binary. With -a, annotate will annotate the file
285 285 anyway, although the results will probably be neither useful
286 286 nor desirable.
287 287
288 288 Returns 0 on success.
289 289 """
290 290 opts = pycompat.byteskwargs(opts)
291 291 if not pats:
292 292 raise error.Abort(_('at least one filename or pattern is required'))
293 293
294 294 if opts.get('follow'):
295 295 # --follow is deprecated and now just an alias for -f/--file
296 296 # to mimic the behavior of Mercurial before version 1.5
297 297 opts['file'] = True
298 298
299 299 rev = opts.get('rev')
300 300 if rev:
301 301 repo = scmutil.unhidehashlikerevs(repo, [rev], 'nowarn')
302 302 ctx = scmutil.revsingle(repo, rev)
303 303
304 304 rootfm = ui.formatter('annotate', opts)
305 305 if ui.quiet:
306 306 datefunc = dateutil.shortdate
307 307 else:
308 308 datefunc = dateutil.datestr
309 309 if ctx.rev() is None:
310 310 def hexfn(node):
311 311 if node is None:
312 312 return None
313 313 else:
314 314 return rootfm.hexfunc(node)
315 315 if opts.get('changeset'):
316 316 # omit "+" suffix which is appended to node hex
317 317 def formatrev(rev):
318 318 if rev is None:
319 319 return '%d' % ctx.p1().rev()
320 320 else:
321 321 return '%d' % rev
322 322 else:
323 323 def formatrev(rev):
324 324 if rev is None:
325 325 return '%d+' % ctx.p1().rev()
326 326 else:
327 327 return '%d ' % rev
328 328 def formathex(hex):
329 329 if hex is None:
330 330 return '%s+' % rootfm.hexfunc(ctx.p1().node())
331 331 else:
332 332 return '%s ' % hex
333 333 else:
334 334 hexfn = rootfm.hexfunc
335 335 formatrev = formathex = pycompat.bytestr
336 336
337 337 opmap = [('user', ' ', lambda x: x.fctx.user(), ui.shortuser),
338 338 ('number', ' ', lambda x: x.fctx.rev(), formatrev),
339 339 ('changeset', ' ', lambda x: hexfn(x.fctx.node()), formathex),
340 340 ('date', ' ', lambda x: x.fctx.date(), util.cachefunc(datefunc)),
341 341 ('file', ' ', lambda x: x.fctx.path(), pycompat.bytestr),
342 342 ('line_number', ':', lambda x: x.lineno, pycompat.bytestr),
343 343 ]
344 344 fieldnamemap = {'number': 'rev', 'changeset': 'node'}
345 345
346 346 if (not opts.get('user') and not opts.get('changeset')
347 347 and not opts.get('date') and not opts.get('file')):
348 348 opts['number'] = True
349 349
350 350 linenumber = opts.get('line_number') is not None
351 351 if linenumber and (not opts.get('changeset')) and (not opts.get('number')):
352 352 raise error.Abort(_('at least one of -n/-c is required for -l'))
353 353
354 354 ui.pager('annotate')
355 355
356 356 if rootfm.isplain():
357 357 def makefunc(get, fmt):
358 358 return lambda x: fmt(get(x))
359 359 else:
360 360 def makefunc(get, fmt):
361 361 return get
362 362 funcmap = [(makefunc(get, fmt), sep) for op, sep, get, fmt in opmap
363 363 if opts.get(op)]
364 364 funcmap[0] = (funcmap[0][0], '') # no separator in front of first column
365 365 fields = ' '.join(fieldnamemap.get(op, op) for op, sep, get, fmt in opmap
366 366 if opts.get(op))
367 367
368 368 def bad(x, y):
369 369 raise error.Abort("%s: %s" % (x, y))
370 370
371 371 m = scmutil.match(ctx, pats, opts, badfn=bad)
372 372
373 373 follow = not opts.get('no_follow')
374 374 diffopts = patch.difffeatureopts(ui, opts, section='annotate',
375 375 whitespace=True)
376 376 skiprevs = opts.get('skip')
377 377 if skiprevs:
378 378 skiprevs = scmutil.revrange(repo, skiprevs)
379 379
380 380 for abs in ctx.walk(m):
381 381 fctx = ctx[abs]
382 382 rootfm.startitem()
383 383 rootfm.data(abspath=abs, path=m.rel(abs))
384 384 if not opts.get('text') and fctx.isbinary():
385 385 rootfm.plain(_("%s: binary file\n")
386 386 % ((pats and m.rel(abs)) or abs))
387 387 continue
388 388
389 389 fm = rootfm.nested('lines', tmpl='{rev}: {line}')
390 390 lines = fctx.annotate(follow=follow, skiprevs=skiprevs,
391 391 diffopts=diffopts)
392 392 if not lines:
393 393 fm.end()
394 394 continue
395 395 formats = []
396 396 pieces = []
397 397
398 398 for f, sep in funcmap:
399 399 l = [f(n) for n in lines]
400 400 if fm.isplain():
401 401 sizes = [encoding.colwidth(x) for x in l]
402 402 ml = max(sizes)
403 403 formats.append([sep + ' ' * (ml - w) + '%s' for w in sizes])
404 404 else:
405 405 formats.append(['%s' for x in l])
406 406 pieces.append(l)
407 407
408 408 for f, p, n in zip(zip(*formats), zip(*pieces), lines):
409 409 fm.startitem()
410 410 fm.context(fctx=n.fctx)
411 411 fm.write(fields, "".join(f), *p)
412 412 if n.skip:
413 413 fmt = "* %s"
414 414 else:
415 415 fmt = ": %s"
416 416 fm.write('line', fmt, n.text)
417 417
418 418 if not lines[-1].text.endswith('\n'):
419 419 fm.plain('\n')
420 420 fm.end()
421 421
422 422 rootfm.end()
423 423
424 424 @command('archive',
425 425 [('', 'no-decode', None, _('do not pass files through decoders')),
426 426 ('p', 'prefix', '', _('directory prefix for files in archive'),
427 427 _('PREFIX')),
428 428 ('r', 'rev', '', _('revision to distribute'), _('REV')),
429 429 ('t', 'type', '', _('type of distribution to create'), _('TYPE')),
430 430 ] + subrepoopts + walkopts,
431 431 _('[OPTION]... DEST'))
432 432 def archive(ui, repo, dest, **opts):
433 433 '''create an unversioned archive of a repository revision
434 434
435 435 By default, the revision used is the parent of the working
436 436 directory; use -r/--rev to specify a different revision.
437 437
438 438 The archive type is automatically detected based on file
439 439 extension (to override, use -t/--type).
440 440
441 441 .. container:: verbose
442 442
443 443 Examples:
444 444
445 445 - create a zip file containing the 1.0 release::
446 446
447 447 hg archive -r 1.0 project-1.0.zip
448 448
449 449 - create a tarball excluding .hg files::
450 450
451 451 hg archive project.tar.gz -X ".hg*"
452 452
453 453 Valid types are:
454 454
455 455 :``files``: a directory full of files (default)
456 456 :``tar``: tar archive, uncompressed
457 457 :``tbz2``: tar archive, compressed using bzip2
458 458 :``tgz``: tar archive, compressed using gzip
459 459 :``uzip``: zip archive, uncompressed
460 460 :``zip``: zip archive, compressed using deflate
461 461
462 462 The exact name of the destination archive or directory is given
463 463 using a format string; see :hg:`help export` for details.
464 464
465 465 Each member added to an archive file has a directory prefix
466 466 prepended. Use -p/--prefix to specify a format string for the
467 467 prefix. The default is the basename of the archive, with suffixes
468 468 removed.
469 469
470 470 Returns 0 on success.
471 471 '''
472 472
473 473 opts = pycompat.byteskwargs(opts)
474 474 rev = opts.get('rev')
475 475 if rev:
476 476 repo = scmutil.unhidehashlikerevs(repo, [rev], 'nowarn')
477 477 ctx = scmutil.revsingle(repo, rev)
478 478 if not ctx:
479 479 raise error.Abort(_('no working directory: please specify a revision'))
480 480 node = ctx.node()
481 481 dest = cmdutil.makefilename(ctx, dest)
482 482 if os.path.realpath(dest) == repo.root:
483 483 raise error.Abort(_('repository root cannot be destination'))
484 484
485 485 kind = opts.get('type') or archival.guesskind(dest) or 'files'
486 486 prefix = opts.get('prefix')
487 487
488 488 if dest == '-':
489 489 if kind == 'files':
490 490 raise error.Abort(_('cannot archive plain files to stdout'))
491 491 dest = cmdutil.makefileobj(ctx, dest)
492 492 if not prefix:
493 493 prefix = os.path.basename(repo.root) + '-%h'
494 494
495 495 prefix = cmdutil.makefilename(ctx, prefix)
496 496 match = scmutil.match(ctx, [], opts)
497 497 archival.archive(repo, dest, node, kind, not opts.get('no_decode'),
498 498 match, prefix, subrepos=opts.get('subrepos'))
499 499
500 500 @command('backout',
501 501 [('', 'merge', None, _('merge with old dirstate parent after backout')),
502 502 ('', 'commit', None,
503 503 _('commit if no conflicts were encountered (DEPRECATED)')),
504 504 ('', 'no-commit', None, _('do not commit')),
505 505 ('', 'parent', '',
506 506 _('parent to choose when backing out merge (DEPRECATED)'), _('REV')),
507 507 ('r', 'rev', '', _('revision to backout'), _('REV')),
508 508 ('e', 'edit', False, _('invoke editor on commit messages')),
509 509 ] + mergetoolopts + walkopts + commitopts + commitopts2,
510 510 _('[OPTION]... [-r] REV'))
511 511 def backout(ui, repo, node=None, rev=None, **opts):
512 512 '''reverse effect of earlier changeset
513 513
514 514 Prepare a new changeset with the effect of REV undone in the
515 515 current working directory. If no conflicts were encountered,
516 516 it will be committed immediately.
517 517
518 518 If REV is the parent of the working directory, then this new changeset
519 519 is committed automatically (unless --no-commit is specified).
520 520
521 521 .. note::
522 522
523 523 :hg:`backout` cannot be used to fix either an unwanted or
524 524 incorrect merge.
525 525
526 526 .. container:: verbose
527 527
528 528 Examples:
529 529
530 530 - Reverse the effect of the parent of the working directory.
531 531 This backout will be committed immediately::
532 532
533 533 hg backout -r .
534 534
535 535 - Reverse the effect of previous bad revision 23::
536 536
537 537 hg backout -r 23
538 538
539 539 - Reverse the effect of previous bad revision 23 and
540 540 leave changes uncommitted::
541 541
542 542 hg backout -r 23 --no-commit
543 543 hg commit -m "Backout revision 23"
544 544
545 545 By default, the pending changeset will have one parent,
546 546 maintaining a linear history. With --merge, the pending
547 547 changeset will instead have two parents: the old parent of the
548 548 working directory and a new child of REV that simply undoes REV.
549 549
550 550 Before version 1.7, the behavior without --merge was equivalent
551 551 to specifying --merge followed by :hg:`update --clean .` to
552 552 cancel the merge and leave the child of REV as a head to be
553 553 merged separately.
554 554
555 555 See :hg:`help dates` for a list of formats valid for -d/--date.
556 556
557 557 See :hg:`help revert` for a way to restore files to the state
558 558 of another revision.
559 559
560 560 Returns 0 on success, 1 if nothing to backout or there are unresolved
561 561 files.
562 562 '''
563 563 wlock = lock = None
564 564 try:
565 565 wlock = repo.wlock()
566 566 lock = repo.lock()
567 567 return _dobackout(ui, repo, node, rev, **opts)
568 568 finally:
569 569 release(lock, wlock)
570 570
571 571 def _dobackout(ui, repo, node=None, rev=None, **opts):
572 572 opts = pycompat.byteskwargs(opts)
573 573 if opts.get('commit') and opts.get('no_commit'):
574 574 raise error.Abort(_("cannot use --commit with --no-commit"))
575 575 if opts.get('merge') and opts.get('no_commit'):
576 576 raise error.Abort(_("cannot use --merge with --no-commit"))
577 577
578 578 if rev and node:
579 579 raise error.Abort(_("please specify just one revision"))
580 580
581 581 if not rev:
582 582 rev = node
583 583
584 584 if not rev:
585 585 raise error.Abort(_("please specify a revision to backout"))
586 586
587 587 date = opts.get('date')
588 588 if date:
589 589 opts['date'] = dateutil.parsedate(date)
590 590
591 591 cmdutil.checkunfinished(repo)
592 592 cmdutil.bailifchanged(repo)
593 593 node = scmutil.revsingle(repo, rev).node()
594 594
595 595 op1, op2 = repo.dirstate.parents()
596 596 if not repo.changelog.isancestor(node, op1):
597 597 raise error.Abort(_('cannot backout change that is not an ancestor'))
598 598
599 599 p1, p2 = repo.changelog.parents(node)
600 600 if p1 == nullid:
601 601 raise error.Abort(_('cannot backout a change with no parents'))
602 602 if p2 != nullid:
603 603 if not opts.get('parent'):
604 604 raise error.Abort(_('cannot backout a merge changeset'))
605 605 p = repo.lookup(opts['parent'])
606 606 if p not in (p1, p2):
607 607 raise error.Abort(_('%s is not a parent of %s') %
608 608 (short(p), short(node)))
609 609 parent = p
610 610 else:
611 611 if opts.get('parent'):
612 612 raise error.Abort(_('cannot use --parent on non-merge changeset'))
613 613 parent = p1
614 614
615 615 # the backout should appear on the same branch
616 616 branch = repo.dirstate.branch()
617 617 bheads = repo.branchheads(branch)
618 618 rctx = scmutil.revsingle(repo, hex(parent))
619 619 if not opts.get('merge') and op1 != node:
620 620 dsguard = dirstateguard.dirstateguard(repo, 'backout')
621 621 try:
622 622 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
623 623 'backout')
624 624 stats = mergemod.update(repo, parent, True, True, node, False)
625 625 repo.setparents(op1, op2)
626 626 dsguard.close()
627 627 hg._showstats(repo, stats)
628 628 if stats.unresolvedcount:
629 629 repo.ui.status(_("use 'hg resolve' to retry unresolved "
630 630 "file merges\n"))
631 631 return 1
632 632 finally:
633 633 ui.setconfig('ui', 'forcemerge', '', '')
634 634 lockmod.release(dsguard)
635 635 else:
636 636 hg.clean(repo, node, show_stats=False)
637 637 repo.dirstate.setbranch(branch)
638 638 cmdutil.revert(ui, repo, rctx, repo.dirstate.parents())
639 639
640 640 if opts.get('no_commit'):
641 641 msg = _("changeset %s backed out, "
642 642 "don't forget to commit.\n")
643 643 ui.status(msg % short(node))
644 644 return 0
645 645
646 646 def commitfunc(ui, repo, message, match, opts):
647 647 editform = 'backout'
648 648 e = cmdutil.getcommiteditor(editform=editform,
649 649 **pycompat.strkwargs(opts))
650 650 if not message:
651 651 # we don't translate commit messages
652 652 message = "Backed out changeset %s" % short(node)
653 653 e = cmdutil.getcommiteditor(edit=True, editform=editform)
654 654 return repo.commit(message, opts.get('user'), opts.get('date'),
655 655 match, editor=e)
656 656 newnode = cmdutil.commit(ui, repo, commitfunc, [], opts)
657 657 if not newnode:
658 658 ui.status(_("nothing changed\n"))
659 659 return 1
660 660 cmdutil.commitstatus(repo, newnode, branch, bheads)
661 661
662 662 def nice(node):
663 663 return '%d:%s' % (repo.changelog.rev(node), short(node))
664 664 ui.status(_('changeset %s backs out changeset %s\n') %
665 665 (nice(repo.changelog.tip()), nice(node)))
666 666 if opts.get('merge') and op1 != node:
667 667 hg.clean(repo, op1, show_stats=False)
668 668 ui.status(_('merging with changeset %s\n')
669 669 % nice(repo.changelog.tip()))
670 670 try:
671 671 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
672 672 'backout')
673 673 return hg.merge(repo, hex(repo.changelog.tip()))
674 674 finally:
675 675 ui.setconfig('ui', 'forcemerge', '', '')
676 676 return 0
677 677
678 678 @command('bisect',
679 679 [('r', 'reset', False, _('reset bisect state')),
680 680 ('g', 'good', False, _('mark changeset good')),
681 681 ('b', 'bad', False, _('mark changeset bad')),
682 682 ('s', 'skip', False, _('skip testing changeset')),
683 683 ('e', 'extend', False, _('extend the bisect range')),
684 684 ('c', 'command', '', _('use command to check changeset state'), _('CMD')),
685 685 ('U', 'noupdate', False, _('do not update to target'))],
686 686 _("[-gbsr] [-U] [-c CMD] [REV]"))
687 687 def bisect(ui, repo, rev=None, extra=None, command=None,
688 688 reset=None, good=None, bad=None, skip=None, extend=None,
689 689 noupdate=None):
690 690 """subdivision search of changesets
691 691
692 692 This command helps to find changesets which introduce problems. To
693 693 use, mark the earliest changeset you know exhibits the problem as
694 694 bad, then mark the latest changeset which is free from the problem
695 695 as good. Bisect will update your working directory to a revision
696 696 for testing (unless the -U/--noupdate option is specified). Once
697 697 you have performed tests, mark the working directory as good or
698 698 bad, and bisect will either update to another candidate changeset
699 699 or announce that it has found the bad revision.
700 700
701 701 As a shortcut, you can also use the revision argument to mark a
702 702 revision as good or bad without checking it out first.
703 703
704 704 If you supply a command, it will be used for automatic bisection.
705 705 The environment variable HG_NODE will contain the ID of the
706 706 changeset being tested. The exit status of the command will be
707 707 used to mark revisions as good or bad: status 0 means good, 125
708 708 means to skip the revision, 127 (command not found) will abort the
709 709 bisection, and any other non-zero exit status means the revision
710 710 is bad.
711 711
712 712 .. container:: verbose
713 713
714 714 Some examples:
715 715
716 716 - start a bisection with known bad revision 34, and good revision 12::
717 717
718 718 hg bisect --bad 34
719 719 hg bisect --good 12
720 720
721 721 - advance the current bisection by marking current revision as good or
722 722 bad::
723 723
724 724 hg bisect --good
725 725 hg bisect --bad
726 726
727 727 - mark the current revision, or a known revision, to be skipped (e.g. if
728 728 that revision is not usable because of another issue)::
729 729
730 730 hg bisect --skip
731 731 hg bisect --skip 23
732 732
733 733 - skip all revisions that do not touch directories ``foo`` or ``bar``::
734 734
735 735 hg bisect --skip "!( file('path:foo') & file('path:bar') )"
736 736
737 737 - forget the current bisection::
738 738
739 739 hg bisect --reset
740 740
741 741 - use 'make && make tests' to automatically find the first broken
742 742 revision::
743 743
744 744 hg bisect --reset
745 745 hg bisect --bad 34
746 746 hg bisect --good 12
747 747 hg bisect --command "make && make tests"
748 748
749 749 - see all changesets whose states are already known in the current
750 750 bisection::
751 751
752 752 hg log -r "bisect(pruned)"
753 753
754 754 - see the changeset currently being bisected (especially useful
755 755 if running with -U/--noupdate)::
756 756
757 757 hg log -r "bisect(current)"
758 758
759 759 - see all changesets that took part in the current bisection::
760 760
761 761 hg log -r "bisect(range)"
762 762
763 763 - you can even get a nice graph::
764 764
765 765 hg log --graph -r "bisect(range)"
766 766
767 767 See :hg:`help revisions.bisect` for more about the `bisect()` predicate.
768 768
769 769 Returns 0 on success.
770 770 """
771 771 # backward compatibility
772 772 if rev in "good bad reset init".split():
773 773 ui.warn(_("(use of 'hg bisect <cmd>' is deprecated)\n"))
774 774 cmd, rev, extra = rev, extra, None
775 775 if cmd == "good":
776 776 good = True
777 777 elif cmd == "bad":
778 778 bad = True
779 779 else:
780 780 reset = True
781 781 elif extra:
782 782 raise error.Abort(_('incompatible arguments'))
783 783
784 784 incompatibles = {
785 785 '--bad': bad,
786 786 '--command': bool(command),
787 787 '--extend': extend,
788 788 '--good': good,
789 789 '--reset': reset,
790 790 '--skip': skip,
791 791 }
792 792
793 793 enabled = [x for x in incompatibles if incompatibles[x]]
794 794
795 795 if len(enabled) > 1:
796 796 raise error.Abort(_('%s and %s are incompatible') %
797 797 tuple(sorted(enabled)[0:2]))
798 798
799 799 if reset:
800 800 hbisect.resetstate(repo)
801 801 return
802 802
803 803 state = hbisect.load_state(repo)
804 804
805 805 # update state
806 806 if good or bad or skip:
807 807 if rev:
808 808 nodes = [repo[i].node() for i in scmutil.revrange(repo, [rev])]
809 809 else:
810 810 nodes = [repo.lookup('.')]
811 811 if good:
812 812 state['good'] += nodes
813 813 elif bad:
814 814 state['bad'] += nodes
815 815 elif skip:
816 816 state['skip'] += nodes
817 817 hbisect.save_state(repo, state)
818 818 if not (state['good'] and state['bad']):
819 819 return
820 820
821 821 def mayupdate(repo, node, show_stats=True):
822 822 """common used update sequence"""
823 823 if noupdate:
824 824 return
825 825 cmdutil.checkunfinished(repo)
826 826 cmdutil.bailifchanged(repo)
827 827 return hg.clean(repo, node, show_stats=show_stats)
828 828
829 829 displayer = logcmdutil.changesetdisplayer(ui, repo, {})
830 830
831 831 if command:
832 832 changesets = 1
833 833 if noupdate:
834 834 try:
835 835 node = state['current'][0]
836 836 except LookupError:
837 837 raise error.Abort(_('current bisect revision is unknown - '
838 838 'start a new bisect to fix'))
839 839 else:
840 840 node, p2 = repo.dirstate.parents()
841 841 if p2 != nullid:
842 842 raise error.Abort(_('current bisect revision is a merge'))
843 843 if rev:
844 844 node = repo[scmutil.revsingle(repo, rev, node)].node()
845 845 try:
846 846 while changesets:
847 847 # update state
848 848 state['current'] = [node]
849 849 hbisect.save_state(repo, state)
850 850 status = ui.system(command, environ={'HG_NODE': hex(node)},
851 851 blockedtag='bisect_check')
852 852 if status == 125:
853 853 transition = "skip"
854 854 elif status == 0:
855 855 transition = "good"
856 856 # status < 0 means process was killed
857 857 elif status == 127:
858 858 raise error.Abort(_("failed to execute %s") % command)
859 859 elif status < 0:
860 860 raise error.Abort(_("%s killed") % command)
861 861 else:
862 862 transition = "bad"
863 863 state[transition].append(node)
864 864 ctx = repo[node]
865 865 ui.status(_('changeset %d:%s: %s\n') % (ctx.rev(), ctx,
866 866 transition))
867 867 hbisect.checkstate(state)
868 868 # bisect
869 869 nodes, changesets, bgood = hbisect.bisect(repo, state)
870 870 # update to next check
871 871 node = nodes[0]
872 872 mayupdate(repo, node, show_stats=False)
873 873 finally:
874 874 state['current'] = [node]
875 875 hbisect.save_state(repo, state)
876 876 hbisect.printresult(ui, repo, state, displayer, nodes, bgood)
877 877 return
878 878
879 879 hbisect.checkstate(state)
880 880
881 881 # actually bisect
882 882 nodes, changesets, good = hbisect.bisect(repo, state)
883 883 if extend:
884 884 if not changesets:
885 885 extendnode = hbisect.extendrange(repo, state, nodes, good)
886 886 if extendnode is not None:
887 887 ui.write(_("Extending search to changeset %d:%s\n")
888 888 % (extendnode.rev(), extendnode))
889 889 state['current'] = [extendnode.node()]
890 890 hbisect.save_state(repo, state)
891 891 return mayupdate(repo, extendnode.node())
892 892 raise error.Abort(_("nothing to extend"))
893 893
894 894 if changesets == 0:
895 895 hbisect.printresult(ui, repo, state, displayer, nodes, good)
896 896 else:
897 897 assert len(nodes) == 1 # only a single node can be tested next
898 898 node = nodes[0]
899 899 # compute the approximate number of remaining tests
900 900 tests, size = 0, 2
901 901 while size <= changesets:
902 902 tests, size = tests + 1, size * 2
903 903 rev = repo.changelog.rev(node)
904 904 ui.write(_("Testing changeset %d:%s "
905 905 "(%d changesets remaining, ~%d tests)\n")
906 906 % (rev, short(node), changesets, tests))
907 907 state['current'] = [node]
908 908 hbisect.save_state(repo, state)
909 909 return mayupdate(repo, node)
910 910
911 911 @command('bookmarks|bookmark',
912 912 [('f', 'force', False, _('force')),
913 913 ('r', 'rev', '', _('revision for bookmark action'), _('REV')),
914 914 ('d', 'delete', False, _('delete a given bookmark')),
915 915 ('m', 'rename', '', _('rename a given bookmark'), _('OLD')),
916 916 ('i', 'inactive', False, _('mark a bookmark inactive')),
917 917 ] + formatteropts,
918 918 _('hg bookmarks [OPTIONS]... [NAME]...'))
919 919 def bookmark(ui, repo, *names, **opts):
920 920 '''create a new bookmark or list existing bookmarks
921 921
922 922 Bookmarks are labels on changesets to help track lines of development.
923 923 Bookmarks are unversioned and can be moved, renamed and deleted.
924 924 Deleting or moving a bookmark has no effect on the associated changesets.
925 925
926 926 Creating or updating to a bookmark causes it to be marked as 'active'.
927 927 The active bookmark is indicated with a '*'.
928 928 When a commit is made, the active bookmark will advance to the new commit.
929 929 A plain :hg:`update` will also advance an active bookmark, if possible.
930 930 Updating away from a bookmark will cause it to be deactivated.
931 931
932 932 Bookmarks can be pushed and pulled between repositories (see
933 933 :hg:`help push` and :hg:`help pull`). If a shared bookmark has
934 934 diverged, a new 'divergent bookmark' of the form 'name@path' will
935 935 be created. Using :hg:`merge` will resolve the divergence.
936 936
937 937 Specifying bookmark as '.' to -m or -d options is equivalent to specifying
938 938 the active bookmark's name.
939 939
940 940 A bookmark named '@' has the special property that :hg:`clone` will
941 941 check it out by default if it exists.
942 942
943 943 .. container:: verbose
944 944
945 945 Examples:
946 946
947 947 - create an active bookmark for a new line of development::
948 948
949 949 hg book new-feature
950 950
951 951 - create an inactive bookmark as a place marker::
952 952
953 953 hg book -i reviewed
954 954
955 955 - create an inactive bookmark on another changeset::
956 956
957 957 hg book -r .^ tested
958 958
959 959 - rename bookmark turkey to dinner::
960 960
961 961 hg book -m turkey dinner
962 962
963 963 - move the '@' bookmark from another branch::
964 964
965 965 hg book -f @
966 966 '''
967 967 force = opts.get(r'force')
968 968 rev = opts.get(r'rev')
969 969 delete = opts.get(r'delete')
970 970 rename = opts.get(r'rename')
971 971 inactive = opts.get(r'inactive')
972 972
973 973 if delete and rename:
974 974 raise error.Abort(_("--delete and --rename are incompatible"))
975 975 if delete and rev:
976 976 raise error.Abort(_("--rev is incompatible with --delete"))
977 977 if rename and rev:
978 978 raise error.Abort(_("--rev is incompatible with --rename"))
979 979 if not names and (delete or rev):
980 980 raise error.Abort(_("bookmark name required"))
981 981
982 982 if delete or rename or names or inactive:
983 983 with repo.wlock(), repo.lock(), repo.transaction('bookmark') as tr:
984 984 if delete:
985 985 names = pycompat.maplist(repo._bookmarks.expandname, names)
986 986 bookmarks.delete(repo, tr, names)
987 987 elif rename:
988 988 if not names:
989 989 raise error.Abort(_("new bookmark name required"))
990 990 elif len(names) > 1:
991 991 raise error.Abort(_("only one new bookmark name allowed"))
992 992 rename = repo._bookmarks.expandname(rename)
993 993 bookmarks.rename(repo, tr, rename, names[0], force, inactive)
994 994 elif names:
995 995 bookmarks.addbookmarks(repo, tr, names, rev, force, inactive)
996 996 elif inactive:
997 997 if len(repo._bookmarks) == 0:
998 998 ui.status(_("no bookmarks set\n"))
999 999 elif not repo._activebookmark:
1000 1000 ui.status(_("no active bookmark\n"))
1001 1001 else:
1002 1002 bookmarks.deactivate(repo)
1003 1003 else: # show bookmarks
1004 1004 bookmarks.printbookmarks(ui, repo, **opts)
1005 1005
1006 1006 @command('branch',
1007 1007 [('f', 'force', None,
1008 1008 _('set branch name even if it shadows an existing branch')),
1009 1009 ('C', 'clean', None, _('reset branch name to parent branch name')),
1010 1010 ('r', 'rev', [], _('change branches of the given revs (EXPERIMENTAL)')),
1011 1011 ],
1012 1012 _('[-fC] [NAME]'))
1013 1013 def branch(ui, repo, label=None, **opts):
1014 1014 """set or show the current branch name
1015 1015
1016 1016 .. note::
1017 1017
1018 1018 Branch names are permanent and global. Use :hg:`bookmark` to create a
1019 1019 light-weight bookmark instead. See :hg:`help glossary` for more
1020 1020 information about named branches and bookmarks.
1021 1021
1022 1022 With no argument, show the current branch name. With one argument,
1023 1023 set the working directory branch name (the branch will not exist
1024 1024 in the repository until the next commit). Standard practice
1025 1025 recommends that primary development take place on the 'default'
1026 1026 branch.
1027 1027
1028 1028 Unless -f/--force is specified, branch will not let you set a
1029 1029 branch name that already exists.
1030 1030
1031 1031 Use -C/--clean to reset the working directory branch to that of
1032 1032 the parent of the working directory, negating a previous branch
1033 1033 change.
1034 1034
1035 1035 Use the command :hg:`update` to switch to an existing branch. Use
1036 1036 :hg:`commit --close-branch` to mark this branch head as closed.
1037 1037 When all heads of a branch are closed, the branch will be
1038 1038 considered closed.
1039 1039
1040 1040 Returns 0 on success.
1041 1041 """
1042 1042 opts = pycompat.byteskwargs(opts)
1043 1043 revs = opts.get('rev')
1044 1044 if label:
1045 1045 label = label.strip()
1046 1046
1047 1047 if not opts.get('clean') and not label:
1048 1048 if revs:
1049 1049 raise error.Abort(_("no branch name specified for the revisions"))
1050 1050 ui.write("%s\n" % repo.dirstate.branch())
1051 1051 return
1052 1052
1053 1053 with repo.wlock():
1054 1054 if opts.get('clean'):
1055 1055 label = repo[None].p1().branch()
1056 1056 repo.dirstate.setbranch(label)
1057 1057 ui.status(_('reset working directory to branch %s\n') % label)
1058 1058 elif label:
1059 1059
1060 1060 scmutil.checknewlabel(repo, label, 'branch')
1061 1061 if revs:
1062 1062 return cmdutil.changebranch(ui, repo, revs, label)
1063 1063
1064 1064 if not opts.get('force') and label in repo.branchmap():
1065 1065 if label not in [p.branch() for p in repo[None].parents()]:
1066 1066 raise error.Abort(_('a branch of the same name already'
1067 1067 ' exists'),
1068 1068 # i18n: "it" refers to an existing branch
1069 1069 hint=_("use 'hg update' to switch to it"))
1070 1070
1071 1071 repo.dirstate.setbranch(label)
1072 1072 ui.status(_('marked working directory as branch %s\n') % label)
1073 1073
1074 1074 # find any open named branches aside from default
1075 1075 others = [n for n, h, t, c in repo.branchmap().iterbranches()
1076 1076 if n != "default" and not c]
1077 1077 if not others:
1078 1078 ui.status(_('(branches are permanent and global, '
1079 1079 'did you want a bookmark?)\n'))
1080 1080
1081 1081 @command('branches',
1082 1082 [('a', 'active', False,
1083 1083 _('show only branches that have unmerged heads (DEPRECATED)')),
1084 1084 ('c', 'closed', False, _('show normal and closed branches')),
1085 1085 ] + formatteropts,
1086 1086 _('[-c]'), cmdtype=readonly)
1087 1087 def branches(ui, repo, active=False, closed=False, **opts):
1088 1088 """list repository named branches
1089 1089
1090 1090 List the repository's named branches, indicating which ones are
1091 1091 inactive. If -c/--closed is specified, also list branches which have
1092 1092 been marked closed (see :hg:`commit --close-branch`).
1093 1093
1094 1094 Use the command :hg:`update` to switch to an existing branch.
1095 1095
1096 1096 Returns 0.
1097 1097 """
1098 1098
1099 1099 opts = pycompat.byteskwargs(opts)
1100 1100 ui.pager('branches')
1101 1101 fm = ui.formatter('branches', opts)
1102 1102 hexfunc = fm.hexfunc
1103 1103
1104 1104 allheads = set(repo.heads())
1105 1105 branches = []
1106 1106 for tag, heads, tip, isclosed in repo.branchmap().iterbranches():
1107 1107 isactive = False
1108 1108 if not isclosed:
1109 1109 openheads = set(repo.branchmap().iteropen(heads))
1110 1110 isactive = bool(openheads & allheads)
1111 1111 branches.append((tag, repo[tip], isactive, not isclosed))
1112 1112 branches.sort(key=lambda i: (i[2], i[1].rev(), i[0], i[3]),
1113 1113 reverse=True)
1114 1114
1115 1115 for tag, ctx, isactive, isopen in branches:
1116 1116 if active and not isactive:
1117 1117 continue
1118 1118 if isactive:
1119 1119 label = 'branches.active'
1120 1120 notice = ''
1121 1121 elif not isopen:
1122 1122 if not closed:
1123 1123 continue
1124 1124 label = 'branches.closed'
1125 1125 notice = _(' (closed)')
1126 1126 else:
1127 1127 label = 'branches.inactive'
1128 1128 notice = _(' (inactive)')
1129 1129 current = (tag == repo.dirstate.branch())
1130 1130 if current:
1131 1131 label = 'branches.current'
1132 1132
1133 1133 fm.startitem()
1134 1134 fm.write('branch', '%s', tag, label=label)
1135 1135 rev = ctx.rev()
1136 1136 padsize = max(31 - len("%d" % rev) - encoding.colwidth(tag), 0)
1137 1137 fmt = ' ' * padsize + ' %d:%s'
1138 1138 fm.condwrite(not ui.quiet, 'rev node', fmt, rev, hexfunc(ctx.node()),
1139 1139 label='log.changeset changeset.%s' % ctx.phasestr())
1140 1140 fm.context(ctx=ctx)
1141 1141 fm.data(active=isactive, closed=not isopen, current=current)
1142 1142 if not ui.quiet:
1143 1143 fm.plain(notice)
1144 1144 fm.plain('\n')
1145 1145 fm.end()
1146 1146
1147 1147 @command('bundle',
1148 1148 [('f', 'force', None, _('run even when the destination is unrelated')),
1149 1149 ('r', 'rev', [], _('a changeset intended to be added to the destination'),
1150 1150 _('REV')),
1151 1151 ('b', 'branch', [], _('a specific branch you would like to bundle'),
1152 1152 _('BRANCH')),
1153 1153 ('', 'base', [],
1154 1154 _('a base changeset assumed to be available at the destination'),
1155 1155 _('REV')),
1156 1156 ('a', 'all', None, _('bundle all changesets in the repository')),
1157 1157 ('t', 'type', 'bzip2', _('bundle compression type to use'), _('TYPE')),
1158 1158 ] + remoteopts,
1159 1159 _('[-f] [-t BUNDLESPEC] [-a] [-r REV]... [--base REV]... FILE [DEST]'))
1160 1160 def bundle(ui, repo, fname, dest=None, **opts):
1161 1161 """create a bundle file
1162 1162
1163 1163 Generate a bundle file containing data to be transferred to another
1164 1164 repository.
1165 1165
1166 1166 To create a bundle containing all changesets, use -a/--all
1167 1167 (or --base null). Otherwise, hg assumes the destination will have
1168 1168 all the nodes you specify with --base parameters. Otherwise, hg
1169 1169 will assume the repository has all the nodes in destination, or
1170 1170 default-push/default if no destination is specified, where destination
1171 1171 is the repository you provide through DEST option.
1172 1172
1173 1173 You can change bundle format with the -t/--type option. See
1174 1174 :hg:`help bundlespec` for documentation on this format. By default,
1175 1175 the most appropriate format is used and compression defaults to
1176 1176 bzip2.
1177 1177
1178 1178 The bundle file can then be transferred using conventional means
1179 1179 and applied to another repository with the unbundle or pull
1180 1180 command. This is useful when direct push and pull are not
1181 1181 available or when exporting an entire repository is undesirable.
1182 1182
1183 1183 Applying bundles preserves all changeset contents including
1184 1184 permissions, copy/rename information, and revision history.
1185 1185
1186 1186 Returns 0 on success, 1 if no changes found.
1187 1187 """
1188 1188 opts = pycompat.byteskwargs(opts)
1189 1189 revs = None
1190 1190 if 'rev' in opts:
1191 1191 revstrings = opts['rev']
1192 1192 revs = scmutil.revrange(repo, revstrings)
1193 1193 if revstrings and not revs:
1194 1194 raise error.Abort(_('no commits to bundle'))
1195 1195
1196 1196 bundletype = opts.get('type', 'bzip2').lower()
1197 1197 try:
1198 1198 bundlespec = exchange.parsebundlespec(repo, bundletype, strict=False)
1199 1199 except error.UnsupportedBundleSpecification as e:
1200 1200 raise error.Abort(pycompat.bytestr(e),
1201 1201 hint=_("see 'hg help bundlespec' for supported "
1202 1202 "values for --type"))
1203 1203 cgversion = bundlespec.contentopts["cg.version"]
1204 1204
1205 1205 # Packed bundles are a pseudo bundle format for now.
1206 1206 if cgversion == 's1':
1207 1207 raise error.Abort(_('packed bundles cannot be produced by "hg bundle"'),
1208 1208 hint=_("use 'hg debugcreatestreamclonebundle'"))
1209 1209
1210 1210 if opts.get('all'):
1211 1211 if dest:
1212 1212 raise error.Abort(_("--all is incompatible with specifying "
1213 1213 "a destination"))
1214 1214 if opts.get('base'):
1215 1215 ui.warn(_("ignoring --base because --all was specified\n"))
1216 1216 base = ['null']
1217 1217 else:
1218 1218 base = scmutil.revrange(repo, opts.get('base'))
1219 1219 if cgversion not in changegroup.supportedoutgoingversions(repo):
1220 1220 raise error.Abort(_("repository does not support bundle version %s") %
1221 1221 cgversion)
1222 1222
1223 1223 if base:
1224 1224 if dest:
1225 1225 raise error.Abort(_("--base is incompatible with specifying "
1226 1226 "a destination"))
1227 1227 common = [repo[rev].node() for rev in base]
1228 1228 heads = [repo[r].node() for r in revs] if revs else None
1229 1229 outgoing = discovery.outgoing(repo, common, heads)
1230 1230 else:
1231 1231 dest = ui.expandpath(dest or 'default-push', dest or 'default')
1232 1232 dest, branches = hg.parseurl(dest, opts.get('branch'))
1233 1233 other = hg.peer(repo, opts, dest)
1234 1234 revs = [repo[r].hex() for r in revs]
1235 1235 revs, checkout = hg.addbranchrevs(repo, repo, branches, revs)
1236 1236 heads = revs and map(repo.lookup, revs) or revs
1237 1237 outgoing = discovery.findcommonoutgoing(repo, other,
1238 1238 onlyheads=heads,
1239 1239 force=opts.get('force'),
1240 1240 portable=True)
1241 1241
1242 1242 if not outgoing.missing:
1243 1243 scmutil.nochangesfound(ui, repo, not base and outgoing.excluded)
1244 1244 return 1
1245 1245
1246 1246 bcompression = bundlespec.compression
1247 1247 if cgversion == '01': #bundle1
1248 1248 if bcompression is None:
1249 1249 bcompression = 'UN'
1250 1250 bversion = 'HG10' + bcompression
1251 1251 bcompression = None
1252 1252 elif cgversion in ('02', '03'):
1253 1253 bversion = 'HG20'
1254 1254 else:
1255 1255 raise error.ProgrammingError(
1256 1256 'bundle: unexpected changegroup version %s' % cgversion)
1257 1257
1258 1258 # TODO compression options should be derived from bundlespec parsing.
1259 1259 # This is a temporary hack to allow adjusting bundle compression
1260 1260 # level without a) formalizing the bundlespec changes to declare it
1261 1261 # b) introducing a command flag.
1262 1262 compopts = {}
1263 1263 complevel = ui.configint('experimental', 'bundlecomplevel')
1264 1264 if complevel is not None:
1265 1265 compopts['level'] = complevel
1266 1266
1267 1267 # Allow overriding the bundling of obsmarker in phases through
1268 1268 # configuration while we don't have a bundle version that include them
1269 1269 if repo.ui.configbool('experimental', 'evolution.bundle-obsmarker'):
1270 1270 bundlespec.contentopts['obsolescence'] = True
1271 1271 if repo.ui.configbool('experimental', 'bundle-phases'):
1272 1272 bundlespec.contentopts['phases'] = True
1273 1273
1274 1274 bundle2.writenewbundle(ui, repo, 'bundle', fname, bversion, outgoing,
1275 1275 bundlespec.contentopts, compression=bcompression,
1276 1276 compopts=compopts)
1277 1277
1278 1278 @command('cat',
1279 1279 [('o', 'output', '',
1280 1280 _('print output to file with formatted name'), _('FORMAT')),
1281 1281 ('r', 'rev', '', _('print the given revision'), _('REV')),
1282 1282 ('', 'decode', None, _('apply any matching decode filter')),
1283 1283 ] + walkopts + formatteropts,
1284 1284 _('[OPTION]... FILE...'),
1285 1285 inferrepo=True, cmdtype=readonly)
1286 1286 def cat(ui, repo, file1, *pats, **opts):
1287 1287 """output the current or given revision of files
1288 1288
1289 1289 Print the specified files as they were at the given revision. If
1290 1290 no revision is given, the parent of the working directory is used.
1291 1291
1292 1292 Output may be to a file, in which case the name of the file is
1293 1293 given using a template string. See :hg:`help templates`. In addition
1294 1294 to the common template keywords, the following formatting rules are
1295 1295 supported:
1296 1296
1297 1297 :``%%``: literal "%" character
1298 1298 :``%s``: basename of file being printed
1299 1299 :``%d``: dirname of file being printed, or '.' if in repository root
1300 1300 :``%p``: root-relative path name of file being printed
1301 1301 :``%H``: changeset hash (40 hexadecimal digits)
1302 1302 :``%R``: changeset revision number
1303 1303 :``%h``: short-form changeset hash (12 hexadecimal digits)
1304 1304 :``%r``: zero-padded changeset revision number
1305 1305 :``%b``: basename of the exporting repository
1306 1306 :``\\``: literal "\\" character
1307 1307
1308 1308 Returns 0 on success.
1309 1309 """
1310 1310 opts = pycompat.byteskwargs(opts)
1311 1311 rev = opts.get('rev')
1312 1312 if rev:
1313 1313 repo = scmutil.unhidehashlikerevs(repo, [rev], 'nowarn')
1314 1314 ctx = scmutil.revsingle(repo, rev)
1315 1315 m = scmutil.match(ctx, (file1,) + pats, opts)
1316 1316 fntemplate = opts.pop('output', '')
1317 1317 if cmdutil.isstdiofilename(fntemplate):
1318 1318 fntemplate = ''
1319 1319
1320 1320 if fntemplate:
1321 1321 fm = formatter.nullformatter(ui, 'cat', opts)
1322 1322 else:
1323 1323 ui.pager('cat')
1324 1324 fm = ui.formatter('cat', opts)
1325 1325 with fm:
1326 1326 return cmdutil.cat(ui, repo, ctx, m, fm, fntemplate, '',
1327 1327 **pycompat.strkwargs(opts))
1328 1328
1329 1329 @command('^clone',
1330 1330 [('U', 'noupdate', None, _('the clone will include an empty working '
1331 1331 'directory (only a repository)')),
1332 1332 ('u', 'updaterev', '', _('revision, tag, or branch to check out'),
1333 1333 _('REV')),
1334 1334 ('r', 'rev', [], _('do not clone everything, but include this changeset'
1335 1335 ' and its ancestors'), _('REV')),
1336 1336 ('b', 'branch', [], _('do not clone everything, but include this branch\'s'
1337 1337 ' changesets and their ancestors'), _('BRANCH')),
1338 1338 ('', 'pull', None, _('use pull protocol to copy metadata')),
1339 1339 ('', 'uncompressed', None,
1340 1340 _('an alias to --stream (DEPRECATED)')),
1341 1341 ('', 'stream', None,
1342 1342 _('clone with minimal data processing')),
1343 1343 ] + remoteopts,
1344 1344 _('[OPTION]... SOURCE [DEST]'),
1345 1345 norepo=True)
1346 1346 def clone(ui, source, dest=None, **opts):
1347 1347 """make a copy of an existing repository
1348 1348
1349 1349 Create a copy of an existing repository in a new directory.
1350 1350
1351 1351 If no destination directory name is specified, it defaults to the
1352 1352 basename of the source.
1353 1353
1354 1354 The location of the source is added to the new repository's
1355 1355 ``.hg/hgrc`` file, as the default to be used for future pulls.
1356 1356
1357 1357 Only local paths and ``ssh://`` URLs are supported as
1358 1358 destinations. For ``ssh://`` destinations, no working directory or
1359 1359 ``.hg/hgrc`` will be created on the remote side.
1360 1360
1361 1361 If the source repository has a bookmark called '@' set, that
1362 1362 revision will be checked out in the new repository by default.
1363 1363
1364 1364 To check out a particular version, use -u/--update, or
1365 1365 -U/--noupdate to create a clone with no working directory.
1366 1366
1367 1367 To pull only a subset of changesets, specify one or more revisions
1368 1368 identifiers with -r/--rev or branches with -b/--branch. The
1369 1369 resulting clone will contain only the specified changesets and
1370 1370 their ancestors. These options (or 'clone src#rev dest') imply
1371 1371 --pull, even for local source repositories.
1372 1372
1373 1373 In normal clone mode, the remote normalizes repository data into a common
1374 1374 exchange format and the receiving end translates this data into its local
1375 1375 storage format. --stream activates a different clone mode that essentially
1376 1376 copies repository files from the remote with minimal data processing. This
1377 1377 significantly reduces the CPU cost of a clone both remotely and locally.
1378 1378 However, it often increases the transferred data size by 30-40%. This can
1379 1379 result in substantially faster clones where I/O throughput is plentiful,
1380 1380 especially for larger repositories. A side-effect of --stream clones is
1381 1381 that storage settings and requirements on the remote are applied locally:
1382 1382 a modern client may inherit legacy or inefficient storage used by the
1383 1383 remote or a legacy Mercurial client may not be able to clone from a
1384 1384 modern Mercurial remote.
1385 1385
1386 1386 .. note::
1387 1387
1388 1388 Specifying a tag will include the tagged changeset but not the
1389 1389 changeset containing the tag.
1390 1390
1391 1391 .. container:: verbose
1392 1392
1393 1393 For efficiency, hardlinks are used for cloning whenever the
1394 1394 source and destination are on the same filesystem (note this
1395 1395 applies only to the repository data, not to the working
1396 1396 directory). Some filesystems, such as AFS, implement hardlinking
1397 1397 incorrectly, but do not report errors. In these cases, use the
1398 1398 --pull option to avoid hardlinking.
1399 1399
1400 1400 Mercurial will update the working directory to the first applicable
1401 1401 revision from this list:
1402 1402
1403 1403 a) null if -U or the source repository has no changesets
1404 1404 b) if -u . and the source repository is local, the first parent of
1405 1405 the source repository's working directory
1406 1406 c) the changeset specified with -u (if a branch name, this means the
1407 1407 latest head of that branch)
1408 1408 d) the changeset specified with -r
1409 1409 e) the tipmost head specified with -b
1410 1410 f) the tipmost head specified with the url#branch source syntax
1411 1411 g) the revision marked with the '@' bookmark, if present
1412 1412 h) the tipmost head of the default branch
1413 1413 i) tip
1414 1414
1415 1415 When cloning from servers that support it, Mercurial may fetch
1416 1416 pre-generated data from a server-advertised URL or inline from the
1417 1417 same stream. When this is done, hooks operating on incoming changesets
1418 1418 and changegroups may fire more than once, once for each pre-generated
1419 1419 bundle and as well as for any additional remaining data. In addition,
1420 1420 if an error occurs, the repository may be rolled back to a partial
1421 1421 clone. This behavior may change in future releases.
1422 1422 See :hg:`help -e clonebundles` for more.
1423 1423
1424 1424 Examples:
1425 1425
1426 1426 - clone a remote repository to a new directory named hg/::
1427 1427
1428 1428 hg clone https://www.mercurial-scm.org/repo/hg/
1429 1429
1430 1430 - create a lightweight local clone::
1431 1431
1432 1432 hg clone project/ project-feature/
1433 1433
1434 1434 - clone from an absolute path on an ssh server (note double-slash)::
1435 1435
1436 1436 hg clone ssh://user@server//home/projects/alpha/
1437 1437
1438 1438 - do a streaming clone while checking out a specified version::
1439 1439
1440 1440 hg clone --stream http://server/repo -u 1.5
1441 1441
1442 1442 - create a repository without changesets after a particular revision::
1443 1443
1444 1444 hg clone -r 04e544 experimental/ good/
1445 1445
1446 1446 - clone (and track) a particular named branch::
1447 1447
1448 1448 hg clone https://www.mercurial-scm.org/repo/hg/#stable
1449 1449
1450 1450 See :hg:`help urls` for details on specifying URLs.
1451 1451
1452 1452 Returns 0 on success.
1453 1453 """
1454 1454 opts = pycompat.byteskwargs(opts)
1455 1455 if opts.get('noupdate') and opts.get('updaterev'):
1456 1456 raise error.Abort(_("cannot specify both --noupdate and --updaterev"))
1457 1457
1458 1458 r = hg.clone(ui, opts, source, dest,
1459 1459 pull=opts.get('pull'),
1460 1460 stream=opts.get('stream') or opts.get('uncompressed'),
1461 1461 revs=opts.get('rev'),
1462 1462 update=opts.get('updaterev') or not opts.get('noupdate'),
1463 1463 branch=opts.get('branch'),
1464 1464 shareopts=opts.get('shareopts'))
1465 1465
1466 1466 return r is None
1467 1467
1468 1468 @command('^commit|ci',
1469 1469 [('A', 'addremove', None,
1470 1470 _('mark new/missing files as added/removed before committing')),
1471 1471 ('', 'close-branch', None,
1472 1472 _('mark a branch head as closed')),
1473 1473 ('', 'amend', None, _('amend the parent of the working directory')),
1474 1474 ('s', 'secret', None, _('use the secret phase for committing')),
1475 1475 ('e', 'edit', None, _('invoke editor on commit messages')),
1476 1476 ('i', 'interactive', None, _('use interactive mode')),
1477 1477 ] + walkopts + commitopts + commitopts2 + subrepoopts,
1478 1478 _('[OPTION]... [FILE]...'),
1479 1479 inferrepo=True)
1480 1480 def commit(ui, repo, *pats, **opts):
1481 1481 """commit the specified files or all outstanding changes
1482 1482
1483 1483 Commit changes to the given files into the repository. Unlike a
1484 1484 centralized SCM, this operation is a local operation. See
1485 1485 :hg:`push` for a way to actively distribute your changes.
1486 1486
1487 1487 If a list of files is omitted, all changes reported by :hg:`status`
1488 1488 will be committed.
1489 1489
1490 1490 If you are committing the result of a merge, do not provide any
1491 1491 filenames or -I/-X filters.
1492 1492
1493 1493 If no commit message is specified, Mercurial starts your
1494 1494 configured editor where you can enter a message. In case your
1495 1495 commit fails, you will find a backup of your message in
1496 1496 ``.hg/last-message.txt``.
1497 1497
1498 1498 The --close-branch flag can be used to mark the current branch
1499 1499 head closed. When all heads of a branch are closed, the branch
1500 1500 will be considered closed and no longer listed.
1501 1501
1502 1502 The --amend flag can be used to amend the parent of the
1503 1503 working directory with a new commit that contains the changes
1504 1504 in the parent in addition to those currently reported by :hg:`status`,
1505 1505 if there are any. The old commit is stored in a backup bundle in
1506 1506 ``.hg/strip-backup`` (see :hg:`help bundle` and :hg:`help unbundle`
1507 1507 on how to restore it).
1508 1508
1509 1509 Message, user and date are taken from the amended commit unless
1510 1510 specified. When a message isn't specified on the command line,
1511 1511 the editor will open with the message of the amended commit.
1512 1512
1513 1513 It is not possible to amend public changesets (see :hg:`help phases`)
1514 1514 or changesets that have children.
1515 1515
1516 1516 See :hg:`help dates` for a list of formats valid for -d/--date.
1517 1517
1518 1518 Returns 0 on success, 1 if nothing changed.
1519 1519
1520 1520 .. container:: verbose
1521 1521
1522 1522 Examples:
1523 1523
1524 1524 - commit all files ending in .py::
1525 1525
1526 1526 hg commit --include "set:**.py"
1527 1527
1528 1528 - commit all non-binary files::
1529 1529
1530 1530 hg commit --exclude "set:binary()"
1531 1531
1532 1532 - amend the current commit and set the date to now::
1533 1533
1534 1534 hg commit --amend --date now
1535 1535 """
1536 1536 wlock = lock = None
1537 1537 try:
1538 1538 wlock = repo.wlock()
1539 1539 lock = repo.lock()
1540 1540 return _docommit(ui, repo, *pats, **opts)
1541 1541 finally:
1542 1542 release(lock, wlock)
1543 1543
1544 1544 def _docommit(ui, repo, *pats, **opts):
1545 1545 if opts.get(r'interactive'):
1546 1546 opts.pop(r'interactive')
1547 1547 ret = cmdutil.dorecord(ui, repo, commit, None, False,
1548 1548 cmdutil.recordfilter, *pats,
1549 1549 **opts)
1550 1550 # ret can be 0 (no changes to record) or the value returned by
1551 1551 # commit(), 1 if nothing changed or None on success.
1552 1552 return 1 if ret == 0 else ret
1553 1553
1554 1554 opts = pycompat.byteskwargs(opts)
1555 1555 if opts.get('subrepos'):
1556 1556 if opts.get('amend'):
1557 1557 raise error.Abort(_('cannot amend with --subrepos'))
1558 1558 # Let --subrepos on the command line override config setting.
1559 1559 ui.setconfig('ui', 'commitsubrepos', True, 'commit')
1560 1560
1561 1561 cmdutil.checkunfinished(repo, commit=True)
1562 1562
1563 1563 branch = repo[None].branch()
1564 1564 bheads = repo.branchheads(branch)
1565 1565
1566 1566 extra = {}
1567 1567 if opts.get('close_branch'):
1568 1568 extra['close'] = '1'
1569 1569
1570 1570 if not bheads:
1571 1571 raise error.Abort(_('can only close branch heads'))
1572 1572 elif opts.get('amend'):
1573 1573 if repo[None].parents()[0].p1().branch() != branch and \
1574 1574 repo[None].parents()[0].p2().branch() != branch:
1575 1575 raise error.Abort(_('can only close branch heads'))
1576 1576
1577 1577 if opts.get('amend'):
1578 1578 if ui.configbool('ui', 'commitsubrepos'):
1579 1579 raise error.Abort(_('cannot amend with ui.commitsubrepos enabled'))
1580 1580
1581 1581 old = repo['.']
1582 1582 rewriteutil.precheck(repo, [old.rev()], 'amend')
1583 1583
1584 1584 # Currently histedit gets confused if an amend happens while histedit
1585 1585 # is in progress. Since we have a checkunfinished command, we are
1586 1586 # temporarily honoring it.
1587 1587 #
1588 1588 # Note: eventually this guard will be removed. Please do not expect
1589 1589 # this behavior to remain.
1590 1590 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1591 1591 cmdutil.checkunfinished(repo)
1592 1592
1593 1593 node = cmdutil.amend(ui, repo, old, extra, pats, opts)
1594 1594 if node == old.node():
1595 1595 ui.status(_("nothing changed\n"))
1596 1596 return 1
1597 1597 else:
1598 1598 def commitfunc(ui, repo, message, match, opts):
1599 1599 overrides = {}
1600 1600 if opts.get('secret'):
1601 1601 overrides[('phases', 'new-commit')] = 'secret'
1602 1602
1603 1603 baseui = repo.baseui
1604 1604 with baseui.configoverride(overrides, 'commit'):
1605 1605 with ui.configoverride(overrides, 'commit'):
1606 1606 editform = cmdutil.mergeeditform(repo[None],
1607 1607 'commit.normal')
1608 1608 editor = cmdutil.getcommiteditor(
1609 1609 editform=editform, **pycompat.strkwargs(opts))
1610 1610 return repo.commit(message,
1611 1611 opts.get('user'),
1612 1612 opts.get('date'),
1613 1613 match,
1614 1614 editor=editor,
1615 1615 extra=extra)
1616 1616
1617 1617 node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
1618 1618
1619 1619 if not node:
1620 1620 stat = cmdutil.postcommitstatus(repo, pats, opts)
1621 1621 if stat[3]:
1622 1622 ui.status(_("nothing changed (%d missing files, see "
1623 1623 "'hg status')\n") % len(stat[3]))
1624 1624 else:
1625 1625 ui.status(_("nothing changed\n"))
1626 1626 return 1
1627 1627
1628 1628 cmdutil.commitstatus(repo, node, branch, bheads, opts)
1629 1629
1630 1630 @command('config|showconfig|debugconfig',
1631 1631 [('u', 'untrusted', None, _('show untrusted configuration options')),
1632 1632 ('e', 'edit', None, _('edit user config')),
1633 1633 ('l', 'local', None, _('edit repository config')),
1634 1634 ('g', 'global', None, _('edit global config'))] + formatteropts,
1635 1635 _('[-u] [NAME]...'),
1636 1636 optionalrepo=True, cmdtype=readonly)
1637 1637 def config(ui, repo, *values, **opts):
1638 1638 """show combined config settings from all hgrc files
1639 1639
1640 1640 With no arguments, print names and values of all config items.
1641 1641
1642 1642 With one argument of the form section.name, print just the value
1643 1643 of that config item.
1644 1644
1645 1645 With multiple arguments, print names and values of all config
1646 1646 items with matching section names or section.names.
1647 1647
1648 1648 With --edit, start an editor on the user-level config file. With
1649 1649 --global, edit the system-wide config file. With --local, edit the
1650 1650 repository-level config file.
1651 1651
1652 1652 With --debug, the source (filename and line number) is printed
1653 1653 for each config item.
1654 1654
1655 1655 See :hg:`help config` for more information about config files.
1656 1656
1657 1657 Returns 0 on success, 1 if NAME does not exist.
1658 1658
1659 1659 """
1660 1660
1661 1661 opts = pycompat.byteskwargs(opts)
1662 1662 if opts.get('edit') or opts.get('local') or opts.get('global'):
1663 1663 if opts.get('local') and opts.get('global'):
1664 1664 raise error.Abort(_("can't use --local and --global together"))
1665 1665
1666 1666 if opts.get('local'):
1667 1667 if not repo:
1668 1668 raise error.Abort(_("can't use --local outside a repository"))
1669 1669 paths = [repo.vfs.join('hgrc')]
1670 1670 elif opts.get('global'):
1671 1671 paths = rcutil.systemrcpath()
1672 1672 else:
1673 1673 paths = rcutil.userrcpath()
1674 1674
1675 1675 for f in paths:
1676 1676 if os.path.exists(f):
1677 1677 break
1678 1678 else:
1679 1679 if opts.get('global'):
1680 1680 samplehgrc = uimod.samplehgrcs['global']
1681 1681 elif opts.get('local'):
1682 1682 samplehgrc = uimod.samplehgrcs['local']
1683 1683 else:
1684 1684 samplehgrc = uimod.samplehgrcs['user']
1685 1685
1686 1686 f = paths[0]
1687 1687 fp = open(f, "wb")
1688 1688 fp.write(util.tonativeeol(samplehgrc))
1689 1689 fp.close()
1690 1690
1691 1691 editor = ui.geteditor()
1692 1692 ui.system("%s \"%s\"" % (editor, f),
1693 1693 onerr=error.Abort, errprefix=_("edit failed"),
1694 1694 blockedtag='config_edit')
1695 1695 return
1696 1696 ui.pager('config')
1697 1697 fm = ui.formatter('config', opts)
1698 1698 for t, f in rcutil.rccomponents():
1699 1699 if t == 'path':
1700 1700 ui.debug('read config from: %s\n' % f)
1701 1701 elif t == 'items':
1702 1702 for section, name, value, source in f:
1703 1703 ui.debug('set config by: %s\n' % source)
1704 1704 else:
1705 1705 raise error.ProgrammingError('unknown rctype: %s' % t)
1706 1706 untrusted = bool(opts.get('untrusted'))
1707 1707
1708 1708 selsections = selentries = []
1709 1709 if values:
1710 1710 selsections = [v for v in values if '.' not in v]
1711 1711 selentries = [v for v in values if '.' in v]
1712 1712 uniquesel = (len(selentries) == 1 and not selsections)
1713 1713 selsections = set(selsections)
1714 1714 selentries = set(selentries)
1715 1715
1716 1716 matched = False
1717 1717 for section, name, value in ui.walkconfig(untrusted=untrusted):
1718 1718 source = ui.configsource(section, name, untrusted)
1719 1719 value = pycompat.bytestr(value)
1720 1720 if fm.isplain():
1721 1721 source = source or 'none'
1722 1722 value = value.replace('\n', '\\n')
1723 1723 entryname = section + '.' + name
1724 1724 if values and not (section in selsections or entryname in selentries):
1725 1725 continue
1726 1726 fm.startitem()
1727 1727 fm.condwrite(ui.debugflag, 'source', '%s: ', source)
1728 1728 if uniquesel:
1729 1729 fm.data(name=entryname)
1730 1730 fm.write('value', '%s\n', value)
1731 1731 else:
1732 1732 fm.write('name value', '%s=%s\n', entryname, value)
1733 1733 matched = True
1734 1734 fm.end()
1735 1735 if matched:
1736 1736 return 0
1737 1737 return 1
1738 1738
1739 1739 @command('copy|cp',
1740 1740 [('A', 'after', None, _('record a copy that has already occurred')),
1741 1741 ('f', 'force', None, _('forcibly copy over an existing managed file')),
1742 1742 ] + walkopts + dryrunopts,
1743 1743 _('[OPTION]... [SOURCE]... DEST'))
1744 1744 def copy(ui, repo, *pats, **opts):
1745 1745 """mark files as copied for the next commit
1746 1746
1747 1747 Mark dest as having copies of source files. If dest is a
1748 1748 directory, copies are put in that directory. If dest is a file,
1749 1749 the source must be a single file.
1750 1750
1751 1751 By default, this command copies the contents of files as they
1752 1752 exist in the working directory. If invoked with -A/--after, the
1753 1753 operation is recorded, but no copying is performed.
1754 1754
1755 1755 This command takes effect with the next commit. To undo a copy
1756 1756 before that, see :hg:`revert`.
1757 1757
1758 1758 Returns 0 on success, 1 if errors are encountered.
1759 1759 """
1760 1760 opts = pycompat.byteskwargs(opts)
1761 1761 with repo.wlock(False):
1762 1762 return cmdutil.copy(ui, repo, pats, opts)
1763 1763
1764 1764 @command('debugcommands', [], _('[COMMAND]'), norepo=True)
1765 1765 def debugcommands(ui, cmd='', *args):
1766 1766 """list all available commands and options"""
1767 1767 for cmd, vals in sorted(table.iteritems()):
1768 1768 cmd = cmd.split('|')[0].strip('^')
1769 1769 opts = ', '.join([i[1] for i in vals[1]])
1770 1770 ui.write('%s: %s\n' % (cmd, opts))
1771 1771
1772 1772 @command('debugcomplete',
1773 1773 [('o', 'options', None, _('show the command options'))],
1774 1774 _('[-o] CMD'),
1775 1775 norepo=True)
1776 1776 def debugcomplete(ui, cmd='', **opts):
1777 1777 """returns the completion list associated with the given command"""
1778 1778
1779 1779 if opts.get(r'options'):
1780 1780 options = []
1781 1781 otables = [globalopts]
1782 1782 if cmd:
1783 1783 aliases, entry = cmdutil.findcmd(cmd, table, False)
1784 1784 otables.append(entry[1])
1785 1785 for t in otables:
1786 1786 for o in t:
1787 1787 if "(DEPRECATED)" in o[3]:
1788 1788 continue
1789 1789 if o[0]:
1790 1790 options.append('-%s' % o[0])
1791 1791 options.append('--%s' % o[1])
1792 1792 ui.write("%s\n" % "\n".join(options))
1793 1793 return
1794 1794
1795 1795 cmdlist, unused_allcmds = cmdutil.findpossible(cmd, table)
1796 1796 if ui.verbose:
1797 1797 cmdlist = [' '.join(c[0]) for c in cmdlist.values()]
1798 1798 ui.write("%s\n" % "\n".join(sorted(cmdlist)))
1799 1799
1800 1800 @command('^diff',
1801 1801 [('r', 'rev', [], _('revision'), _('REV')),
1802 1802 ('c', 'change', '', _('change made by revision'), _('REV'))
1803 1803 ] + diffopts + diffopts2 + walkopts + subrepoopts,
1804 1804 _('[OPTION]... ([-c REV] | [-r REV1 [-r REV2]]) [FILE]...'),
1805 1805 inferrepo=True, cmdtype=readonly)
1806 1806 def diff(ui, repo, *pats, **opts):
1807 1807 """diff repository (or selected files)
1808 1808
1809 1809 Show differences between revisions for the specified files.
1810 1810
1811 1811 Differences between files are shown using the unified diff format.
1812 1812
1813 1813 .. note::
1814 1814
1815 1815 :hg:`diff` may generate unexpected results for merges, as it will
1816 1816 default to comparing against the working directory's first
1817 1817 parent changeset if no revisions are specified.
1818 1818
1819 1819 When two revision arguments are given, then changes are shown
1820 1820 between those revisions. If only one revision is specified then
1821 1821 that revision is compared to the working directory, and, when no
1822 1822 revisions are specified, the working directory files are compared
1823 1823 to its first parent.
1824 1824
1825 1825 Alternatively you can specify -c/--change with a revision to see
1826 1826 the changes in that changeset relative to its first parent.
1827 1827
1828 1828 Without the -a/--text option, diff will avoid generating diffs of
1829 1829 files it detects as binary. With -a, diff will generate a diff
1830 1830 anyway, probably with undesirable results.
1831 1831
1832 1832 Use the -g/--git option to generate diffs in the git extended diff
1833 1833 format. For more information, read :hg:`help diffs`.
1834 1834
1835 1835 .. container:: verbose
1836 1836
1837 1837 Examples:
1838 1838
1839 1839 - compare a file in the current working directory to its parent::
1840 1840
1841 1841 hg diff foo.c
1842 1842
1843 1843 - compare two historical versions of a directory, with rename info::
1844 1844
1845 1845 hg diff --git -r 1.0:1.2 lib/
1846 1846
1847 1847 - get change stats relative to the last change on some date::
1848 1848
1849 1849 hg diff --stat -r "date('may 2')"
1850 1850
1851 1851 - diff all newly-added files that contain a keyword::
1852 1852
1853 1853 hg diff "set:added() and grep(GNU)"
1854 1854
1855 1855 - compare a revision and its parents::
1856 1856
1857 1857 hg diff -c 9353 # compare against first parent
1858 1858 hg diff -r 9353^:9353 # same using revset syntax
1859 1859 hg diff -r 9353^2:9353 # compare against the second parent
1860 1860
1861 1861 Returns 0 on success.
1862 1862 """
1863 1863
1864 1864 opts = pycompat.byteskwargs(opts)
1865 1865 revs = opts.get('rev')
1866 1866 change = opts.get('change')
1867 1867 stat = opts.get('stat')
1868 1868 reverse = opts.get('reverse')
1869 1869
1870 1870 if revs and change:
1871 1871 msg = _('cannot specify --rev and --change at the same time')
1872 1872 raise error.Abort(msg)
1873 1873 elif change:
1874 1874 repo = scmutil.unhidehashlikerevs(repo, [change], 'nowarn')
1875 1875 ctx2 = scmutil.revsingle(repo, change, None)
1876 1876 ctx1 = ctx2.p1()
1877 1877 else:
1878 1878 repo = scmutil.unhidehashlikerevs(repo, revs, 'nowarn')
1879 1879 ctx1, ctx2 = scmutil.revpair(repo, revs)
1880 1880 node1, node2 = ctx1.node(), ctx2.node()
1881 1881
1882 1882 if reverse:
1883 1883 node1, node2 = node2, node1
1884 1884
1885 1885 diffopts = patch.diffallopts(ui, opts)
1886 1886 m = scmutil.match(ctx2, pats, opts)
1887 1887 ui.pager('diff')
1888 1888 logcmdutil.diffordiffstat(ui, repo, diffopts, node1, node2, m, stat=stat,
1889 1889 listsubrepos=opts.get('subrepos'),
1890 1890 root=opts.get('root'))
1891 1891
1892 1892 @command('^export',
1893 1893 [('o', 'output', '',
1894 1894 _('print output to file with formatted name'), _('FORMAT')),
1895 1895 ('', 'switch-parent', None, _('diff against the second parent')),
1896 1896 ('r', 'rev', [], _('revisions to export'), _('REV')),
1897 1897 ] + diffopts + formatteropts,
1898 1898 _('[OPTION]... [-o OUTFILESPEC] [-r] [REV]...'), cmdtype=readonly)
1899 1899 def export(ui, repo, *changesets, **opts):
1900 1900 """dump the header and diffs for one or more changesets
1901 1901
1902 1902 Print the changeset header and diffs for one or more revisions.
1903 1903 If no revision is given, the parent of the working directory is used.
1904 1904
1905 1905 The information shown in the changeset header is: author, date,
1906 1906 branch name (if non-default), changeset hash, parent(s) and commit
1907 1907 comment.
1908 1908
1909 1909 .. note::
1910 1910
1911 1911 :hg:`export` may generate unexpected diff output for merge
1912 1912 changesets, as it will compare the merge changeset against its
1913 1913 first parent only.
1914 1914
1915 1915 Output may be to a file, in which case the name of the file is
1916 1916 given using a template string. See :hg:`help templates`. In addition
1917 1917 to the common template keywords, the following formatting rules are
1918 1918 supported:
1919 1919
1920 1920 :``%%``: literal "%" character
1921 1921 :``%H``: changeset hash (40 hexadecimal digits)
1922 1922 :``%N``: number of patches being generated
1923 1923 :``%R``: changeset revision number
1924 1924 :``%b``: basename of the exporting repository
1925 1925 :``%h``: short-form changeset hash (12 hexadecimal digits)
1926 1926 :``%m``: first line of the commit message (only alphanumeric characters)
1927 1927 :``%n``: zero-padded sequence number, starting at 1
1928 1928 :``%r``: zero-padded changeset revision number
1929 1929 :``\\``: literal "\\" character
1930 1930
1931 1931 Without the -a/--text option, export will avoid generating diffs
1932 1932 of files it detects as binary. With -a, export will generate a
1933 1933 diff anyway, probably with undesirable results.
1934 1934
1935 1935 Use the -g/--git option to generate diffs in the git extended diff
1936 1936 format. See :hg:`help diffs` for more information.
1937 1937
1938 1938 With the --switch-parent option, the diff will be against the
1939 1939 second parent. It can be useful to review a merge.
1940 1940
1941 1941 .. container:: verbose
1942 1942
1943 1943 Examples:
1944 1944
1945 1945 - use export and import to transplant a bugfix to the current
1946 1946 branch::
1947 1947
1948 1948 hg export -r 9353 | hg import -
1949 1949
1950 1950 - export all the changesets between two revisions to a file with
1951 1951 rename information::
1952 1952
1953 1953 hg export --git -r 123:150 > changes.txt
1954 1954
1955 1955 - split outgoing changes into a series of patches with
1956 1956 descriptive names::
1957 1957
1958 1958 hg export -r "outgoing()" -o "%n-%m.patch"
1959 1959
1960 1960 Returns 0 on success.
1961 1961 """
1962 1962 opts = pycompat.byteskwargs(opts)
1963 1963 changesets += tuple(opts.get('rev', []))
1964 1964 if not changesets:
1965 1965 changesets = ['.']
1966 1966 repo = scmutil.unhidehashlikerevs(repo, changesets, 'nowarn')
1967 1967 revs = scmutil.revrange(repo, changesets)
1968 1968 if not revs:
1969 1969 raise error.Abort(_("export requires at least one changeset"))
1970 1970 if len(revs) > 1:
1971 1971 ui.note(_('exporting patches:\n'))
1972 1972 else:
1973 1973 ui.note(_('exporting patch:\n'))
1974 1974
1975 1975 fntemplate = opts.get('output')
1976 1976 if cmdutil.isstdiofilename(fntemplate):
1977 1977 fntemplate = ''
1978 1978
1979 1979 if fntemplate:
1980 1980 fm = formatter.nullformatter(ui, 'export', opts)
1981 1981 else:
1982 1982 ui.pager('export')
1983 1983 fm = ui.formatter('export', opts)
1984 1984 with fm:
1985 1985 cmdutil.export(repo, revs, fm, fntemplate=fntemplate,
1986 1986 switch_parent=opts.get('switch_parent'),
1987 1987 opts=patch.diffallopts(ui, opts))
1988 1988
1989 1989 @command('files',
1990 1990 [('r', 'rev', '', _('search the repository as it is in REV'), _('REV')),
1991 1991 ('0', 'print0', None, _('end filenames with NUL, for use with xargs')),
1992 1992 ] + walkopts + formatteropts + subrepoopts,
1993 1993 _('[OPTION]... [FILE]...'), cmdtype=readonly)
1994 1994 def files(ui, repo, *pats, **opts):
1995 1995 """list tracked files
1996 1996
1997 1997 Print files under Mercurial control in the working directory or
1998 1998 specified revision for given files (excluding removed files).
1999 1999 Files can be specified as filenames or filesets.
2000 2000
2001 2001 If no files are given to match, this command prints the names
2002 2002 of all files under Mercurial control.
2003 2003
2004 2004 .. container:: verbose
2005 2005
2006 2006 Examples:
2007 2007
2008 2008 - list all files under the current directory::
2009 2009
2010 2010 hg files .
2011 2011
2012 2012 - shows sizes and flags for current revision::
2013 2013
2014 2014 hg files -vr .
2015 2015
2016 2016 - list all files named README::
2017 2017
2018 2018 hg files -I "**/README"
2019 2019
2020 2020 - list all binary files::
2021 2021
2022 2022 hg files "set:binary()"
2023 2023
2024 2024 - find files containing a regular expression::
2025 2025
2026 2026 hg files "set:grep('bob')"
2027 2027
2028 2028 - search tracked file contents with xargs and grep::
2029 2029
2030 2030 hg files -0 | xargs -0 grep foo
2031 2031
2032 2032 See :hg:`help patterns` and :hg:`help filesets` for more information
2033 2033 on specifying file patterns.
2034 2034
2035 2035 Returns 0 if a match is found, 1 otherwise.
2036 2036
2037 2037 """
2038 2038
2039 2039 opts = pycompat.byteskwargs(opts)
2040 2040 rev = opts.get('rev')
2041 2041 if rev:
2042 2042 repo = scmutil.unhidehashlikerevs(repo, [rev], 'nowarn')
2043 2043 ctx = scmutil.revsingle(repo, rev, None)
2044 2044
2045 2045 end = '\n'
2046 2046 if opts.get('print0'):
2047 2047 end = '\0'
2048 2048 fmt = '%s' + end
2049 2049
2050 2050 m = scmutil.match(ctx, pats, opts)
2051 2051 ui.pager('files')
2052 2052 with ui.formatter('files', opts) as fm:
2053 2053 return cmdutil.files(ui, ctx, m, fm, fmt, opts.get('subrepos'))
2054 2054
2055 2055 @command(
2056 2056 '^forget',
2057 2057 walkopts + dryrunopts,
2058 2058 _('[OPTION]... FILE...'), inferrepo=True)
2059 2059 def forget(ui, repo, *pats, **opts):
2060 2060 """forget the specified files on the next commit
2061 2061
2062 2062 Mark the specified files so they will no longer be tracked
2063 2063 after the next commit.
2064 2064
2065 2065 This only removes files from the current branch, not from the
2066 2066 entire project history, and it does not delete them from the
2067 2067 working directory.
2068 2068
2069 2069 To delete the file from the working directory, see :hg:`remove`.
2070 2070
2071 2071 To undo a forget before the next commit, see :hg:`add`.
2072 2072
2073 2073 .. container:: verbose
2074 2074
2075 2075 Examples:
2076 2076
2077 2077 - forget newly-added binary files::
2078 2078
2079 2079 hg forget "set:added() and binary()"
2080 2080
2081 2081 - forget files that would be excluded by .hgignore::
2082 2082
2083 2083 hg forget "set:hgignore()"
2084 2084
2085 2085 Returns 0 on success.
2086 2086 """
2087 2087
2088 2088 opts = pycompat.byteskwargs(opts)
2089 2089 if not pats:
2090 2090 raise error.Abort(_('no files specified'))
2091 2091
2092 2092 m = scmutil.match(repo[None], pats, opts)
2093 2093 dryrun = opts.get(r'dry_run')
2094 2094 rejected = cmdutil.forget(ui, repo, m, prefix="",
2095 2095 explicitonly=False, dryrun=dryrun)[0]
2096 2096 return rejected and 1 or 0
2097 2097
2098 2098 @command(
2099 2099 'graft',
2100 2100 [('r', 'rev', [], _('revisions to graft'), _('REV')),
2101 2101 ('c', 'continue', False, _('resume interrupted graft')),
2102 2102 ('e', 'edit', False, _('invoke editor on commit messages')),
2103 2103 ('', 'log', None, _('append graft info to log message')),
2104 2104 ('f', 'force', False, _('force graft')),
2105 2105 ('D', 'currentdate', False,
2106 2106 _('record the current date as commit date')),
2107 2107 ('U', 'currentuser', False,
2108 2108 _('record the current user as committer'), _('DATE'))]
2109 2109 + commitopts2 + mergetoolopts + dryrunopts,
2110 2110 _('[OPTION]... [-r REV]... REV...'))
2111 2111 def graft(ui, repo, *revs, **opts):
2112 2112 '''copy changes from other branches onto the current branch
2113 2113
2114 2114 This command uses Mercurial's merge logic to copy individual
2115 2115 changes from other branches without merging branches in the
2116 2116 history graph. This is sometimes known as 'backporting' or
2117 2117 'cherry-picking'. By default, graft will copy user, date, and
2118 2118 description from the source changesets.
2119 2119
2120 2120 Changesets that are ancestors of the current revision, that have
2121 2121 already been grafted, or that are merges will be skipped.
2122 2122
2123 2123 If --log is specified, log messages will have a comment appended
2124 2124 of the form::
2125 2125
2126 2126 (grafted from CHANGESETHASH)
2127 2127
2128 2128 If --force is specified, revisions will be grafted even if they
2129 2129 are already ancestors of, or have been grafted to, the destination.
2130 2130 This is useful when the revisions have since been backed out.
2131 2131
2132 2132 If a graft merge results in conflicts, the graft process is
2133 2133 interrupted so that the current merge can be manually resolved.
2134 2134 Once all conflicts are addressed, the graft process can be
2135 2135 continued with the -c/--continue option.
2136 2136
2137 2137 .. note::
2138 2138
2139 2139 The -c/--continue option does not reapply earlier options, except
2140 2140 for --force.
2141 2141
2142 2142 .. container:: verbose
2143 2143
2144 2144 Examples:
2145 2145
2146 2146 - copy a single change to the stable branch and edit its description::
2147 2147
2148 2148 hg update stable
2149 2149 hg graft --edit 9393
2150 2150
2151 2151 - graft a range of changesets with one exception, updating dates::
2152 2152
2153 2153 hg graft -D "2085::2093 and not 2091"
2154 2154
2155 2155 - continue a graft after resolving conflicts::
2156 2156
2157 2157 hg graft -c
2158 2158
2159 2159 - show the source of a grafted changeset::
2160 2160
2161 2161 hg log --debug -r .
2162 2162
2163 2163 - show revisions sorted by date::
2164 2164
2165 2165 hg log -r "sort(all(), date)"
2166 2166
2167 2167 See :hg:`help revisions` for more about specifying revisions.
2168 2168
2169 2169 Returns 0 on successful completion.
2170 2170 '''
2171 2171 with repo.wlock():
2172 2172 return _dograft(ui, repo, *revs, **opts)
2173 2173
2174 2174 def _dograft(ui, repo, *revs, **opts):
2175 2175 opts = pycompat.byteskwargs(opts)
2176 2176 if revs and opts.get('rev'):
2177 2177 ui.warn(_('warning: inconsistent use of --rev might give unexpected '
2178 2178 'revision ordering!\n'))
2179 2179
2180 2180 revs = list(revs)
2181 2181 revs.extend(opts.get('rev'))
2182 2182
2183 2183 if not opts.get('user') and opts.get('currentuser'):
2184 2184 opts['user'] = ui.username()
2185 2185 if not opts.get('date') and opts.get('currentdate'):
2186 2186 opts['date'] = "%d %d" % dateutil.makedate()
2187 2187
2188 2188 editor = cmdutil.getcommiteditor(editform='graft',
2189 2189 **pycompat.strkwargs(opts))
2190 2190
2191 2191 cont = False
2192 2192 if opts.get('continue'):
2193 2193 cont = True
2194 2194 if revs:
2195 2195 raise error.Abort(_("can't specify --continue and revisions"))
2196 2196 # read in unfinished revisions
2197 2197 try:
2198 2198 nodes = repo.vfs.read('graftstate').splitlines()
2199 2199 revs = [repo[node].rev() for node in nodes]
2200 2200 except IOError as inst:
2201 2201 if inst.errno != errno.ENOENT:
2202 2202 raise
2203 2203 cmdutil.wrongtooltocontinue(repo, _('graft'))
2204 2204 else:
2205 2205 if not revs:
2206 2206 raise error.Abort(_('no revisions specified'))
2207 2207 cmdutil.checkunfinished(repo)
2208 2208 cmdutil.bailifchanged(repo)
2209 2209 revs = scmutil.revrange(repo, revs)
2210 2210
2211 2211 skipped = set()
2212 2212 # check for merges
2213 2213 for rev in repo.revs('%ld and merge()', revs):
2214 2214 ui.warn(_('skipping ungraftable merge revision %d\n') % rev)
2215 2215 skipped.add(rev)
2216 2216 revs = [r for r in revs if r not in skipped]
2217 2217 if not revs:
2218 2218 return -1
2219 2219
2220 2220 # Don't check in the --continue case, in effect retaining --force across
2221 2221 # --continues. That's because without --force, any revisions we decided to
2222 2222 # skip would have been filtered out here, so they wouldn't have made their
2223 2223 # way to the graftstate. With --force, any revisions we would have otherwise
2224 2224 # skipped would not have been filtered out, and if they hadn't been applied
2225 2225 # already, they'd have been in the graftstate.
2226 2226 if not (cont or opts.get('force')):
2227 2227 # check for ancestors of dest branch
2228 2228 crev = repo['.'].rev()
2229 2229 ancestors = repo.changelog.ancestors([crev], inclusive=True)
2230 2230 # XXX make this lazy in the future
2231 2231 # don't mutate while iterating, create a copy
2232 2232 for rev in list(revs):
2233 2233 if rev in ancestors:
2234 2234 ui.warn(_('skipping ancestor revision %d:%s\n') %
2235 2235 (rev, repo[rev]))
2236 2236 # XXX remove on list is slow
2237 2237 revs.remove(rev)
2238 2238 if not revs:
2239 2239 return -1
2240 2240
2241 2241 # analyze revs for earlier grafts
2242 2242 ids = {}
2243 2243 for ctx in repo.set("%ld", revs):
2244 2244 ids[ctx.hex()] = ctx.rev()
2245 2245 n = ctx.extra().get('source')
2246 2246 if n:
2247 2247 ids[n] = ctx.rev()
2248 2248
2249 2249 # check ancestors for earlier grafts
2250 2250 ui.debug('scanning for duplicate grafts\n')
2251 2251
2252 2252 # The only changesets we can be sure doesn't contain grafts of any
2253 2253 # revs, are the ones that are common ancestors of *all* revs:
2254 2254 for rev in repo.revs('only(%d,ancestor(%ld))', crev, revs):
2255 2255 ctx = repo[rev]
2256 2256 n = ctx.extra().get('source')
2257 2257 if n in ids:
2258 2258 try:
2259 2259 r = repo[n].rev()
2260 2260 except error.RepoLookupError:
2261 2261 r = None
2262 2262 if r in revs:
2263 2263 ui.warn(_('skipping revision %d:%s '
2264 2264 '(already grafted to %d:%s)\n')
2265 2265 % (r, repo[r], rev, ctx))
2266 2266 revs.remove(r)
2267 2267 elif ids[n] in revs:
2268 2268 if r is None:
2269 2269 ui.warn(_('skipping already grafted revision %d:%s '
2270 2270 '(%d:%s also has unknown origin %s)\n')
2271 2271 % (ids[n], repo[ids[n]], rev, ctx, n[:12]))
2272 2272 else:
2273 2273 ui.warn(_('skipping already grafted revision %d:%s '
2274 2274 '(%d:%s also has origin %d:%s)\n')
2275 2275 % (ids[n], repo[ids[n]], rev, ctx, r, n[:12]))
2276 2276 revs.remove(ids[n])
2277 2277 elif ctx.hex() in ids:
2278 2278 r = ids[ctx.hex()]
2279 2279 ui.warn(_('skipping already grafted revision %d:%s '
2280 2280 '(was grafted from %d:%s)\n') %
2281 2281 (r, repo[r], rev, ctx))
2282 2282 revs.remove(r)
2283 2283 if not revs:
2284 2284 return -1
2285 2285
2286 2286 for pos, ctx in enumerate(repo.set("%ld", revs)):
2287 2287 desc = '%d:%s "%s"' % (ctx.rev(), ctx,
2288 2288 ctx.description().split('\n', 1)[0])
2289 2289 names = repo.nodetags(ctx.node()) + repo.nodebookmarks(ctx.node())
2290 2290 if names:
2291 2291 desc += ' (%s)' % ' '.join(names)
2292 2292 ui.status(_('grafting %s\n') % desc)
2293 2293 if opts.get('dry_run'):
2294 2294 continue
2295 2295
2296 2296 source = ctx.extra().get('source')
2297 2297 extra = {}
2298 2298 if source:
2299 2299 extra['source'] = source
2300 2300 extra['intermediate-source'] = ctx.hex()
2301 2301 else:
2302 2302 extra['source'] = ctx.hex()
2303 2303 user = ctx.user()
2304 2304 if opts.get('user'):
2305 2305 user = opts['user']
2306 2306 date = ctx.date()
2307 2307 if opts.get('date'):
2308 2308 date = opts['date']
2309 2309 message = ctx.description()
2310 2310 if opts.get('log'):
2311 2311 message += '\n(grafted from %s)' % ctx.hex()
2312 2312
2313 2313 # we don't merge the first commit when continuing
2314 2314 if not cont:
2315 2315 # perform the graft merge with p1(rev) as 'ancestor'
2316 2316 try:
2317 2317 # ui.forcemerge is an internal variable, do not document
2318 2318 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
2319 2319 'graft')
2320 2320 stats = mergemod.graft(repo, ctx, ctx.p1(),
2321 2321 ['local', 'graft'])
2322 2322 finally:
2323 2323 repo.ui.setconfig('ui', 'forcemerge', '', 'graft')
2324 2324 # report any conflicts
2325 2325 if stats.unresolvedcount > 0:
2326 2326 # write out state for --continue
2327 2327 nodelines = [repo[rev].hex() + "\n" for rev in revs[pos:]]
2328 2328 repo.vfs.write('graftstate', ''.join(nodelines))
2329 2329 extra = ''
2330 2330 if opts.get('user'):
2331 2331 extra += ' --user %s' % procutil.shellquote(opts['user'])
2332 2332 if opts.get('date'):
2333 2333 extra += ' --date %s' % procutil.shellquote(opts['date'])
2334 2334 if opts.get('log'):
2335 2335 extra += ' --log'
2336 2336 hint=_("use 'hg resolve' and 'hg graft --continue%s'") % extra
2337 2337 raise error.Abort(
2338 2338 _("unresolved conflicts, can't continue"),
2339 2339 hint=hint)
2340 2340 else:
2341 2341 cont = False
2342 2342
2343 2343 # commit
2344 2344 node = repo.commit(text=message, user=user,
2345 2345 date=date, extra=extra, editor=editor)
2346 2346 if node is None:
2347 2347 ui.warn(
2348 2348 _('note: graft of %d:%s created no changes to commit\n') %
2349 2349 (ctx.rev(), ctx))
2350 2350
2351 2351 # remove state when we complete successfully
2352 2352 if not opts.get('dry_run'):
2353 2353 repo.vfs.unlinkpath('graftstate', ignoremissing=True)
2354 2354
2355 2355 return 0
2356 2356
2357 2357 @command('grep',
2358 2358 [('0', 'print0', None, _('end fields with NUL')),
2359 2359 ('', 'all', None, _('print all revisions that match')),
2360 2360 ('a', 'text', None, _('treat all files as text')),
2361 2361 ('f', 'follow', None,
2362 2362 _('follow changeset history,'
2363 2363 ' or file history across copies and renames')),
2364 2364 ('i', 'ignore-case', None, _('ignore case when matching')),
2365 2365 ('l', 'files-with-matches', None,
2366 2366 _('print only filenames and revisions that match')),
2367 2367 ('n', 'line-number', None, _('print matching line numbers')),
2368 2368 ('r', 'rev', [],
2369 2369 _('only search files changed within revision range'), _('REV')),
2370 2370 ('u', 'user', None, _('list the author (long with -v)')),
2371 2371 ('d', 'date', None, _('list the date (short with -q)')),
2372 2372 ] + formatteropts + walkopts,
2373 2373 _('[OPTION]... PATTERN [FILE]...'),
2374 2374 inferrepo=True, cmdtype=readonly)
2375 2375 def grep(ui, repo, pattern, *pats, **opts):
2376 2376 """search revision history for a pattern in specified files
2377 2377
2378 2378 Search revision history for a regular expression in the specified
2379 2379 files or the entire project.
2380 2380
2381 2381 By default, grep prints the most recent revision number for each
2382 2382 file in which it finds a match. To get it to print every revision
2383 2383 that contains a change in match status ("-" for a match that becomes
2384 2384 a non-match, or "+" for a non-match that becomes a match), use the
2385 2385 --all flag.
2386 2386
2387 2387 PATTERN can be any Python (roughly Perl-compatible) regular
2388 2388 expression.
2389 2389
2390 2390 If no FILEs are specified (and -f/--follow isn't set), all files in
2391 2391 the repository are searched, including those that don't exist in the
2392 2392 current branch or have been deleted in a prior changeset.
2393 2393
2394 2394 Returns 0 if a match is found, 1 otherwise.
2395 2395 """
2396 2396 opts = pycompat.byteskwargs(opts)
2397 2397 reflags = re.M
2398 2398 if opts.get('ignore_case'):
2399 2399 reflags |= re.I
2400 2400 try:
2401 2401 regexp = util.re.compile(pattern, reflags)
2402 2402 except re.error as inst:
2403 2403 ui.warn(_("grep: invalid match pattern: %s\n") % pycompat.bytestr(inst))
2404 2404 return 1
2405 2405 sep, eol = ':', '\n'
2406 2406 if opts.get('print0'):
2407 2407 sep = eol = '\0'
2408 2408
2409 2409 getfile = util.lrucachefunc(repo.file)
2410 2410
2411 2411 def matchlines(body):
2412 2412 begin = 0
2413 2413 linenum = 0
2414 2414 while begin < len(body):
2415 2415 match = regexp.search(body, begin)
2416 2416 if not match:
2417 2417 break
2418 2418 mstart, mend = match.span()
2419 2419 linenum += body.count('\n', begin, mstart) + 1
2420 2420 lstart = body.rfind('\n', begin, mstart) + 1 or begin
2421 2421 begin = body.find('\n', mend) + 1 or len(body) + 1
2422 2422 lend = begin - 1
2423 2423 yield linenum, mstart - lstart, mend - lstart, body[lstart:lend]
2424 2424
2425 2425 class linestate(object):
2426 2426 def __init__(self, line, linenum, colstart, colend):
2427 2427 self.line = line
2428 2428 self.linenum = linenum
2429 2429 self.colstart = colstart
2430 2430 self.colend = colend
2431 2431
2432 2432 def __hash__(self):
2433 2433 return hash((self.linenum, self.line))
2434 2434
2435 2435 def __eq__(self, other):
2436 2436 return self.line == other.line
2437 2437
2438 2438 def findpos(self):
2439 2439 """Iterate all (start, end) indices of matches"""
2440 2440 yield self.colstart, self.colend
2441 2441 p = self.colend
2442 2442 while p < len(self.line):
2443 2443 m = regexp.search(self.line, p)
2444 2444 if not m:
2445 2445 break
2446 2446 yield m.span()
2447 2447 p = m.end()
2448 2448
2449 2449 matches = {}
2450 2450 copies = {}
2451 2451 def grepbody(fn, rev, body):
2452 2452 matches[rev].setdefault(fn, [])
2453 2453 m = matches[rev][fn]
2454 2454 for lnum, cstart, cend, line in matchlines(body):
2455 2455 s = linestate(line, lnum, cstart, cend)
2456 2456 m.append(s)
2457 2457
2458 2458 def difflinestates(a, b):
2459 2459 sm = difflib.SequenceMatcher(None, a, b)
2460 2460 for tag, alo, ahi, blo, bhi in sm.get_opcodes():
2461 2461 if tag == 'insert':
2462 2462 for i in xrange(blo, bhi):
2463 2463 yield ('+', b[i])
2464 2464 elif tag == 'delete':
2465 2465 for i in xrange(alo, ahi):
2466 2466 yield ('-', a[i])
2467 2467 elif tag == 'replace':
2468 2468 for i in xrange(alo, ahi):
2469 2469 yield ('-', a[i])
2470 2470 for i in xrange(blo, bhi):
2471 2471 yield ('+', b[i])
2472 2472
2473 2473 def display(fm, fn, ctx, pstates, states):
2474 2474 rev = ctx.rev()
2475 2475 if fm.isplain():
2476 2476 formatuser = ui.shortuser
2477 2477 else:
2478 2478 formatuser = str
2479 2479 if ui.quiet:
2480 2480 datefmt = '%Y-%m-%d'
2481 2481 else:
2482 2482 datefmt = '%a %b %d %H:%M:%S %Y %1%2'
2483 2483 found = False
2484 2484 @util.cachefunc
2485 2485 def binary():
2486 2486 flog = getfile(fn)
2487 2487 return stringutil.binary(flog.read(ctx.filenode(fn)))
2488 2488
2489 2489 fieldnamemap = {'filename': 'file', 'linenumber': 'line_number'}
2490 2490 if opts.get('all'):
2491 2491 iter = difflinestates(pstates, states)
2492 2492 else:
2493 2493 iter = [('', l) for l in states]
2494 2494 for change, l in iter:
2495 2495 fm.startitem()
2496 2496 fm.data(node=fm.hexfunc(ctx.node()))
2497 2497 cols = [
2498 2498 ('filename', fn, True),
2499 2499 ('rev', rev, True),
2500 2500 ('linenumber', l.linenum, opts.get('line_number')),
2501 2501 ]
2502 2502 if opts.get('all'):
2503 2503 cols.append(('change', change, True))
2504 2504 cols.extend([
2505 2505 ('user', formatuser(ctx.user()), opts.get('user')),
2506 2506 ('date', fm.formatdate(ctx.date(), datefmt), opts.get('date')),
2507 2507 ])
2508 2508 lastcol = next(name for name, data, cond in reversed(cols) if cond)
2509 2509 for name, data, cond in cols:
2510 2510 field = fieldnamemap.get(name, name)
2511 2511 fm.condwrite(cond, field, '%s', data, label='grep.%s' % name)
2512 2512 if cond and name != lastcol:
2513 2513 fm.plain(sep, label='grep.sep')
2514 2514 if not opts.get('files_with_matches'):
2515 2515 fm.plain(sep, label='grep.sep')
2516 2516 if not opts.get('text') and binary():
2517 2517 fm.plain(_(" Binary file matches"))
2518 2518 else:
2519 2519 displaymatches(fm.nested('texts', tmpl='{text}'), l)
2520 2520 fm.plain(eol)
2521 2521 found = True
2522 2522 if opts.get('files_with_matches'):
2523 2523 break
2524 2524 return found
2525 2525
2526 2526 def displaymatches(fm, l):
2527 2527 p = 0
2528 2528 for s, e in l.findpos():
2529 2529 if p < s:
2530 2530 fm.startitem()
2531 2531 fm.write('text', '%s', l.line[p:s])
2532 2532 fm.data(matched=False)
2533 2533 fm.startitem()
2534 2534 fm.write('text', '%s', l.line[s:e], label='grep.match')
2535 2535 fm.data(matched=True)
2536 2536 p = e
2537 2537 if p < len(l.line):
2538 2538 fm.startitem()
2539 2539 fm.write('text', '%s', l.line[p:])
2540 2540 fm.data(matched=False)
2541 2541 fm.end()
2542 2542
2543 2543 skip = {}
2544 2544 revfiles = {}
2545 2545 match = scmutil.match(repo[None], pats, opts)
2546 2546 found = False
2547 2547 follow = opts.get('follow')
2548 2548
2549 2549 def prep(ctx, fns):
2550 2550 rev = ctx.rev()
2551 2551 pctx = ctx.p1()
2552 2552 parent = pctx.rev()
2553 2553 matches.setdefault(rev, {})
2554 2554 matches.setdefault(parent, {})
2555 2555 files = revfiles.setdefault(rev, [])
2556 2556 for fn in fns:
2557 2557 flog = getfile(fn)
2558 2558 try:
2559 2559 fnode = ctx.filenode(fn)
2560 2560 except error.LookupError:
2561 2561 continue
2562 2562
2563 2563 copied = flog.renamed(fnode)
2564 2564 copy = follow and copied and copied[0]
2565 2565 if copy:
2566 2566 copies.setdefault(rev, {})[fn] = copy
2567 2567 if fn in skip:
2568 2568 if copy:
2569 2569 skip[copy] = True
2570 2570 continue
2571 2571 files.append(fn)
2572 2572
2573 2573 if fn not in matches[rev]:
2574 2574 grepbody(fn, rev, flog.read(fnode))
2575 2575
2576 2576 pfn = copy or fn
2577 2577 if pfn not in matches[parent]:
2578 2578 try:
2579 2579 fnode = pctx.filenode(pfn)
2580 2580 grepbody(pfn, parent, flog.read(fnode))
2581 2581 except error.LookupError:
2582 2582 pass
2583 2583
2584 2584 ui.pager('grep')
2585 2585 fm = ui.formatter('grep', opts)
2586 2586 for ctx in cmdutil.walkchangerevs(repo, match, opts, prep):
2587 2587 rev = ctx.rev()
2588 2588 parent = ctx.p1().rev()
2589 2589 for fn in sorted(revfiles.get(rev, [])):
2590 2590 states = matches[rev][fn]
2591 2591 copy = copies.get(rev, {}).get(fn)
2592 2592 if fn in skip:
2593 2593 if copy:
2594 2594 skip[copy] = True
2595 2595 continue
2596 2596 pstates = matches.get(parent, {}).get(copy or fn, [])
2597 2597 if pstates or states:
2598 2598 r = display(fm, fn, ctx, pstates, states)
2599 2599 found = found or r
2600 2600 if r and not opts.get('all'):
2601 2601 skip[fn] = True
2602 2602 if copy:
2603 2603 skip[copy] = True
2604 2604 del revfiles[rev]
2605 2605 # We will keep the matches dict for the duration of the window
2606 2606 # clear the matches dict once the window is over
2607 2607 if not revfiles:
2608 2608 matches.clear()
2609 2609 fm.end()
2610 2610
2611 2611 return not found
2612 2612
2613 2613 @command('heads',
2614 2614 [('r', 'rev', '',
2615 2615 _('show only heads which are descendants of STARTREV'), _('STARTREV')),
2616 2616 ('t', 'topo', False, _('show topological heads only')),
2617 2617 ('a', 'active', False, _('show active branchheads only (DEPRECATED)')),
2618 2618 ('c', 'closed', False, _('show normal and closed branch heads')),
2619 2619 ] + templateopts,
2620 2620 _('[-ct] [-r STARTREV] [REV]...'), cmdtype=readonly)
2621 2621 def heads(ui, repo, *branchrevs, **opts):
2622 2622 """show branch heads
2623 2623
2624 2624 With no arguments, show all open branch heads in the repository.
2625 2625 Branch heads are changesets that have no descendants on the
2626 2626 same branch. They are where development generally takes place and
2627 2627 are the usual targets for update and merge operations.
2628 2628
2629 2629 If one or more REVs are given, only open branch heads on the
2630 2630 branches associated with the specified changesets are shown. This
2631 2631 means that you can use :hg:`heads .` to see the heads on the
2632 2632 currently checked-out branch.
2633 2633
2634 2634 If -c/--closed is specified, also show branch heads marked closed
2635 2635 (see :hg:`commit --close-branch`).
2636 2636
2637 2637 If STARTREV is specified, only those heads that are descendants of
2638 2638 STARTREV will be displayed.
2639 2639
2640 2640 If -t/--topo is specified, named branch mechanics will be ignored and only
2641 2641 topological heads (changesets with no children) will be shown.
2642 2642
2643 2643 Returns 0 if matching heads are found, 1 if not.
2644 2644 """
2645 2645
2646 2646 opts = pycompat.byteskwargs(opts)
2647 2647 start = None
2648 2648 rev = opts.get('rev')
2649 2649 if rev:
2650 2650 repo = scmutil.unhidehashlikerevs(repo, [rev], 'nowarn')
2651 2651 start = scmutil.revsingle(repo, rev, None).node()
2652 2652
2653 2653 if opts.get('topo'):
2654 2654 heads = [repo[h] for h in repo.heads(start)]
2655 2655 else:
2656 2656 heads = []
2657 2657 for branch in repo.branchmap():
2658 2658 heads += repo.branchheads(branch, start, opts.get('closed'))
2659 2659 heads = [repo[h] for h in heads]
2660 2660
2661 2661 if branchrevs:
2662 2662 branches = set(repo[r].branch()
2663 2663 for r in scmutil.revrange(repo, branchrevs))
2664 2664 heads = [h for h in heads if h.branch() in branches]
2665 2665
2666 2666 if opts.get('active') and branchrevs:
2667 2667 dagheads = repo.heads(start)
2668 2668 heads = [h for h in heads if h.node() in dagheads]
2669 2669
2670 2670 if branchrevs:
2671 2671 haveheads = set(h.branch() for h in heads)
2672 2672 if branches - haveheads:
2673 2673 headless = ', '.join(b for b in branches - haveheads)
2674 2674 msg = _('no open branch heads found on branches %s')
2675 2675 if opts.get('rev'):
2676 2676 msg += _(' (started at %s)') % opts['rev']
2677 2677 ui.warn((msg + '\n') % headless)
2678 2678
2679 2679 if not heads:
2680 2680 return 1
2681 2681
2682 2682 ui.pager('heads')
2683 2683 heads = sorted(heads, key=lambda x: -x.rev())
2684 2684 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
2685 2685 for ctx in heads:
2686 2686 displayer.show(ctx)
2687 2687 displayer.close()
2688 2688
2689 2689 @command('help',
2690 2690 [('e', 'extension', None, _('show only help for extensions')),
2691 2691 ('c', 'command', None, _('show only help for commands')),
2692 2692 ('k', 'keyword', None, _('show topics matching keyword')),
2693 2693 ('s', 'system', [], _('show help for specific platform(s)')),
2694 2694 ],
2695 2695 _('[-ecks] [TOPIC]'),
2696 2696 norepo=True, cmdtype=readonly)
2697 2697 def help_(ui, name=None, **opts):
2698 2698 """show help for a given topic or a help overview
2699 2699
2700 2700 With no arguments, print a list of commands with short help messages.
2701 2701
2702 2702 Given a topic, extension, or command name, print help for that
2703 2703 topic.
2704 2704
2705 2705 Returns 0 if successful.
2706 2706 """
2707 2707
2708 2708 keep = opts.get(r'system') or []
2709 2709 if len(keep) == 0:
2710 2710 if pycompat.sysplatform.startswith('win'):
2711 2711 keep.append('windows')
2712 2712 elif pycompat.sysplatform == 'OpenVMS':
2713 2713 keep.append('vms')
2714 2714 elif pycompat.sysplatform == 'plan9':
2715 2715 keep.append('plan9')
2716 2716 else:
2717 2717 keep.append('unix')
2718 2718 keep.append(pycompat.sysplatform.lower())
2719 2719 if ui.verbose:
2720 2720 keep.append('verbose')
2721 2721
2722 2722 commands = sys.modules[__name__]
2723 2723 formatted = help.formattedhelp(ui, commands, name, keep=keep, **opts)
2724 2724 ui.pager('help')
2725 2725 ui.write(formatted)
2726 2726
2727 2727
2728 2728 @command('identify|id',
2729 2729 [('r', 'rev', '',
2730 2730 _('identify the specified revision'), _('REV')),
2731 2731 ('n', 'num', None, _('show local revision number')),
2732 2732 ('i', 'id', None, _('show global revision id')),
2733 2733 ('b', 'branch', None, _('show branch')),
2734 2734 ('t', 'tags', None, _('show tags')),
2735 2735 ('B', 'bookmarks', None, _('show bookmarks')),
2736 2736 ] + remoteopts + formatteropts,
2737 2737 _('[-nibtB] [-r REV] [SOURCE]'),
2738 2738 optionalrepo=True, cmdtype=readonly)
2739 2739 def identify(ui, repo, source=None, rev=None,
2740 2740 num=None, id=None, branch=None, tags=None, bookmarks=None, **opts):
2741 2741 """identify the working directory or specified revision
2742 2742
2743 2743 Print a summary identifying the repository state at REV using one or
2744 2744 two parent hash identifiers, followed by a "+" if the working
2745 2745 directory has uncommitted changes, the branch name (if not default),
2746 2746 a list of tags, and a list of bookmarks.
2747 2747
2748 2748 When REV is not given, print a summary of the current state of the
2749 2749 repository including the working directory. Specify -r. to get information
2750 2750 of the working directory parent without scanning uncommitted changes.
2751 2751
2752 2752 Specifying a path to a repository root or Mercurial bundle will
2753 2753 cause lookup to operate on that repository/bundle.
2754 2754
2755 2755 .. container:: verbose
2756 2756
2757 2757 Examples:
2758 2758
2759 2759 - generate a build identifier for the working directory::
2760 2760
2761 2761 hg id --id > build-id.dat
2762 2762
2763 2763 - find the revision corresponding to a tag::
2764 2764
2765 2765 hg id -n -r 1.3
2766 2766
2767 2767 - check the most recent revision of a remote repository::
2768 2768
2769 2769 hg id -r tip https://www.mercurial-scm.org/repo/hg/
2770 2770
2771 2771 See :hg:`log` for generating more information about specific revisions,
2772 2772 including full hash identifiers.
2773 2773
2774 2774 Returns 0 if successful.
2775 2775 """
2776 2776
2777 2777 opts = pycompat.byteskwargs(opts)
2778 2778 if not repo and not source:
2779 2779 raise error.Abort(_("there is no Mercurial repository here "
2780 2780 "(.hg not found)"))
2781 2781
2782 2782 if ui.debugflag:
2783 2783 hexfunc = hex
2784 2784 else:
2785 2785 hexfunc = short
2786 2786 default = not (num or id or branch or tags or bookmarks)
2787 2787 output = []
2788 2788 revs = []
2789 2789
2790 2790 if source:
2791 2791 source, branches = hg.parseurl(ui.expandpath(source))
2792 2792 peer = hg.peer(repo or ui, opts, source) # only pass ui when no repo
2793 2793 repo = peer.local()
2794 2794 revs, checkout = hg.addbranchrevs(repo, peer, branches, None)
2795 2795
2796 2796 fm = ui.formatter('identify', opts)
2797 2797 fm.startitem()
2798 2798
2799 2799 if not repo:
2800 2800 if num or branch or tags:
2801 2801 raise error.Abort(
2802 2802 _("can't query remote revision number, branch, or tags"))
2803 2803 if not rev and revs:
2804 2804 rev = revs[0]
2805 2805 if not rev:
2806 2806 rev = "tip"
2807 2807
2808 2808 remoterev = peer.lookup(rev)
2809 2809 hexrev = hexfunc(remoterev)
2810 2810 if default or id:
2811 2811 output = [hexrev]
2812 2812 fm.data(id=hexrev)
2813 2813
2814 2814 def getbms():
2815 2815 bms = []
2816 2816
2817 2817 if 'bookmarks' in peer.listkeys('namespaces'):
2818 2818 hexremoterev = hex(remoterev)
2819 2819 bms = [bm for bm, bmr in peer.listkeys('bookmarks').iteritems()
2820 2820 if bmr == hexremoterev]
2821 2821
2822 2822 return sorted(bms)
2823 2823
2824 2824 bms = getbms()
2825 2825 if bookmarks:
2826 2826 output.extend(bms)
2827 2827 elif default and not ui.quiet:
2828 2828 # multiple bookmarks for a single parent separated by '/'
2829 2829 bm = '/'.join(bms)
2830 2830 if bm:
2831 2831 output.append(bm)
2832 2832
2833 2833 fm.data(node=hex(remoterev))
2834 2834 fm.data(bookmarks=fm.formatlist(bms, name='bookmark'))
2835 2835 else:
2836 2836 if rev:
2837 2837 repo = scmutil.unhidehashlikerevs(repo, [rev], 'nowarn')
2838 2838 ctx = scmutil.revsingle(repo, rev, None)
2839 2839
2840 2840 if ctx.rev() is None:
2841 2841 ctx = repo[None]
2842 2842 parents = ctx.parents()
2843 2843 taglist = []
2844 2844 for p in parents:
2845 2845 taglist.extend(p.tags())
2846 2846
2847 2847 dirty = ""
2848 2848 if ctx.dirty(missing=True, merge=False, branch=False):
2849 2849 dirty = '+'
2850 2850 fm.data(dirty=dirty)
2851 2851
2852 2852 hexoutput = [hexfunc(p.node()) for p in parents]
2853 2853 if default or id:
2854 2854 output = ["%s%s" % ('+'.join(hexoutput), dirty)]
2855 2855 fm.data(id="%s%s" % ('+'.join(hexoutput), dirty))
2856 2856
2857 2857 if num:
2858 2858 numoutput = ["%d" % p.rev() for p in parents]
2859 2859 output.append("%s%s" % ('+'.join(numoutput), dirty))
2860 2860
2861 2861 fn = fm.nested('parents', tmpl='{rev}:{node|formatnode}', sep=' ')
2862 2862 for p in parents:
2863 2863 fn.startitem()
2864 2864 fn.data(rev=p.rev())
2865 2865 fn.data(node=p.hex())
2866 2866 fn.context(ctx=p)
2867 2867 fn.end()
2868 2868 else:
2869 2869 hexoutput = hexfunc(ctx.node())
2870 2870 if default or id:
2871 2871 output = [hexoutput]
2872 2872 fm.data(id=hexoutput)
2873 2873
2874 2874 if num:
2875 2875 output.append(pycompat.bytestr(ctx.rev()))
2876 2876 taglist = ctx.tags()
2877 2877
2878 2878 if default and not ui.quiet:
2879 2879 b = ctx.branch()
2880 2880 if b != 'default':
2881 2881 output.append("(%s)" % b)
2882 2882
2883 2883 # multiple tags for a single parent separated by '/'
2884 2884 t = '/'.join(taglist)
2885 2885 if t:
2886 2886 output.append(t)
2887 2887
2888 2888 # multiple bookmarks for a single parent separated by '/'
2889 2889 bm = '/'.join(ctx.bookmarks())
2890 2890 if bm:
2891 2891 output.append(bm)
2892 2892 else:
2893 2893 if branch:
2894 2894 output.append(ctx.branch())
2895 2895
2896 2896 if tags:
2897 2897 output.extend(taglist)
2898 2898
2899 2899 if bookmarks:
2900 2900 output.extend(ctx.bookmarks())
2901 2901
2902 2902 fm.data(node=ctx.hex())
2903 2903 fm.data(branch=ctx.branch())
2904 2904 fm.data(tags=fm.formatlist(taglist, name='tag', sep=':'))
2905 2905 fm.data(bookmarks=fm.formatlist(ctx.bookmarks(), name='bookmark'))
2906 2906 fm.context(ctx=ctx)
2907 2907
2908 2908 fm.plain("%s\n" % ' '.join(output))
2909 2909 fm.end()
2910 2910
2911 2911 @command('import|patch',
2912 2912 [('p', 'strip', 1,
2913 2913 _('directory strip option for patch. This has the same '
2914 2914 'meaning as the corresponding patch option'), _('NUM')),
2915 2915 ('b', 'base', '', _('base path (DEPRECATED)'), _('PATH')),
2916 2916 ('e', 'edit', False, _('invoke editor on commit messages')),
2917 2917 ('f', 'force', None,
2918 2918 _('skip check for outstanding uncommitted changes (DEPRECATED)')),
2919 2919 ('', 'no-commit', None,
2920 2920 _("don't commit, just update the working directory")),
2921 2921 ('', 'bypass', None,
2922 2922 _("apply patch without touching the working directory")),
2923 2923 ('', 'partial', None,
2924 2924 _('commit even if some hunks fail')),
2925 2925 ('', 'exact', None,
2926 2926 _('abort if patch would apply lossily')),
2927 2927 ('', 'prefix', '',
2928 2928 _('apply patch to subdirectory'), _('DIR')),
2929 2929 ('', 'import-branch', None,
2930 2930 _('use any branch information in patch (implied by --exact)'))] +
2931 2931 commitopts + commitopts2 + similarityopts,
2932 2932 _('[OPTION]... PATCH...'))
2933 2933 def import_(ui, repo, patch1=None, *patches, **opts):
2934 2934 """import an ordered set of patches
2935 2935
2936 2936 Import a list of patches and commit them individually (unless
2937 2937 --no-commit is specified).
2938 2938
2939 2939 To read a patch from standard input (stdin), use "-" as the patch
2940 2940 name. If a URL is specified, the patch will be downloaded from
2941 2941 there.
2942 2942
2943 2943 Import first applies changes to the working directory (unless
2944 2944 --bypass is specified), import will abort if there are outstanding
2945 2945 changes.
2946 2946
2947 2947 Use --bypass to apply and commit patches directly to the
2948 2948 repository, without affecting the working directory. Without
2949 2949 --exact, patches will be applied on top of the working directory
2950 2950 parent revision.
2951 2951
2952 2952 You can import a patch straight from a mail message. Even patches
2953 2953 as attachments work (to use the body part, it must have type
2954 2954 text/plain or text/x-patch). From and Subject headers of email
2955 2955 message are used as default committer and commit message. All
2956 2956 text/plain body parts before first diff are added to the commit
2957 2957 message.
2958 2958
2959 2959 If the imported patch was generated by :hg:`export`, user and
2960 2960 description from patch override values from message headers and
2961 2961 body. Values given on command line with -m/--message and -u/--user
2962 2962 override these.
2963 2963
2964 2964 If --exact is specified, import will set the working directory to
2965 2965 the parent of each patch before applying it, and will abort if the
2966 2966 resulting changeset has a different ID than the one recorded in
2967 2967 the patch. This will guard against various ways that portable
2968 2968 patch formats and mail systems might fail to transfer Mercurial
2969 2969 data or metadata. See :hg:`bundle` for lossless transmission.
2970 2970
2971 2971 Use --partial to ensure a changeset will be created from the patch
2972 2972 even if some hunks fail to apply. Hunks that fail to apply will be
2973 2973 written to a <target-file>.rej file. Conflicts can then be resolved
2974 2974 by hand before :hg:`commit --amend` is run to update the created
2975 2975 changeset. This flag exists to let people import patches that
2976 2976 partially apply without losing the associated metadata (author,
2977 2977 date, description, ...).
2978 2978
2979 2979 .. note::
2980 2980
2981 2981 When no hunks apply cleanly, :hg:`import --partial` will create
2982 2982 an empty changeset, importing only the patch metadata.
2983 2983
2984 2984 With -s/--similarity, hg will attempt to discover renames and
2985 2985 copies in the patch in the same way as :hg:`addremove`.
2986 2986
2987 2987 It is possible to use external patch programs to perform the patch
2988 2988 by setting the ``ui.patch`` configuration option. For the default
2989 2989 internal tool, the fuzz can also be configured via ``patch.fuzz``.
2990 2990 See :hg:`help config` for more information about configuration
2991 2991 files and how to use these options.
2992 2992
2993 2993 See :hg:`help dates` for a list of formats valid for -d/--date.
2994 2994
2995 2995 .. container:: verbose
2996 2996
2997 2997 Examples:
2998 2998
2999 2999 - import a traditional patch from a website and detect renames::
3000 3000
3001 3001 hg import -s 80 http://example.com/bugfix.patch
3002 3002
3003 3003 - import a changeset from an hgweb server::
3004 3004
3005 3005 hg import https://www.mercurial-scm.org/repo/hg/rev/5ca8c111e9aa
3006 3006
3007 3007 - import all the patches in an Unix-style mbox::
3008 3008
3009 3009 hg import incoming-patches.mbox
3010 3010
3011 3011 - import patches from stdin::
3012 3012
3013 3013 hg import -
3014 3014
3015 3015 - attempt to exactly restore an exported changeset (not always
3016 3016 possible)::
3017 3017
3018 3018 hg import --exact proposed-fix.patch
3019 3019
3020 3020 - use an external tool to apply a patch which is too fuzzy for
3021 3021 the default internal tool.
3022 3022
3023 3023 hg import --config ui.patch="patch --merge" fuzzy.patch
3024 3024
3025 3025 - change the default fuzzing from 2 to a less strict 7
3026 3026
3027 3027 hg import --config ui.fuzz=7 fuzz.patch
3028 3028
3029 3029 Returns 0 on success, 1 on partial success (see --partial).
3030 3030 """
3031 3031
3032 3032 opts = pycompat.byteskwargs(opts)
3033 3033 if not patch1:
3034 3034 raise error.Abort(_('need at least one patch to import'))
3035 3035
3036 3036 patches = (patch1,) + patches
3037 3037
3038 3038 date = opts.get('date')
3039 3039 if date:
3040 3040 opts['date'] = dateutil.parsedate(date)
3041 3041
3042 3042 exact = opts.get('exact')
3043 3043 update = not opts.get('bypass')
3044 3044 if not update and opts.get('no_commit'):
3045 3045 raise error.Abort(_('cannot use --no-commit with --bypass'))
3046 3046 try:
3047 3047 sim = float(opts.get('similarity') or 0)
3048 3048 except ValueError:
3049 3049 raise error.Abort(_('similarity must be a number'))
3050 3050 if sim < 0 or sim > 100:
3051 3051 raise error.Abort(_('similarity must be between 0 and 100'))
3052 3052 if sim and not update:
3053 3053 raise error.Abort(_('cannot use --similarity with --bypass'))
3054 3054 if exact:
3055 3055 if opts.get('edit'):
3056 3056 raise error.Abort(_('cannot use --exact with --edit'))
3057 3057 if opts.get('prefix'):
3058 3058 raise error.Abort(_('cannot use --exact with --prefix'))
3059 3059
3060 3060 base = opts["base"]
3061 3061 wlock = dsguard = lock = tr = None
3062 3062 msgs = []
3063 3063 ret = 0
3064 3064
3065 3065
3066 3066 try:
3067 3067 wlock = repo.wlock()
3068 3068
3069 3069 if update:
3070 3070 cmdutil.checkunfinished(repo)
3071 3071 if (exact or not opts.get('force')):
3072 3072 cmdutil.bailifchanged(repo)
3073 3073
3074 3074 if not opts.get('no_commit'):
3075 3075 lock = repo.lock()
3076 3076 tr = repo.transaction('import')
3077 3077 else:
3078 3078 dsguard = dirstateguard.dirstateguard(repo, 'import')
3079 3079 parents = repo[None].parents()
3080 3080 for patchurl in patches:
3081 3081 if patchurl == '-':
3082 3082 ui.status(_('applying patch from stdin\n'))
3083 3083 patchfile = ui.fin
3084 3084 patchurl = 'stdin' # for error message
3085 3085 else:
3086 3086 patchurl = os.path.join(base, patchurl)
3087 3087 ui.status(_('applying %s\n') % patchurl)
3088 3088 patchfile = hg.openpath(ui, patchurl)
3089 3089
3090 3090 haspatch = False
3091 3091 for hunk in patch.split(patchfile):
3092 patchdata = patch.extract(ui, hunk)
3093
3094 msg, node, rej = cmdutil.tryimportone(ui, repo, patchdata,
3095 parents, opts,
3096 msgs, hg.clean)
3092 with patch.extract(ui, hunk) as patchdata:
3093 msg, node, rej = cmdutil.tryimportone(ui, repo, patchdata,
3094 parents, opts,
3095 msgs, hg.clean)
3097 3096 if msg:
3098 3097 haspatch = True
3099 3098 ui.note(msg + '\n')
3100 3099 if update or exact:
3101 3100 parents = repo[None].parents()
3102 3101 else:
3103 3102 parents = [repo[node]]
3104 3103 if rej:
3105 3104 ui.write_err(_("patch applied partially\n"))
3106 3105 ui.write_err(_("(fix the .rej files and run "
3107 3106 "`hg commit --amend`)\n"))
3108 3107 ret = 1
3109 3108 break
3110 3109
3111 3110 if not haspatch:
3112 3111 raise error.Abort(_('%s: no diffs found') % patchurl)
3113 3112
3114 3113 if tr:
3115 3114 tr.close()
3116 3115 if msgs:
3117 3116 repo.savecommitmessage('\n* * *\n'.join(msgs))
3118 3117 if dsguard:
3119 3118 dsguard.close()
3120 3119 return ret
3121 3120 finally:
3122 3121 if tr:
3123 3122 tr.release()
3124 3123 release(lock, dsguard, wlock)
3125 3124
3126 3125 @command('incoming|in',
3127 3126 [('f', 'force', None,
3128 3127 _('run even if remote repository is unrelated')),
3129 3128 ('n', 'newest-first', None, _('show newest record first')),
3130 3129 ('', 'bundle', '',
3131 3130 _('file to store the bundles into'), _('FILE')),
3132 3131 ('r', 'rev', [], _('a remote changeset intended to be added'), _('REV')),
3133 3132 ('B', 'bookmarks', False, _("compare bookmarks")),
3134 3133 ('b', 'branch', [],
3135 3134 _('a specific branch you would like to pull'), _('BRANCH')),
3136 3135 ] + logopts + remoteopts + subrepoopts,
3137 3136 _('[-p] [-n] [-M] [-f] [-r REV]... [--bundle FILENAME] [SOURCE]'))
3138 3137 def incoming(ui, repo, source="default", **opts):
3139 3138 """show new changesets found in source
3140 3139
3141 3140 Show new changesets found in the specified path/URL or the default
3142 3141 pull location. These are the changesets that would have been pulled
3143 3142 by :hg:`pull` at the time you issued this command.
3144 3143
3145 3144 See pull for valid source format details.
3146 3145
3147 3146 .. container:: verbose
3148 3147
3149 3148 With -B/--bookmarks, the result of bookmark comparison between
3150 3149 local and remote repositories is displayed. With -v/--verbose,
3151 3150 status is also displayed for each bookmark like below::
3152 3151
3153 3152 BM1 01234567890a added
3154 3153 BM2 1234567890ab advanced
3155 3154 BM3 234567890abc diverged
3156 3155 BM4 34567890abcd changed
3157 3156
3158 3157 The action taken locally when pulling depends on the
3159 3158 status of each bookmark:
3160 3159
3161 3160 :``added``: pull will create it
3162 3161 :``advanced``: pull will update it
3163 3162 :``diverged``: pull will create a divergent bookmark
3164 3163 :``changed``: result depends on remote changesets
3165 3164
3166 3165 From the point of view of pulling behavior, bookmark
3167 3166 existing only in the remote repository are treated as ``added``,
3168 3167 even if it is in fact locally deleted.
3169 3168
3170 3169 .. container:: verbose
3171 3170
3172 3171 For remote repository, using --bundle avoids downloading the
3173 3172 changesets twice if the incoming is followed by a pull.
3174 3173
3175 3174 Examples:
3176 3175
3177 3176 - show incoming changes with patches and full description::
3178 3177
3179 3178 hg incoming -vp
3180 3179
3181 3180 - show incoming changes excluding merges, store a bundle::
3182 3181
3183 3182 hg in -vpM --bundle incoming.hg
3184 3183 hg pull incoming.hg
3185 3184
3186 3185 - briefly list changes inside a bundle::
3187 3186
3188 3187 hg in changes.hg -T "{desc|firstline}\\n"
3189 3188
3190 3189 Returns 0 if there are incoming changes, 1 otherwise.
3191 3190 """
3192 3191 opts = pycompat.byteskwargs(opts)
3193 3192 if opts.get('graph'):
3194 3193 logcmdutil.checkunsupportedgraphflags([], opts)
3195 3194 def display(other, chlist, displayer):
3196 3195 revdag = logcmdutil.graphrevs(other, chlist, opts)
3197 3196 logcmdutil.displaygraph(ui, repo, revdag, displayer,
3198 3197 graphmod.asciiedges)
3199 3198
3200 3199 hg._incoming(display, lambda: 1, ui, repo, source, opts, buffered=True)
3201 3200 return 0
3202 3201
3203 3202 if opts.get('bundle') and opts.get('subrepos'):
3204 3203 raise error.Abort(_('cannot combine --bundle and --subrepos'))
3205 3204
3206 3205 if opts.get('bookmarks'):
3207 3206 source, branches = hg.parseurl(ui.expandpath(source),
3208 3207 opts.get('branch'))
3209 3208 other = hg.peer(repo, opts, source)
3210 3209 if 'bookmarks' not in other.listkeys('namespaces'):
3211 3210 ui.warn(_("remote doesn't support bookmarks\n"))
3212 3211 return 0
3213 3212 ui.pager('incoming')
3214 3213 ui.status(_('comparing with %s\n') % util.hidepassword(source))
3215 3214 return bookmarks.incoming(ui, repo, other)
3216 3215
3217 3216 repo._subtoppath = ui.expandpath(source)
3218 3217 try:
3219 3218 return hg.incoming(ui, repo, source, opts)
3220 3219 finally:
3221 3220 del repo._subtoppath
3222 3221
3223 3222
3224 3223 @command('^init', remoteopts, _('[-e CMD] [--remotecmd CMD] [DEST]'),
3225 3224 norepo=True)
3226 3225 def init(ui, dest=".", **opts):
3227 3226 """create a new repository in the given directory
3228 3227
3229 3228 Initialize a new repository in the given directory. If the given
3230 3229 directory does not exist, it will be created.
3231 3230
3232 3231 If no directory is given, the current directory is used.
3233 3232
3234 3233 It is possible to specify an ``ssh://`` URL as the destination.
3235 3234 See :hg:`help urls` for more information.
3236 3235
3237 3236 Returns 0 on success.
3238 3237 """
3239 3238 opts = pycompat.byteskwargs(opts)
3240 3239 hg.peer(ui, opts, ui.expandpath(dest), create=True)
3241 3240
3242 3241 @command('locate',
3243 3242 [('r', 'rev', '', _('search the repository as it is in REV'), _('REV')),
3244 3243 ('0', 'print0', None, _('end filenames with NUL, for use with xargs')),
3245 3244 ('f', 'fullpath', None, _('print complete paths from the filesystem root')),
3246 3245 ] + walkopts,
3247 3246 _('[OPTION]... [PATTERN]...'))
3248 3247 def locate(ui, repo, *pats, **opts):
3249 3248 """locate files matching specific patterns (DEPRECATED)
3250 3249
3251 3250 Print files under Mercurial control in the working directory whose
3252 3251 names match the given patterns.
3253 3252
3254 3253 By default, this command searches all directories in the working
3255 3254 directory. To search just the current directory and its
3256 3255 subdirectories, use "--include .".
3257 3256
3258 3257 If no patterns are given to match, this command prints the names
3259 3258 of all files under Mercurial control in the working directory.
3260 3259
3261 3260 If you want to feed the output of this command into the "xargs"
3262 3261 command, use the -0 option to both this command and "xargs". This
3263 3262 will avoid the problem of "xargs" treating single filenames that
3264 3263 contain whitespace as multiple filenames.
3265 3264
3266 3265 See :hg:`help files` for a more versatile command.
3267 3266
3268 3267 Returns 0 if a match is found, 1 otherwise.
3269 3268 """
3270 3269 opts = pycompat.byteskwargs(opts)
3271 3270 if opts.get('print0'):
3272 3271 end = '\0'
3273 3272 else:
3274 3273 end = '\n'
3275 3274 ctx = scmutil.revsingle(repo, opts.get('rev'), None)
3276 3275
3277 3276 ret = 1
3278 3277 m = scmutil.match(ctx, pats, opts, default='relglob',
3279 3278 badfn=lambda x, y: False)
3280 3279
3281 3280 ui.pager('locate')
3282 3281 for abs in ctx.matches(m):
3283 3282 if opts.get('fullpath'):
3284 3283 ui.write(repo.wjoin(abs), end)
3285 3284 else:
3286 3285 ui.write(((pats and m.rel(abs)) or abs), end)
3287 3286 ret = 0
3288 3287
3289 3288 return ret
3290 3289
3291 3290 @command('^log|history',
3292 3291 [('f', 'follow', None,
3293 3292 _('follow changeset history, or file history across copies and renames')),
3294 3293 ('', 'follow-first', None,
3295 3294 _('only follow the first parent of merge changesets (DEPRECATED)')),
3296 3295 ('d', 'date', '', _('show revisions matching date spec'), _('DATE')),
3297 3296 ('C', 'copies', None, _('show copied files')),
3298 3297 ('k', 'keyword', [],
3299 3298 _('do case-insensitive search for a given text'), _('TEXT')),
3300 3299 ('r', 'rev', [], _('show the specified revision or revset'), _('REV')),
3301 3300 ('L', 'line-range', [],
3302 3301 _('follow line range of specified file (EXPERIMENTAL)'),
3303 3302 _('FILE,RANGE')),
3304 3303 ('', 'removed', None, _('include revisions where files were removed')),
3305 3304 ('m', 'only-merges', None, _('show only merges (DEPRECATED)')),
3306 3305 ('u', 'user', [], _('revisions committed by user'), _('USER')),
3307 3306 ('', 'only-branch', [],
3308 3307 _('show only changesets within the given named branch (DEPRECATED)'),
3309 3308 _('BRANCH')),
3310 3309 ('b', 'branch', [],
3311 3310 _('show changesets within the given named branch'), _('BRANCH')),
3312 3311 ('P', 'prune', [],
3313 3312 _('do not display revision or any of its ancestors'), _('REV')),
3314 3313 ] + logopts + walkopts,
3315 3314 _('[OPTION]... [FILE]'),
3316 3315 inferrepo=True, cmdtype=readonly)
3317 3316 def log(ui, repo, *pats, **opts):
3318 3317 """show revision history of entire repository or files
3319 3318
3320 3319 Print the revision history of the specified files or the entire
3321 3320 project.
3322 3321
3323 3322 If no revision range is specified, the default is ``tip:0`` unless
3324 3323 --follow is set, in which case the working directory parent is
3325 3324 used as the starting revision.
3326 3325
3327 3326 File history is shown without following rename or copy history of
3328 3327 files. Use -f/--follow with a filename to follow history across
3329 3328 renames and copies. --follow without a filename will only show
3330 3329 ancestors of the starting revision.
3331 3330
3332 3331 By default this command prints revision number and changeset id,
3333 3332 tags, non-trivial parents, user, date and time, and a summary for
3334 3333 each commit. When the -v/--verbose switch is used, the list of
3335 3334 changed files and full commit message are shown.
3336 3335
3337 3336 With --graph the revisions are shown as an ASCII art DAG with the most
3338 3337 recent changeset at the top.
3339 3338 'o' is a changeset, '@' is a working directory parent, '_' closes a branch,
3340 3339 'x' is obsolete, '*' is unstable, and '+' represents a fork where the
3341 3340 changeset from the lines below is a parent of the 'o' merge on the same
3342 3341 line.
3343 3342 Paths in the DAG are represented with '|', '/' and so forth. ':' in place
3344 3343 of a '|' indicates one or more revisions in a path are omitted.
3345 3344
3346 3345 .. container:: verbose
3347 3346
3348 3347 Use -L/--line-range FILE,M:N options to follow the history of lines
3349 3348 from M to N in FILE. With -p/--patch only diff hunks affecting
3350 3349 specified line range will be shown. This option requires --follow;
3351 3350 it can be specified multiple times. Currently, this option is not
3352 3351 compatible with --graph. This option is experimental.
3353 3352
3354 3353 .. note::
3355 3354
3356 3355 :hg:`log --patch` may generate unexpected diff output for merge
3357 3356 changesets, as it will only compare the merge changeset against
3358 3357 its first parent. Also, only files different from BOTH parents
3359 3358 will appear in files:.
3360 3359
3361 3360 .. note::
3362 3361
3363 3362 For performance reasons, :hg:`log FILE` may omit duplicate changes
3364 3363 made on branches and will not show removals or mode changes. To
3365 3364 see all such changes, use the --removed switch.
3366 3365
3367 3366 .. container:: verbose
3368 3367
3369 3368 .. note::
3370 3369
3371 3370 The history resulting from -L/--line-range options depends on diff
3372 3371 options; for instance if white-spaces are ignored, respective changes
3373 3372 with only white-spaces in specified line range will not be listed.
3374 3373
3375 3374 .. container:: verbose
3376 3375
3377 3376 Some examples:
3378 3377
3379 3378 - changesets with full descriptions and file lists::
3380 3379
3381 3380 hg log -v
3382 3381
3383 3382 - changesets ancestral to the working directory::
3384 3383
3385 3384 hg log -f
3386 3385
3387 3386 - last 10 commits on the current branch::
3388 3387
3389 3388 hg log -l 10 -b .
3390 3389
3391 3390 - changesets showing all modifications of a file, including removals::
3392 3391
3393 3392 hg log --removed file.c
3394 3393
3395 3394 - all changesets that touch a directory, with diffs, excluding merges::
3396 3395
3397 3396 hg log -Mp lib/
3398 3397
3399 3398 - all revision numbers that match a keyword::
3400 3399
3401 3400 hg log -k bug --template "{rev}\\n"
3402 3401
3403 3402 - the full hash identifier of the working directory parent::
3404 3403
3405 3404 hg log -r . --template "{node}\\n"
3406 3405
3407 3406 - list available log templates::
3408 3407
3409 3408 hg log -T list
3410 3409
3411 3410 - check if a given changeset is included in a tagged release::
3412 3411
3413 3412 hg log -r "a21ccf and ancestor(1.9)"
3414 3413
3415 3414 - find all changesets by some user in a date range::
3416 3415
3417 3416 hg log -k alice -d "may 2008 to jul 2008"
3418 3417
3419 3418 - summary of all changesets after the last tag::
3420 3419
3421 3420 hg log -r "last(tagged())::" --template "{desc|firstline}\\n"
3422 3421
3423 3422 - changesets touching lines 13 to 23 for file.c::
3424 3423
3425 3424 hg log -L file.c,13:23
3426 3425
3427 3426 - changesets touching lines 13 to 23 for file.c and lines 2 to 6 of
3428 3427 main.c with patch::
3429 3428
3430 3429 hg log -L file.c,13:23 -L main.c,2:6 -p
3431 3430
3432 3431 See :hg:`help dates` for a list of formats valid for -d/--date.
3433 3432
3434 3433 See :hg:`help revisions` for more about specifying and ordering
3435 3434 revisions.
3436 3435
3437 3436 See :hg:`help templates` for more about pre-packaged styles and
3438 3437 specifying custom templates. The default template used by the log
3439 3438 command can be customized via the ``ui.logtemplate`` configuration
3440 3439 setting.
3441 3440
3442 3441 Returns 0 on success.
3443 3442
3444 3443 """
3445 3444 opts = pycompat.byteskwargs(opts)
3446 3445 linerange = opts.get('line_range')
3447 3446
3448 3447 if linerange and not opts.get('follow'):
3449 3448 raise error.Abort(_('--line-range requires --follow'))
3450 3449
3451 3450 if linerange and pats:
3452 3451 # TODO: take pats as patterns with no line-range filter
3453 3452 raise error.Abort(
3454 3453 _('FILE arguments are not compatible with --line-range option')
3455 3454 )
3456 3455
3457 3456 repo = scmutil.unhidehashlikerevs(repo, opts.get('rev'), 'nowarn')
3458 3457 revs, differ = logcmdutil.getrevs(repo, pats, opts)
3459 3458 if linerange:
3460 3459 # TODO: should follow file history from logcmdutil._initialrevs(),
3461 3460 # then filter the result by logcmdutil._makerevset() and --limit
3462 3461 revs, differ = logcmdutil.getlinerangerevs(repo, revs, opts)
3463 3462
3464 3463 getrenamed = None
3465 3464 if opts.get('copies'):
3466 3465 endrev = None
3467 3466 if opts.get('rev'):
3468 3467 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
3469 3468 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
3470 3469
3471 3470 ui.pager('log')
3472 3471 displayer = logcmdutil.changesetdisplayer(ui, repo, opts, differ,
3473 3472 buffered=True)
3474 3473 if opts.get('graph'):
3475 3474 displayfn = logcmdutil.displaygraphrevs
3476 3475 else:
3477 3476 displayfn = logcmdutil.displayrevs
3478 3477 displayfn(ui, repo, revs, displayer, getrenamed)
3479 3478
3480 3479 @command('manifest',
3481 3480 [('r', 'rev', '', _('revision to display'), _('REV')),
3482 3481 ('', 'all', False, _("list files from all revisions"))]
3483 3482 + formatteropts,
3484 3483 _('[-r REV]'), cmdtype=readonly)
3485 3484 def manifest(ui, repo, node=None, rev=None, **opts):
3486 3485 """output the current or given revision of the project manifest
3487 3486
3488 3487 Print a list of version controlled files for the given revision.
3489 3488 If no revision is given, the first parent of the working directory
3490 3489 is used, or the null revision if no revision is checked out.
3491 3490
3492 3491 With -v, print file permissions, symlink and executable bits.
3493 3492 With --debug, print file revision hashes.
3494 3493
3495 3494 If option --all is specified, the list of all files from all revisions
3496 3495 is printed. This includes deleted and renamed files.
3497 3496
3498 3497 Returns 0 on success.
3499 3498 """
3500 3499 opts = pycompat.byteskwargs(opts)
3501 3500 fm = ui.formatter('manifest', opts)
3502 3501
3503 3502 if opts.get('all'):
3504 3503 if rev or node:
3505 3504 raise error.Abort(_("can't specify a revision with --all"))
3506 3505
3507 3506 res = set()
3508 3507 for rev in repo:
3509 3508 ctx = repo[rev]
3510 3509 res |= set(ctx.files())
3511 3510
3512 3511 ui.pager('manifest')
3513 3512 for f in sorted(res):
3514 3513 fm.startitem()
3515 3514 fm.write("path", '%s\n', f)
3516 3515 fm.end()
3517 3516 return
3518 3517
3519 3518 if rev and node:
3520 3519 raise error.Abort(_("please specify just one revision"))
3521 3520
3522 3521 if not node:
3523 3522 node = rev
3524 3523
3525 3524 char = {'l': '@', 'x': '*', '': '', 't': 'd'}
3526 3525 mode = {'l': '644', 'x': '755', '': '644', 't': '755'}
3527 3526 if node:
3528 3527 repo = scmutil.unhidehashlikerevs(repo, [node], 'nowarn')
3529 3528 ctx = scmutil.revsingle(repo, node)
3530 3529 mf = ctx.manifest()
3531 3530 ui.pager('manifest')
3532 3531 for f in ctx:
3533 3532 fm.startitem()
3534 3533 fl = ctx[f].flags()
3535 3534 fm.condwrite(ui.debugflag, 'hash', '%s ', hex(mf[f]))
3536 3535 fm.condwrite(ui.verbose, 'mode type', '%s %1s ', mode[fl], char[fl])
3537 3536 fm.write('path', '%s\n', f)
3538 3537 fm.end()
3539 3538
3540 3539 @command('^merge',
3541 3540 [('f', 'force', None,
3542 3541 _('force a merge including outstanding changes (DEPRECATED)')),
3543 3542 ('r', 'rev', '', _('revision to merge'), _('REV')),
3544 3543 ('P', 'preview', None,
3545 3544 _('review revisions to merge (no merge is performed)')),
3546 3545 ('', 'abort', None, _('abort the ongoing merge')),
3547 3546 ] + mergetoolopts,
3548 3547 _('[-P] [[-r] REV]'))
3549 3548 def merge(ui, repo, node=None, **opts):
3550 3549 """merge another revision into working directory
3551 3550
3552 3551 The current working directory is updated with all changes made in
3553 3552 the requested revision since the last common predecessor revision.
3554 3553
3555 3554 Files that changed between either parent are marked as changed for
3556 3555 the next commit and a commit must be performed before any further
3557 3556 updates to the repository are allowed. The next commit will have
3558 3557 two parents.
3559 3558
3560 3559 ``--tool`` can be used to specify the merge tool used for file
3561 3560 merges. It overrides the HGMERGE environment variable and your
3562 3561 configuration files. See :hg:`help merge-tools` for options.
3563 3562
3564 3563 If no revision is specified, the working directory's parent is a
3565 3564 head revision, and the current branch contains exactly one other
3566 3565 head, the other head is merged with by default. Otherwise, an
3567 3566 explicit revision with which to merge with must be provided.
3568 3567
3569 3568 See :hg:`help resolve` for information on handling file conflicts.
3570 3569
3571 3570 To undo an uncommitted merge, use :hg:`merge --abort` which
3572 3571 will check out a clean copy of the original merge parent, losing
3573 3572 all changes.
3574 3573
3575 3574 Returns 0 on success, 1 if there are unresolved files.
3576 3575 """
3577 3576
3578 3577 opts = pycompat.byteskwargs(opts)
3579 3578 abort = opts.get('abort')
3580 3579 if abort and repo.dirstate.p2() == nullid:
3581 3580 cmdutil.wrongtooltocontinue(repo, _('merge'))
3582 3581 if abort:
3583 3582 if node:
3584 3583 raise error.Abort(_("cannot specify a node with --abort"))
3585 3584 if opts.get('rev'):
3586 3585 raise error.Abort(_("cannot specify both --rev and --abort"))
3587 3586 if opts.get('preview'):
3588 3587 raise error.Abort(_("cannot specify --preview with --abort"))
3589 3588 if opts.get('rev') and node:
3590 3589 raise error.Abort(_("please specify just one revision"))
3591 3590 if not node:
3592 3591 node = opts.get('rev')
3593 3592
3594 3593 if node:
3595 3594 node = scmutil.revsingle(repo, node).node()
3596 3595
3597 3596 if not node and not abort:
3598 3597 node = repo[destutil.destmerge(repo)].node()
3599 3598
3600 3599 if opts.get('preview'):
3601 3600 # find nodes that are ancestors of p2 but not of p1
3602 3601 p1 = repo.lookup('.')
3603 3602 p2 = node
3604 3603 nodes = repo.changelog.findmissing(common=[p1], heads=[p2])
3605 3604
3606 3605 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
3607 3606 for node in nodes:
3608 3607 displayer.show(repo[node])
3609 3608 displayer.close()
3610 3609 return 0
3611 3610
3612 3611 try:
3613 3612 # ui.forcemerge is an internal variable, do not document
3614 3613 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''), 'merge')
3615 3614 force = opts.get('force')
3616 3615 labels = ['working copy', 'merge rev']
3617 3616 return hg.merge(repo, node, force=force, mergeforce=force,
3618 3617 labels=labels, abort=abort)
3619 3618 finally:
3620 3619 ui.setconfig('ui', 'forcemerge', '', 'merge')
3621 3620
3622 3621 @command('outgoing|out',
3623 3622 [('f', 'force', None, _('run even when the destination is unrelated')),
3624 3623 ('r', 'rev', [],
3625 3624 _('a changeset intended to be included in the destination'), _('REV')),
3626 3625 ('n', 'newest-first', None, _('show newest record first')),
3627 3626 ('B', 'bookmarks', False, _('compare bookmarks')),
3628 3627 ('b', 'branch', [], _('a specific branch you would like to push'),
3629 3628 _('BRANCH')),
3630 3629 ] + logopts + remoteopts + subrepoopts,
3631 3630 _('[-M] [-p] [-n] [-f] [-r REV]... [DEST]'))
3632 3631 def outgoing(ui, repo, dest=None, **opts):
3633 3632 """show changesets not found in the destination
3634 3633
3635 3634 Show changesets not found in the specified destination repository
3636 3635 or the default push location. These are the changesets that would
3637 3636 be pushed if a push was requested.
3638 3637
3639 3638 See pull for details of valid destination formats.
3640 3639
3641 3640 .. container:: verbose
3642 3641
3643 3642 With -B/--bookmarks, the result of bookmark comparison between
3644 3643 local and remote repositories is displayed. With -v/--verbose,
3645 3644 status is also displayed for each bookmark like below::
3646 3645
3647 3646 BM1 01234567890a added
3648 3647 BM2 deleted
3649 3648 BM3 234567890abc advanced
3650 3649 BM4 34567890abcd diverged
3651 3650 BM5 4567890abcde changed
3652 3651
3653 3652 The action taken when pushing depends on the
3654 3653 status of each bookmark:
3655 3654
3656 3655 :``added``: push with ``-B`` will create it
3657 3656 :``deleted``: push with ``-B`` will delete it
3658 3657 :``advanced``: push will update it
3659 3658 :``diverged``: push with ``-B`` will update it
3660 3659 :``changed``: push with ``-B`` will update it
3661 3660
3662 3661 From the point of view of pushing behavior, bookmarks
3663 3662 existing only in the remote repository are treated as
3664 3663 ``deleted``, even if it is in fact added remotely.
3665 3664
3666 3665 Returns 0 if there are outgoing changes, 1 otherwise.
3667 3666 """
3668 3667 opts = pycompat.byteskwargs(opts)
3669 3668 if opts.get('graph'):
3670 3669 logcmdutil.checkunsupportedgraphflags([], opts)
3671 3670 o, other = hg._outgoing(ui, repo, dest, opts)
3672 3671 if not o:
3673 3672 cmdutil.outgoinghooks(ui, repo, other, opts, o)
3674 3673 return
3675 3674
3676 3675 revdag = logcmdutil.graphrevs(repo, o, opts)
3677 3676 ui.pager('outgoing')
3678 3677 displayer = logcmdutil.changesetdisplayer(ui, repo, opts, buffered=True)
3679 3678 logcmdutil.displaygraph(ui, repo, revdag, displayer,
3680 3679 graphmod.asciiedges)
3681 3680 cmdutil.outgoinghooks(ui, repo, other, opts, o)
3682 3681 return 0
3683 3682
3684 3683 if opts.get('bookmarks'):
3685 3684 dest = ui.expandpath(dest or 'default-push', dest or 'default')
3686 3685 dest, branches = hg.parseurl(dest, opts.get('branch'))
3687 3686 other = hg.peer(repo, opts, dest)
3688 3687 if 'bookmarks' not in other.listkeys('namespaces'):
3689 3688 ui.warn(_("remote doesn't support bookmarks\n"))
3690 3689 return 0
3691 3690 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
3692 3691 ui.pager('outgoing')
3693 3692 return bookmarks.outgoing(ui, repo, other)
3694 3693
3695 3694 repo._subtoppath = ui.expandpath(dest or 'default-push', dest or 'default')
3696 3695 try:
3697 3696 return hg.outgoing(ui, repo, dest, opts)
3698 3697 finally:
3699 3698 del repo._subtoppath
3700 3699
3701 3700 @command('parents',
3702 3701 [('r', 'rev', '', _('show parents of the specified revision'), _('REV')),
3703 3702 ] + templateopts,
3704 3703 _('[-r REV] [FILE]'),
3705 3704 inferrepo=True)
3706 3705 def parents(ui, repo, file_=None, **opts):
3707 3706 """show the parents of the working directory or revision (DEPRECATED)
3708 3707
3709 3708 Print the working directory's parent revisions. If a revision is
3710 3709 given via -r/--rev, the parent of that revision will be printed.
3711 3710 If a file argument is given, the revision in which the file was
3712 3711 last changed (before the working directory revision or the
3713 3712 argument to --rev if given) is printed.
3714 3713
3715 3714 This command is equivalent to::
3716 3715
3717 3716 hg log -r "p1()+p2()" or
3718 3717 hg log -r "p1(REV)+p2(REV)" or
3719 3718 hg log -r "max(::p1() and file(FILE))+max(::p2() and file(FILE))" or
3720 3719 hg log -r "max(::p1(REV) and file(FILE))+max(::p2(REV) and file(FILE))"
3721 3720
3722 3721 See :hg:`summary` and :hg:`help revsets` for related information.
3723 3722
3724 3723 Returns 0 on success.
3725 3724 """
3726 3725
3727 3726 opts = pycompat.byteskwargs(opts)
3728 3727 rev = opts.get('rev')
3729 3728 if rev:
3730 3729 repo = scmutil.unhidehashlikerevs(repo, [rev], 'nowarn')
3731 3730 ctx = scmutil.revsingle(repo, rev, None)
3732 3731
3733 3732 if file_:
3734 3733 m = scmutil.match(ctx, (file_,), opts)
3735 3734 if m.anypats() or len(m.files()) != 1:
3736 3735 raise error.Abort(_('can only specify an explicit filename'))
3737 3736 file_ = m.files()[0]
3738 3737 filenodes = []
3739 3738 for cp in ctx.parents():
3740 3739 if not cp:
3741 3740 continue
3742 3741 try:
3743 3742 filenodes.append(cp.filenode(file_))
3744 3743 except error.LookupError:
3745 3744 pass
3746 3745 if not filenodes:
3747 3746 raise error.Abort(_("'%s' not found in manifest!") % file_)
3748 3747 p = []
3749 3748 for fn in filenodes:
3750 3749 fctx = repo.filectx(file_, fileid=fn)
3751 3750 p.append(fctx.node())
3752 3751 else:
3753 3752 p = [cp.node() for cp in ctx.parents()]
3754 3753
3755 3754 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
3756 3755 for n in p:
3757 3756 if n != nullid:
3758 3757 displayer.show(repo[n])
3759 3758 displayer.close()
3760 3759
3761 3760 @command('paths', formatteropts, _('[NAME]'), optionalrepo=True,
3762 3761 cmdtype=readonly)
3763 3762 def paths(ui, repo, search=None, **opts):
3764 3763 """show aliases for remote repositories
3765 3764
3766 3765 Show definition of symbolic path name NAME. If no name is given,
3767 3766 show definition of all available names.
3768 3767
3769 3768 Option -q/--quiet suppresses all output when searching for NAME
3770 3769 and shows only the path names when listing all definitions.
3771 3770
3772 3771 Path names are defined in the [paths] section of your
3773 3772 configuration file and in ``/etc/mercurial/hgrc``. If run inside a
3774 3773 repository, ``.hg/hgrc`` is used, too.
3775 3774
3776 3775 The path names ``default`` and ``default-push`` have a special
3777 3776 meaning. When performing a push or pull operation, they are used
3778 3777 as fallbacks if no location is specified on the command-line.
3779 3778 When ``default-push`` is set, it will be used for push and
3780 3779 ``default`` will be used for pull; otherwise ``default`` is used
3781 3780 as the fallback for both. When cloning a repository, the clone
3782 3781 source is written as ``default`` in ``.hg/hgrc``.
3783 3782
3784 3783 .. note::
3785 3784
3786 3785 ``default`` and ``default-push`` apply to all inbound (e.g.
3787 3786 :hg:`incoming`) and outbound (e.g. :hg:`outgoing`, :hg:`email`
3788 3787 and :hg:`bundle`) operations.
3789 3788
3790 3789 See :hg:`help urls` for more information.
3791 3790
3792 3791 Returns 0 on success.
3793 3792 """
3794 3793
3795 3794 opts = pycompat.byteskwargs(opts)
3796 3795 ui.pager('paths')
3797 3796 if search:
3798 3797 pathitems = [(name, path) for name, path in ui.paths.iteritems()
3799 3798 if name == search]
3800 3799 else:
3801 3800 pathitems = sorted(ui.paths.iteritems())
3802 3801
3803 3802 fm = ui.formatter('paths', opts)
3804 3803 if fm.isplain():
3805 3804 hidepassword = util.hidepassword
3806 3805 else:
3807 3806 hidepassword = bytes
3808 3807 if ui.quiet:
3809 3808 namefmt = '%s\n'
3810 3809 else:
3811 3810 namefmt = '%s = '
3812 3811 showsubopts = not search and not ui.quiet
3813 3812
3814 3813 for name, path in pathitems:
3815 3814 fm.startitem()
3816 3815 fm.condwrite(not search, 'name', namefmt, name)
3817 3816 fm.condwrite(not ui.quiet, 'url', '%s\n', hidepassword(path.rawloc))
3818 3817 for subopt, value in sorted(path.suboptions.items()):
3819 3818 assert subopt not in ('name', 'url')
3820 3819 if showsubopts:
3821 3820 fm.plain('%s:%s = ' % (name, subopt))
3822 3821 fm.condwrite(showsubopts, subopt, '%s\n', value)
3823 3822
3824 3823 fm.end()
3825 3824
3826 3825 if search and not pathitems:
3827 3826 if not ui.quiet:
3828 3827 ui.warn(_("not found!\n"))
3829 3828 return 1
3830 3829 else:
3831 3830 return 0
3832 3831
3833 3832 @command('phase',
3834 3833 [('p', 'public', False, _('set changeset phase to public')),
3835 3834 ('d', 'draft', False, _('set changeset phase to draft')),
3836 3835 ('s', 'secret', False, _('set changeset phase to secret')),
3837 3836 ('f', 'force', False, _('allow to move boundary backward')),
3838 3837 ('r', 'rev', [], _('target revision'), _('REV')),
3839 3838 ],
3840 3839 _('[-p|-d|-s] [-f] [-r] [REV...]'))
3841 3840 def phase(ui, repo, *revs, **opts):
3842 3841 """set or show the current phase name
3843 3842
3844 3843 With no argument, show the phase name of the current revision(s).
3845 3844
3846 3845 With one of -p/--public, -d/--draft or -s/--secret, change the
3847 3846 phase value of the specified revisions.
3848 3847
3849 3848 Unless -f/--force is specified, :hg:`phase` won't move changesets from a
3850 3849 lower phase to a higher phase. Phases are ordered as follows::
3851 3850
3852 3851 public < draft < secret
3853 3852
3854 3853 Returns 0 on success, 1 if some phases could not be changed.
3855 3854
3856 3855 (For more information about the phases concept, see :hg:`help phases`.)
3857 3856 """
3858 3857 opts = pycompat.byteskwargs(opts)
3859 3858 # search for a unique phase argument
3860 3859 targetphase = None
3861 3860 for idx, name in enumerate(phases.phasenames):
3862 3861 if opts[name]:
3863 3862 if targetphase is not None:
3864 3863 raise error.Abort(_('only one phase can be specified'))
3865 3864 targetphase = idx
3866 3865
3867 3866 # look for specified revision
3868 3867 revs = list(revs)
3869 3868 revs.extend(opts['rev'])
3870 3869 if not revs:
3871 3870 # display both parents as the second parent phase can influence
3872 3871 # the phase of a merge commit
3873 3872 revs = [c.rev() for c in repo[None].parents()]
3874 3873
3875 3874 revs = scmutil.revrange(repo, revs)
3876 3875
3877 3876 ret = 0
3878 3877 if targetphase is None:
3879 3878 # display
3880 3879 for r in revs:
3881 3880 ctx = repo[r]
3882 3881 ui.write('%i: %s\n' % (ctx.rev(), ctx.phasestr()))
3883 3882 else:
3884 3883 with repo.lock(), repo.transaction("phase") as tr:
3885 3884 # set phase
3886 3885 if not revs:
3887 3886 raise error.Abort(_('empty revision set'))
3888 3887 nodes = [repo[r].node() for r in revs]
3889 3888 # moving revision from public to draft may hide them
3890 3889 # We have to check result on an unfiltered repository
3891 3890 unfi = repo.unfiltered()
3892 3891 getphase = unfi._phasecache.phase
3893 3892 olddata = [getphase(unfi, r) for r in unfi]
3894 3893 phases.advanceboundary(repo, tr, targetphase, nodes)
3895 3894 if opts['force']:
3896 3895 phases.retractboundary(repo, tr, targetphase, nodes)
3897 3896 getphase = unfi._phasecache.phase
3898 3897 newdata = [getphase(unfi, r) for r in unfi]
3899 3898 changes = sum(newdata[r] != olddata[r] for r in unfi)
3900 3899 cl = unfi.changelog
3901 3900 rejected = [n for n in nodes
3902 3901 if newdata[cl.rev(n)] < targetphase]
3903 3902 if rejected:
3904 3903 ui.warn(_('cannot move %i changesets to a higher '
3905 3904 'phase, use --force\n') % len(rejected))
3906 3905 ret = 1
3907 3906 if changes:
3908 3907 msg = _('phase changed for %i changesets\n') % changes
3909 3908 if ret:
3910 3909 ui.status(msg)
3911 3910 else:
3912 3911 ui.note(msg)
3913 3912 else:
3914 3913 ui.warn(_('no phases changed\n'))
3915 3914 return ret
3916 3915
3917 3916 def postincoming(ui, repo, modheads, optupdate, checkout, brev):
3918 3917 """Run after a changegroup has been added via pull/unbundle
3919 3918
3920 3919 This takes arguments below:
3921 3920
3922 3921 :modheads: change of heads by pull/unbundle
3923 3922 :optupdate: updating working directory is needed or not
3924 3923 :checkout: update destination revision (or None to default destination)
3925 3924 :brev: a name, which might be a bookmark to be activated after updating
3926 3925 """
3927 3926 if modheads == 0:
3928 3927 return
3929 3928 if optupdate:
3930 3929 try:
3931 3930 return hg.updatetotally(ui, repo, checkout, brev)
3932 3931 except error.UpdateAbort as inst:
3933 3932 msg = _("not updating: %s") % stringutil.forcebytestr(inst)
3934 3933 hint = inst.hint
3935 3934 raise error.UpdateAbort(msg, hint=hint)
3936 3935 if modheads > 1:
3937 3936 currentbranchheads = len(repo.branchheads())
3938 3937 if currentbranchheads == modheads:
3939 3938 ui.status(_("(run 'hg heads' to see heads, 'hg merge' to merge)\n"))
3940 3939 elif currentbranchheads > 1:
3941 3940 ui.status(_("(run 'hg heads .' to see heads, 'hg merge' to "
3942 3941 "merge)\n"))
3943 3942 else:
3944 3943 ui.status(_("(run 'hg heads' to see heads)\n"))
3945 3944 elif not ui.configbool('commands', 'update.requiredest'):
3946 3945 ui.status(_("(run 'hg update' to get a working copy)\n"))
3947 3946
3948 3947 @command('^pull',
3949 3948 [('u', 'update', None,
3950 3949 _('update to new branch head if new descendants were pulled')),
3951 3950 ('f', 'force', None, _('run even when remote repository is unrelated')),
3952 3951 ('r', 'rev', [], _('a remote changeset intended to be added'), _('REV')),
3953 3952 ('B', 'bookmark', [], _("bookmark to pull"), _('BOOKMARK')),
3954 3953 ('b', 'branch', [], _('a specific branch you would like to pull'),
3955 3954 _('BRANCH')),
3956 3955 ] + remoteopts,
3957 3956 _('[-u] [-f] [-r REV]... [-e CMD] [--remotecmd CMD] [SOURCE]'))
3958 3957 def pull(ui, repo, source="default", **opts):
3959 3958 """pull changes from the specified source
3960 3959
3961 3960 Pull changes from a remote repository to a local one.
3962 3961
3963 3962 This finds all changes from the repository at the specified path
3964 3963 or URL and adds them to a local repository (the current one unless
3965 3964 -R is specified). By default, this does not update the copy of the
3966 3965 project in the working directory.
3967 3966
3968 3967 When cloning from servers that support it, Mercurial may fetch
3969 3968 pre-generated data. When this is done, hooks operating on incoming
3970 3969 changesets and changegroups may fire more than once, once for each
3971 3970 pre-generated bundle and as well as for any additional remaining
3972 3971 data. See :hg:`help -e clonebundles` for more.
3973 3972
3974 3973 Use :hg:`incoming` if you want to see what would have been added
3975 3974 by a pull at the time you issued this command. If you then decide
3976 3975 to add those changes to the repository, you should use :hg:`pull
3977 3976 -r X` where ``X`` is the last changeset listed by :hg:`incoming`.
3978 3977
3979 3978 If SOURCE is omitted, the 'default' path will be used.
3980 3979 See :hg:`help urls` for more information.
3981 3980
3982 3981 Specifying bookmark as ``.`` is equivalent to specifying the active
3983 3982 bookmark's name.
3984 3983
3985 3984 Returns 0 on success, 1 if an update had unresolved files.
3986 3985 """
3987 3986
3988 3987 opts = pycompat.byteskwargs(opts)
3989 3988 if ui.configbool('commands', 'update.requiredest') and opts.get('update'):
3990 3989 msg = _('update destination required by configuration')
3991 3990 hint = _('use hg pull followed by hg update DEST')
3992 3991 raise error.Abort(msg, hint=hint)
3993 3992
3994 3993 source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch'))
3995 3994 ui.status(_('pulling from %s\n') % util.hidepassword(source))
3996 3995 other = hg.peer(repo, opts, source)
3997 3996 try:
3998 3997 revs, checkout = hg.addbranchrevs(repo, other, branches,
3999 3998 opts.get('rev'))
4000 3999
4001 4000
4002 4001 pullopargs = {}
4003 4002 if opts.get('bookmark'):
4004 4003 if not revs:
4005 4004 revs = []
4006 4005 # The list of bookmark used here is not the one used to actually
4007 4006 # update the bookmark name. This can result in the revision pulled
4008 4007 # not ending up with the name of the bookmark because of a race
4009 4008 # condition on the server. (See issue 4689 for details)
4010 4009 remotebookmarks = other.listkeys('bookmarks')
4011 4010 remotebookmarks = bookmarks.unhexlifybookmarks(remotebookmarks)
4012 4011 pullopargs['remotebookmarks'] = remotebookmarks
4013 4012 for b in opts['bookmark']:
4014 4013 b = repo._bookmarks.expandname(b)
4015 4014 if b not in remotebookmarks:
4016 4015 raise error.Abort(_('remote bookmark %s not found!') % b)
4017 4016 revs.append(hex(remotebookmarks[b]))
4018 4017
4019 4018 if revs:
4020 4019 try:
4021 4020 # When 'rev' is a bookmark name, we cannot guarantee that it
4022 4021 # will be updated with that name because of a race condition
4023 4022 # server side. (See issue 4689 for details)
4024 4023 oldrevs = revs
4025 4024 revs = [] # actually, nodes
4026 4025 for r in oldrevs:
4027 4026 node = other.lookup(r)
4028 4027 revs.append(node)
4029 4028 if r == checkout:
4030 4029 checkout = node
4031 4030 except error.CapabilityError:
4032 4031 err = _("other repository doesn't support revision lookup, "
4033 4032 "so a rev cannot be specified.")
4034 4033 raise error.Abort(err)
4035 4034
4036 4035 wlock = util.nullcontextmanager()
4037 4036 if opts.get('update'):
4038 4037 wlock = repo.wlock()
4039 4038 with wlock:
4040 4039 pullopargs.update(opts.get('opargs', {}))
4041 4040 modheads = exchange.pull(repo, other, heads=revs,
4042 4041 force=opts.get('force'),
4043 4042 bookmarks=opts.get('bookmark', ()),
4044 4043 opargs=pullopargs).cgresult
4045 4044
4046 4045 # brev is a name, which might be a bookmark to be activated at
4047 4046 # the end of the update. In other words, it is an explicit
4048 4047 # destination of the update
4049 4048 brev = None
4050 4049
4051 4050 if checkout:
4052 4051 checkout = repo.changelog.rev(checkout)
4053 4052
4054 4053 # order below depends on implementation of
4055 4054 # hg.addbranchrevs(). opts['bookmark'] is ignored,
4056 4055 # because 'checkout' is determined without it.
4057 4056 if opts.get('rev'):
4058 4057 brev = opts['rev'][0]
4059 4058 elif opts.get('branch'):
4060 4059 brev = opts['branch'][0]
4061 4060 else:
4062 4061 brev = branches[0]
4063 4062 repo._subtoppath = source
4064 4063 try:
4065 4064 ret = postincoming(ui, repo, modheads, opts.get('update'),
4066 4065 checkout, brev)
4067 4066
4068 4067 finally:
4069 4068 del repo._subtoppath
4070 4069
4071 4070 finally:
4072 4071 other.close()
4073 4072 return ret
4074 4073
4075 4074 @command('^push',
4076 4075 [('f', 'force', None, _('force push')),
4077 4076 ('r', 'rev', [],
4078 4077 _('a changeset intended to be included in the destination'),
4079 4078 _('REV')),
4080 4079 ('B', 'bookmark', [], _("bookmark to push"), _('BOOKMARK')),
4081 4080 ('b', 'branch', [],
4082 4081 _('a specific branch you would like to push'), _('BRANCH')),
4083 4082 ('', 'new-branch', False, _('allow pushing a new branch')),
4084 4083 ('', 'pushvars', [], _('variables that can be sent to server (ADVANCED)')),
4085 4084 ] + remoteopts,
4086 4085 _('[-f] [-r REV]... [-e CMD] [--remotecmd CMD] [DEST]'))
4087 4086 def push(ui, repo, dest=None, **opts):
4088 4087 """push changes to the specified destination
4089 4088
4090 4089 Push changesets from the local repository to the specified
4091 4090 destination.
4092 4091
4093 4092 This operation is symmetrical to pull: it is identical to a pull
4094 4093 in the destination repository from the current one.
4095 4094
4096 4095 By default, push will not allow creation of new heads at the
4097 4096 destination, since multiple heads would make it unclear which head
4098 4097 to use. In this situation, it is recommended to pull and merge
4099 4098 before pushing.
4100 4099
4101 4100 Use --new-branch if you want to allow push to create a new named
4102 4101 branch that is not present at the destination. This allows you to
4103 4102 only create a new branch without forcing other changes.
4104 4103
4105 4104 .. note::
4106 4105
4107 4106 Extra care should be taken with the -f/--force option,
4108 4107 which will push all new heads on all branches, an action which will
4109 4108 almost always cause confusion for collaborators.
4110 4109
4111 4110 If -r/--rev is used, the specified revision and all its ancestors
4112 4111 will be pushed to the remote repository.
4113 4112
4114 4113 If -B/--bookmark is used, the specified bookmarked revision, its
4115 4114 ancestors, and the bookmark will be pushed to the remote
4116 4115 repository. Specifying ``.`` is equivalent to specifying the active
4117 4116 bookmark's name.
4118 4117
4119 4118 Please see :hg:`help urls` for important details about ``ssh://``
4120 4119 URLs. If DESTINATION is omitted, a default path will be used.
4121 4120
4122 4121 .. container:: verbose
4123 4122
4124 4123 The --pushvars option sends strings to the server that become
4125 4124 environment variables prepended with ``HG_USERVAR_``. For example,
4126 4125 ``--pushvars ENABLE_FEATURE=true``, provides the server side hooks with
4127 4126 ``HG_USERVAR_ENABLE_FEATURE=true`` as part of their environment.
4128 4127
4129 4128 pushvars can provide for user-overridable hooks as well as set debug
4130 4129 levels. One example is having a hook that blocks commits containing
4131 4130 conflict markers, but enables the user to override the hook if the file
4132 4131 is using conflict markers for testing purposes or the file format has
4133 4132 strings that look like conflict markers.
4134 4133
4135 4134 By default, servers will ignore `--pushvars`. To enable it add the
4136 4135 following to your configuration file::
4137 4136
4138 4137 [push]
4139 4138 pushvars.server = true
4140 4139
4141 4140 Returns 0 if push was successful, 1 if nothing to push.
4142 4141 """
4143 4142
4144 4143 opts = pycompat.byteskwargs(opts)
4145 4144 if opts.get('bookmark'):
4146 4145 ui.setconfig('bookmarks', 'pushing', opts['bookmark'], 'push')
4147 4146 for b in opts['bookmark']:
4148 4147 # translate -B options to -r so changesets get pushed
4149 4148 b = repo._bookmarks.expandname(b)
4150 4149 if b in repo._bookmarks:
4151 4150 opts.setdefault('rev', []).append(b)
4152 4151 else:
4153 4152 # if we try to push a deleted bookmark, translate it to null
4154 4153 # this lets simultaneous -r, -b options continue working
4155 4154 opts.setdefault('rev', []).append("null")
4156 4155
4157 4156 path = ui.paths.getpath(dest, default=('default-push', 'default'))
4158 4157 if not path:
4159 4158 raise error.Abort(_('default repository not configured!'),
4160 4159 hint=_("see 'hg help config.paths'"))
4161 4160 dest = path.pushloc or path.loc
4162 4161 branches = (path.branch, opts.get('branch') or [])
4163 4162 ui.status(_('pushing to %s\n') % util.hidepassword(dest))
4164 4163 revs, checkout = hg.addbranchrevs(repo, repo, branches, opts.get('rev'))
4165 4164 other = hg.peer(repo, opts, dest)
4166 4165
4167 4166 if revs:
4168 4167 revs = [repo[r].node() for r in scmutil.revrange(repo, revs)]
4169 4168 if not revs:
4170 4169 raise error.Abort(_("specified revisions evaluate to an empty set"),
4171 4170 hint=_("use different revision arguments"))
4172 4171 elif path.pushrev:
4173 4172 # It doesn't make any sense to specify ancestor revisions. So limit
4174 4173 # to DAG heads to make discovery simpler.
4175 4174 expr = revsetlang.formatspec('heads(%r)', path.pushrev)
4176 4175 revs = scmutil.revrange(repo, [expr])
4177 4176 revs = [repo[rev].node() for rev in revs]
4178 4177 if not revs:
4179 4178 raise error.Abort(_('default push revset for path evaluates to an '
4180 4179 'empty set'))
4181 4180
4182 4181 repo._subtoppath = dest
4183 4182 try:
4184 4183 # push subrepos depth-first for coherent ordering
4185 4184 c = repo['.']
4186 4185 subs = c.substate # only repos that are committed
4187 4186 for s in sorted(subs):
4188 4187 result = c.sub(s).push(opts)
4189 4188 if result == 0:
4190 4189 return not result
4191 4190 finally:
4192 4191 del repo._subtoppath
4193 4192
4194 4193 opargs = dict(opts.get('opargs', {})) # copy opargs since we may mutate it
4195 4194 opargs.setdefault('pushvars', []).extend(opts.get('pushvars', []))
4196 4195
4197 4196 pushop = exchange.push(repo, other, opts.get('force'), revs=revs,
4198 4197 newbranch=opts.get('new_branch'),
4199 4198 bookmarks=opts.get('bookmark', ()),
4200 4199 opargs=opargs)
4201 4200
4202 4201 result = not pushop.cgresult
4203 4202
4204 4203 if pushop.bkresult is not None:
4205 4204 if pushop.bkresult == 2:
4206 4205 result = 2
4207 4206 elif not result and pushop.bkresult:
4208 4207 result = 2
4209 4208
4210 4209 return result
4211 4210
4212 4211 @command('recover', [])
4213 4212 def recover(ui, repo):
4214 4213 """roll back an interrupted transaction
4215 4214
4216 4215 Recover from an interrupted commit or pull.
4217 4216
4218 4217 This command tries to fix the repository status after an
4219 4218 interrupted operation. It should only be necessary when Mercurial
4220 4219 suggests it.
4221 4220
4222 4221 Returns 0 if successful, 1 if nothing to recover or verify fails.
4223 4222 """
4224 4223 if repo.recover():
4225 4224 return hg.verify(repo)
4226 4225 return 1
4227 4226
4228 4227 @command('^remove|rm',
4229 4228 [('A', 'after', None, _('record delete for missing files')),
4230 4229 ('f', 'force', None,
4231 4230 _('forget added files, delete modified files')),
4232 4231 ] + subrepoopts + walkopts + dryrunopts,
4233 4232 _('[OPTION]... FILE...'),
4234 4233 inferrepo=True)
4235 4234 def remove(ui, repo, *pats, **opts):
4236 4235 """remove the specified files on the next commit
4237 4236
4238 4237 Schedule the indicated files for removal from the current branch.
4239 4238
4240 4239 This command schedules the files to be removed at the next commit.
4241 4240 To undo a remove before that, see :hg:`revert`. To undo added
4242 4241 files, see :hg:`forget`.
4243 4242
4244 4243 .. container:: verbose
4245 4244
4246 4245 -A/--after can be used to remove only files that have already
4247 4246 been deleted, -f/--force can be used to force deletion, and -Af
4248 4247 can be used to remove files from the next revision without
4249 4248 deleting them from the working directory.
4250 4249
4251 4250 The following table details the behavior of remove for different
4252 4251 file states (columns) and option combinations (rows). The file
4253 4252 states are Added [A], Clean [C], Modified [M] and Missing [!]
4254 4253 (as reported by :hg:`status`). The actions are Warn, Remove
4255 4254 (from branch) and Delete (from disk):
4256 4255
4257 4256 ========= == == == ==
4258 4257 opt/state A C M !
4259 4258 ========= == == == ==
4260 4259 none W RD W R
4261 4260 -f R RD RD R
4262 4261 -A W W W R
4263 4262 -Af R R R R
4264 4263 ========= == == == ==
4265 4264
4266 4265 .. note::
4267 4266
4268 4267 :hg:`remove` never deletes files in Added [A] state from the
4269 4268 working directory, not even if ``--force`` is specified.
4270 4269
4271 4270 Returns 0 on success, 1 if any warnings encountered.
4272 4271 """
4273 4272
4274 4273 opts = pycompat.byteskwargs(opts)
4275 4274 after, force = opts.get('after'), opts.get('force')
4276 4275 dryrun = opts.get('dry_run')
4277 4276 if not pats and not after:
4278 4277 raise error.Abort(_('no files specified'))
4279 4278
4280 4279 m = scmutil.match(repo[None], pats, opts)
4281 4280 subrepos = opts.get('subrepos')
4282 4281 return cmdutil.remove(ui, repo, m, "", after, force, subrepos,
4283 4282 dryrun=dryrun)
4284 4283
4285 4284 @command('rename|move|mv',
4286 4285 [('A', 'after', None, _('record a rename that has already occurred')),
4287 4286 ('f', 'force', None, _('forcibly copy over an existing managed file')),
4288 4287 ] + walkopts + dryrunopts,
4289 4288 _('[OPTION]... SOURCE... DEST'))
4290 4289 def rename(ui, repo, *pats, **opts):
4291 4290 """rename files; equivalent of copy + remove
4292 4291
4293 4292 Mark dest as copies of sources; mark sources for deletion. If dest
4294 4293 is a directory, copies are put in that directory. If dest is a
4295 4294 file, there can only be one source.
4296 4295
4297 4296 By default, this command copies the contents of files as they
4298 4297 exist in the working directory. If invoked with -A/--after, the
4299 4298 operation is recorded, but no copying is performed.
4300 4299
4301 4300 This command takes effect at the next commit. To undo a rename
4302 4301 before that, see :hg:`revert`.
4303 4302
4304 4303 Returns 0 on success, 1 if errors are encountered.
4305 4304 """
4306 4305 opts = pycompat.byteskwargs(opts)
4307 4306 with repo.wlock(False):
4308 4307 return cmdutil.copy(ui, repo, pats, opts, rename=True)
4309 4308
4310 4309 @command('resolve',
4311 4310 [('a', 'all', None, _('select all unresolved files')),
4312 4311 ('l', 'list', None, _('list state of files needing merge')),
4313 4312 ('m', 'mark', None, _('mark files as resolved')),
4314 4313 ('u', 'unmark', None, _('mark files as unresolved')),
4315 4314 ('n', 'no-status', None, _('hide status prefix'))]
4316 4315 + mergetoolopts + walkopts + formatteropts,
4317 4316 _('[OPTION]... [FILE]...'),
4318 4317 inferrepo=True)
4319 4318 def resolve(ui, repo, *pats, **opts):
4320 4319 """redo merges or set/view the merge status of files
4321 4320
4322 4321 Merges with unresolved conflicts are often the result of
4323 4322 non-interactive merging using the ``internal:merge`` configuration
4324 4323 setting, or a command-line merge tool like ``diff3``. The resolve
4325 4324 command is used to manage the files involved in a merge, after
4326 4325 :hg:`merge` has been run, and before :hg:`commit` is run (i.e. the
4327 4326 working directory must have two parents). See :hg:`help
4328 4327 merge-tools` for information on configuring merge tools.
4329 4328
4330 4329 The resolve command can be used in the following ways:
4331 4330
4332 4331 - :hg:`resolve [--tool TOOL] FILE...`: attempt to re-merge the specified
4333 4332 files, discarding any previous merge attempts. Re-merging is not
4334 4333 performed for files already marked as resolved. Use ``--all/-a``
4335 4334 to select all unresolved files. ``--tool`` can be used to specify
4336 4335 the merge tool used for the given files. It overrides the HGMERGE
4337 4336 environment variable and your configuration files. Previous file
4338 4337 contents are saved with a ``.orig`` suffix.
4339 4338
4340 4339 - :hg:`resolve -m [FILE]`: mark a file as having been resolved
4341 4340 (e.g. after having manually fixed-up the files). The default is
4342 4341 to mark all unresolved files.
4343 4342
4344 4343 - :hg:`resolve -u [FILE]...`: mark a file as unresolved. The
4345 4344 default is to mark all resolved files.
4346 4345
4347 4346 - :hg:`resolve -l`: list files which had or still have conflicts.
4348 4347 In the printed list, ``U`` = unresolved and ``R`` = resolved.
4349 4348 You can use ``set:unresolved()`` or ``set:resolved()`` to filter
4350 4349 the list. See :hg:`help filesets` for details.
4351 4350
4352 4351 .. note::
4353 4352
4354 4353 Mercurial will not let you commit files with unresolved merge
4355 4354 conflicts. You must use :hg:`resolve -m ...` before you can
4356 4355 commit after a conflicting merge.
4357 4356
4358 4357 Returns 0 on success, 1 if any files fail a resolve attempt.
4359 4358 """
4360 4359
4361 4360 opts = pycompat.byteskwargs(opts)
4362 4361 flaglist = 'all mark unmark list no_status'.split()
4363 4362 all, mark, unmark, show, nostatus = \
4364 4363 [opts.get(o) for o in flaglist]
4365 4364
4366 4365 if (show and (mark or unmark)) or (mark and unmark):
4367 4366 raise error.Abort(_("too many options specified"))
4368 4367 if pats and all:
4369 4368 raise error.Abort(_("can't specify --all and patterns"))
4370 4369 if not (all or pats or show or mark or unmark):
4371 4370 raise error.Abort(_('no files or directories specified'),
4372 4371 hint=('use --all to re-merge all unresolved files'))
4373 4372
4374 4373 if show:
4375 4374 ui.pager('resolve')
4376 4375 fm = ui.formatter('resolve', opts)
4377 4376 ms = mergemod.mergestate.read(repo)
4378 4377 m = scmutil.match(repo[None], pats, opts)
4379 4378
4380 4379 # Labels and keys based on merge state. Unresolved path conflicts show
4381 4380 # as 'P'. Resolved path conflicts show as 'R', the same as normal
4382 4381 # resolved conflicts.
4383 4382 mergestateinfo = {
4384 4383 mergemod.MERGE_RECORD_UNRESOLVED: ('resolve.unresolved', 'U'),
4385 4384 mergemod.MERGE_RECORD_RESOLVED: ('resolve.resolved', 'R'),
4386 4385 mergemod.MERGE_RECORD_UNRESOLVED_PATH: ('resolve.unresolved', 'P'),
4387 4386 mergemod.MERGE_RECORD_RESOLVED_PATH: ('resolve.resolved', 'R'),
4388 4387 mergemod.MERGE_RECORD_DRIVER_RESOLVED: ('resolve.driverresolved',
4389 4388 'D'),
4390 4389 }
4391 4390
4392 4391 for f in ms:
4393 4392 if not m(f):
4394 4393 continue
4395 4394
4396 4395 label, key = mergestateinfo[ms[f]]
4397 4396 fm.startitem()
4398 4397 fm.condwrite(not nostatus, 'status', '%s ', key, label=label)
4399 4398 fm.write('path', '%s\n', f, label=label)
4400 4399 fm.end()
4401 4400 return 0
4402 4401
4403 4402 with repo.wlock():
4404 4403 ms = mergemod.mergestate.read(repo)
4405 4404
4406 4405 if not (ms.active() or repo.dirstate.p2() != nullid):
4407 4406 raise error.Abort(
4408 4407 _('resolve command not applicable when not merging'))
4409 4408
4410 4409 wctx = repo[None]
4411 4410
4412 4411 if (ms.mergedriver
4413 4412 and ms.mdstate() == mergemod.MERGE_DRIVER_STATE_UNMARKED):
4414 4413 proceed = mergemod.driverpreprocess(repo, ms, wctx)
4415 4414 ms.commit()
4416 4415 # allow mark and unmark to go through
4417 4416 if not mark and not unmark and not proceed:
4418 4417 return 1
4419 4418
4420 4419 m = scmutil.match(wctx, pats, opts)
4421 4420 ret = 0
4422 4421 didwork = False
4423 4422 runconclude = False
4424 4423
4425 4424 tocomplete = []
4426 4425 for f in ms:
4427 4426 if not m(f):
4428 4427 continue
4429 4428
4430 4429 didwork = True
4431 4430
4432 4431 # don't let driver-resolved files be marked, and run the conclude
4433 4432 # step if asked to resolve
4434 4433 if ms[f] == mergemod.MERGE_RECORD_DRIVER_RESOLVED:
4435 4434 exact = m.exact(f)
4436 4435 if mark:
4437 4436 if exact:
4438 4437 ui.warn(_('not marking %s as it is driver-resolved\n')
4439 4438 % f)
4440 4439 elif unmark:
4441 4440 if exact:
4442 4441 ui.warn(_('not unmarking %s as it is driver-resolved\n')
4443 4442 % f)
4444 4443 else:
4445 4444 runconclude = True
4446 4445 continue
4447 4446
4448 4447 # path conflicts must be resolved manually
4449 4448 if ms[f] in (mergemod.MERGE_RECORD_UNRESOLVED_PATH,
4450 4449 mergemod.MERGE_RECORD_RESOLVED_PATH):
4451 4450 if mark:
4452 4451 ms.mark(f, mergemod.MERGE_RECORD_RESOLVED_PATH)
4453 4452 elif unmark:
4454 4453 ms.mark(f, mergemod.MERGE_RECORD_UNRESOLVED_PATH)
4455 4454 elif ms[f] == mergemod.MERGE_RECORD_UNRESOLVED_PATH:
4456 4455 ui.warn(_('%s: path conflict must be resolved manually\n')
4457 4456 % f)
4458 4457 continue
4459 4458
4460 4459 if mark:
4461 4460 ms.mark(f, mergemod.MERGE_RECORD_RESOLVED)
4462 4461 elif unmark:
4463 4462 ms.mark(f, mergemod.MERGE_RECORD_UNRESOLVED)
4464 4463 else:
4465 4464 # backup pre-resolve (merge uses .orig for its own purposes)
4466 4465 a = repo.wjoin(f)
4467 4466 try:
4468 4467 util.copyfile(a, a + ".resolve")
4469 4468 except (IOError, OSError) as inst:
4470 4469 if inst.errno != errno.ENOENT:
4471 4470 raise
4472 4471
4473 4472 try:
4474 4473 # preresolve file
4475 4474 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
4476 4475 'resolve')
4477 4476 complete, r = ms.preresolve(f, wctx)
4478 4477 if not complete:
4479 4478 tocomplete.append(f)
4480 4479 elif r:
4481 4480 ret = 1
4482 4481 finally:
4483 4482 ui.setconfig('ui', 'forcemerge', '', 'resolve')
4484 4483 ms.commit()
4485 4484
4486 4485 # replace filemerge's .orig file with our resolve file, but only
4487 4486 # for merges that are complete
4488 4487 if complete:
4489 4488 try:
4490 4489 util.rename(a + ".resolve",
4491 4490 scmutil.origpath(ui, repo, a))
4492 4491 except OSError as inst:
4493 4492 if inst.errno != errno.ENOENT:
4494 4493 raise
4495 4494
4496 4495 for f in tocomplete:
4497 4496 try:
4498 4497 # resolve file
4499 4498 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
4500 4499 'resolve')
4501 4500 r = ms.resolve(f, wctx)
4502 4501 if r:
4503 4502 ret = 1
4504 4503 finally:
4505 4504 ui.setconfig('ui', 'forcemerge', '', 'resolve')
4506 4505 ms.commit()
4507 4506
4508 4507 # replace filemerge's .orig file with our resolve file
4509 4508 a = repo.wjoin(f)
4510 4509 try:
4511 4510 util.rename(a + ".resolve", scmutil.origpath(ui, repo, a))
4512 4511 except OSError as inst:
4513 4512 if inst.errno != errno.ENOENT:
4514 4513 raise
4515 4514
4516 4515 ms.commit()
4517 4516 ms.recordactions()
4518 4517
4519 4518 if not didwork and pats:
4520 4519 hint = None
4521 4520 if not any([p for p in pats if p.find(':') >= 0]):
4522 4521 pats = ['path:%s' % p for p in pats]
4523 4522 m = scmutil.match(wctx, pats, opts)
4524 4523 for f in ms:
4525 4524 if not m(f):
4526 4525 continue
4527 4526 flags = ''.join(['-%s ' % o[0:1] for o in flaglist
4528 4527 if opts.get(o)])
4529 4528 hint = _("(try: hg resolve %s%s)\n") % (
4530 4529 flags,
4531 4530 ' '.join(pats))
4532 4531 break
4533 4532 ui.warn(_("arguments do not match paths that need resolving\n"))
4534 4533 if hint:
4535 4534 ui.warn(hint)
4536 4535 elif ms.mergedriver and ms.mdstate() != 's':
4537 4536 # run conclude step when either a driver-resolved file is requested
4538 4537 # or there are no driver-resolved files
4539 4538 # we can't use 'ret' to determine whether any files are unresolved
4540 4539 # because we might not have tried to resolve some
4541 4540 if ((runconclude or not list(ms.driverresolved()))
4542 4541 and not list(ms.unresolved())):
4543 4542 proceed = mergemod.driverconclude(repo, ms, wctx)
4544 4543 ms.commit()
4545 4544 if not proceed:
4546 4545 return 1
4547 4546
4548 4547 # Nudge users into finishing an unfinished operation
4549 4548 unresolvedf = list(ms.unresolved())
4550 4549 driverresolvedf = list(ms.driverresolved())
4551 4550 if not unresolvedf and not driverresolvedf:
4552 4551 ui.status(_('(no more unresolved files)\n'))
4553 4552 cmdutil.checkafterresolved(repo)
4554 4553 elif not unresolvedf:
4555 4554 ui.status(_('(no more unresolved files -- '
4556 4555 'run "hg resolve --all" to conclude)\n'))
4557 4556
4558 4557 return ret
4559 4558
4560 4559 @command('revert',
4561 4560 [('a', 'all', None, _('revert all changes when no arguments given')),
4562 4561 ('d', 'date', '', _('tipmost revision matching date'), _('DATE')),
4563 4562 ('r', 'rev', '', _('revert to the specified revision'), _('REV')),
4564 4563 ('C', 'no-backup', None, _('do not save backup copies of files')),
4565 4564 ('i', 'interactive', None, _('interactively select the changes')),
4566 4565 ] + walkopts + dryrunopts,
4567 4566 _('[OPTION]... [-r REV] [NAME]...'))
4568 4567 def revert(ui, repo, *pats, **opts):
4569 4568 """restore files to their checkout state
4570 4569
4571 4570 .. note::
4572 4571
4573 4572 To check out earlier revisions, you should use :hg:`update REV`.
4574 4573 To cancel an uncommitted merge (and lose your changes),
4575 4574 use :hg:`merge --abort`.
4576 4575
4577 4576 With no revision specified, revert the specified files or directories
4578 4577 to the contents they had in the parent of the working directory.
4579 4578 This restores the contents of files to an unmodified
4580 4579 state and unschedules adds, removes, copies, and renames. If the
4581 4580 working directory has two parents, you must explicitly specify a
4582 4581 revision.
4583 4582
4584 4583 Using the -r/--rev or -d/--date options, revert the given files or
4585 4584 directories to their states as of a specific revision. Because
4586 4585 revert does not change the working directory parents, this will
4587 4586 cause these files to appear modified. This can be helpful to "back
4588 4587 out" some or all of an earlier change. See :hg:`backout` for a
4589 4588 related method.
4590 4589
4591 4590 Modified files are saved with a .orig suffix before reverting.
4592 4591 To disable these backups, use --no-backup. It is possible to store
4593 4592 the backup files in a custom directory relative to the root of the
4594 4593 repository by setting the ``ui.origbackuppath`` configuration
4595 4594 option.
4596 4595
4597 4596 See :hg:`help dates` for a list of formats valid for -d/--date.
4598 4597
4599 4598 See :hg:`help backout` for a way to reverse the effect of an
4600 4599 earlier changeset.
4601 4600
4602 4601 Returns 0 on success.
4603 4602 """
4604 4603
4605 4604 opts = pycompat.byteskwargs(opts)
4606 4605 if opts.get("date"):
4607 4606 if opts.get("rev"):
4608 4607 raise error.Abort(_("you can't specify a revision and a date"))
4609 4608 opts["rev"] = cmdutil.finddate(ui, repo, opts["date"])
4610 4609
4611 4610 parent, p2 = repo.dirstate.parents()
4612 4611 if not opts.get('rev') and p2 != nullid:
4613 4612 # revert after merge is a trap for new users (issue2915)
4614 4613 raise error.Abort(_('uncommitted merge with no revision specified'),
4615 4614 hint=_("use 'hg update' or see 'hg help revert'"))
4616 4615
4617 4616 rev = opts.get('rev')
4618 4617 if rev:
4619 4618 repo = scmutil.unhidehashlikerevs(repo, [rev], 'nowarn')
4620 4619 ctx = scmutil.revsingle(repo, rev)
4621 4620
4622 4621 if (not (pats or opts.get('include') or opts.get('exclude') or
4623 4622 opts.get('all') or opts.get('interactive'))):
4624 4623 msg = _("no files or directories specified")
4625 4624 if p2 != nullid:
4626 4625 hint = _("uncommitted merge, use --all to discard all changes,"
4627 4626 " or 'hg update -C .' to abort the merge")
4628 4627 raise error.Abort(msg, hint=hint)
4629 4628 dirty = any(repo.status())
4630 4629 node = ctx.node()
4631 4630 if node != parent:
4632 4631 if dirty:
4633 4632 hint = _("uncommitted changes, use --all to discard all"
4634 4633 " changes, or 'hg update %s' to update") % ctx.rev()
4635 4634 else:
4636 4635 hint = _("use --all to revert all files,"
4637 4636 " or 'hg update %s' to update") % ctx.rev()
4638 4637 elif dirty:
4639 4638 hint = _("uncommitted changes, use --all to discard all changes")
4640 4639 else:
4641 4640 hint = _("use --all to revert all files")
4642 4641 raise error.Abort(msg, hint=hint)
4643 4642
4644 4643 return cmdutil.revert(ui, repo, ctx, (parent, p2), *pats,
4645 4644 **pycompat.strkwargs(opts))
4646 4645
4647 4646 @command('rollback', dryrunopts +
4648 4647 [('f', 'force', False, _('ignore safety measures'))])
4649 4648 def rollback(ui, repo, **opts):
4650 4649 """roll back the last transaction (DANGEROUS) (DEPRECATED)
4651 4650
4652 4651 Please use :hg:`commit --amend` instead of rollback to correct
4653 4652 mistakes in the last commit.
4654 4653
4655 4654 This command should be used with care. There is only one level of
4656 4655 rollback, and there is no way to undo a rollback. It will also
4657 4656 restore the dirstate at the time of the last transaction, losing
4658 4657 any dirstate changes since that time. This command does not alter
4659 4658 the working directory.
4660 4659
4661 4660 Transactions are used to encapsulate the effects of all commands
4662 4661 that create new changesets or propagate existing changesets into a
4663 4662 repository.
4664 4663
4665 4664 .. container:: verbose
4666 4665
4667 4666 For example, the following commands are transactional, and their
4668 4667 effects can be rolled back:
4669 4668
4670 4669 - commit
4671 4670 - import
4672 4671 - pull
4673 4672 - push (with this repository as the destination)
4674 4673 - unbundle
4675 4674
4676 4675 To avoid permanent data loss, rollback will refuse to rollback a
4677 4676 commit transaction if it isn't checked out. Use --force to
4678 4677 override this protection.
4679 4678
4680 4679 The rollback command can be entirely disabled by setting the
4681 4680 ``ui.rollback`` configuration setting to false. If you're here
4682 4681 because you want to use rollback and it's disabled, you can
4683 4682 re-enable the command by setting ``ui.rollback`` to true.
4684 4683
4685 4684 This command is not intended for use on public repositories. Once
4686 4685 changes are visible for pull by other users, rolling a transaction
4687 4686 back locally is ineffective (someone else may already have pulled
4688 4687 the changes). Furthermore, a race is possible with readers of the
4689 4688 repository; for example an in-progress pull from the repository
4690 4689 may fail if a rollback is performed.
4691 4690
4692 4691 Returns 0 on success, 1 if no rollback data is available.
4693 4692 """
4694 4693 if not ui.configbool('ui', 'rollback'):
4695 4694 raise error.Abort(_('rollback is disabled because it is unsafe'),
4696 4695 hint=('see `hg help -v rollback` for information'))
4697 4696 return repo.rollback(dryrun=opts.get(r'dry_run'),
4698 4697 force=opts.get(r'force'))
4699 4698
4700 4699 @command('root', [], cmdtype=readonly)
4701 4700 def root(ui, repo):
4702 4701 """print the root (top) of the current working directory
4703 4702
4704 4703 Print the root directory of the current repository.
4705 4704
4706 4705 Returns 0 on success.
4707 4706 """
4708 4707 ui.write(repo.root + "\n")
4709 4708
4710 4709 @command('^serve',
4711 4710 [('A', 'accesslog', '', _('name of access log file to write to'),
4712 4711 _('FILE')),
4713 4712 ('d', 'daemon', None, _('run server in background')),
4714 4713 ('', 'daemon-postexec', [], _('used internally by daemon mode')),
4715 4714 ('E', 'errorlog', '', _('name of error log file to write to'), _('FILE')),
4716 4715 # use string type, then we can check if something was passed
4717 4716 ('p', 'port', '', _('port to listen on (default: 8000)'), _('PORT')),
4718 4717 ('a', 'address', '', _('address to listen on (default: all interfaces)'),
4719 4718 _('ADDR')),
4720 4719 ('', 'prefix', '', _('prefix path to serve from (default: server root)'),
4721 4720 _('PREFIX')),
4722 4721 ('n', 'name', '',
4723 4722 _('name to show in web pages (default: working directory)'), _('NAME')),
4724 4723 ('', 'web-conf', '',
4725 4724 _("name of the hgweb config file (see 'hg help hgweb')"), _('FILE')),
4726 4725 ('', 'webdir-conf', '', _('name of the hgweb config file (DEPRECATED)'),
4727 4726 _('FILE')),
4728 4727 ('', 'pid-file', '', _('name of file to write process ID to'), _('FILE')),
4729 4728 ('', 'stdio', None, _('for remote clients (ADVANCED)')),
4730 4729 ('', 'cmdserver', '', _('for remote clients (ADVANCED)'), _('MODE')),
4731 4730 ('t', 'templates', '', _('web templates to use'), _('TEMPLATE')),
4732 4731 ('', 'style', '', _('template style to use'), _('STYLE')),
4733 4732 ('6', 'ipv6', None, _('use IPv6 in addition to IPv4')),
4734 4733 ('', 'certificate', '', _('SSL certificate file'), _('FILE'))]
4735 4734 + subrepoopts,
4736 4735 _('[OPTION]...'),
4737 4736 optionalrepo=True)
4738 4737 def serve(ui, repo, **opts):
4739 4738 """start stand-alone webserver
4740 4739
4741 4740 Start a local HTTP repository browser and pull server. You can use
4742 4741 this for ad-hoc sharing and browsing of repositories. It is
4743 4742 recommended to use a real web server to serve a repository for
4744 4743 longer periods of time.
4745 4744
4746 4745 Please note that the server does not implement access control.
4747 4746 This means that, by default, anybody can read from the server and
4748 4747 nobody can write to it by default. Set the ``web.allow-push``
4749 4748 option to ``*`` to allow everybody to push to the server. You
4750 4749 should use a real web server if you need to authenticate users.
4751 4750
4752 4751 By default, the server logs accesses to stdout and errors to
4753 4752 stderr. Use the -A/--accesslog and -E/--errorlog options to log to
4754 4753 files.
4755 4754
4756 4755 To have the server choose a free port number to listen on, specify
4757 4756 a port number of 0; in this case, the server will print the port
4758 4757 number it uses.
4759 4758
4760 4759 Returns 0 on success.
4761 4760 """
4762 4761
4763 4762 opts = pycompat.byteskwargs(opts)
4764 4763 if opts["stdio"] and opts["cmdserver"]:
4765 4764 raise error.Abort(_("cannot use --stdio with --cmdserver"))
4766 4765
4767 4766 if opts["stdio"]:
4768 4767 if repo is None:
4769 4768 raise error.RepoError(_("there is no Mercurial repository here"
4770 4769 " (.hg not found)"))
4771 4770 s = wireprotoserver.sshserver(ui, repo)
4772 4771 s.serve_forever()
4773 4772
4774 4773 service = server.createservice(ui, repo, opts)
4775 4774 return server.runservice(opts, initfn=service.init, runfn=service.run)
4776 4775
4777 4776 @command('^status|st',
4778 4777 [('A', 'all', None, _('show status of all files')),
4779 4778 ('m', 'modified', None, _('show only modified files')),
4780 4779 ('a', 'added', None, _('show only added files')),
4781 4780 ('r', 'removed', None, _('show only removed files')),
4782 4781 ('d', 'deleted', None, _('show only deleted (but tracked) files')),
4783 4782 ('c', 'clean', None, _('show only files without changes')),
4784 4783 ('u', 'unknown', None, _('show only unknown (not tracked) files')),
4785 4784 ('i', 'ignored', None, _('show only ignored files')),
4786 4785 ('n', 'no-status', None, _('hide status prefix')),
4787 4786 ('t', 'terse', '', _('show the terse output (EXPERIMENTAL)')),
4788 4787 ('C', 'copies', None, _('show source of copied files')),
4789 4788 ('0', 'print0', None, _('end filenames with NUL, for use with xargs')),
4790 4789 ('', 'rev', [], _('show difference from revision'), _('REV')),
4791 4790 ('', 'change', '', _('list the changed files of a revision'), _('REV')),
4792 4791 ] + walkopts + subrepoopts + formatteropts,
4793 4792 _('[OPTION]... [FILE]...'),
4794 4793 inferrepo=True, cmdtype=readonly)
4795 4794 def status(ui, repo, *pats, **opts):
4796 4795 """show changed files in the working directory
4797 4796
4798 4797 Show status of files in the repository. If names are given, only
4799 4798 files that match are shown. Files that are clean or ignored or
4800 4799 the source of a copy/move operation, are not listed unless
4801 4800 -c/--clean, -i/--ignored, -C/--copies or -A/--all are given.
4802 4801 Unless options described with "show only ..." are given, the
4803 4802 options -mardu are used.
4804 4803
4805 4804 Option -q/--quiet hides untracked (unknown and ignored) files
4806 4805 unless explicitly requested with -u/--unknown or -i/--ignored.
4807 4806
4808 4807 .. note::
4809 4808
4810 4809 :hg:`status` may appear to disagree with diff if permissions have
4811 4810 changed or a merge has occurred. The standard diff format does
4812 4811 not report permission changes and diff only reports changes
4813 4812 relative to one merge parent.
4814 4813
4815 4814 If one revision is given, it is used as the base revision.
4816 4815 If two revisions are given, the differences between them are
4817 4816 shown. The --change option can also be used as a shortcut to list
4818 4817 the changed files of a revision from its first parent.
4819 4818
4820 4819 The codes used to show the status of files are::
4821 4820
4822 4821 M = modified
4823 4822 A = added
4824 4823 R = removed
4825 4824 C = clean
4826 4825 ! = missing (deleted by non-hg command, but still tracked)
4827 4826 ? = not tracked
4828 4827 I = ignored
4829 4828 = origin of the previous file (with --copies)
4830 4829
4831 4830 .. container:: verbose
4832 4831
4833 4832 The -t/--terse option abbreviates the output by showing only the directory
4834 4833 name if all the files in it share the same status. The option takes an
4835 4834 argument indicating the statuses to abbreviate: 'm' for 'modified', 'a'
4836 4835 for 'added', 'r' for 'removed', 'd' for 'deleted', 'u' for 'unknown', 'i'
4837 4836 for 'ignored' and 'c' for clean.
4838 4837
4839 4838 It abbreviates only those statuses which are passed. Note that clean and
4840 4839 ignored files are not displayed with '--terse ic' unless the -c/--clean
4841 4840 and -i/--ignored options are also used.
4842 4841
4843 4842 The -v/--verbose option shows information when the repository is in an
4844 4843 unfinished merge, shelve, rebase state etc. You can have this behavior
4845 4844 turned on by default by enabling the ``commands.status.verbose`` option.
4846 4845
4847 4846 You can skip displaying some of these states by setting
4848 4847 ``commands.status.skipstates`` to one or more of: 'bisect', 'graft',
4849 4848 'histedit', 'merge', 'rebase', or 'unshelve'.
4850 4849
4851 4850 Examples:
4852 4851
4853 4852 - show changes in the working directory relative to a
4854 4853 changeset::
4855 4854
4856 4855 hg status --rev 9353
4857 4856
4858 4857 - show changes in the working directory relative to the
4859 4858 current directory (see :hg:`help patterns` for more information)::
4860 4859
4861 4860 hg status re:
4862 4861
4863 4862 - show all changes including copies in an existing changeset::
4864 4863
4865 4864 hg status --copies --change 9353
4866 4865
4867 4866 - get a NUL separated list of added files, suitable for xargs::
4868 4867
4869 4868 hg status -an0
4870 4869
4871 4870 - show more information about the repository status, abbreviating
4872 4871 added, removed, modified, deleted, and untracked paths::
4873 4872
4874 4873 hg status -v -t mardu
4875 4874
4876 4875 Returns 0 on success.
4877 4876
4878 4877 """
4879 4878
4880 4879 opts = pycompat.byteskwargs(opts)
4881 4880 revs = opts.get('rev')
4882 4881 change = opts.get('change')
4883 4882 terse = opts.get('terse')
4884 4883
4885 4884 if revs and change:
4886 4885 msg = _('cannot specify --rev and --change at the same time')
4887 4886 raise error.Abort(msg)
4888 4887 elif revs and terse:
4889 4888 msg = _('cannot use --terse with --rev')
4890 4889 raise error.Abort(msg)
4891 4890 elif change:
4892 4891 repo = scmutil.unhidehashlikerevs(repo, [change], 'nowarn')
4893 4892 ctx2 = scmutil.revsingle(repo, change, None)
4894 4893 ctx1 = ctx2.p1()
4895 4894 else:
4896 4895 repo = scmutil.unhidehashlikerevs(repo, revs, 'nowarn')
4897 4896 ctx1, ctx2 = scmutil.revpair(repo, revs)
4898 4897
4899 4898 if pats or ui.configbool('commands', 'status.relative'):
4900 4899 cwd = repo.getcwd()
4901 4900 else:
4902 4901 cwd = ''
4903 4902
4904 4903 if opts.get('print0'):
4905 4904 end = '\0'
4906 4905 else:
4907 4906 end = '\n'
4908 4907 copy = {}
4909 4908 states = 'modified added removed deleted unknown ignored clean'.split()
4910 4909 show = [k for k in states if opts.get(k)]
4911 4910 if opts.get('all'):
4912 4911 show += ui.quiet and (states[:4] + ['clean']) or states
4913 4912
4914 4913 if not show:
4915 4914 if ui.quiet:
4916 4915 show = states[:4]
4917 4916 else:
4918 4917 show = states[:5]
4919 4918
4920 4919 m = scmutil.match(ctx2, pats, opts)
4921 4920 if terse:
4922 4921 # we need to compute clean and unknown to terse
4923 4922 stat = repo.status(ctx1.node(), ctx2.node(), m,
4924 4923 'ignored' in show or 'i' in terse,
4925 4924 True, True, opts.get('subrepos'))
4926 4925
4927 4926 stat = cmdutil.tersedir(stat, terse)
4928 4927 else:
4929 4928 stat = repo.status(ctx1.node(), ctx2.node(), m,
4930 4929 'ignored' in show, 'clean' in show,
4931 4930 'unknown' in show, opts.get('subrepos'))
4932 4931
4933 4932 changestates = zip(states, pycompat.iterbytestr('MAR!?IC'), stat)
4934 4933
4935 4934 if (opts.get('all') or opts.get('copies')
4936 4935 or ui.configbool('ui', 'statuscopies')) and not opts.get('no_status'):
4937 4936 copy = copies.pathcopies(ctx1, ctx2, m)
4938 4937
4939 4938 ui.pager('status')
4940 4939 fm = ui.formatter('status', opts)
4941 4940 fmt = '%s' + end
4942 4941 showchar = not opts.get('no_status')
4943 4942
4944 4943 for state, char, files in changestates:
4945 4944 if state in show:
4946 4945 label = 'status.' + state
4947 4946 for f in files:
4948 4947 fm.startitem()
4949 4948 fm.condwrite(showchar, 'status', '%s ', char, label=label)
4950 4949 fm.write('path', fmt, repo.pathto(f, cwd), label=label)
4951 4950 if f in copy:
4952 4951 fm.write("copy", ' %s' + end, repo.pathto(copy[f], cwd),
4953 4952 label='status.copied')
4954 4953
4955 4954 if ((ui.verbose or ui.configbool('commands', 'status.verbose'))
4956 4955 and not ui.plain()):
4957 4956 cmdutil.morestatus(repo, fm)
4958 4957 fm.end()
4959 4958
4960 4959 @command('^summary|sum',
4961 4960 [('', 'remote', None, _('check for push and pull'))],
4962 4961 '[--remote]', cmdtype=readonly)
4963 4962 def summary(ui, repo, **opts):
4964 4963 """summarize working directory state
4965 4964
4966 4965 This generates a brief summary of the working directory state,
4967 4966 including parents, branch, commit status, phase and available updates.
4968 4967
4969 4968 With the --remote option, this will check the default paths for
4970 4969 incoming and outgoing changes. This can be time-consuming.
4971 4970
4972 4971 Returns 0 on success.
4973 4972 """
4974 4973
4975 4974 opts = pycompat.byteskwargs(opts)
4976 4975 ui.pager('summary')
4977 4976 ctx = repo[None]
4978 4977 parents = ctx.parents()
4979 4978 pnode = parents[0].node()
4980 4979 marks = []
4981 4980
4982 4981 ms = None
4983 4982 try:
4984 4983 ms = mergemod.mergestate.read(repo)
4985 4984 except error.UnsupportedMergeRecords as e:
4986 4985 s = ' '.join(e.recordtypes)
4987 4986 ui.warn(
4988 4987 _('warning: merge state has unsupported record types: %s\n') % s)
4989 4988 unresolved = []
4990 4989 else:
4991 4990 unresolved = list(ms.unresolved())
4992 4991
4993 4992 for p in parents:
4994 4993 # label with log.changeset (instead of log.parent) since this
4995 4994 # shows a working directory parent *changeset*:
4996 4995 # i18n: column positioning for "hg summary"
4997 4996 ui.write(_('parent: %d:%s ') % (p.rev(), p),
4998 4997 label=logcmdutil.changesetlabels(p))
4999 4998 ui.write(' '.join(p.tags()), label='log.tag')
5000 4999 if p.bookmarks():
5001 5000 marks.extend(p.bookmarks())
5002 5001 if p.rev() == -1:
5003 5002 if not len(repo):
5004 5003 ui.write(_(' (empty repository)'))
5005 5004 else:
5006 5005 ui.write(_(' (no revision checked out)'))
5007 5006 if p.obsolete():
5008 5007 ui.write(_(' (obsolete)'))
5009 5008 if p.isunstable():
5010 5009 instabilities = (ui.label(instability, 'trouble.%s' % instability)
5011 5010 for instability in p.instabilities())
5012 5011 ui.write(' ('
5013 5012 + ', '.join(instabilities)
5014 5013 + ')')
5015 5014 ui.write('\n')
5016 5015 if p.description():
5017 5016 ui.status(' ' + p.description().splitlines()[0].strip() + '\n',
5018 5017 label='log.summary')
5019 5018
5020 5019 branch = ctx.branch()
5021 5020 bheads = repo.branchheads(branch)
5022 5021 # i18n: column positioning for "hg summary"
5023 5022 m = _('branch: %s\n') % branch
5024 5023 if branch != 'default':
5025 5024 ui.write(m, label='log.branch')
5026 5025 else:
5027 5026 ui.status(m, label='log.branch')
5028 5027
5029 5028 if marks:
5030 5029 active = repo._activebookmark
5031 5030 # i18n: column positioning for "hg summary"
5032 5031 ui.write(_('bookmarks:'), label='log.bookmark')
5033 5032 if active is not None:
5034 5033 if active in marks:
5035 5034 ui.write(' *' + active, label=bookmarks.activebookmarklabel)
5036 5035 marks.remove(active)
5037 5036 else:
5038 5037 ui.write(' [%s]' % active, label=bookmarks.activebookmarklabel)
5039 5038 for m in marks:
5040 5039 ui.write(' ' + m, label='log.bookmark')
5041 5040 ui.write('\n', label='log.bookmark')
5042 5041
5043 5042 status = repo.status(unknown=True)
5044 5043
5045 5044 c = repo.dirstate.copies()
5046 5045 copied, renamed = [], []
5047 5046 for d, s in c.iteritems():
5048 5047 if s in status.removed:
5049 5048 status.removed.remove(s)
5050 5049 renamed.append(d)
5051 5050 else:
5052 5051 copied.append(d)
5053 5052 if d in status.added:
5054 5053 status.added.remove(d)
5055 5054
5056 5055 subs = [s for s in ctx.substate if ctx.sub(s).dirty()]
5057 5056
5058 5057 labels = [(ui.label(_('%d modified'), 'status.modified'), status.modified),
5059 5058 (ui.label(_('%d added'), 'status.added'), status.added),
5060 5059 (ui.label(_('%d removed'), 'status.removed'), status.removed),
5061 5060 (ui.label(_('%d renamed'), 'status.copied'), renamed),
5062 5061 (ui.label(_('%d copied'), 'status.copied'), copied),
5063 5062 (ui.label(_('%d deleted'), 'status.deleted'), status.deleted),
5064 5063 (ui.label(_('%d unknown'), 'status.unknown'), status.unknown),
5065 5064 (ui.label(_('%d unresolved'), 'resolve.unresolved'), unresolved),
5066 5065 (ui.label(_('%d subrepos'), 'status.modified'), subs)]
5067 5066 t = []
5068 5067 for l, s in labels:
5069 5068 if s:
5070 5069 t.append(l % len(s))
5071 5070
5072 5071 t = ', '.join(t)
5073 5072 cleanworkdir = False
5074 5073
5075 5074 if repo.vfs.exists('graftstate'):
5076 5075 t += _(' (graft in progress)')
5077 5076 if repo.vfs.exists('updatestate'):
5078 5077 t += _(' (interrupted update)')
5079 5078 elif len(parents) > 1:
5080 5079 t += _(' (merge)')
5081 5080 elif branch != parents[0].branch():
5082 5081 t += _(' (new branch)')
5083 5082 elif (parents[0].closesbranch() and
5084 5083 pnode in repo.branchheads(branch, closed=True)):
5085 5084 t += _(' (head closed)')
5086 5085 elif not (status.modified or status.added or status.removed or renamed or
5087 5086 copied or subs):
5088 5087 t += _(' (clean)')
5089 5088 cleanworkdir = True
5090 5089 elif pnode not in bheads:
5091 5090 t += _(' (new branch head)')
5092 5091
5093 5092 if parents:
5094 5093 pendingphase = max(p.phase() for p in parents)
5095 5094 else:
5096 5095 pendingphase = phases.public
5097 5096
5098 5097 if pendingphase > phases.newcommitphase(ui):
5099 5098 t += ' (%s)' % phases.phasenames[pendingphase]
5100 5099
5101 5100 if cleanworkdir:
5102 5101 # i18n: column positioning for "hg summary"
5103 5102 ui.status(_('commit: %s\n') % t.strip())
5104 5103 else:
5105 5104 # i18n: column positioning for "hg summary"
5106 5105 ui.write(_('commit: %s\n') % t.strip())
5107 5106
5108 5107 # all ancestors of branch heads - all ancestors of parent = new csets
5109 5108 new = len(repo.changelog.findmissing([pctx.node() for pctx in parents],
5110 5109 bheads))
5111 5110
5112 5111 if new == 0:
5113 5112 # i18n: column positioning for "hg summary"
5114 5113 ui.status(_('update: (current)\n'))
5115 5114 elif pnode not in bheads:
5116 5115 # i18n: column positioning for "hg summary"
5117 5116 ui.write(_('update: %d new changesets (update)\n') % new)
5118 5117 else:
5119 5118 # i18n: column positioning for "hg summary"
5120 5119 ui.write(_('update: %d new changesets, %d branch heads (merge)\n') %
5121 5120 (new, len(bheads)))
5122 5121
5123 5122 t = []
5124 5123 draft = len(repo.revs('draft()'))
5125 5124 if draft:
5126 5125 t.append(_('%d draft') % draft)
5127 5126 secret = len(repo.revs('secret()'))
5128 5127 if secret:
5129 5128 t.append(_('%d secret') % secret)
5130 5129
5131 5130 if draft or secret:
5132 5131 ui.status(_('phases: %s\n') % ', '.join(t))
5133 5132
5134 5133 if obsolete.isenabled(repo, obsolete.createmarkersopt):
5135 5134 for trouble in ("orphan", "contentdivergent", "phasedivergent"):
5136 5135 numtrouble = len(repo.revs(trouble + "()"))
5137 5136 # We write all the possibilities to ease translation
5138 5137 troublemsg = {
5139 5138 "orphan": _("orphan: %d changesets"),
5140 5139 "contentdivergent": _("content-divergent: %d changesets"),
5141 5140 "phasedivergent": _("phase-divergent: %d changesets"),
5142 5141 }
5143 5142 if numtrouble > 0:
5144 5143 ui.status(troublemsg[trouble] % numtrouble + "\n")
5145 5144
5146 5145 cmdutil.summaryhooks(ui, repo)
5147 5146
5148 5147 if opts.get('remote'):
5149 5148 needsincoming, needsoutgoing = True, True
5150 5149 else:
5151 5150 needsincoming, needsoutgoing = False, False
5152 5151 for i, o in cmdutil.summaryremotehooks(ui, repo, opts, None):
5153 5152 if i:
5154 5153 needsincoming = True
5155 5154 if o:
5156 5155 needsoutgoing = True
5157 5156 if not needsincoming and not needsoutgoing:
5158 5157 return
5159 5158
5160 5159 def getincoming():
5161 5160 source, branches = hg.parseurl(ui.expandpath('default'))
5162 5161 sbranch = branches[0]
5163 5162 try:
5164 5163 other = hg.peer(repo, {}, source)
5165 5164 except error.RepoError:
5166 5165 if opts.get('remote'):
5167 5166 raise
5168 5167 return source, sbranch, None, None, None
5169 5168 revs, checkout = hg.addbranchrevs(repo, other, branches, None)
5170 5169 if revs:
5171 5170 revs = [other.lookup(rev) for rev in revs]
5172 5171 ui.debug('comparing with %s\n' % util.hidepassword(source))
5173 5172 repo.ui.pushbuffer()
5174 5173 commoninc = discovery.findcommonincoming(repo, other, heads=revs)
5175 5174 repo.ui.popbuffer()
5176 5175 return source, sbranch, other, commoninc, commoninc[1]
5177 5176
5178 5177 if needsincoming:
5179 5178 source, sbranch, sother, commoninc, incoming = getincoming()
5180 5179 else:
5181 5180 source = sbranch = sother = commoninc = incoming = None
5182 5181
5183 5182 def getoutgoing():
5184 5183 dest, branches = hg.parseurl(ui.expandpath('default-push', 'default'))
5185 5184 dbranch = branches[0]
5186 5185 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
5187 5186 if source != dest:
5188 5187 try:
5189 5188 dother = hg.peer(repo, {}, dest)
5190 5189 except error.RepoError:
5191 5190 if opts.get('remote'):
5192 5191 raise
5193 5192 return dest, dbranch, None, None
5194 5193 ui.debug('comparing with %s\n' % util.hidepassword(dest))
5195 5194 elif sother is None:
5196 5195 # there is no explicit destination peer, but source one is invalid
5197 5196 return dest, dbranch, None, None
5198 5197 else:
5199 5198 dother = sother
5200 5199 if (source != dest or (sbranch is not None and sbranch != dbranch)):
5201 5200 common = None
5202 5201 else:
5203 5202 common = commoninc
5204 5203 if revs:
5205 5204 revs = [repo.lookup(rev) for rev in revs]
5206 5205 repo.ui.pushbuffer()
5207 5206 outgoing = discovery.findcommonoutgoing(repo, dother, onlyheads=revs,
5208 5207 commoninc=common)
5209 5208 repo.ui.popbuffer()
5210 5209 return dest, dbranch, dother, outgoing
5211 5210
5212 5211 if needsoutgoing:
5213 5212 dest, dbranch, dother, outgoing = getoutgoing()
5214 5213 else:
5215 5214 dest = dbranch = dother = outgoing = None
5216 5215
5217 5216 if opts.get('remote'):
5218 5217 t = []
5219 5218 if incoming:
5220 5219 t.append(_('1 or more incoming'))
5221 5220 o = outgoing.missing
5222 5221 if o:
5223 5222 t.append(_('%d outgoing') % len(o))
5224 5223 other = dother or sother
5225 5224 if 'bookmarks' in other.listkeys('namespaces'):
5226 5225 counts = bookmarks.summary(repo, other)
5227 5226 if counts[0] > 0:
5228 5227 t.append(_('%d incoming bookmarks') % counts[0])
5229 5228 if counts[1] > 0:
5230 5229 t.append(_('%d outgoing bookmarks') % counts[1])
5231 5230
5232 5231 if t:
5233 5232 # i18n: column positioning for "hg summary"
5234 5233 ui.write(_('remote: %s\n') % (', '.join(t)))
5235 5234 else:
5236 5235 # i18n: column positioning for "hg summary"
5237 5236 ui.status(_('remote: (synced)\n'))
5238 5237
5239 5238 cmdutil.summaryremotehooks(ui, repo, opts,
5240 5239 ((source, sbranch, sother, commoninc),
5241 5240 (dest, dbranch, dother, outgoing)))
5242 5241
5243 5242 @command('tag',
5244 5243 [('f', 'force', None, _('force tag')),
5245 5244 ('l', 'local', None, _('make the tag local')),
5246 5245 ('r', 'rev', '', _('revision to tag'), _('REV')),
5247 5246 ('', 'remove', None, _('remove a tag')),
5248 5247 # -l/--local is already there, commitopts cannot be used
5249 5248 ('e', 'edit', None, _('invoke editor on commit messages')),
5250 5249 ('m', 'message', '', _('use text as commit message'), _('TEXT')),
5251 5250 ] + commitopts2,
5252 5251 _('[-f] [-l] [-m TEXT] [-d DATE] [-u USER] [-r REV] NAME...'))
5253 5252 def tag(ui, repo, name1, *names, **opts):
5254 5253 """add one or more tags for the current or given revision
5255 5254
5256 5255 Name a particular revision using <name>.
5257 5256
5258 5257 Tags are used to name particular revisions of the repository and are
5259 5258 very useful to compare different revisions, to go back to significant
5260 5259 earlier versions or to mark branch points as releases, etc. Changing
5261 5260 an existing tag is normally disallowed; use -f/--force to override.
5262 5261
5263 5262 If no revision is given, the parent of the working directory is
5264 5263 used.
5265 5264
5266 5265 To facilitate version control, distribution, and merging of tags,
5267 5266 they are stored as a file named ".hgtags" which is managed similarly
5268 5267 to other project files and can be hand-edited if necessary. This
5269 5268 also means that tagging creates a new commit. The file
5270 5269 ".hg/localtags" is used for local tags (not shared among
5271 5270 repositories).
5272 5271
5273 5272 Tag commits are usually made at the head of a branch. If the parent
5274 5273 of the working directory is not a branch head, :hg:`tag` aborts; use
5275 5274 -f/--force to force the tag commit to be based on a non-head
5276 5275 changeset.
5277 5276
5278 5277 See :hg:`help dates` for a list of formats valid for -d/--date.
5279 5278
5280 5279 Since tag names have priority over branch names during revision
5281 5280 lookup, using an existing branch name as a tag name is discouraged.
5282 5281
5283 5282 Returns 0 on success.
5284 5283 """
5285 5284 opts = pycompat.byteskwargs(opts)
5286 5285 wlock = lock = None
5287 5286 try:
5288 5287 wlock = repo.wlock()
5289 5288 lock = repo.lock()
5290 5289 rev_ = "."
5291 5290 names = [t.strip() for t in (name1,) + names]
5292 5291 if len(names) != len(set(names)):
5293 5292 raise error.Abort(_('tag names must be unique'))
5294 5293 for n in names:
5295 5294 scmutil.checknewlabel(repo, n, 'tag')
5296 5295 if not n:
5297 5296 raise error.Abort(_('tag names cannot consist entirely of '
5298 5297 'whitespace'))
5299 5298 if opts.get('rev') and opts.get('remove'):
5300 5299 raise error.Abort(_("--rev and --remove are incompatible"))
5301 5300 if opts.get('rev'):
5302 5301 rev_ = opts['rev']
5303 5302 message = opts.get('message')
5304 5303 if opts.get('remove'):
5305 5304 if opts.get('local'):
5306 5305 expectedtype = 'local'
5307 5306 else:
5308 5307 expectedtype = 'global'
5309 5308
5310 5309 for n in names:
5311 5310 if not repo.tagtype(n):
5312 5311 raise error.Abort(_("tag '%s' does not exist") % n)
5313 5312 if repo.tagtype(n) != expectedtype:
5314 5313 if expectedtype == 'global':
5315 5314 raise error.Abort(_("tag '%s' is not a global tag") % n)
5316 5315 else:
5317 5316 raise error.Abort(_("tag '%s' is not a local tag") % n)
5318 5317 rev_ = 'null'
5319 5318 if not message:
5320 5319 # we don't translate commit messages
5321 5320 message = 'Removed tag %s' % ', '.join(names)
5322 5321 elif not opts.get('force'):
5323 5322 for n in names:
5324 5323 if n in repo.tags():
5325 5324 raise error.Abort(_("tag '%s' already exists "
5326 5325 "(use -f to force)") % n)
5327 5326 if not opts.get('local'):
5328 5327 p1, p2 = repo.dirstate.parents()
5329 5328 if p2 != nullid:
5330 5329 raise error.Abort(_('uncommitted merge'))
5331 5330 bheads = repo.branchheads()
5332 5331 if not opts.get('force') and bheads and p1 not in bheads:
5333 5332 raise error.Abort(_('working directory is not at a branch head '
5334 5333 '(use -f to force)'))
5335 5334 node = scmutil.revsingle(repo, rev_).node()
5336 5335
5337 5336 if not message:
5338 5337 # we don't translate commit messages
5339 5338 message = ('Added tag %s for changeset %s' %
5340 5339 (', '.join(names), short(node)))
5341 5340
5342 5341 date = opts.get('date')
5343 5342 if date:
5344 5343 date = dateutil.parsedate(date)
5345 5344
5346 5345 if opts.get('remove'):
5347 5346 editform = 'tag.remove'
5348 5347 else:
5349 5348 editform = 'tag.add'
5350 5349 editor = cmdutil.getcommiteditor(editform=editform,
5351 5350 **pycompat.strkwargs(opts))
5352 5351
5353 5352 # don't allow tagging the null rev
5354 5353 if (not opts.get('remove') and
5355 5354 scmutil.revsingle(repo, rev_).rev() == nullrev):
5356 5355 raise error.Abort(_("cannot tag null revision"))
5357 5356
5358 5357 tagsmod.tag(repo, names, node, message, opts.get('local'),
5359 5358 opts.get('user'), date, editor=editor)
5360 5359 finally:
5361 5360 release(lock, wlock)
5362 5361
5363 5362 @command('tags', formatteropts, '', cmdtype=readonly)
5364 5363 def tags(ui, repo, **opts):
5365 5364 """list repository tags
5366 5365
5367 5366 This lists both regular and local tags. When the -v/--verbose
5368 5367 switch is used, a third column "local" is printed for local tags.
5369 5368 When the -q/--quiet switch is used, only the tag name is printed.
5370 5369
5371 5370 Returns 0 on success.
5372 5371 """
5373 5372
5374 5373 opts = pycompat.byteskwargs(opts)
5375 5374 ui.pager('tags')
5376 5375 fm = ui.formatter('tags', opts)
5377 5376 hexfunc = fm.hexfunc
5378 5377 tagtype = ""
5379 5378
5380 5379 for t, n in reversed(repo.tagslist()):
5381 5380 hn = hexfunc(n)
5382 5381 label = 'tags.normal'
5383 5382 tagtype = ''
5384 5383 if repo.tagtype(t) == 'local':
5385 5384 label = 'tags.local'
5386 5385 tagtype = 'local'
5387 5386
5388 5387 fm.startitem()
5389 5388 fm.write('tag', '%s', t, label=label)
5390 5389 fmt = " " * (30 - encoding.colwidth(t)) + ' %5d:%s'
5391 5390 fm.condwrite(not ui.quiet, 'rev node', fmt,
5392 5391 repo.changelog.rev(n), hn, label=label)
5393 5392 fm.condwrite(ui.verbose and tagtype, 'type', ' %s',
5394 5393 tagtype, label=label)
5395 5394 fm.plain('\n')
5396 5395 fm.end()
5397 5396
5398 5397 @command('tip',
5399 5398 [('p', 'patch', None, _('show patch')),
5400 5399 ('g', 'git', None, _('use git extended diff format')),
5401 5400 ] + templateopts,
5402 5401 _('[-p] [-g]'))
5403 5402 def tip(ui, repo, **opts):
5404 5403 """show the tip revision (DEPRECATED)
5405 5404
5406 5405 The tip revision (usually just called the tip) is the changeset
5407 5406 most recently added to the repository (and therefore the most
5408 5407 recently changed head).
5409 5408
5410 5409 If you have just made a commit, that commit will be the tip. If
5411 5410 you have just pulled changes from another repository, the tip of
5412 5411 that repository becomes the current tip. The "tip" tag is special
5413 5412 and cannot be renamed or assigned to a different changeset.
5414 5413
5415 5414 This command is deprecated, please use :hg:`heads` instead.
5416 5415
5417 5416 Returns 0 on success.
5418 5417 """
5419 5418 opts = pycompat.byteskwargs(opts)
5420 5419 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
5421 5420 displayer.show(repo['tip'])
5422 5421 displayer.close()
5423 5422
5424 5423 @command('unbundle',
5425 5424 [('u', 'update', None,
5426 5425 _('update to new branch head if changesets were unbundled'))],
5427 5426 _('[-u] FILE...'))
5428 5427 def unbundle(ui, repo, fname1, *fnames, **opts):
5429 5428 """apply one or more bundle files
5430 5429
5431 5430 Apply one or more bundle files generated by :hg:`bundle`.
5432 5431
5433 5432 Returns 0 on success, 1 if an update has unresolved files.
5434 5433 """
5435 5434 fnames = (fname1,) + fnames
5436 5435
5437 5436 with repo.lock():
5438 5437 for fname in fnames:
5439 5438 f = hg.openpath(ui, fname)
5440 5439 gen = exchange.readbundle(ui, f, fname)
5441 5440 if isinstance(gen, streamclone.streamcloneapplier):
5442 5441 raise error.Abort(
5443 5442 _('packed bundles cannot be applied with '
5444 5443 '"hg unbundle"'),
5445 5444 hint=_('use "hg debugapplystreamclonebundle"'))
5446 5445 url = 'bundle:' + fname
5447 5446 try:
5448 5447 txnname = 'unbundle'
5449 5448 if not isinstance(gen, bundle2.unbundle20):
5450 5449 txnname = 'unbundle\n%s' % util.hidepassword(url)
5451 5450 with repo.transaction(txnname) as tr:
5452 5451 op = bundle2.applybundle(repo, gen, tr, source='unbundle',
5453 5452 url=url)
5454 5453 except error.BundleUnknownFeatureError as exc:
5455 5454 raise error.Abort(
5456 5455 _('%s: unknown bundle feature, %s') % (fname, exc),
5457 5456 hint=_("see https://mercurial-scm.org/"
5458 5457 "wiki/BundleFeature for more "
5459 5458 "information"))
5460 5459 modheads = bundle2.combinechangegroupresults(op)
5461 5460
5462 5461 return postincoming(ui, repo, modheads, opts.get(r'update'), None, None)
5463 5462
5464 5463 @command('^update|up|checkout|co',
5465 5464 [('C', 'clean', None, _('discard uncommitted changes (no backup)')),
5466 5465 ('c', 'check', None, _('require clean working directory')),
5467 5466 ('m', 'merge', None, _('merge uncommitted changes')),
5468 5467 ('d', 'date', '', _('tipmost revision matching date'), _('DATE')),
5469 5468 ('r', 'rev', '', _('revision'), _('REV'))
5470 5469 ] + mergetoolopts,
5471 5470 _('[-C|-c|-m] [-d DATE] [[-r] REV]'))
5472 5471 def update(ui, repo, node=None, **opts):
5473 5472 """update working directory (or switch revisions)
5474 5473
5475 5474 Update the repository's working directory to the specified
5476 5475 changeset. If no changeset is specified, update to the tip of the
5477 5476 current named branch and move the active bookmark (see :hg:`help
5478 5477 bookmarks`).
5479 5478
5480 5479 Update sets the working directory's parent revision to the specified
5481 5480 changeset (see :hg:`help parents`).
5482 5481
5483 5482 If the changeset is not a descendant or ancestor of the working
5484 5483 directory's parent and there are uncommitted changes, the update is
5485 5484 aborted. With the -c/--check option, the working directory is checked
5486 5485 for uncommitted changes; if none are found, the working directory is
5487 5486 updated to the specified changeset.
5488 5487
5489 5488 .. container:: verbose
5490 5489
5491 5490 The -C/--clean, -c/--check, and -m/--merge options control what
5492 5491 happens if the working directory contains uncommitted changes.
5493 5492 At most of one of them can be specified.
5494 5493
5495 5494 1. If no option is specified, and if
5496 5495 the requested changeset is an ancestor or descendant of
5497 5496 the working directory's parent, the uncommitted changes
5498 5497 are merged into the requested changeset and the merged
5499 5498 result is left uncommitted. If the requested changeset is
5500 5499 not an ancestor or descendant (that is, it is on another
5501 5500 branch), the update is aborted and the uncommitted changes
5502 5501 are preserved.
5503 5502
5504 5503 2. With the -m/--merge option, the update is allowed even if the
5505 5504 requested changeset is not an ancestor or descendant of
5506 5505 the working directory's parent.
5507 5506
5508 5507 3. With the -c/--check option, the update is aborted and the
5509 5508 uncommitted changes are preserved.
5510 5509
5511 5510 4. With the -C/--clean option, uncommitted changes are discarded and
5512 5511 the working directory is updated to the requested changeset.
5513 5512
5514 5513 To cancel an uncommitted merge (and lose your changes), use
5515 5514 :hg:`merge --abort`.
5516 5515
5517 5516 Use null as the changeset to remove the working directory (like
5518 5517 :hg:`clone -U`).
5519 5518
5520 5519 If you want to revert just one file to an older revision, use
5521 5520 :hg:`revert [-r REV] NAME`.
5522 5521
5523 5522 See :hg:`help dates` for a list of formats valid for -d/--date.
5524 5523
5525 5524 Returns 0 on success, 1 if there are unresolved files.
5526 5525 """
5527 5526 rev = opts.get(r'rev')
5528 5527 date = opts.get(r'date')
5529 5528 clean = opts.get(r'clean')
5530 5529 check = opts.get(r'check')
5531 5530 merge = opts.get(r'merge')
5532 5531 if rev and node:
5533 5532 raise error.Abort(_("please specify just one revision"))
5534 5533
5535 5534 if ui.configbool('commands', 'update.requiredest'):
5536 5535 if not node and not rev and not date:
5537 5536 raise error.Abort(_('you must specify a destination'),
5538 5537 hint=_('for example: hg update ".::"'))
5539 5538
5540 5539 if rev is None or rev == '':
5541 5540 rev = node
5542 5541
5543 5542 if date and rev is not None:
5544 5543 raise error.Abort(_("you can't specify a revision and a date"))
5545 5544
5546 5545 if len([x for x in (clean, check, merge) if x]) > 1:
5547 5546 raise error.Abort(_("can only specify one of -C/--clean, -c/--check, "
5548 5547 "or -m/--merge"))
5549 5548
5550 5549 updatecheck = None
5551 5550 if check:
5552 5551 updatecheck = 'abort'
5553 5552 elif merge:
5554 5553 updatecheck = 'none'
5555 5554
5556 5555 with repo.wlock():
5557 5556 cmdutil.clearunfinished(repo)
5558 5557
5559 5558 if date:
5560 5559 rev = cmdutil.finddate(ui, repo, date)
5561 5560
5562 5561 # if we defined a bookmark, we have to remember the original name
5563 5562 brev = rev
5564 5563 if rev:
5565 5564 repo = scmutil.unhidehashlikerevs(repo, [rev], 'nowarn')
5566 5565 ctx = scmutil.revsingle(repo, rev, rev)
5567 5566 rev = ctx.rev()
5568 5567 if ctx.hidden():
5569 5568 ctxstr = ctx.hex()[:12]
5570 5569 ui.warn(_("updating to a hidden changeset %s\n") % ctxstr)
5571 5570
5572 5571 if ctx.obsolete():
5573 5572 obsfatemsg = obsutil._getfilteredreason(repo, ctxstr, ctx)
5574 5573 ui.warn("(%s)\n" % obsfatemsg)
5575 5574
5576 5575 repo.ui.setconfig('ui', 'forcemerge', opts.get(r'tool'), 'update')
5577 5576
5578 5577 return hg.updatetotally(ui, repo, rev, brev, clean=clean,
5579 5578 updatecheck=updatecheck)
5580 5579
5581 5580 @command('verify', [])
5582 5581 def verify(ui, repo):
5583 5582 """verify the integrity of the repository
5584 5583
5585 5584 Verify the integrity of the current repository.
5586 5585
5587 5586 This will perform an extensive check of the repository's
5588 5587 integrity, validating the hashes and checksums of each entry in
5589 5588 the changelog, manifest, and tracked files, as well as the
5590 5589 integrity of their crosslinks and indices.
5591 5590
5592 5591 Please see https://mercurial-scm.org/wiki/RepositoryCorruption
5593 5592 for more information about recovery from corruption of the
5594 5593 repository.
5595 5594
5596 5595 Returns 0 on success, 1 if errors are encountered.
5597 5596 """
5598 5597 return hg.verify(repo)
5599 5598
5600 5599 @command('version', [] + formatteropts, norepo=True, cmdtype=readonly)
5601 5600 def version_(ui, **opts):
5602 5601 """output version and copyright information"""
5603 5602 opts = pycompat.byteskwargs(opts)
5604 5603 if ui.verbose:
5605 5604 ui.pager('version')
5606 5605 fm = ui.formatter("version", opts)
5607 5606 fm.startitem()
5608 5607 fm.write("ver", _("Mercurial Distributed SCM (version %s)\n"),
5609 5608 util.version())
5610 5609 license = _(
5611 5610 "(see https://mercurial-scm.org for more information)\n"
5612 5611 "\nCopyright (C) 2005-2018 Matt Mackall and others\n"
5613 5612 "This is free software; see the source for copying conditions. "
5614 5613 "There is NO\nwarranty; "
5615 5614 "not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"
5616 5615 )
5617 5616 if not ui.quiet:
5618 5617 fm.plain(license)
5619 5618
5620 5619 if ui.verbose:
5621 5620 fm.plain(_("\nEnabled extensions:\n\n"))
5622 5621 # format names and versions into columns
5623 5622 names = []
5624 5623 vers = []
5625 5624 isinternals = []
5626 5625 for name, module in extensions.extensions():
5627 5626 names.append(name)
5628 5627 vers.append(extensions.moduleversion(module) or None)
5629 5628 isinternals.append(extensions.ismoduleinternal(module))
5630 5629 fn = fm.nested("extensions", tmpl='{name}\n')
5631 5630 if names:
5632 5631 namefmt = " %%-%ds " % max(len(n) for n in names)
5633 5632 places = [_("external"), _("internal")]
5634 5633 for n, v, p in zip(names, vers, isinternals):
5635 5634 fn.startitem()
5636 5635 fn.condwrite(ui.verbose, "name", namefmt, n)
5637 5636 if ui.verbose:
5638 5637 fn.plain("%s " % places[p])
5639 5638 fn.data(bundled=p)
5640 5639 fn.condwrite(ui.verbose and v, "ver", "%s", v)
5641 5640 if ui.verbose:
5642 5641 fn.plain("\n")
5643 5642 fn.end()
5644 5643 fm.end()
5645 5644
5646 5645 def loadcmdtable(ui, name, cmdtable):
5647 5646 """Load command functions from specified cmdtable
5648 5647 """
5649 5648 overrides = [cmd for cmd in cmdtable if cmd in table]
5650 5649 if overrides:
5651 5650 ui.warn(_("extension '%s' overrides commands: %s\n")
5652 5651 % (name, " ".join(overrides)))
5653 5652 table.update(cmdtable)
@@ -1,2915 +1,2920 b''
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import, print_function
10 10
11 11 import collections
12 import contextlib
12 13 import copy
13 14 import difflib
14 15 import email
15 16 import errno
16 17 import hashlib
17 18 import os
18 19 import posixpath
19 20 import re
20 21 import shutil
21 22 import tempfile
22 23 import zlib
23 24
24 25 from .i18n import _
25 26 from .node import (
26 27 hex,
27 28 short,
28 29 )
29 30 from . import (
30 31 copies,
31 32 diffhelpers,
32 33 encoding,
33 34 error,
34 35 mail,
35 36 mdiff,
36 37 pathutil,
37 38 pycompat,
38 39 scmutil,
39 40 similar,
40 41 util,
41 42 vfs as vfsmod,
42 43 )
43 44 from .utils import (
44 45 dateutil,
45 46 procutil,
46 47 stringutil,
47 48 )
48 49
49 50 stringio = util.stringio
50 51
51 52 gitre = re.compile(br'diff --git a/(.*) b/(.*)')
52 53 tabsplitter = re.compile(br'(\t+|[^\t]+)')
53 54 _nonwordre = re.compile(br'([^a-zA-Z0-9_\x80-\xff])')
54 55
55 56 PatchError = error.PatchError
56 57
57 58 # public functions
58 59
59 60 def split(stream):
60 61 '''return an iterator of individual patches from a stream'''
61 62 def isheader(line, inheader):
62 63 if inheader and line.startswith((' ', '\t')):
63 64 # continuation
64 65 return True
65 66 if line.startswith((' ', '-', '+')):
66 67 # diff line - don't check for header pattern in there
67 68 return False
68 69 l = line.split(': ', 1)
69 70 return len(l) == 2 and ' ' not in l[0]
70 71
71 72 def chunk(lines):
72 73 return stringio(''.join(lines))
73 74
74 75 def hgsplit(stream, cur):
75 76 inheader = True
76 77
77 78 for line in stream:
78 79 if not line.strip():
79 80 inheader = False
80 81 if not inheader and line.startswith('# HG changeset patch'):
81 82 yield chunk(cur)
82 83 cur = []
83 84 inheader = True
84 85
85 86 cur.append(line)
86 87
87 88 if cur:
88 89 yield chunk(cur)
89 90
90 91 def mboxsplit(stream, cur):
91 92 for line in stream:
92 93 if line.startswith('From '):
93 94 for c in split(chunk(cur[1:])):
94 95 yield c
95 96 cur = []
96 97
97 98 cur.append(line)
98 99
99 100 if cur:
100 101 for c in split(chunk(cur[1:])):
101 102 yield c
102 103
103 104 def mimesplit(stream, cur):
104 105 def msgfp(m):
105 106 fp = stringio()
106 107 g = email.Generator.Generator(fp, mangle_from_=False)
107 108 g.flatten(m)
108 109 fp.seek(0)
109 110 return fp
110 111
111 112 for line in stream:
112 113 cur.append(line)
113 114 c = chunk(cur)
114 115
115 116 m = pycompat.emailparser().parse(c)
116 117 if not m.is_multipart():
117 118 yield msgfp(m)
118 119 else:
119 120 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
120 121 for part in m.walk():
121 122 ct = part.get_content_type()
122 123 if ct not in ok_types:
123 124 continue
124 125 yield msgfp(part)
125 126
126 127 def headersplit(stream, cur):
127 128 inheader = False
128 129
129 130 for line in stream:
130 131 if not inheader and isheader(line, inheader):
131 132 yield chunk(cur)
132 133 cur = []
133 134 inheader = True
134 135 if inheader and not isheader(line, inheader):
135 136 inheader = False
136 137
137 138 cur.append(line)
138 139
139 140 if cur:
140 141 yield chunk(cur)
141 142
142 143 def remainder(cur):
143 144 yield chunk(cur)
144 145
145 146 class fiter(object):
146 147 def __init__(self, fp):
147 148 self.fp = fp
148 149
149 150 def __iter__(self):
150 151 return self
151 152
152 153 def next(self):
153 154 l = self.fp.readline()
154 155 if not l:
155 156 raise StopIteration
156 157 return l
157 158
158 159 __next__ = next
159 160
160 161 inheader = False
161 162 cur = []
162 163
163 164 mimeheaders = ['content-type']
164 165
165 166 if not util.safehasattr(stream, 'next'):
166 167 # http responses, for example, have readline but not next
167 168 stream = fiter(stream)
168 169
169 170 for line in stream:
170 171 cur.append(line)
171 172 if line.startswith('# HG changeset patch'):
172 173 return hgsplit(stream, cur)
173 174 elif line.startswith('From '):
174 175 return mboxsplit(stream, cur)
175 176 elif isheader(line, inheader):
176 177 inheader = True
177 178 if line.split(':', 1)[0].lower() in mimeheaders:
178 179 # let email parser handle this
179 180 return mimesplit(stream, cur)
180 181 elif line.startswith('--- ') and inheader:
181 182 # No evil headers seen by diff start, split by hand
182 183 return headersplit(stream, cur)
183 184 # Not enough info, keep reading
184 185
185 186 # if we are here, we have a very plain patch
186 187 return remainder(cur)
187 188
188 189 ## Some facility for extensible patch parsing:
189 190 # list of pairs ("header to match", "data key")
190 191 patchheadermap = [('Date', 'date'),
191 192 ('Branch', 'branch'),
192 193 ('Node ID', 'nodeid'),
193 194 ]
194 195
196 @contextlib.contextmanager
195 197 def extract(ui, fileobj):
196 198 '''extract patch from data read from fileobj.
197 199
198 200 patch can be a normal patch or contained in an email message.
199 201
200 202 return a dictionary. Standard keys are:
201 203 - filename,
202 204 - message,
203 205 - user,
204 206 - date,
205 207 - branch,
206 208 - node,
207 209 - p1,
208 210 - p2.
209 211 Any item can be missing from the dictionary. If filename is missing,
210 212 fileobj did not contain a patch. Caller must unlink filename when done.'''
211 213
214 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
215 tmpfp = os.fdopen(fd, r'wb')
216 try:
217 yield _extract(ui, fileobj, tmpname, tmpfp)
218 finally:
219 tmpfp.close()
220 os.unlink(tmpname)
221
222 def _extract(ui, fileobj, tmpname, tmpfp):
223
212 224 # attempt to detect the start of a patch
213 225 # (this heuristic is borrowed from quilt)
214 226 diffre = re.compile(br'^(?:Index:[ \t]|diff[ \t]-|RCS file: |'
215 227 br'retrieving revision [0-9]+(\.[0-9]+)*$|'
216 228 br'---[ \t].*?^\+\+\+[ \t]|'
217 229 br'\*\*\*[ \t].*?^---[ \t])',
218 230 re.MULTILINE | re.DOTALL)
219 231
220 232 data = {}
221 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
222 tmpfp = os.fdopen(fd, r'wb')
223 try:
224 msg = pycompat.emailparser().parse(fileobj)
233
234 msg = pycompat.emailparser().parse(fileobj)
225 235
226 subject = msg[r'Subject'] and mail.headdecode(msg[r'Subject'])
227 data['user'] = msg[r'From'] and mail.headdecode(msg[r'From'])
228 if not subject and not data['user']:
229 # Not an email, restore parsed headers if any
230 subject = '\n'.join(': '.join(map(encoding.strtolocal, h))
231 for h in msg.items()) + '\n'
236 subject = msg[r'Subject'] and mail.headdecode(msg[r'Subject'])
237 data['user'] = msg[r'From'] and mail.headdecode(msg[r'From'])
238 if not subject and not data['user']:
239 # Not an email, restore parsed headers if any
240 subject = '\n'.join(': '.join(map(encoding.strtolocal, h))
241 for h in msg.items()) + '\n'
232 242
233 # should try to parse msg['Date']
234 parents = []
243 # should try to parse msg['Date']
244 parents = []
235 245
236 if subject:
237 if subject.startswith('[PATCH'):
238 pend = subject.find(']')
239 if pend >= 0:
240 subject = subject[pend + 1:].lstrip()
241 subject = re.sub(br'\n[ \t]+', ' ', subject)
242 ui.debug('Subject: %s\n' % subject)
243 if data['user']:
244 ui.debug('From: %s\n' % data['user'])
245 diffs_seen = 0
246 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
247 message = ''
248 for part in msg.walk():
249 content_type = pycompat.bytestr(part.get_content_type())
250 ui.debug('Content-Type: %s\n' % content_type)
251 if content_type not in ok_types:
252 continue
253 payload = part.get_payload(decode=True)
254 m = diffre.search(payload)
255 if m:
256 hgpatch = False
257 hgpatchheader = False
258 ignoretext = False
246 if subject:
247 if subject.startswith('[PATCH'):
248 pend = subject.find(']')
249 if pend >= 0:
250 subject = subject[pend + 1:].lstrip()
251 subject = re.sub(br'\n[ \t]+', ' ', subject)
252 ui.debug('Subject: %s\n' % subject)
253 if data['user']:
254 ui.debug('From: %s\n' % data['user'])
255 diffs_seen = 0
256 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
257 message = ''
258 for part in msg.walk():
259 content_type = pycompat.bytestr(part.get_content_type())
260 ui.debug('Content-Type: %s\n' % content_type)
261 if content_type not in ok_types:
262 continue
263 payload = part.get_payload(decode=True)
264 m = diffre.search(payload)
265 if m:
266 hgpatch = False
267 hgpatchheader = False
268 ignoretext = False
259 269
260 ui.debug('found patch at byte %d\n' % m.start(0))
261 diffs_seen += 1
262 cfp = stringio()
263 for line in payload[:m.start(0)].splitlines():
264 if line.startswith('# HG changeset patch') and not hgpatch:
265 ui.debug('patch generated by hg export\n')
266 hgpatch = True
267 hgpatchheader = True
268 # drop earlier commit message content
269 cfp.seek(0)
270 cfp.truncate()
271 subject = None
272 elif hgpatchheader:
273 if line.startswith('# User '):
274 data['user'] = line[7:]
275 ui.debug('From: %s\n' % data['user'])
276 elif line.startswith("# Parent "):
277 parents.append(line[9:].lstrip())
278 elif line.startswith("# "):
279 for header, key in patchheadermap:
280 prefix = '# %s ' % header
281 if line.startswith(prefix):
282 data[key] = line[len(prefix):]
283 else:
284 hgpatchheader = False
285 elif line == '---':
286 ignoretext = True
287 if not hgpatchheader and not ignoretext:
288 cfp.write(line)
289 cfp.write('\n')
290 message = cfp.getvalue()
291 if tmpfp:
292 tmpfp.write(payload)
293 if not payload.endswith('\n'):
294 tmpfp.write('\n')
295 elif not diffs_seen and message and content_type == 'text/plain':
296 message += '\n' + payload
297 except: # re-raises
298 tmpfp.close()
299 os.unlink(tmpname)
300 raise
270 ui.debug('found patch at byte %d\n' % m.start(0))
271 diffs_seen += 1
272 cfp = stringio()
273 for line in payload[:m.start(0)].splitlines():
274 if line.startswith('# HG changeset patch') and not hgpatch:
275 ui.debug('patch generated by hg export\n')
276 hgpatch = True
277 hgpatchheader = True
278 # drop earlier commit message content
279 cfp.seek(0)
280 cfp.truncate()
281 subject = None
282 elif hgpatchheader:
283 if line.startswith('# User '):
284 data['user'] = line[7:]
285 ui.debug('From: %s\n' % data['user'])
286 elif line.startswith("# Parent "):
287 parents.append(line[9:].lstrip())
288 elif line.startswith("# "):
289 for header, key in patchheadermap:
290 prefix = '# %s ' % header
291 if line.startswith(prefix):
292 data[key] = line[len(prefix):]
293 else:
294 hgpatchheader = False
295 elif line == '---':
296 ignoretext = True
297 if not hgpatchheader and not ignoretext:
298 cfp.write(line)
299 cfp.write('\n')
300 message = cfp.getvalue()
301 if tmpfp:
302 tmpfp.write(payload)
303 if not payload.endswith('\n'):
304 tmpfp.write('\n')
305 elif not diffs_seen and message and content_type == 'text/plain':
306 message += '\n' + payload
301 307
302 308 if subject and not message.startswith(subject):
303 309 message = '%s\n%s' % (subject, message)
304 310 data['message'] = message
305 311 tmpfp.close()
306 312 if parents:
307 313 data['p1'] = parents.pop(0)
308 314 if parents:
309 315 data['p2'] = parents.pop(0)
310 316
311 317 if diffs_seen:
312 318 data['filename'] = tmpname
313 else:
314 os.unlink(tmpname)
319
315 320 return data
316 321
317 322 class patchmeta(object):
318 323 """Patched file metadata
319 324
320 325 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
321 326 or COPY. 'path' is patched file path. 'oldpath' is set to the
322 327 origin file when 'op' is either COPY or RENAME, None otherwise. If
323 328 file mode is changed, 'mode' is a tuple (islink, isexec) where
324 329 'islink' is True if the file is a symlink and 'isexec' is True if
325 330 the file is executable. Otherwise, 'mode' is None.
326 331 """
327 332 def __init__(self, path):
328 333 self.path = path
329 334 self.oldpath = None
330 335 self.mode = None
331 336 self.op = 'MODIFY'
332 337 self.binary = False
333 338
334 339 def setmode(self, mode):
335 340 islink = mode & 0o20000
336 341 isexec = mode & 0o100
337 342 self.mode = (islink, isexec)
338 343
339 344 def copy(self):
340 345 other = patchmeta(self.path)
341 346 other.oldpath = self.oldpath
342 347 other.mode = self.mode
343 348 other.op = self.op
344 349 other.binary = self.binary
345 350 return other
346 351
347 352 def _ispatchinga(self, afile):
348 353 if afile == '/dev/null':
349 354 return self.op == 'ADD'
350 355 return afile == 'a/' + (self.oldpath or self.path)
351 356
352 357 def _ispatchingb(self, bfile):
353 358 if bfile == '/dev/null':
354 359 return self.op == 'DELETE'
355 360 return bfile == 'b/' + self.path
356 361
357 362 def ispatching(self, afile, bfile):
358 363 return self._ispatchinga(afile) and self._ispatchingb(bfile)
359 364
360 365 def __repr__(self):
361 366 return "<patchmeta %s %r>" % (self.op, self.path)
362 367
363 368 def readgitpatch(lr):
364 369 """extract git-style metadata about patches from <patchname>"""
365 370
366 371 # Filter patch for git information
367 372 gp = None
368 373 gitpatches = []
369 374 for line in lr:
370 375 line = line.rstrip(' \r\n')
371 376 if line.startswith('diff --git a/'):
372 377 m = gitre.match(line)
373 378 if m:
374 379 if gp:
375 380 gitpatches.append(gp)
376 381 dst = m.group(2)
377 382 gp = patchmeta(dst)
378 383 elif gp:
379 384 if line.startswith('--- '):
380 385 gitpatches.append(gp)
381 386 gp = None
382 387 continue
383 388 if line.startswith('rename from '):
384 389 gp.op = 'RENAME'
385 390 gp.oldpath = line[12:]
386 391 elif line.startswith('rename to '):
387 392 gp.path = line[10:]
388 393 elif line.startswith('copy from '):
389 394 gp.op = 'COPY'
390 395 gp.oldpath = line[10:]
391 396 elif line.startswith('copy to '):
392 397 gp.path = line[8:]
393 398 elif line.startswith('deleted file'):
394 399 gp.op = 'DELETE'
395 400 elif line.startswith('new file mode '):
396 401 gp.op = 'ADD'
397 402 gp.setmode(int(line[-6:], 8))
398 403 elif line.startswith('new mode '):
399 404 gp.setmode(int(line[-6:], 8))
400 405 elif line.startswith('GIT binary patch'):
401 406 gp.binary = True
402 407 if gp:
403 408 gitpatches.append(gp)
404 409
405 410 return gitpatches
406 411
407 412 class linereader(object):
408 413 # simple class to allow pushing lines back into the input stream
409 414 def __init__(self, fp):
410 415 self.fp = fp
411 416 self.buf = []
412 417
413 418 def push(self, line):
414 419 if line is not None:
415 420 self.buf.append(line)
416 421
417 422 def readline(self):
418 423 if self.buf:
419 424 l = self.buf[0]
420 425 del self.buf[0]
421 426 return l
422 427 return self.fp.readline()
423 428
424 429 def __iter__(self):
425 430 return iter(self.readline, '')
426 431
427 432 class abstractbackend(object):
428 433 def __init__(self, ui):
429 434 self.ui = ui
430 435
431 436 def getfile(self, fname):
432 437 """Return target file data and flags as a (data, (islink,
433 438 isexec)) tuple. Data is None if file is missing/deleted.
434 439 """
435 440 raise NotImplementedError
436 441
437 442 def setfile(self, fname, data, mode, copysource):
438 443 """Write data to target file fname and set its mode. mode is a
439 444 (islink, isexec) tuple. If data is None, the file content should
440 445 be left unchanged. If the file is modified after being copied,
441 446 copysource is set to the original file name.
442 447 """
443 448 raise NotImplementedError
444 449
445 450 def unlink(self, fname):
446 451 """Unlink target file."""
447 452 raise NotImplementedError
448 453
449 454 def writerej(self, fname, failed, total, lines):
450 455 """Write rejected lines for fname. total is the number of hunks
451 456 which failed to apply and total the total number of hunks for this
452 457 files.
453 458 """
454 459
455 460 def exists(self, fname):
456 461 raise NotImplementedError
457 462
458 463 def close(self):
459 464 raise NotImplementedError
460 465
461 466 class fsbackend(abstractbackend):
462 467 def __init__(self, ui, basedir):
463 468 super(fsbackend, self).__init__(ui)
464 469 self.opener = vfsmod.vfs(basedir)
465 470
466 471 def getfile(self, fname):
467 472 if self.opener.islink(fname):
468 473 return (self.opener.readlink(fname), (True, False))
469 474
470 475 isexec = False
471 476 try:
472 477 isexec = self.opener.lstat(fname).st_mode & 0o100 != 0
473 478 except OSError as e:
474 479 if e.errno != errno.ENOENT:
475 480 raise
476 481 try:
477 482 return (self.opener.read(fname), (False, isexec))
478 483 except IOError as e:
479 484 if e.errno != errno.ENOENT:
480 485 raise
481 486 return None, None
482 487
483 488 def setfile(self, fname, data, mode, copysource):
484 489 islink, isexec = mode
485 490 if data is None:
486 491 self.opener.setflags(fname, islink, isexec)
487 492 return
488 493 if islink:
489 494 self.opener.symlink(data, fname)
490 495 else:
491 496 self.opener.write(fname, data)
492 497 if isexec:
493 498 self.opener.setflags(fname, False, True)
494 499
495 500 def unlink(self, fname):
496 501 self.opener.unlinkpath(fname, ignoremissing=True)
497 502
498 503 def writerej(self, fname, failed, total, lines):
499 504 fname = fname + ".rej"
500 505 self.ui.warn(
501 506 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
502 507 (failed, total, fname))
503 508 fp = self.opener(fname, 'w')
504 509 fp.writelines(lines)
505 510 fp.close()
506 511
507 512 def exists(self, fname):
508 513 return self.opener.lexists(fname)
509 514
510 515 class workingbackend(fsbackend):
511 516 def __init__(self, ui, repo, similarity):
512 517 super(workingbackend, self).__init__(ui, repo.root)
513 518 self.repo = repo
514 519 self.similarity = similarity
515 520 self.removed = set()
516 521 self.changed = set()
517 522 self.copied = []
518 523
519 524 def _checkknown(self, fname):
520 525 if self.repo.dirstate[fname] == '?' and self.exists(fname):
521 526 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
522 527
523 528 def setfile(self, fname, data, mode, copysource):
524 529 self._checkknown(fname)
525 530 super(workingbackend, self).setfile(fname, data, mode, copysource)
526 531 if copysource is not None:
527 532 self.copied.append((copysource, fname))
528 533 self.changed.add(fname)
529 534
530 535 def unlink(self, fname):
531 536 self._checkknown(fname)
532 537 super(workingbackend, self).unlink(fname)
533 538 self.removed.add(fname)
534 539 self.changed.add(fname)
535 540
536 541 def close(self):
537 542 wctx = self.repo[None]
538 543 changed = set(self.changed)
539 544 for src, dst in self.copied:
540 545 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
541 546 if self.removed:
542 547 wctx.forget(sorted(self.removed))
543 548 for f in self.removed:
544 549 if f not in self.repo.dirstate:
545 550 # File was deleted and no longer belongs to the
546 551 # dirstate, it was probably marked added then
547 552 # deleted, and should not be considered by
548 553 # marktouched().
549 554 changed.discard(f)
550 555 if changed:
551 556 scmutil.marktouched(self.repo, changed, self.similarity)
552 557 return sorted(self.changed)
553 558
554 559 class filestore(object):
555 560 def __init__(self, maxsize=None):
556 561 self.opener = None
557 562 self.files = {}
558 563 self.created = 0
559 564 self.maxsize = maxsize
560 565 if self.maxsize is None:
561 566 self.maxsize = 4*(2**20)
562 567 self.size = 0
563 568 self.data = {}
564 569
565 570 def setfile(self, fname, data, mode, copied=None):
566 571 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
567 572 self.data[fname] = (data, mode, copied)
568 573 self.size += len(data)
569 574 else:
570 575 if self.opener is None:
571 576 root = tempfile.mkdtemp(prefix='hg-patch-')
572 577 self.opener = vfsmod.vfs(root)
573 578 # Avoid filename issues with these simple names
574 579 fn = '%d' % self.created
575 580 self.opener.write(fn, data)
576 581 self.created += 1
577 582 self.files[fname] = (fn, mode, copied)
578 583
579 584 def getfile(self, fname):
580 585 if fname in self.data:
581 586 return self.data[fname]
582 587 if not self.opener or fname not in self.files:
583 588 return None, None, None
584 589 fn, mode, copied = self.files[fname]
585 590 return self.opener.read(fn), mode, copied
586 591
587 592 def close(self):
588 593 if self.opener:
589 594 shutil.rmtree(self.opener.base)
590 595
591 596 class repobackend(abstractbackend):
592 597 def __init__(self, ui, repo, ctx, store):
593 598 super(repobackend, self).__init__(ui)
594 599 self.repo = repo
595 600 self.ctx = ctx
596 601 self.store = store
597 602 self.changed = set()
598 603 self.removed = set()
599 604 self.copied = {}
600 605
601 606 def _checkknown(self, fname):
602 607 if fname not in self.ctx:
603 608 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
604 609
605 610 def getfile(self, fname):
606 611 try:
607 612 fctx = self.ctx[fname]
608 613 except error.LookupError:
609 614 return None, None
610 615 flags = fctx.flags()
611 616 return fctx.data(), ('l' in flags, 'x' in flags)
612 617
613 618 def setfile(self, fname, data, mode, copysource):
614 619 if copysource:
615 620 self._checkknown(copysource)
616 621 if data is None:
617 622 data = self.ctx[fname].data()
618 623 self.store.setfile(fname, data, mode, copysource)
619 624 self.changed.add(fname)
620 625 if copysource:
621 626 self.copied[fname] = copysource
622 627
623 628 def unlink(self, fname):
624 629 self._checkknown(fname)
625 630 self.removed.add(fname)
626 631
627 632 def exists(self, fname):
628 633 return fname in self.ctx
629 634
630 635 def close(self):
631 636 return self.changed | self.removed
632 637
633 638 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
634 639 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
635 640 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
636 641 eolmodes = ['strict', 'crlf', 'lf', 'auto']
637 642
638 643 class patchfile(object):
639 644 def __init__(self, ui, gp, backend, store, eolmode='strict'):
640 645 self.fname = gp.path
641 646 self.eolmode = eolmode
642 647 self.eol = None
643 648 self.backend = backend
644 649 self.ui = ui
645 650 self.lines = []
646 651 self.exists = False
647 652 self.missing = True
648 653 self.mode = gp.mode
649 654 self.copysource = gp.oldpath
650 655 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
651 656 self.remove = gp.op == 'DELETE'
652 657 if self.copysource is None:
653 658 data, mode = backend.getfile(self.fname)
654 659 else:
655 660 data, mode = store.getfile(self.copysource)[:2]
656 661 if data is not None:
657 662 self.exists = self.copysource is None or backend.exists(self.fname)
658 663 self.missing = False
659 664 if data:
660 665 self.lines = mdiff.splitnewlines(data)
661 666 if self.mode is None:
662 667 self.mode = mode
663 668 if self.lines:
664 669 # Normalize line endings
665 670 if self.lines[0].endswith('\r\n'):
666 671 self.eol = '\r\n'
667 672 elif self.lines[0].endswith('\n'):
668 673 self.eol = '\n'
669 674 if eolmode != 'strict':
670 675 nlines = []
671 676 for l in self.lines:
672 677 if l.endswith('\r\n'):
673 678 l = l[:-2] + '\n'
674 679 nlines.append(l)
675 680 self.lines = nlines
676 681 else:
677 682 if self.create:
678 683 self.missing = False
679 684 if self.mode is None:
680 685 self.mode = (False, False)
681 686 if self.missing:
682 687 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
683 688 self.ui.warn(_("(use '--prefix' to apply patch relative to the "
684 689 "current directory)\n"))
685 690
686 691 self.hash = {}
687 692 self.dirty = 0
688 693 self.offset = 0
689 694 self.skew = 0
690 695 self.rej = []
691 696 self.fileprinted = False
692 697 self.printfile(False)
693 698 self.hunks = 0
694 699
695 700 def writelines(self, fname, lines, mode):
696 701 if self.eolmode == 'auto':
697 702 eol = self.eol
698 703 elif self.eolmode == 'crlf':
699 704 eol = '\r\n'
700 705 else:
701 706 eol = '\n'
702 707
703 708 if self.eolmode != 'strict' and eol and eol != '\n':
704 709 rawlines = []
705 710 for l in lines:
706 711 if l and l[-1] == '\n':
707 712 l = l[:-1] + eol
708 713 rawlines.append(l)
709 714 lines = rawlines
710 715
711 716 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
712 717
713 718 def printfile(self, warn):
714 719 if self.fileprinted:
715 720 return
716 721 if warn or self.ui.verbose:
717 722 self.fileprinted = True
718 723 s = _("patching file %s\n") % self.fname
719 724 if warn:
720 725 self.ui.warn(s)
721 726 else:
722 727 self.ui.note(s)
723 728
724 729
725 730 def findlines(self, l, linenum):
726 731 # looks through the hash and finds candidate lines. The
727 732 # result is a list of line numbers sorted based on distance
728 733 # from linenum
729 734
730 735 cand = self.hash.get(l, [])
731 736 if len(cand) > 1:
732 737 # resort our list of potentials forward then back.
733 738 cand.sort(key=lambda x: abs(x - linenum))
734 739 return cand
735 740
736 741 def write_rej(self):
737 742 # our rejects are a little different from patch(1). This always
738 743 # creates rejects in the same form as the original patch. A file
739 744 # header is inserted so that you can run the reject through patch again
740 745 # without having to type the filename.
741 746 if not self.rej:
742 747 return
743 748 base = os.path.basename(self.fname)
744 749 lines = ["--- %s\n+++ %s\n" % (base, base)]
745 750 for x in self.rej:
746 751 for l in x.hunk:
747 752 lines.append(l)
748 753 if l[-1:] != '\n':
749 754 lines.append("\n\ No newline at end of file\n")
750 755 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
751 756
752 757 def apply(self, h):
753 758 if not h.complete():
754 759 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
755 760 (h.number, h.desc, len(h.a), h.lena, len(h.b),
756 761 h.lenb))
757 762
758 763 self.hunks += 1
759 764
760 765 if self.missing:
761 766 self.rej.append(h)
762 767 return -1
763 768
764 769 if self.exists and self.create:
765 770 if self.copysource:
766 771 self.ui.warn(_("cannot create %s: destination already "
767 772 "exists\n") % self.fname)
768 773 else:
769 774 self.ui.warn(_("file %s already exists\n") % self.fname)
770 775 self.rej.append(h)
771 776 return -1
772 777
773 778 if isinstance(h, binhunk):
774 779 if self.remove:
775 780 self.backend.unlink(self.fname)
776 781 else:
777 782 l = h.new(self.lines)
778 783 self.lines[:] = l
779 784 self.offset += len(l)
780 785 self.dirty = True
781 786 return 0
782 787
783 788 horig = h
784 789 if (self.eolmode in ('crlf', 'lf')
785 790 or self.eolmode == 'auto' and self.eol):
786 791 # If new eols are going to be normalized, then normalize
787 792 # hunk data before patching. Otherwise, preserve input
788 793 # line-endings.
789 794 h = h.getnormalized()
790 795
791 796 # fast case first, no offsets, no fuzz
792 797 old, oldstart, new, newstart = h.fuzzit(0, False)
793 798 oldstart += self.offset
794 799 orig_start = oldstart
795 800 # if there's skew we want to emit the "(offset %d lines)" even
796 801 # when the hunk cleanly applies at start + skew, so skip the
797 802 # fast case code
798 803 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, oldstart):
799 804 if self.remove:
800 805 self.backend.unlink(self.fname)
801 806 else:
802 807 self.lines[oldstart:oldstart + len(old)] = new
803 808 self.offset += len(new) - len(old)
804 809 self.dirty = True
805 810 return 0
806 811
807 812 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
808 813 self.hash = {}
809 814 for x, s in enumerate(self.lines):
810 815 self.hash.setdefault(s, []).append(x)
811 816
812 817 for fuzzlen in xrange(self.ui.configint("patch", "fuzz") + 1):
813 818 for toponly in [True, False]:
814 819 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
815 820 oldstart = oldstart + self.offset + self.skew
816 821 oldstart = min(oldstart, len(self.lines))
817 822 if old:
818 823 cand = self.findlines(old[0][1:], oldstart)
819 824 else:
820 825 # Only adding lines with no or fuzzed context, just
821 826 # take the skew in account
822 827 cand = [oldstart]
823 828
824 829 for l in cand:
825 830 if not old or diffhelpers.testhunk(old, self.lines, l):
826 831 self.lines[l : l + len(old)] = new
827 832 self.offset += len(new) - len(old)
828 833 self.skew = l - orig_start
829 834 self.dirty = True
830 835 offset = l - orig_start - fuzzlen
831 836 if fuzzlen:
832 837 msg = _("Hunk #%d succeeded at %d "
833 838 "with fuzz %d "
834 839 "(offset %d lines).\n")
835 840 self.printfile(True)
836 841 self.ui.warn(msg %
837 842 (h.number, l + 1, fuzzlen, offset))
838 843 else:
839 844 msg = _("Hunk #%d succeeded at %d "
840 845 "(offset %d lines).\n")
841 846 self.ui.note(msg % (h.number, l + 1, offset))
842 847 return fuzzlen
843 848 self.printfile(True)
844 849 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
845 850 self.rej.append(horig)
846 851 return -1
847 852
848 853 def close(self):
849 854 if self.dirty:
850 855 self.writelines(self.fname, self.lines, self.mode)
851 856 self.write_rej()
852 857 return len(self.rej)
853 858
854 859 class header(object):
855 860 """patch header
856 861 """
857 862 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
858 863 diff_re = re.compile('diff -r .* (.*)$')
859 864 allhunks_re = re.compile('(?:index|deleted file) ')
860 865 pretty_re = re.compile('(?:new file|deleted file) ')
861 866 special_re = re.compile('(?:index|deleted|copy|rename) ')
862 867 newfile_re = re.compile('(?:new file)')
863 868
864 869 def __init__(self, header):
865 870 self.header = header
866 871 self.hunks = []
867 872
868 873 def binary(self):
869 874 return any(h.startswith('index ') for h in self.header)
870 875
871 876 def pretty(self, fp):
872 877 for h in self.header:
873 878 if h.startswith('index '):
874 879 fp.write(_('this modifies a binary file (all or nothing)\n'))
875 880 break
876 881 if self.pretty_re.match(h):
877 882 fp.write(h)
878 883 if self.binary():
879 884 fp.write(_('this is a binary file\n'))
880 885 break
881 886 if h.startswith('---'):
882 887 fp.write(_('%d hunks, %d lines changed\n') %
883 888 (len(self.hunks),
884 889 sum([max(h.added, h.removed) for h in self.hunks])))
885 890 break
886 891 fp.write(h)
887 892
888 893 def write(self, fp):
889 894 fp.write(''.join(self.header))
890 895
891 896 def allhunks(self):
892 897 return any(self.allhunks_re.match(h) for h in self.header)
893 898
894 899 def files(self):
895 900 match = self.diffgit_re.match(self.header[0])
896 901 if match:
897 902 fromfile, tofile = match.groups()
898 903 if fromfile == tofile:
899 904 return [fromfile]
900 905 return [fromfile, tofile]
901 906 else:
902 907 return self.diff_re.match(self.header[0]).groups()
903 908
904 909 def filename(self):
905 910 return self.files()[-1]
906 911
907 912 def __repr__(self):
908 913 return '<header %s>' % (' '.join(map(repr, self.files())))
909 914
910 915 def isnewfile(self):
911 916 return any(self.newfile_re.match(h) for h in self.header)
912 917
913 918 def special(self):
914 919 # Special files are shown only at the header level and not at the hunk
915 920 # level for example a file that has been deleted is a special file.
916 921 # The user cannot change the content of the operation, in the case of
917 922 # the deleted file he has to take the deletion or not take it, he
918 923 # cannot take some of it.
919 924 # Newly added files are special if they are empty, they are not special
920 925 # if they have some content as we want to be able to change it
921 926 nocontent = len(self.header) == 2
922 927 emptynewfile = self.isnewfile() and nocontent
923 928 return emptynewfile or \
924 929 any(self.special_re.match(h) for h in self.header)
925 930
926 931 class recordhunk(object):
927 932 """patch hunk
928 933
929 934 XXX shouldn't we merge this with the other hunk class?
930 935 """
931 936
932 937 def __init__(self, header, fromline, toline, proc, before, hunk, after,
933 938 maxcontext=None):
934 939 def trimcontext(lines, reverse=False):
935 940 if maxcontext is not None:
936 941 delta = len(lines) - maxcontext
937 942 if delta > 0:
938 943 if reverse:
939 944 return delta, lines[delta:]
940 945 else:
941 946 return delta, lines[:maxcontext]
942 947 return 0, lines
943 948
944 949 self.header = header
945 950 trimedbefore, self.before = trimcontext(before, True)
946 951 self.fromline = fromline + trimedbefore
947 952 self.toline = toline + trimedbefore
948 953 _trimedafter, self.after = trimcontext(after, False)
949 954 self.proc = proc
950 955 self.hunk = hunk
951 956 self.added, self.removed = self.countchanges(self.hunk)
952 957
953 958 def __eq__(self, v):
954 959 if not isinstance(v, recordhunk):
955 960 return False
956 961
957 962 return ((v.hunk == self.hunk) and
958 963 (v.proc == self.proc) and
959 964 (self.fromline == v.fromline) and
960 965 (self.header.files() == v.header.files()))
961 966
962 967 def __hash__(self):
963 968 return hash((tuple(self.hunk),
964 969 tuple(self.header.files()),
965 970 self.fromline,
966 971 self.proc))
967 972
968 973 def countchanges(self, hunk):
969 974 """hunk -> (n+,n-)"""
970 975 add = len([h for h in hunk if h.startswith('+')])
971 976 rem = len([h for h in hunk if h.startswith('-')])
972 977 return add, rem
973 978
974 979 def reversehunk(self):
975 980 """return another recordhunk which is the reverse of the hunk
976 981
977 982 If this hunk is diff(A, B), the returned hunk is diff(B, A). To do
978 983 that, swap fromline/toline and +/- signs while keep other things
979 984 unchanged.
980 985 """
981 986 m = {'+': '-', '-': '+', '\\': '\\'}
982 987 hunk = ['%s%s' % (m[l[0:1]], l[1:]) for l in self.hunk]
983 988 return recordhunk(self.header, self.toline, self.fromline, self.proc,
984 989 self.before, hunk, self.after)
985 990
986 991 def write(self, fp):
987 992 delta = len(self.before) + len(self.after)
988 993 if self.after and self.after[-1] == '\\ No newline at end of file\n':
989 994 delta -= 1
990 995 fromlen = delta + self.removed
991 996 tolen = delta + self.added
992 997 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
993 998 (self.fromline, fromlen, self.toline, tolen,
994 999 self.proc and (' ' + self.proc)))
995 1000 fp.write(''.join(self.before + self.hunk + self.after))
996 1001
997 1002 pretty = write
998 1003
999 1004 def filename(self):
1000 1005 return self.header.filename()
1001 1006
1002 1007 def __repr__(self):
1003 1008 return '<hunk %r@%d>' % (self.filename(), self.fromline)
1004 1009
1005 1010 def getmessages():
1006 1011 return {
1007 1012 'multiple': {
1008 1013 'apply': _("apply change %d/%d to '%s'?"),
1009 1014 'discard': _("discard change %d/%d to '%s'?"),
1010 1015 'record': _("record change %d/%d to '%s'?"),
1011 1016 },
1012 1017 'single': {
1013 1018 'apply': _("apply this change to '%s'?"),
1014 1019 'discard': _("discard this change to '%s'?"),
1015 1020 'record': _("record this change to '%s'?"),
1016 1021 },
1017 1022 'help': {
1018 1023 'apply': _('[Ynesfdaq?]'
1019 1024 '$$ &Yes, apply this change'
1020 1025 '$$ &No, skip this change'
1021 1026 '$$ &Edit this change manually'
1022 1027 '$$ &Skip remaining changes to this file'
1023 1028 '$$ Apply remaining changes to this &file'
1024 1029 '$$ &Done, skip remaining changes and files'
1025 1030 '$$ Apply &all changes to all remaining files'
1026 1031 '$$ &Quit, applying no changes'
1027 1032 '$$ &? (display help)'),
1028 1033 'discard': _('[Ynesfdaq?]'
1029 1034 '$$ &Yes, discard this change'
1030 1035 '$$ &No, skip this change'
1031 1036 '$$ &Edit this change manually'
1032 1037 '$$ &Skip remaining changes to this file'
1033 1038 '$$ Discard remaining changes to this &file'
1034 1039 '$$ &Done, skip remaining changes and files'
1035 1040 '$$ Discard &all changes to all remaining files'
1036 1041 '$$ &Quit, discarding no changes'
1037 1042 '$$ &? (display help)'),
1038 1043 'record': _('[Ynesfdaq?]'
1039 1044 '$$ &Yes, record this change'
1040 1045 '$$ &No, skip this change'
1041 1046 '$$ &Edit this change manually'
1042 1047 '$$ &Skip remaining changes to this file'
1043 1048 '$$ Record remaining changes to this &file'
1044 1049 '$$ &Done, skip remaining changes and files'
1045 1050 '$$ Record &all changes to all remaining files'
1046 1051 '$$ &Quit, recording no changes'
1047 1052 '$$ &? (display help)'),
1048 1053 }
1049 1054 }
1050 1055
1051 1056 def filterpatch(ui, headers, operation=None):
1052 1057 """Interactively filter patch chunks into applied-only chunks"""
1053 1058 messages = getmessages()
1054 1059
1055 1060 if operation is None:
1056 1061 operation = 'record'
1057 1062
1058 1063 def prompt(skipfile, skipall, query, chunk):
1059 1064 """prompt query, and process base inputs
1060 1065
1061 1066 - y/n for the rest of file
1062 1067 - y/n for the rest
1063 1068 - ? (help)
1064 1069 - q (quit)
1065 1070
1066 1071 Return True/False and possibly updated skipfile and skipall.
1067 1072 """
1068 1073 newpatches = None
1069 1074 if skipall is not None:
1070 1075 return skipall, skipfile, skipall, newpatches
1071 1076 if skipfile is not None:
1072 1077 return skipfile, skipfile, skipall, newpatches
1073 1078 while True:
1074 1079 resps = messages['help'][operation]
1075 1080 r = ui.promptchoice("%s %s" % (query, resps))
1076 1081 ui.write("\n")
1077 1082 if r == 8: # ?
1078 1083 for c, t in ui.extractchoices(resps)[1]:
1079 1084 ui.write('%s - %s\n' % (c, encoding.lower(t)))
1080 1085 continue
1081 1086 elif r == 0: # yes
1082 1087 ret = True
1083 1088 elif r == 1: # no
1084 1089 ret = False
1085 1090 elif r == 2: # Edit patch
1086 1091 if chunk is None:
1087 1092 ui.write(_('cannot edit patch for whole file'))
1088 1093 ui.write("\n")
1089 1094 continue
1090 1095 if chunk.header.binary():
1091 1096 ui.write(_('cannot edit patch for binary file'))
1092 1097 ui.write("\n")
1093 1098 continue
1094 1099 # Patch comment based on the Git one (based on comment at end of
1095 1100 # https://mercurial-scm.org/wiki/RecordExtension)
1096 1101 phelp = '---' + _("""
1097 1102 To remove '-' lines, make them ' ' lines (context).
1098 1103 To remove '+' lines, delete them.
1099 1104 Lines starting with # will be removed from the patch.
1100 1105
1101 1106 If the patch applies cleanly, the edited hunk will immediately be
1102 1107 added to the record list. If it does not apply cleanly, a rejects
1103 1108 file will be generated: you can use that when you try again. If
1104 1109 all lines of the hunk are removed, then the edit is aborted and
1105 1110 the hunk is left unchanged.
1106 1111 """)
1107 1112 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
1108 1113 suffix=".diff")
1109 1114 ncpatchfp = None
1110 1115 try:
1111 1116 # Write the initial patch
1112 1117 f = util.nativeeolwriter(os.fdopen(patchfd, r'wb'))
1113 1118 chunk.header.write(f)
1114 1119 chunk.write(f)
1115 1120 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1116 1121 f.close()
1117 1122 # Start the editor and wait for it to complete
1118 1123 editor = ui.geteditor()
1119 1124 ret = ui.system("%s \"%s\"" % (editor, patchfn),
1120 1125 environ={'HGUSER': ui.username()},
1121 1126 blockedtag='filterpatch')
1122 1127 if ret != 0:
1123 1128 ui.warn(_("editor exited with exit code %d\n") % ret)
1124 1129 continue
1125 1130 # Remove comment lines
1126 1131 patchfp = open(patchfn, r'rb')
1127 1132 ncpatchfp = stringio()
1128 1133 for line in util.iterfile(patchfp):
1129 1134 line = util.fromnativeeol(line)
1130 1135 if not line.startswith('#'):
1131 1136 ncpatchfp.write(line)
1132 1137 patchfp.close()
1133 1138 ncpatchfp.seek(0)
1134 1139 newpatches = parsepatch(ncpatchfp)
1135 1140 finally:
1136 1141 os.unlink(patchfn)
1137 1142 del ncpatchfp
1138 1143 # Signal that the chunk shouldn't be applied as-is, but
1139 1144 # provide the new patch to be used instead.
1140 1145 ret = False
1141 1146 elif r == 3: # Skip
1142 1147 ret = skipfile = False
1143 1148 elif r == 4: # file (Record remaining)
1144 1149 ret = skipfile = True
1145 1150 elif r == 5: # done, skip remaining
1146 1151 ret = skipall = False
1147 1152 elif r == 6: # all
1148 1153 ret = skipall = True
1149 1154 elif r == 7: # quit
1150 1155 raise error.Abort(_('user quit'))
1151 1156 return ret, skipfile, skipall, newpatches
1152 1157
1153 1158 seen = set()
1154 1159 applied = {} # 'filename' -> [] of chunks
1155 1160 skipfile, skipall = None, None
1156 1161 pos, total = 1, sum(len(h.hunks) for h in headers)
1157 1162 for h in headers:
1158 1163 pos += len(h.hunks)
1159 1164 skipfile = None
1160 1165 fixoffset = 0
1161 1166 hdr = ''.join(h.header)
1162 1167 if hdr in seen:
1163 1168 continue
1164 1169 seen.add(hdr)
1165 1170 if skipall is None:
1166 1171 h.pretty(ui)
1167 1172 msg = (_('examine changes to %s?') %
1168 1173 _(' and ').join("'%s'" % f for f in h.files()))
1169 1174 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1170 1175 if not r:
1171 1176 continue
1172 1177 applied[h.filename()] = [h]
1173 1178 if h.allhunks():
1174 1179 applied[h.filename()] += h.hunks
1175 1180 continue
1176 1181 for i, chunk in enumerate(h.hunks):
1177 1182 if skipfile is None and skipall is None:
1178 1183 chunk.pretty(ui)
1179 1184 if total == 1:
1180 1185 msg = messages['single'][operation] % chunk.filename()
1181 1186 else:
1182 1187 idx = pos - len(h.hunks) + i
1183 1188 msg = messages['multiple'][operation] % (idx, total,
1184 1189 chunk.filename())
1185 1190 r, skipfile, skipall, newpatches = prompt(skipfile,
1186 1191 skipall, msg, chunk)
1187 1192 if r:
1188 1193 if fixoffset:
1189 1194 chunk = copy.copy(chunk)
1190 1195 chunk.toline += fixoffset
1191 1196 applied[chunk.filename()].append(chunk)
1192 1197 elif newpatches is not None:
1193 1198 for newpatch in newpatches:
1194 1199 for newhunk in newpatch.hunks:
1195 1200 if fixoffset:
1196 1201 newhunk.toline += fixoffset
1197 1202 applied[newhunk.filename()].append(newhunk)
1198 1203 else:
1199 1204 fixoffset += chunk.removed - chunk.added
1200 1205 return (sum([h for h in applied.itervalues()
1201 1206 if h[0].special() or len(h) > 1], []), {})
1202 1207 class hunk(object):
1203 1208 def __init__(self, desc, num, lr, context):
1204 1209 self.number = num
1205 1210 self.desc = desc
1206 1211 self.hunk = [desc]
1207 1212 self.a = []
1208 1213 self.b = []
1209 1214 self.starta = self.lena = None
1210 1215 self.startb = self.lenb = None
1211 1216 if lr is not None:
1212 1217 if context:
1213 1218 self.read_context_hunk(lr)
1214 1219 else:
1215 1220 self.read_unified_hunk(lr)
1216 1221
1217 1222 def getnormalized(self):
1218 1223 """Return a copy with line endings normalized to LF."""
1219 1224
1220 1225 def normalize(lines):
1221 1226 nlines = []
1222 1227 for line in lines:
1223 1228 if line.endswith('\r\n'):
1224 1229 line = line[:-2] + '\n'
1225 1230 nlines.append(line)
1226 1231 return nlines
1227 1232
1228 1233 # Dummy object, it is rebuilt manually
1229 1234 nh = hunk(self.desc, self.number, None, None)
1230 1235 nh.number = self.number
1231 1236 nh.desc = self.desc
1232 1237 nh.hunk = self.hunk
1233 1238 nh.a = normalize(self.a)
1234 1239 nh.b = normalize(self.b)
1235 1240 nh.starta = self.starta
1236 1241 nh.startb = self.startb
1237 1242 nh.lena = self.lena
1238 1243 nh.lenb = self.lenb
1239 1244 return nh
1240 1245
1241 1246 def read_unified_hunk(self, lr):
1242 1247 m = unidesc.match(self.desc)
1243 1248 if not m:
1244 1249 raise PatchError(_("bad hunk #%d") % self.number)
1245 1250 self.starta, self.lena, self.startb, self.lenb = m.groups()
1246 1251 if self.lena is None:
1247 1252 self.lena = 1
1248 1253 else:
1249 1254 self.lena = int(self.lena)
1250 1255 if self.lenb is None:
1251 1256 self.lenb = 1
1252 1257 else:
1253 1258 self.lenb = int(self.lenb)
1254 1259 self.starta = int(self.starta)
1255 1260 self.startb = int(self.startb)
1256 1261 try:
1257 1262 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb,
1258 1263 self.a, self.b)
1259 1264 except error.ParseError as e:
1260 1265 raise PatchError(_("bad hunk #%d: %s") % (self.number, e))
1261 1266 # if we hit eof before finishing out the hunk, the last line will
1262 1267 # be zero length. Lets try to fix it up.
1263 1268 while len(self.hunk[-1]) == 0:
1264 1269 del self.hunk[-1]
1265 1270 del self.a[-1]
1266 1271 del self.b[-1]
1267 1272 self.lena -= 1
1268 1273 self.lenb -= 1
1269 1274 self._fixnewline(lr)
1270 1275
1271 1276 def read_context_hunk(self, lr):
1272 1277 self.desc = lr.readline()
1273 1278 m = contextdesc.match(self.desc)
1274 1279 if not m:
1275 1280 raise PatchError(_("bad hunk #%d") % self.number)
1276 1281 self.starta, aend = m.groups()
1277 1282 self.starta = int(self.starta)
1278 1283 if aend is None:
1279 1284 aend = self.starta
1280 1285 self.lena = int(aend) - self.starta
1281 1286 if self.starta:
1282 1287 self.lena += 1
1283 1288 for x in xrange(self.lena):
1284 1289 l = lr.readline()
1285 1290 if l.startswith('---'):
1286 1291 # lines addition, old block is empty
1287 1292 lr.push(l)
1288 1293 break
1289 1294 s = l[2:]
1290 1295 if l.startswith('- ') or l.startswith('! '):
1291 1296 u = '-' + s
1292 1297 elif l.startswith(' '):
1293 1298 u = ' ' + s
1294 1299 else:
1295 1300 raise PatchError(_("bad hunk #%d old text line %d") %
1296 1301 (self.number, x))
1297 1302 self.a.append(u)
1298 1303 self.hunk.append(u)
1299 1304
1300 1305 l = lr.readline()
1301 1306 if l.startswith('\ '):
1302 1307 s = self.a[-1][:-1]
1303 1308 self.a[-1] = s
1304 1309 self.hunk[-1] = s
1305 1310 l = lr.readline()
1306 1311 m = contextdesc.match(l)
1307 1312 if not m:
1308 1313 raise PatchError(_("bad hunk #%d") % self.number)
1309 1314 self.startb, bend = m.groups()
1310 1315 self.startb = int(self.startb)
1311 1316 if bend is None:
1312 1317 bend = self.startb
1313 1318 self.lenb = int(bend) - self.startb
1314 1319 if self.startb:
1315 1320 self.lenb += 1
1316 1321 hunki = 1
1317 1322 for x in xrange(self.lenb):
1318 1323 l = lr.readline()
1319 1324 if l.startswith('\ '):
1320 1325 # XXX: the only way to hit this is with an invalid line range.
1321 1326 # The no-eol marker is not counted in the line range, but I
1322 1327 # guess there are diff(1) out there which behave differently.
1323 1328 s = self.b[-1][:-1]
1324 1329 self.b[-1] = s
1325 1330 self.hunk[hunki - 1] = s
1326 1331 continue
1327 1332 if not l:
1328 1333 # line deletions, new block is empty and we hit EOF
1329 1334 lr.push(l)
1330 1335 break
1331 1336 s = l[2:]
1332 1337 if l.startswith('+ ') or l.startswith('! '):
1333 1338 u = '+' + s
1334 1339 elif l.startswith(' '):
1335 1340 u = ' ' + s
1336 1341 elif len(self.b) == 0:
1337 1342 # line deletions, new block is empty
1338 1343 lr.push(l)
1339 1344 break
1340 1345 else:
1341 1346 raise PatchError(_("bad hunk #%d old text line %d") %
1342 1347 (self.number, x))
1343 1348 self.b.append(s)
1344 1349 while True:
1345 1350 if hunki >= len(self.hunk):
1346 1351 h = ""
1347 1352 else:
1348 1353 h = self.hunk[hunki]
1349 1354 hunki += 1
1350 1355 if h == u:
1351 1356 break
1352 1357 elif h.startswith('-'):
1353 1358 continue
1354 1359 else:
1355 1360 self.hunk.insert(hunki - 1, u)
1356 1361 break
1357 1362
1358 1363 if not self.a:
1359 1364 # this happens when lines were only added to the hunk
1360 1365 for x in self.hunk:
1361 1366 if x.startswith('-') or x.startswith(' '):
1362 1367 self.a.append(x)
1363 1368 if not self.b:
1364 1369 # this happens when lines were only deleted from the hunk
1365 1370 for x in self.hunk:
1366 1371 if x.startswith('+') or x.startswith(' '):
1367 1372 self.b.append(x[1:])
1368 1373 # @@ -start,len +start,len @@
1369 1374 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
1370 1375 self.startb, self.lenb)
1371 1376 self.hunk[0] = self.desc
1372 1377 self._fixnewline(lr)
1373 1378
1374 1379 def _fixnewline(self, lr):
1375 1380 l = lr.readline()
1376 1381 if l.startswith('\ '):
1377 1382 diffhelpers.fixnewline(self.hunk, self.a, self.b)
1378 1383 else:
1379 1384 lr.push(l)
1380 1385
1381 1386 def complete(self):
1382 1387 return len(self.a) == self.lena and len(self.b) == self.lenb
1383 1388
1384 1389 def _fuzzit(self, old, new, fuzz, toponly):
1385 1390 # this removes context lines from the top and bottom of list 'l'. It
1386 1391 # checks the hunk to make sure only context lines are removed, and then
1387 1392 # returns a new shortened list of lines.
1388 1393 fuzz = min(fuzz, len(old))
1389 1394 if fuzz:
1390 1395 top = 0
1391 1396 bot = 0
1392 1397 hlen = len(self.hunk)
1393 1398 for x in xrange(hlen - 1):
1394 1399 # the hunk starts with the @@ line, so use x+1
1395 1400 if self.hunk[x + 1].startswith(' '):
1396 1401 top += 1
1397 1402 else:
1398 1403 break
1399 1404 if not toponly:
1400 1405 for x in xrange(hlen - 1):
1401 1406 if self.hunk[hlen - bot - 1].startswith(' '):
1402 1407 bot += 1
1403 1408 else:
1404 1409 break
1405 1410
1406 1411 bot = min(fuzz, bot)
1407 1412 top = min(fuzz, top)
1408 1413 return old[top:len(old) - bot], new[top:len(new) - bot], top
1409 1414 return old, new, 0
1410 1415
1411 1416 def fuzzit(self, fuzz, toponly):
1412 1417 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1413 1418 oldstart = self.starta + top
1414 1419 newstart = self.startb + top
1415 1420 # zero length hunk ranges already have their start decremented
1416 1421 if self.lena and oldstart > 0:
1417 1422 oldstart -= 1
1418 1423 if self.lenb and newstart > 0:
1419 1424 newstart -= 1
1420 1425 return old, oldstart, new, newstart
1421 1426
1422 1427 class binhunk(object):
1423 1428 'A binary patch file.'
1424 1429 def __init__(self, lr, fname):
1425 1430 self.text = None
1426 1431 self.delta = False
1427 1432 self.hunk = ['GIT binary patch\n']
1428 1433 self._fname = fname
1429 1434 self._read(lr)
1430 1435
1431 1436 def complete(self):
1432 1437 return self.text is not None
1433 1438
1434 1439 def new(self, lines):
1435 1440 if self.delta:
1436 1441 return [applybindelta(self.text, ''.join(lines))]
1437 1442 return [self.text]
1438 1443
1439 1444 def _read(self, lr):
1440 1445 def getline(lr, hunk):
1441 1446 l = lr.readline()
1442 1447 hunk.append(l)
1443 1448 return l.rstrip('\r\n')
1444 1449
1445 1450 size = 0
1446 1451 while True:
1447 1452 line = getline(lr, self.hunk)
1448 1453 if not line:
1449 1454 raise PatchError(_('could not extract "%s" binary data')
1450 1455 % self._fname)
1451 1456 if line.startswith('literal '):
1452 1457 size = int(line[8:].rstrip())
1453 1458 break
1454 1459 if line.startswith('delta '):
1455 1460 size = int(line[6:].rstrip())
1456 1461 self.delta = True
1457 1462 break
1458 1463 dec = []
1459 1464 line = getline(lr, self.hunk)
1460 1465 while len(line) > 1:
1461 1466 l = line[0:1]
1462 1467 if l <= 'Z' and l >= 'A':
1463 1468 l = ord(l) - ord('A') + 1
1464 1469 else:
1465 1470 l = ord(l) - ord('a') + 27
1466 1471 try:
1467 1472 dec.append(util.b85decode(line[1:])[:l])
1468 1473 except ValueError as e:
1469 1474 raise PatchError(_('could not decode "%s" binary patch: %s')
1470 1475 % (self._fname, stringutil.forcebytestr(e)))
1471 1476 line = getline(lr, self.hunk)
1472 1477 text = zlib.decompress(''.join(dec))
1473 1478 if len(text) != size:
1474 1479 raise PatchError(_('"%s" length is %d bytes, should be %d')
1475 1480 % (self._fname, len(text), size))
1476 1481 self.text = text
1477 1482
1478 1483 def parsefilename(str):
1479 1484 # --- filename \t|space stuff
1480 1485 s = str[4:].rstrip('\r\n')
1481 1486 i = s.find('\t')
1482 1487 if i < 0:
1483 1488 i = s.find(' ')
1484 1489 if i < 0:
1485 1490 return s
1486 1491 return s[:i]
1487 1492
1488 1493 def reversehunks(hunks):
1489 1494 '''reverse the signs in the hunks given as argument
1490 1495
1491 1496 This function operates on hunks coming out of patch.filterpatch, that is
1492 1497 a list of the form: [header1, hunk1, hunk2, header2...]. Example usage:
1493 1498
1494 1499 >>> rawpatch = b"""diff --git a/folder1/g b/folder1/g
1495 1500 ... --- a/folder1/g
1496 1501 ... +++ b/folder1/g
1497 1502 ... @@ -1,7 +1,7 @@
1498 1503 ... +firstline
1499 1504 ... c
1500 1505 ... 1
1501 1506 ... 2
1502 1507 ... + 3
1503 1508 ... -4
1504 1509 ... 5
1505 1510 ... d
1506 1511 ... +lastline"""
1507 1512 >>> hunks = parsepatch([rawpatch])
1508 1513 >>> hunkscomingfromfilterpatch = []
1509 1514 >>> for h in hunks:
1510 1515 ... hunkscomingfromfilterpatch.append(h)
1511 1516 ... hunkscomingfromfilterpatch.extend(h.hunks)
1512 1517
1513 1518 >>> reversedhunks = reversehunks(hunkscomingfromfilterpatch)
1514 1519 >>> from . import util
1515 1520 >>> fp = util.stringio()
1516 1521 >>> for c in reversedhunks:
1517 1522 ... c.write(fp)
1518 1523 >>> fp.seek(0) or None
1519 1524 >>> reversedpatch = fp.read()
1520 1525 >>> print(pycompat.sysstr(reversedpatch))
1521 1526 diff --git a/folder1/g b/folder1/g
1522 1527 --- a/folder1/g
1523 1528 +++ b/folder1/g
1524 1529 @@ -1,4 +1,3 @@
1525 1530 -firstline
1526 1531 c
1527 1532 1
1528 1533 2
1529 1534 @@ -2,6 +1,6 @@
1530 1535 c
1531 1536 1
1532 1537 2
1533 1538 - 3
1534 1539 +4
1535 1540 5
1536 1541 d
1537 1542 @@ -6,3 +5,2 @@
1538 1543 5
1539 1544 d
1540 1545 -lastline
1541 1546
1542 1547 '''
1543 1548
1544 1549 newhunks = []
1545 1550 for c in hunks:
1546 1551 if util.safehasattr(c, 'reversehunk'):
1547 1552 c = c.reversehunk()
1548 1553 newhunks.append(c)
1549 1554 return newhunks
1550 1555
1551 1556 def parsepatch(originalchunks, maxcontext=None):
1552 1557 """patch -> [] of headers -> [] of hunks
1553 1558
1554 1559 If maxcontext is not None, trim context lines if necessary.
1555 1560
1556 1561 >>> rawpatch = b'''diff --git a/folder1/g b/folder1/g
1557 1562 ... --- a/folder1/g
1558 1563 ... +++ b/folder1/g
1559 1564 ... @@ -1,8 +1,10 @@
1560 1565 ... 1
1561 1566 ... 2
1562 1567 ... -3
1563 1568 ... 4
1564 1569 ... 5
1565 1570 ... 6
1566 1571 ... +6.1
1567 1572 ... +6.2
1568 1573 ... 7
1569 1574 ... 8
1570 1575 ... +9'''
1571 1576 >>> out = util.stringio()
1572 1577 >>> headers = parsepatch([rawpatch], maxcontext=1)
1573 1578 >>> for header in headers:
1574 1579 ... header.write(out)
1575 1580 ... for hunk in header.hunks:
1576 1581 ... hunk.write(out)
1577 1582 >>> print(pycompat.sysstr(out.getvalue()))
1578 1583 diff --git a/folder1/g b/folder1/g
1579 1584 --- a/folder1/g
1580 1585 +++ b/folder1/g
1581 1586 @@ -2,3 +2,2 @@
1582 1587 2
1583 1588 -3
1584 1589 4
1585 1590 @@ -6,2 +5,4 @@
1586 1591 6
1587 1592 +6.1
1588 1593 +6.2
1589 1594 7
1590 1595 @@ -8,1 +9,2 @@
1591 1596 8
1592 1597 +9
1593 1598 """
1594 1599 class parser(object):
1595 1600 """patch parsing state machine"""
1596 1601 def __init__(self):
1597 1602 self.fromline = 0
1598 1603 self.toline = 0
1599 1604 self.proc = ''
1600 1605 self.header = None
1601 1606 self.context = []
1602 1607 self.before = []
1603 1608 self.hunk = []
1604 1609 self.headers = []
1605 1610
1606 1611 def addrange(self, limits):
1607 1612 fromstart, fromend, tostart, toend, proc = limits
1608 1613 self.fromline = int(fromstart)
1609 1614 self.toline = int(tostart)
1610 1615 self.proc = proc
1611 1616
1612 1617 def addcontext(self, context):
1613 1618 if self.hunk:
1614 1619 h = recordhunk(self.header, self.fromline, self.toline,
1615 1620 self.proc, self.before, self.hunk, context, maxcontext)
1616 1621 self.header.hunks.append(h)
1617 1622 self.fromline += len(self.before) + h.removed
1618 1623 self.toline += len(self.before) + h.added
1619 1624 self.before = []
1620 1625 self.hunk = []
1621 1626 self.context = context
1622 1627
1623 1628 def addhunk(self, hunk):
1624 1629 if self.context:
1625 1630 self.before = self.context
1626 1631 self.context = []
1627 1632 self.hunk = hunk
1628 1633
1629 1634 def newfile(self, hdr):
1630 1635 self.addcontext([])
1631 1636 h = header(hdr)
1632 1637 self.headers.append(h)
1633 1638 self.header = h
1634 1639
1635 1640 def addother(self, line):
1636 1641 pass # 'other' lines are ignored
1637 1642
1638 1643 def finished(self):
1639 1644 self.addcontext([])
1640 1645 return self.headers
1641 1646
1642 1647 transitions = {
1643 1648 'file': {'context': addcontext,
1644 1649 'file': newfile,
1645 1650 'hunk': addhunk,
1646 1651 'range': addrange},
1647 1652 'context': {'file': newfile,
1648 1653 'hunk': addhunk,
1649 1654 'range': addrange,
1650 1655 'other': addother},
1651 1656 'hunk': {'context': addcontext,
1652 1657 'file': newfile,
1653 1658 'range': addrange},
1654 1659 'range': {'context': addcontext,
1655 1660 'hunk': addhunk},
1656 1661 'other': {'other': addother},
1657 1662 }
1658 1663
1659 1664 p = parser()
1660 1665 fp = stringio()
1661 1666 fp.write(''.join(originalchunks))
1662 1667 fp.seek(0)
1663 1668
1664 1669 state = 'context'
1665 1670 for newstate, data in scanpatch(fp):
1666 1671 try:
1667 1672 p.transitions[state][newstate](p, data)
1668 1673 except KeyError:
1669 1674 raise PatchError('unhandled transition: %s -> %s' %
1670 1675 (state, newstate))
1671 1676 state = newstate
1672 1677 del fp
1673 1678 return p.finished()
1674 1679
1675 1680 def pathtransform(path, strip, prefix):
1676 1681 '''turn a path from a patch into a path suitable for the repository
1677 1682
1678 1683 prefix, if not empty, is expected to be normalized with a / at the end.
1679 1684
1680 1685 Returns (stripped components, path in repository).
1681 1686
1682 1687 >>> pathtransform(b'a/b/c', 0, b'')
1683 1688 ('', 'a/b/c')
1684 1689 >>> pathtransform(b' a/b/c ', 0, b'')
1685 1690 ('', ' a/b/c')
1686 1691 >>> pathtransform(b' a/b/c ', 2, b'')
1687 1692 ('a/b/', 'c')
1688 1693 >>> pathtransform(b'a/b/c', 0, b'd/e/')
1689 1694 ('', 'd/e/a/b/c')
1690 1695 >>> pathtransform(b' a//b/c ', 2, b'd/e/')
1691 1696 ('a//b/', 'd/e/c')
1692 1697 >>> pathtransform(b'a/b/c', 3, b'')
1693 1698 Traceback (most recent call last):
1694 1699 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1695 1700 '''
1696 1701 pathlen = len(path)
1697 1702 i = 0
1698 1703 if strip == 0:
1699 1704 return '', prefix + path.rstrip()
1700 1705 count = strip
1701 1706 while count > 0:
1702 1707 i = path.find('/', i)
1703 1708 if i == -1:
1704 1709 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1705 1710 (count, strip, path))
1706 1711 i += 1
1707 1712 # consume '//' in the path
1708 1713 while i < pathlen - 1 and path[i:i + 1] == '/':
1709 1714 i += 1
1710 1715 count -= 1
1711 1716 return path[:i].lstrip(), prefix + path[i:].rstrip()
1712 1717
1713 1718 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1714 1719 nulla = afile_orig == "/dev/null"
1715 1720 nullb = bfile_orig == "/dev/null"
1716 1721 create = nulla and hunk.starta == 0 and hunk.lena == 0
1717 1722 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1718 1723 abase, afile = pathtransform(afile_orig, strip, prefix)
1719 1724 gooda = not nulla and backend.exists(afile)
1720 1725 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1721 1726 if afile == bfile:
1722 1727 goodb = gooda
1723 1728 else:
1724 1729 goodb = not nullb and backend.exists(bfile)
1725 1730 missing = not goodb and not gooda and not create
1726 1731
1727 1732 # some diff programs apparently produce patches where the afile is
1728 1733 # not /dev/null, but afile starts with bfile
1729 1734 abasedir = afile[:afile.rfind('/') + 1]
1730 1735 bbasedir = bfile[:bfile.rfind('/') + 1]
1731 1736 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1732 1737 and hunk.starta == 0 and hunk.lena == 0):
1733 1738 create = True
1734 1739 missing = False
1735 1740
1736 1741 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1737 1742 # diff is between a file and its backup. In this case, the original
1738 1743 # file should be patched (see original mpatch code).
1739 1744 isbackup = (abase == bbase and bfile.startswith(afile))
1740 1745 fname = None
1741 1746 if not missing:
1742 1747 if gooda and goodb:
1743 1748 if isbackup:
1744 1749 fname = afile
1745 1750 else:
1746 1751 fname = bfile
1747 1752 elif gooda:
1748 1753 fname = afile
1749 1754
1750 1755 if not fname:
1751 1756 if not nullb:
1752 1757 if isbackup:
1753 1758 fname = afile
1754 1759 else:
1755 1760 fname = bfile
1756 1761 elif not nulla:
1757 1762 fname = afile
1758 1763 else:
1759 1764 raise PatchError(_("undefined source and destination files"))
1760 1765
1761 1766 gp = patchmeta(fname)
1762 1767 if create:
1763 1768 gp.op = 'ADD'
1764 1769 elif remove:
1765 1770 gp.op = 'DELETE'
1766 1771 return gp
1767 1772
1768 1773 def scanpatch(fp):
1769 1774 """like patch.iterhunks, but yield different events
1770 1775
1771 1776 - ('file', [header_lines + fromfile + tofile])
1772 1777 - ('context', [context_lines])
1773 1778 - ('hunk', [hunk_lines])
1774 1779 - ('range', (-start,len, +start,len, proc))
1775 1780 """
1776 1781 lines_re = re.compile(br'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1777 1782 lr = linereader(fp)
1778 1783
1779 1784 def scanwhile(first, p):
1780 1785 """scan lr while predicate holds"""
1781 1786 lines = [first]
1782 1787 for line in iter(lr.readline, ''):
1783 1788 if p(line):
1784 1789 lines.append(line)
1785 1790 else:
1786 1791 lr.push(line)
1787 1792 break
1788 1793 return lines
1789 1794
1790 1795 for line in iter(lr.readline, ''):
1791 1796 if line.startswith('diff --git a/') or line.startswith('diff -r '):
1792 1797 def notheader(line):
1793 1798 s = line.split(None, 1)
1794 1799 return not s or s[0] not in ('---', 'diff')
1795 1800 header = scanwhile(line, notheader)
1796 1801 fromfile = lr.readline()
1797 1802 if fromfile.startswith('---'):
1798 1803 tofile = lr.readline()
1799 1804 header += [fromfile, tofile]
1800 1805 else:
1801 1806 lr.push(fromfile)
1802 1807 yield 'file', header
1803 1808 elif line.startswith(' '):
1804 1809 cs = (' ', '\\')
1805 1810 yield 'context', scanwhile(line, lambda l: l.startswith(cs))
1806 1811 elif line.startswith(('-', '+')):
1807 1812 cs = ('-', '+', '\\')
1808 1813 yield 'hunk', scanwhile(line, lambda l: l.startswith(cs))
1809 1814 else:
1810 1815 m = lines_re.match(line)
1811 1816 if m:
1812 1817 yield 'range', m.groups()
1813 1818 else:
1814 1819 yield 'other', line
1815 1820
1816 1821 def scangitpatch(lr, firstline):
1817 1822 """
1818 1823 Git patches can emit:
1819 1824 - rename a to b
1820 1825 - change b
1821 1826 - copy a to c
1822 1827 - change c
1823 1828
1824 1829 We cannot apply this sequence as-is, the renamed 'a' could not be
1825 1830 found for it would have been renamed already. And we cannot copy
1826 1831 from 'b' instead because 'b' would have been changed already. So
1827 1832 we scan the git patch for copy and rename commands so we can
1828 1833 perform the copies ahead of time.
1829 1834 """
1830 1835 pos = 0
1831 1836 try:
1832 1837 pos = lr.fp.tell()
1833 1838 fp = lr.fp
1834 1839 except IOError:
1835 1840 fp = stringio(lr.fp.read())
1836 1841 gitlr = linereader(fp)
1837 1842 gitlr.push(firstline)
1838 1843 gitpatches = readgitpatch(gitlr)
1839 1844 fp.seek(pos)
1840 1845 return gitpatches
1841 1846
1842 1847 def iterhunks(fp):
1843 1848 """Read a patch and yield the following events:
1844 1849 - ("file", afile, bfile, firsthunk): select a new target file.
1845 1850 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1846 1851 "file" event.
1847 1852 - ("git", gitchanges): current diff is in git format, gitchanges
1848 1853 maps filenames to gitpatch records. Unique event.
1849 1854 """
1850 1855 afile = ""
1851 1856 bfile = ""
1852 1857 state = None
1853 1858 hunknum = 0
1854 1859 emitfile = newfile = False
1855 1860 gitpatches = None
1856 1861
1857 1862 # our states
1858 1863 BFILE = 1
1859 1864 context = None
1860 1865 lr = linereader(fp)
1861 1866
1862 1867 for x in iter(lr.readline, ''):
1863 1868 if state == BFILE and (
1864 1869 (not context and x.startswith('@'))
1865 1870 or (context is not False and x.startswith('***************'))
1866 1871 or x.startswith('GIT binary patch')):
1867 1872 gp = None
1868 1873 if (gitpatches and
1869 1874 gitpatches[-1].ispatching(afile, bfile)):
1870 1875 gp = gitpatches.pop()
1871 1876 if x.startswith('GIT binary patch'):
1872 1877 h = binhunk(lr, gp.path)
1873 1878 else:
1874 1879 if context is None and x.startswith('***************'):
1875 1880 context = True
1876 1881 h = hunk(x, hunknum + 1, lr, context)
1877 1882 hunknum += 1
1878 1883 if emitfile:
1879 1884 emitfile = False
1880 1885 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1881 1886 yield 'hunk', h
1882 1887 elif x.startswith('diff --git a/'):
1883 1888 m = gitre.match(x.rstrip(' \r\n'))
1884 1889 if not m:
1885 1890 continue
1886 1891 if gitpatches is None:
1887 1892 # scan whole input for git metadata
1888 1893 gitpatches = scangitpatch(lr, x)
1889 1894 yield 'git', [g.copy() for g in gitpatches
1890 1895 if g.op in ('COPY', 'RENAME')]
1891 1896 gitpatches.reverse()
1892 1897 afile = 'a/' + m.group(1)
1893 1898 bfile = 'b/' + m.group(2)
1894 1899 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1895 1900 gp = gitpatches.pop()
1896 1901 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1897 1902 if not gitpatches:
1898 1903 raise PatchError(_('failed to synchronize metadata for "%s"')
1899 1904 % afile[2:])
1900 1905 gp = gitpatches[-1]
1901 1906 newfile = True
1902 1907 elif x.startswith('---'):
1903 1908 # check for a unified diff
1904 1909 l2 = lr.readline()
1905 1910 if not l2.startswith('+++'):
1906 1911 lr.push(l2)
1907 1912 continue
1908 1913 newfile = True
1909 1914 context = False
1910 1915 afile = parsefilename(x)
1911 1916 bfile = parsefilename(l2)
1912 1917 elif x.startswith('***'):
1913 1918 # check for a context diff
1914 1919 l2 = lr.readline()
1915 1920 if not l2.startswith('---'):
1916 1921 lr.push(l2)
1917 1922 continue
1918 1923 l3 = lr.readline()
1919 1924 lr.push(l3)
1920 1925 if not l3.startswith("***************"):
1921 1926 lr.push(l2)
1922 1927 continue
1923 1928 newfile = True
1924 1929 context = True
1925 1930 afile = parsefilename(x)
1926 1931 bfile = parsefilename(l2)
1927 1932
1928 1933 if newfile:
1929 1934 newfile = False
1930 1935 emitfile = True
1931 1936 state = BFILE
1932 1937 hunknum = 0
1933 1938
1934 1939 while gitpatches:
1935 1940 gp = gitpatches.pop()
1936 1941 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1937 1942
1938 1943 def applybindelta(binchunk, data):
1939 1944 """Apply a binary delta hunk
1940 1945 The algorithm used is the algorithm from git's patch-delta.c
1941 1946 """
1942 1947 def deltahead(binchunk):
1943 1948 i = 0
1944 1949 for c in binchunk:
1945 1950 i += 1
1946 1951 if not (ord(c) & 0x80):
1947 1952 return i
1948 1953 return i
1949 1954 out = ""
1950 1955 s = deltahead(binchunk)
1951 1956 binchunk = binchunk[s:]
1952 1957 s = deltahead(binchunk)
1953 1958 binchunk = binchunk[s:]
1954 1959 i = 0
1955 1960 while i < len(binchunk):
1956 1961 cmd = ord(binchunk[i])
1957 1962 i += 1
1958 1963 if (cmd & 0x80):
1959 1964 offset = 0
1960 1965 size = 0
1961 1966 if (cmd & 0x01):
1962 1967 offset = ord(binchunk[i])
1963 1968 i += 1
1964 1969 if (cmd & 0x02):
1965 1970 offset |= ord(binchunk[i]) << 8
1966 1971 i += 1
1967 1972 if (cmd & 0x04):
1968 1973 offset |= ord(binchunk[i]) << 16
1969 1974 i += 1
1970 1975 if (cmd & 0x08):
1971 1976 offset |= ord(binchunk[i]) << 24
1972 1977 i += 1
1973 1978 if (cmd & 0x10):
1974 1979 size = ord(binchunk[i])
1975 1980 i += 1
1976 1981 if (cmd & 0x20):
1977 1982 size |= ord(binchunk[i]) << 8
1978 1983 i += 1
1979 1984 if (cmd & 0x40):
1980 1985 size |= ord(binchunk[i]) << 16
1981 1986 i += 1
1982 1987 if size == 0:
1983 1988 size = 0x10000
1984 1989 offset_end = offset + size
1985 1990 out += data[offset:offset_end]
1986 1991 elif cmd != 0:
1987 1992 offset_end = i + cmd
1988 1993 out += binchunk[i:offset_end]
1989 1994 i += cmd
1990 1995 else:
1991 1996 raise PatchError(_('unexpected delta opcode 0'))
1992 1997 return out
1993 1998
1994 1999 def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'):
1995 2000 """Reads a patch from fp and tries to apply it.
1996 2001
1997 2002 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1998 2003 there was any fuzz.
1999 2004
2000 2005 If 'eolmode' is 'strict', the patch content and patched file are
2001 2006 read in binary mode. Otherwise, line endings are ignored when
2002 2007 patching then normalized according to 'eolmode'.
2003 2008 """
2004 2009 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
2005 2010 prefix=prefix, eolmode=eolmode)
2006 2011
2007 2012 def _canonprefix(repo, prefix):
2008 2013 if prefix:
2009 2014 prefix = pathutil.canonpath(repo.root, repo.getcwd(), prefix)
2010 2015 if prefix != '':
2011 2016 prefix += '/'
2012 2017 return prefix
2013 2018
2014 2019 def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='',
2015 2020 eolmode='strict'):
2016 2021 prefix = _canonprefix(backend.repo, prefix)
2017 2022 def pstrip(p):
2018 2023 return pathtransform(p, strip - 1, prefix)[1]
2019 2024
2020 2025 rejects = 0
2021 2026 err = 0
2022 2027 current_file = None
2023 2028
2024 2029 for state, values in iterhunks(fp):
2025 2030 if state == 'hunk':
2026 2031 if not current_file:
2027 2032 continue
2028 2033 ret = current_file.apply(values)
2029 2034 if ret > 0:
2030 2035 err = 1
2031 2036 elif state == 'file':
2032 2037 if current_file:
2033 2038 rejects += current_file.close()
2034 2039 current_file = None
2035 2040 afile, bfile, first_hunk, gp = values
2036 2041 if gp:
2037 2042 gp.path = pstrip(gp.path)
2038 2043 if gp.oldpath:
2039 2044 gp.oldpath = pstrip(gp.oldpath)
2040 2045 else:
2041 2046 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2042 2047 prefix)
2043 2048 if gp.op == 'RENAME':
2044 2049 backend.unlink(gp.oldpath)
2045 2050 if not first_hunk:
2046 2051 if gp.op == 'DELETE':
2047 2052 backend.unlink(gp.path)
2048 2053 continue
2049 2054 data, mode = None, None
2050 2055 if gp.op in ('RENAME', 'COPY'):
2051 2056 data, mode = store.getfile(gp.oldpath)[:2]
2052 2057 if data is None:
2053 2058 # This means that the old path does not exist
2054 2059 raise PatchError(_("source file '%s' does not exist")
2055 2060 % gp.oldpath)
2056 2061 if gp.mode:
2057 2062 mode = gp.mode
2058 2063 if gp.op == 'ADD':
2059 2064 # Added files without content have no hunk and
2060 2065 # must be created
2061 2066 data = ''
2062 2067 if data or mode:
2063 2068 if (gp.op in ('ADD', 'RENAME', 'COPY')
2064 2069 and backend.exists(gp.path)):
2065 2070 raise PatchError(_("cannot create %s: destination "
2066 2071 "already exists") % gp.path)
2067 2072 backend.setfile(gp.path, data, mode, gp.oldpath)
2068 2073 continue
2069 2074 try:
2070 2075 current_file = patcher(ui, gp, backend, store,
2071 2076 eolmode=eolmode)
2072 2077 except PatchError as inst:
2073 2078 ui.warn(str(inst) + '\n')
2074 2079 current_file = None
2075 2080 rejects += 1
2076 2081 continue
2077 2082 elif state == 'git':
2078 2083 for gp in values:
2079 2084 path = pstrip(gp.oldpath)
2080 2085 data, mode = backend.getfile(path)
2081 2086 if data is None:
2082 2087 # The error ignored here will trigger a getfile()
2083 2088 # error in a place more appropriate for error
2084 2089 # handling, and will not interrupt the patching
2085 2090 # process.
2086 2091 pass
2087 2092 else:
2088 2093 store.setfile(path, data, mode)
2089 2094 else:
2090 2095 raise error.Abort(_('unsupported parser state: %s') % state)
2091 2096
2092 2097 if current_file:
2093 2098 rejects += current_file.close()
2094 2099
2095 2100 if rejects:
2096 2101 return -1
2097 2102 return err
2098 2103
2099 2104 def _externalpatch(ui, repo, patcher, patchname, strip, files,
2100 2105 similarity):
2101 2106 """use <patcher> to apply <patchname> to the working directory.
2102 2107 returns whether patch was applied with fuzz factor."""
2103 2108
2104 2109 fuzz = False
2105 2110 args = []
2106 2111 cwd = repo.root
2107 2112 if cwd:
2108 2113 args.append('-d %s' % procutil.shellquote(cwd))
2109 2114 cmd = ('%s %s -p%d < %s'
2110 2115 % (patcher, ' '.join(args), strip, procutil.shellquote(patchname)))
2111 2116 fp = procutil.popen(cmd, 'rb')
2112 2117 try:
2113 2118 for line in util.iterfile(fp):
2114 2119 line = line.rstrip()
2115 2120 ui.note(line + '\n')
2116 2121 if line.startswith('patching file '):
2117 2122 pf = util.parsepatchoutput(line)
2118 2123 printed_file = False
2119 2124 files.add(pf)
2120 2125 elif line.find('with fuzz') >= 0:
2121 2126 fuzz = True
2122 2127 if not printed_file:
2123 2128 ui.warn(pf + '\n')
2124 2129 printed_file = True
2125 2130 ui.warn(line + '\n')
2126 2131 elif line.find('saving rejects to file') >= 0:
2127 2132 ui.warn(line + '\n')
2128 2133 elif line.find('FAILED') >= 0:
2129 2134 if not printed_file:
2130 2135 ui.warn(pf + '\n')
2131 2136 printed_file = True
2132 2137 ui.warn(line + '\n')
2133 2138 finally:
2134 2139 if files:
2135 2140 scmutil.marktouched(repo, files, similarity)
2136 2141 code = fp.close()
2137 2142 if code:
2138 2143 raise PatchError(_("patch command failed: %s") %
2139 2144 procutil.explainexit(code))
2140 2145 return fuzz
2141 2146
2142 2147 def patchbackend(ui, backend, patchobj, strip, prefix, files=None,
2143 2148 eolmode='strict'):
2144 2149 if files is None:
2145 2150 files = set()
2146 2151 if eolmode is None:
2147 2152 eolmode = ui.config('patch', 'eol')
2148 2153 if eolmode.lower() not in eolmodes:
2149 2154 raise error.Abort(_('unsupported line endings type: %s') % eolmode)
2150 2155 eolmode = eolmode.lower()
2151 2156
2152 2157 store = filestore()
2153 2158 try:
2154 2159 fp = open(patchobj, 'rb')
2155 2160 except TypeError:
2156 2161 fp = patchobj
2157 2162 try:
2158 2163 ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix,
2159 2164 eolmode=eolmode)
2160 2165 finally:
2161 2166 if fp != patchobj:
2162 2167 fp.close()
2163 2168 files.update(backend.close())
2164 2169 store.close()
2165 2170 if ret < 0:
2166 2171 raise PatchError(_('patch failed to apply'))
2167 2172 return ret > 0
2168 2173
2169 2174 def internalpatch(ui, repo, patchobj, strip, prefix='', files=None,
2170 2175 eolmode='strict', similarity=0):
2171 2176 """use builtin patch to apply <patchobj> to the working directory.
2172 2177 returns whether patch was applied with fuzz factor."""
2173 2178 backend = workingbackend(ui, repo, similarity)
2174 2179 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2175 2180
2176 2181 def patchrepo(ui, repo, ctx, store, patchobj, strip, prefix, files=None,
2177 2182 eolmode='strict'):
2178 2183 backend = repobackend(ui, repo, ctx, store)
2179 2184 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2180 2185
2181 2186 def patch(ui, repo, patchname, strip=1, prefix='', files=None, eolmode='strict',
2182 2187 similarity=0):
2183 2188 """Apply <patchname> to the working directory.
2184 2189
2185 2190 'eolmode' specifies how end of lines should be handled. It can be:
2186 2191 - 'strict': inputs are read in binary mode, EOLs are preserved
2187 2192 - 'crlf': EOLs are ignored when patching and reset to CRLF
2188 2193 - 'lf': EOLs are ignored when patching and reset to LF
2189 2194 - None: get it from user settings, default to 'strict'
2190 2195 'eolmode' is ignored when using an external patcher program.
2191 2196
2192 2197 Returns whether patch was applied with fuzz factor.
2193 2198 """
2194 2199 patcher = ui.config('ui', 'patch')
2195 2200 if files is None:
2196 2201 files = set()
2197 2202 if patcher:
2198 2203 return _externalpatch(ui, repo, patcher, patchname, strip,
2199 2204 files, similarity)
2200 2205 return internalpatch(ui, repo, patchname, strip, prefix, files, eolmode,
2201 2206 similarity)
2202 2207
2203 2208 def changedfiles(ui, repo, patchpath, strip=1, prefix=''):
2204 2209 backend = fsbackend(ui, repo.root)
2205 2210 prefix = _canonprefix(repo, prefix)
2206 2211 with open(patchpath, 'rb') as fp:
2207 2212 changed = set()
2208 2213 for state, values in iterhunks(fp):
2209 2214 if state == 'file':
2210 2215 afile, bfile, first_hunk, gp = values
2211 2216 if gp:
2212 2217 gp.path = pathtransform(gp.path, strip - 1, prefix)[1]
2213 2218 if gp.oldpath:
2214 2219 gp.oldpath = pathtransform(gp.oldpath, strip - 1,
2215 2220 prefix)[1]
2216 2221 else:
2217 2222 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2218 2223 prefix)
2219 2224 changed.add(gp.path)
2220 2225 if gp.op == 'RENAME':
2221 2226 changed.add(gp.oldpath)
2222 2227 elif state not in ('hunk', 'git'):
2223 2228 raise error.Abort(_('unsupported parser state: %s') % state)
2224 2229 return changed
2225 2230
2226 2231 class GitDiffRequired(Exception):
2227 2232 pass
2228 2233
2229 2234 def diffallopts(ui, opts=None, untrusted=False, section='diff'):
2230 2235 '''return diffopts with all features supported and parsed'''
2231 2236 return difffeatureopts(ui, opts=opts, untrusted=untrusted, section=section,
2232 2237 git=True, whitespace=True, formatchanging=True)
2233 2238
2234 2239 diffopts = diffallopts
2235 2240
2236 2241 def difffeatureopts(ui, opts=None, untrusted=False, section='diff', git=False,
2237 2242 whitespace=False, formatchanging=False):
2238 2243 '''return diffopts with only opted-in features parsed
2239 2244
2240 2245 Features:
2241 2246 - git: git-style diffs
2242 2247 - whitespace: whitespace options like ignoreblanklines and ignorews
2243 2248 - formatchanging: options that will likely break or cause correctness issues
2244 2249 with most diff parsers
2245 2250 '''
2246 2251 def get(key, name=None, getter=ui.configbool, forceplain=None):
2247 2252 if opts:
2248 2253 v = opts.get(key)
2249 2254 # diffopts flags are either None-default (which is passed
2250 2255 # through unchanged, so we can identify unset values), or
2251 2256 # some other falsey default (eg --unified, which defaults
2252 2257 # to an empty string). We only want to override the config
2253 2258 # entries from hgrc with command line values if they
2254 2259 # appear to have been set, which is any truthy value,
2255 2260 # True, or False.
2256 2261 if v or isinstance(v, bool):
2257 2262 return v
2258 2263 if forceplain is not None and ui.plain():
2259 2264 return forceplain
2260 2265 return getter(section, name or key, untrusted=untrusted)
2261 2266
2262 2267 # core options, expected to be understood by every diff parser
2263 2268 buildopts = {
2264 2269 'nodates': get('nodates'),
2265 2270 'showfunc': get('show_function', 'showfunc'),
2266 2271 'context': get('unified', getter=ui.config),
2267 2272 }
2268 2273 buildopts['worddiff'] = ui.configbool('experimental', 'worddiff')
2269 2274 buildopts['xdiff'] = ui.configbool('experimental', 'xdiff')
2270 2275
2271 2276 if git:
2272 2277 buildopts['git'] = get('git')
2273 2278
2274 2279 # since this is in the experimental section, we need to call
2275 2280 # ui.configbool directory
2276 2281 buildopts['showsimilarity'] = ui.configbool('experimental',
2277 2282 'extendedheader.similarity')
2278 2283
2279 2284 # need to inspect the ui object instead of using get() since we want to
2280 2285 # test for an int
2281 2286 hconf = ui.config('experimental', 'extendedheader.index')
2282 2287 if hconf is not None:
2283 2288 hlen = None
2284 2289 try:
2285 2290 # the hash config could be an integer (for length of hash) or a
2286 2291 # word (e.g. short, full, none)
2287 2292 hlen = int(hconf)
2288 2293 if hlen < 0 or hlen > 40:
2289 2294 msg = _("invalid length for extendedheader.index: '%d'\n")
2290 2295 ui.warn(msg % hlen)
2291 2296 except ValueError:
2292 2297 # default value
2293 2298 if hconf == 'short' or hconf == '':
2294 2299 hlen = 12
2295 2300 elif hconf == 'full':
2296 2301 hlen = 40
2297 2302 elif hconf != 'none':
2298 2303 msg = _("invalid value for extendedheader.index: '%s'\n")
2299 2304 ui.warn(msg % hconf)
2300 2305 finally:
2301 2306 buildopts['index'] = hlen
2302 2307
2303 2308 if whitespace:
2304 2309 buildopts['ignorews'] = get('ignore_all_space', 'ignorews')
2305 2310 buildopts['ignorewsamount'] = get('ignore_space_change',
2306 2311 'ignorewsamount')
2307 2312 buildopts['ignoreblanklines'] = get('ignore_blank_lines',
2308 2313 'ignoreblanklines')
2309 2314 buildopts['ignorewseol'] = get('ignore_space_at_eol', 'ignorewseol')
2310 2315 if formatchanging:
2311 2316 buildopts['text'] = opts and opts.get('text')
2312 2317 binary = None if opts is None else opts.get('binary')
2313 2318 buildopts['nobinary'] = (not binary if binary is not None
2314 2319 else get('nobinary', forceplain=False))
2315 2320 buildopts['noprefix'] = get('noprefix', forceplain=False)
2316 2321
2317 2322 return mdiff.diffopts(**pycompat.strkwargs(buildopts))
2318 2323
2319 2324 def diff(repo, node1=None, node2=None, match=None, changes=None,
2320 2325 opts=None, losedatafn=None, prefix='', relroot='', copy=None,
2321 2326 hunksfilterfn=None):
2322 2327 '''yields diff of changes to files between two nodes, or node and
2323 2328 working directory.
2324 2329
2325 2330 if node1 is None, use first dirstate parent instead.
2326 2331 if node2 is None, compare node1 with working directory.
2327 2332
2328 2333 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2329 2334 every time some change cannot be represented with the current
2330 2335 patch format. Return False to upgrade to git patch format, True to
2331 2336 accept the loss or raise an exception to abort the diff. It is
2332 2337 called with the name of current file being diffed as 'fn'. If set
2333 2338 to None, patches will always be upgraded to git format when
2334 2339 necessary.
2335 2340
2336 2341 prefix is a filename prefix that is prepended to all filenames on
2337 2342 display (used for subrepos).
2338 2343
2339 2344 relroot, if not empty, must be normalized with a trailing /. Any match
2340 2345 patterns that fall outside it will be ignored.
2341 2346
2342 2347 copy, if not empty, should contain mappings {dst@y: src@x} of copy
2343 2348 information.
2344 2349
2345 2350 hunksfilterfn, if not None, should be a function taking a filectx and
2346 2351 hunks generator that may yield filtered hunks.
2347 2352 '''
2348 2353 for fctx1, fctx2, hdr, hunks in diffhunks(
2349 2354 repo, node1=node1, node2=node2,
2350 2355 match=match, changes=changes, opts=opts,
2351 2356 losedatafn=losedatafn, prefix=prefix, relroot=relroot, copy=copy,
2352 2357 ):
2353 2358 if hunksfilterfn is not None:
2354 2359 # If the file has been removed, fctx2 is None; but this should
2355 2360 # not occur here since we catch removed files early in
2356 2361 # logcmdutil.getlinerangerevs() for 'hg log -L'.
2357 2362 assert fctx2 is not None, \
2358 2363 'fctx2 unexpectly None in diff hunks filtering'
2359 2364 hunks = hunksfilterfn(fctx2, hunks)
2360 2365 text = ''.join(sum((list(hlines) for hrange, hlines in hunks), []))
2361 2366 if hdr and (text or len(hdr) > 1):
2362 2367 yield '\n'.join(hdr) + '\n'
2363 2368 if text:
2364 2369 yield text
2365 2370
2366 2371 def diffhunks(repo, node1=None, node2=None, match=None, changes=None,
2367 2372 opts=None, losedatafn=None, prefix='', relroot='', copy=None):
2368 2373 """Yield diff of changes to files in the form of (`header`, `hunks`) tuples
2369 2374 where `header` is a list of diff headers and `hunks` is an iterable of
2370 2375 (`hunkrange`, `hunklines`) tuples.
2371 2376
2372 2377 See diff() for the meaning of parameters.
2373 2378 """
2374 2379
2375 2380 if opts is None:
2376 2381 opts = mdiff.defaultopts
2377 2382
2378 2383 if not node1 and not node2:
2379 2384 node1 = repo.dirstate.p1()
2380 2385
2381 2386 def lrugetfilectx():
2382 2387 cache = {}
2383 2388 order = collections.deque()
2384 2389 def getfilectx(f, ctx):
2385 2390 fctx = ctx.filectx(f, filelog=cache.get(f))
2386 2391 if f not in cache:
2387 2392 if len(cache) > 20:
2388 2393 del cache[order.popleft()]
2389 2394 cache[f] = fctx.filelog()
2390 2395 else:
2391 2396 order.remove(f)
2392 2397 order.append(f)
2393 2398 return fctx
2394 2399 return getfilectx
2395 2400 getfilectx = lrugetfilectx()
2396 2401
2397 2402 ctx1 = repo[node1]
2398 2403 ctx2 = repo[node2]
2399 2404
2400 2405 relfiltered = False
2401 2406 if relroot != '' and match.always():
2402 2407 # as a special case, create a new matcher with just the relroot
2403 2408 pats = [relroot]
2404 2409 match = scmutil.match(ctx2, pats, default='path')
2405 2410 relfiltered = True
2406 2411
2407 2412 if not changes:
2408 2413 changes = repo.status(ctx1, ctx2, match=match)
2409 2414 modified, added, removed = changes[:3]
2410 2415
2411 2416 if not modified and not added and not removed:
2412 2417 return []
2413 2418
2414 2419 if repo.ui.debugflag:
2415 2420 hexfunc = hex
2416 2421 else:
2417 2422 hexfunc = short
2418 2423 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2419 2424
2420 2425 if copy is None:
2421 2426 copy = {}
2422 2427 if opts.git or opts.upgrade:
2423 2428 copy = copies.pathcopies(ctx1, ctx2, match=match)
2424 2429
2425 2430 if relroot is not None:
2426 2431 if not relfiltered:
2427 2432 # XXX this would ideally be done in the matcher, but that is
2428 2433 # generally meant to 'or' patterns, not 'and' them. In this case we
2429 2434 # need to 'and' all the patterns from the matcher with relroot.
2430 2435 def filterrel(l):
2431 2436 return [f for f in l if f.startswith(relroot)]
2432 2437 modified = filterrel(modified)
2433 2438 added = filterrel(added)
2434 2439 removed = filterrel(removed)
2435 2440 relfiltered = True
2436 2441 # filter out copies where either side isn't inside the relative root
2437 2442 copy = dict(((dst, src) for (dst, src) in copy.iteritems()
2438 2443 if dst.startswith(relroot)
2439 2444 and src.startswith(relroot)))
2440 2445
2441 2446 modifiedset = set(modified)
2442 2447 addedset = set(added)
2443 2448 removedset = set(removed)
2444 2449 for f in modified:
2445 2450 if f not in ctx1:
2446 2451 # Fix up added, since merged-in additions appear as
2447 2452 # modifications during merges
2448 2453 modifiedset.remove(f)
2449 2454 addedset.add(f)
2450 2455 for f in removed:
2451 2456 if f not in ctx1:
2452 2457 # Merged-in additions that are then removed are reported as removed.
2453 2458 # They are not in ctx1, so We don't want to show them in the diff.
2454 2459 removedset.remove(f)
2455 2460 modified = sorted(modifiedset)
2456 2461 added = sorted(addedset)
2457 2462 removed = sorted(removedset)
2458 2463 for dst, src in list(copy.items()):
2459 2464 if src not in ctx1:
2460 2465 # Files merged in during a merge and then copied/renamed are
2461 2466 # reported as copies. We want to show them in the diff as additions.
2462 2467 del copy[dst]
2463 2468
2464 2469 def difffn(opts, losedata):
2465 2470 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2466 2471 copy, getfilectx, opts, losedata, prefix, relroot)
2467 2472 if opts.upgrade and not opts.git:
2468 2473 try:
2469 2474 def losedata(fn):
2470 2475 if not losedatafn or not losedatafn(fn=fn):
2471 2476 raise GitDiffRequired
2472 2477 # Buffer the whole output until we are sure it can be generated
2473 2478 return list(difffn(opts.copy(git=False), losedata))
2474 2479 except GitDiffRequired:
2475 2480 return difffn(opts.copy(git=True), None)
2476 2481 else:
2477 2482 return difffn(opts, None)
2478 2483
2479 2484 def difflabel(func, *args, **kw):
2480 2485 '''yields 2-tuples of (output, label) based on the output of func()'''
2481 2486 inlinecolor = False
2482 2487 if kw.get(r'opts'):
2483 2488 inlinecolor = kw[r'opts'].worddiff
2484 2489 headprefixes = [('diff', 'diff.diffline'),
2485 2490 ('copy', 'diff.extended'),
2486 2491 ('rename', 'diff.extended'),
2487 2492 ('old', 'diff.extended'),
2488 2493 ('new', 'diff.extended'),
2489 2494 ('deleted', 'diff.extended'),
2490 2495 ('index', 'diff.extended'),
2491 2496 ('similarity', 'diff.extended'),
2492 2497 ('---', 'diff.file_a'),
2493 2498 ('+++', 'diff.file_b')]
2494 2499 textprefixes = [('@', 'diff.hunk'),
2495 2500 ('-', 'diff.deleted'),
2496 2501 ('+', 'diff.inserted')]
2497 2502 head = False
2498 2503 for chunk in func(*args, **kw):
2499 2504 lines = chunk.split('\n')
2500 2505 matches = {}
2501 2506 if inlinecolor:
2502 2507 matches = _findmatches(lines)
2503 2508 for i, line in enumerate(lines):
2504 2509 if i != 0:
2505 2510 yield ('\n', '')
2506 2511 if head:
2507 2512 if line.startswith('@'):
2508 2513 head = False
2509 2514 else:
2510 2515 if line and not line.startswith((' ', '+', '-', '@', '\\')):
2511 2516 head = True
2512 2517 stripline = line
2513 2518 diffline = False
2514 2519 if not head and line and line.startswith(('+', '-')):
2515 2520 # highlight tabs and trailing whitespace, but only in
2516 2521 # changed lines
2517 2522 stripline = line.rstrip()
2518 2523 diffline = True
2519 2524
2520 2525 prefixes = textprefixes
2521 2526 if head:
2522 2527 prefixes = headprefixes
2523 2528 for prefix, label in prefixes:
2524 2529 if stripline.startswith(prefix):
2525 2530 if diffline:
2526 2531 if i in matches:
2527 2532 for t, l in _inlinediff(lines[i].rstrip(),
2528 2533 lines[matches[i]].rstrip(),
2529 2534 label):
2530 2535 yield (t, l)
2531 2536 else:
2532 2537 for token in tabsplitter.findall(stripline):
2533 2538 if token.startswith('\t'):
2534 2539 yield (token, 'diff.tab')
2535 2540 else:
2536 2541 yield (token, label)
2537 2542 else:
2538 2543 yield (stripline, label)
2539 2544 break
2540 2545 else:
2541 2546 yield (line, '')
2542 2547 if line != stripline:
2543 2548 yield (line[len(stripline):], 'diff.trailingwhitespace')
2544 2549
2545 2550 def _findmatches(slist):
2546 2551 '''Look for insertion matches to deletion and returns a dict of
2547 2552 correspondences.
2548 2553 '''
2549 2554 lastmatch = 0
2550 2555 matches = {}
2551 2556 for i, line in enumerate(slist):
2552 2557 if line == '':
2553 2558 continue
2554 2559 if line.startswith('-'):
2555 2560 lastmatch = max(lastmatch, i)
2556 2561 newgroup = False
2557 2562 for j, newline in enumerate(slist[lastmatch + 1:]):
2558 2563 if newline == '':
2559 2564 continue
2560 2565 if newline.startswith('-') and newgroup: # too far, no match
2561 2566 break
2562 2567 if newline.startswith('+'): # potential match
2563 2568 newgroup = True
2564 2569 sim = difflib.SequenceMatcher(None, line, newline).ratio()
2565 2570 if sim > 0.7:
2566 2571 lastmatch = lastmatch + 1 + j
2567 2572 matches[i] = lastmatch
2568 2573 matches[lastmatch] = i
2569 2574 break
2570 2575 return matches
2571 2576
2572 2577 def _inlinediff(s1, s2, operation):
2573 2578 '''Perform string diff to highlight specific changes.'''
2574 2579 operation_skip = ('+', '?') if operation == 'diff.deleted' else ('-', '?')
2575 2580 if operation == 'diff.deleted':
2576 2581 s2, s1 = s1, s2
2577 2582
2578 2583 buff = []
2579 2584 # we never want to higlight the leading +-
2580 2585 if operation == 'diff.deleted' and s2.startswith('-'):
2581 2586 label = operation
2582 2587 token = '-'
2583 2588 s2 = s2[1:]
2584 2589 s1 = s1[1:]
2585 2590 elif operation == 'diff.inserted' and s1.startswith('+'):
2586 2591 label = operation
2587 2592 token = '+'
2588 2593 s2 = s2[1:]
2589 2594 s1 = s1[1:]
2590 2595 else:
2591 2596 raise error.ProgrammingError("Case not expected, operation = %s" %
2592 2597 operation)
2593 2598
2594 2599 s = difflib.ndiff(_nonwordre.split(s2), _nonwordre.split(s1))
2595 2600 for part in s:
2596 2601 if part.startswith(operation_skip) or len(part) == 2:
2597 2602 continue
2598 2603 l = operation + '.highlight'
2599 2604 if part.startswith(' '):
2600 2605 l = operation
2601 2606 if part[2:] == '\t':
2602 2607 l = 'diff.tab'
2603 2608 if l == label: # contiguous token with same label
2604 2609 token += part[2:]
2605 2610 continue
2606 2611 else:
2607 2612 buff.append((token, label))
2608 2613 label = l
2609 2614 token = part[2:]
2610 2615 buff.append((token, label))
2611 2616
2612 2617 return buff
2613 2618
2614 2619 def diffui(*args, **kw):
2615 2620 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2616 2621 return difflabel(diff, *args, **kw)
2617 2622
2618 2623 def _filepairs(modified, added, removed, copy, opts):
2619 2624 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2620 2625 before and f2 is the the name after. For added files, f1 will be None,
2621 2626 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2622 2627 or 'rename' (the latter two only if opts.git is set).'''
2623 2628 gone = set()
2624 2629
2625 2630 copyto = dict([(v, k) for k, v in copy.items()])
2626 2631
2627 2632 addedset, removedset = set(added), set(removed)
2628 2633
2629 2634 for f in sorted(modified + added + removed):
2630 2635 copyop = None
2631 2636 f1, f2 = f, f
2632 2637 if f in addedset:
2633 2638 f1 = None
2634 2639 if f in copy:
2635 2640 if opts.git:
2636 2641 f1 = copy[f]
2637 2642 if f1 in removedset and f1 not in gone:
2638 2643 copyop = 'rename'
2639 2644 gone.add(f1)
2640 2645 else:
2641 2646 copyop = 'copy'
2642 2647 elif f in removedset:
2643 2648 f2 = None
2644 2649 if opts.git:
2645 2650 # have we already reported a copy above?
2646 2651 if (f in copyto and copyto[f] in addedset
2647 2652 and copy[copyto[f]] == f):
2648 2653 continue
2649 2654 yield f1, f2, copyop
2650 2655
2651 2656 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2652 2657 copy, getfilectx, opts, losedatafn, prefix, relroot):
2653 2658 '''given input data, generate a diff and yield it in blocks
2654 2659
2655 2660 If generating a diff would lose data like flags or binary data and
2656 2661 losedatafn is not None, it will be called.
2657 2662
2658 2663 relroot is removed and prefix is added to every path in the diff output.
2659 2664
2660 2665 If relroot is not empty, this function expects every path in modified,
2661 2666 added, removed and copy to start with it.'''
2662 2667
2663 2668 def gitindex(text):
2664 2669 if not text:
2665 2670 text = ""
2666 2671 l = len(text)
2667 2672 s = hashlib.sha1('blob %d\0' % l)
2668 2673 s.update(text)
2669 2674 return hex(s.digest())
2670 2675
2671 2676 if opts.noprefix:
2672 2677 aprefix = bprefix = ''
2673 2678 else:
2674 2679 aprefix = 'a/'
2675 2680 bprefix = 'b/'
2676 2681
2677 2682 def diffline(f, revs):
2678 2683 revinfo = ' '.join(["-r %s" % rev for rev in revs])
2679 2684 return 'diff %s %s' % (revinfo, f)
2680 2685
2681 2686 def isempty(fctx):
2682 2687 return fctx is None or fctx.size() == 0
2683 2688
2684 2689 date1 = dateutil.datestr(ctx1.date())
2685 2690 date2 = dateutil.datestr(ctx2.date())
2686 2691
2687 2692 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
2688 2693
2689 2694 if relroot != '' and (repo.ui.configbool('devel', 'all-warnings')
2690 2695 or repo.ui.configbool('devel', 'check-relroot')):
2691 2696 for f in modified + added + removed + list(copy) + list(copy.values()):
2692 2697 if f is not None and not f.startswith(relroot):
2693 2698 raise AssertionError(
2694 2699 "file %s doesn't start with relroot %s" % (f, relroot))
2695 2700
2696 2701 for f1, f2, copyop in _filepairs(modified, added, removed, copy, opts):
2697 2702 content1 = None
2698 2703 content2 = None
2699 2704 fctx1 = None
2700 2705 fctx2 = None
2701 2706 flag1 = None
2702 2707 flag2 = None
2703 2708 if f1:
2704 2709 fctx1 = getfilectx(f1, ctx1)
2705 2710 if opts.git or losedatafn:
2706 2711 flag1 = ctx1.flags(f1)
2707 2712 if f2:
2708 2713 fctx2 = getfilectx(f2, ctx2)
2709 2714 if opts.git or losedatafn:
2710 2715 flag2 = ctx2.flags(f2)
2711 2716 # if binary is True, output "summary" or "base85", but not "text diff"
2712 2717 if opts.text:
2713 2718 binary = False
2714 2719 else:
2715 2720 binary = any(f.isbinary() for f in [fctx1, fctx2] if f is not None)
2716 2721
2717 2722 if losedatafn and not opts.git:
2718 2723 if (binary or
2719 2724 # copy/rename
2720 2725 f2 in copy or
2721 2726 # empty file creation
2722 2727 (not f1 and isempty(fctx2)) or
2723 2728 # empty file deletion
2724 2729 (isempty(fctx1) and not f2) or
2725 2730 # create with flags
2726 2731 (not f1 and flag2) or
2727 2732 # change flags
2728 2733 (f1 and f2 and flag1 != flag2)):
2729 2734 losedatafn(f2 or f1)
2730 2735
2731 2736 path1 = f1 or f2
2732 2737 path2 = f2 or f1
2733 2738 path1 = posixpath.join(prefix, path1[len(relroot):])
2734 2739 path2 = posixpath.join(prefix, path2[len(relroot):])
2735 2740 header = []
2736 2741 if opts.git:
2737 2742 header.append('diff --git %s%s %s%s' %
2738 2743 (aprefix, path1, bprefix, path2))
2739 2744 if not f1: # added
2740 2745 header.append('new file mode %s' % gitmode[flag2])
2741 2746 elif not f2: # removed
2742 2747 header.append('deleted file mode %s' % gitmode[flag1])
2743 2748 else: # modified/copied/renamed
2744 2749 mode1, mode2 = gitmode[flag1], gitmode[flag2]
2745 2750 if mode1 != mode2:
2746 2751 header.append('old mode %s' % mode1)
2747 2752 header.append('new mode %s' % mode2)
2748 2753 if copyop is not None:
2749 2754 if opts.showsimilarity:
2750 2755 sim = similar.score(ctx1[path1], ctx2[path2]) * 100
2751 2756 header.append('similarity index %d%%' % sim)
2752 2757 header.append('%s from %s' % (copyop, path1))
2753 2758 header.append('%s to %s' % (copyop, path2))
2754 2759 elif revs and not repo.ui.quiet:
2755 2760 header.append(diffline(path1, revs))
2756 2761
2757 2762 # fctx.is | diffopts | what to | is fctx.data()
2758 2763 # binary() | text nobinary git index | output? | outputted?
2759 2764 # ------------------------------------|----------------------------
2760 2765 # yes | no no no * | summary | no
2761 2766 # yes | no no yes * | base85 | yes
2762 2767 # yes | no yes no * | summary | no
2763 2768 # yes | no yes yes 0 | summary | no
2764 2769 # yes | no yes yes >0 | summary | semi [1]
2765 2770 # yes | yes * * * | text diff | yes
2766 2771 # no | * * * * | text diff | yes
2767 2772 # [1]: hash(fctx.data()) is outputted. so fctx.data() cannot be faked
2768 2773 if binary and (not opts.git or (opts.git and opts.nobinary and not
2769 2774 opts.index)):
2770 2775 # fast path: no binary content will be displayed, content1 and
2771 2776 # content2 are only used for equivalent test. cmp() could have a
2772 2777 # fast path.
2773 2778 if fctx1 is not None:
2774 2779 content1 = b'\0'
2775 2780 if fctx2 is not None:
2776 2781 if fctx1 is not None and not fctx1.cmp(fctx2):
2777 2782 content2 = b'\0' # not different
2778 2783 else:
2779 2784 content2 = b'\0\0'
2780 2785 else:
2781 2786 # normal path: load contents
2782 2787 if fctx1 is not None:
2783 2788 content1 = fctx1.data()
2784 2789 if fctx2 is not None:
2785 2790 content2 = fctx2.data()
2786 2791
2787 2792 if binary and opts.git and not opts.nobinary:
2788 2793 text = mdiff.b85diff(content1, content2)
2789 2794 if text:
2790 2795 header.append('index %s..%s' %
2791 2796 (gitindex(content1), gitindex(content2)))
2792 2797 hunks = (None, [text]),
2793 2798 else:
2794 2799 if opts.git and opts.index > 0:
2795 2800 flag = flag1
2796 2801 if flag is None:
2797 2802 flag = flag2
2798 2803 header.append('index %s..%s %s' %
2799 2804 (gitindex(content1)[0:opts.index],
2800 2805 gitindex(content2)[0:opts.index],
2801 2806 gitmode[flag]))
2802 2807
2803 2808 uheaders, hunks = mdiff.unidiff(content1, date1,
2804 2809 content2, date2,
2805 2810 path1, path2,
2806 2811 binary=binary, opts=opts)
2807 2812 header.extend(uheaders)
2808 2813 yield fctx1, fctx2, header, hunks
2809 2814
2810 2815 def diffstatsum(stats):
2811 2816 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
2812 2817 for f, a, r, b in stats:
2813 2818 maxfile = max(maxfile, encoding.colwidth(f))
2814 2819 maxtotal = max(maxtotal, a + r)
2815 2820 addtotal += a
2816 2821 removetotal += r
2817 2822 binary = binary or b
2818 2823
2819 2824 return maxfile, maxtotal, addtotal, removetotal, binary
2820 2825
2821 2826 def diffstatdata(lines):
2822 2827 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
2823 2828
2824 2829 results = []
2825 2830 filename, adds, removes, isbinary = None, 0, 0, False
2826 2831
2827 2832 def addresult():
2828 2833 if filename:
2829 2834 results.append((filename, adds, removes, isbinary))
2830 2835
2831 2836 # inheader is used to track if a line is in the
2832 2837 # header portion of the diff. This helps properly account
2833 2838 # for lines that start with '--' or '++'
2834 2839 inheader = False
2835 2840
2836 2841 for line in lines:
2837 2842 if line.startswith('diff'):
2838 2843 addresult()
2839 2844 # starting a new file diff
2840 2845 # set numbers to 0 and reset inheader
2841 2846 inheader = True
2842 2847 adds, removes, isbinary = 0, 0, False
2843 2848 if line.startswith('diff --git a/'):
2844 2849 filename = gitre.search(line).group(2)
2845 2850 elif line.startswith('diff -r'):
2846 2851 # format: "diff -r ... -r ... filename"
2847 2852 filename = diffre.search(line).group(1)
2848 2853 elif line.startswith('@@'):
2849 2854 inheader = False
2850 2855 elif line.startswith('+') and not inheader:
2851 2856 adds += 1
2852 2857 elif line.startswith('-') and not inheader:
2853 2858 removes += 1
2854 2859 elif (line.startswith('GIT binary patch') or
2855 2860 line.startswith('Binary file')):
2856 2861 isbinary = True
2857 2862 addresult()
2858 2863 return results
2859 2864
2860 2865 def diffstat(lines, width=80):
2861 2866 output = []
2862 2867 stats = diffstatdata(lines)
2863 2868 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
2864 2869
2865 2870 countwidth = len(str(maxtotal))
2866 2871 if hasbinary and countwidth < 3:
2867 2872 countwidth = 3
2868 2873 graphwidth = width - countwidth - maxname - 6
2869 2874 if graphwidth < 10:
2870 2875 graphwidth = 10
2871 2876
2872 2877 def scale(i):
2873 2878 if maxtotal <= graphwidth:
2874 2879 return i
2875 2880 # If diffstat runs out of room it doesn't print anything,
2876 2881 # which isn't very useful, so always print at least one + or -
2877 2882 # if there were at least some changes.
2878 2883 return max(i * graphwidth // maxtotal, int(bool(i)))
2879 2884
2880 2885 for filename, adds, removes, isbinary in stats:
2881 2886 if isbinary:
2882 2887 count = 'Bin'
2883 2888 else:
2884 2889 count = '%d' % (adds + removes)
2885 2890 pluses = '+' * scale(adds)
2886 2891 minuses = '-' * scale(removes)
2887 2892 output.append(' %s%s | %*s %s%s\n' %
2888 2893 (filename, ' ' * (maxname - encoding.colwidth(filename)),
2889 2894 countwidth, count, pluses, minuses))
2890 2895
2891 2896 if stats:
2892 2897 output.append(_(' %d files changed, %d insertions(+), '
2893 2898 '%d deletions(-)\n')
2894 2899 % (len(stats), totaladds, totalremoves))
2895 2900
2896 2901 return ''.join(output)
2897 2902
2898 2903 def diffstatui(*args, **kw):
2899 2904 '''like diffstat(), but yields 2-tuples of (output, label) for
2900 2905 ui.write()
2901 2906 '''
2902 2907
2903 2908 for line in diffstat(*args, **kw).splitlines():
2904 2909 if line and line[-1] in '+-':
2905 2910 name, graph = line.rsplit(' ', 1)
2906 2911 yield (name + ' ', '')
2907 2912 m = re.search(br'\++', graph)
2908 2913 if m:
2909 2914 yield (m.group(0), 'diffstat.inserted')
2910 2915 m = re.search(br'-+', graph)
2911 2916 if m:
2912 2917 yield (m.group(0), 'diffstat.deleted')
2913 2918 else:
2914 2919 yield (line, '')
2915 2920 yield ('\n', '')
General Comments 0
You need to be logged in to leave comments. Login now