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