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