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