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