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