##// END OF EJS Templates
export: serialize revisions to be exported per destination file...
Yuya Nishihara -
r37619:2e0e6131 default
parent child Browse files
Show More
@@ -1,3224 +1,3225 b''
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import os
12 12 import re
13 13 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 def makefileobj(ctx, pat, mode='wb', modemap=None, **props):
1017 def makefileobj(ctx, pat, mode='wb', **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 if modemap is not None:
1029 mode = modemap.get(fn, mode)
1030 if mode == 'wb':
1031 modemap[fn] = 'ab'
1032 1028 return open(fn, mode)
1033 1029
1034 1030 def openrevlog(repo, cmd, file_, opts):
1035 1031 """opens the changelog, manifest, a filelog or a given revlog"""
1036 1032 cl = opts['changelog']
1037 1033 mf = opts['manifest']
1038 1034 dir = opts['dir']
1039 1035 msg = None
1040 1036 if cl and mf:
1041 1037 msg = _('cannot specify --changelog and --manifest at the same time')
1042 1038 elif cl and dir:
1043 1039 msg = _('cannot specify --changelog and --dir at the same time')
1044 1040 elif cl or mf or dir:
1045 1041 if file_:
1046 1042 msg = _('cannot specify filename with --changelog or --manifest')
1047 1043 elif not repo:
1048 1044 msg = _('cannot specify --changelog or --manifest or --dir '
1049 1045 'without a repository')
1050 1046 if msg:
1051 1047 raise error.Abort(msg)
1052 1048
1053 1049 r = None
1054 1050 if repo:
1055 1051 if cl:
1056 1052 r = repo.unfiltered().changelog
1057 1053 elif dir:
1058 1054 if 'treemanifest' not in repo.requirements:
1059 1055 raise error.Abort(_("--dir can only be used on repos with "
1060 1056 "treemanifest enabled"))
1061 1057 if not dir.endswith('/'):
1062 1058 dir = dir + '/'
1063 1059 dirlog = repo.manifestlog._revlog.dirlog(dir)
1064 1060 if len(dirlog):
1065 1061 r = dirlog
1066 1062 elif mf:
1067 1063 r = repo.manifestlog._revlog
1068 1064 elif file_:
1069 1065 filelog = repo.file(file_)
1070 1066 if len(filelog):
1071 1067 r = filelog
1072 1068 if not r:
1073 1069 if not file_:
1074 1070 raise error.CommandError(cmd, _('invalid arguments'))
1075 1071 if not os.path.isfile(file_):
1076 1072 raise error.Abort(_("revlog '%s' not found") % file_)
1077 1073 r = revlog.revlog(vfsmod.vfs(pycompat.getcwd(), audit=False),
1078 1074 file_[:-2] + ".i")
1079 1075 return r
1080 1076
1081 1077 def copy(ui, repo, pats, opts, rename=False):
1082 1078 # called with the repo lock held
1083 1079 #
1084 1080 # hgsep => pathname that uses "/" to separate directories
1085 1081 # ossep => pathname that uses os.sep to separate directories
1086 1082 cwd = repo.getcwd()
1087 1083 targets = {}
1088 1084 after = opts.get("after")
1089 1085 dryrun = opts.get("dry_run")
1090 1086 wctx = repo[None]
1091 1087
1092 1088 def walkpat(pat):
1093 1089 srcs = []
1094 1090 if after:
1095 1091 badstates = '?'
1096 1092 else:
1097 1093 badstates = '?r'
1098 1094 m = scmutil.match(wctx, [pat], opts, globbed=True)
1099 1095 for abs in wctx.walk(m):
1100 1096 state = repo.dirstate[abs]
1101 1097 rel = m.rel(abs)
1102 1098 exact = m.exact(abs)
1103 1099 if state in badstates:
1104 1100 if exact and state == '?':
1105 1101 ui.warn(_('%s: not copying - file is not managed\n') % rel)
1106 1102 if exact and state == 'r':
1107 1103 ui.warn(_('%s: not copying - file has been marked for'
1108 1104 ' remove\n') % rel)
1109 1105 continue
1110 1106 # abs: hgsep
1111 1107 # rel: ossep
1112 1108 srcs.append((abs, rel, exact))
1113 1109 return srcs
1114 1110
1115 1111 # abssrc: hgsep
1116 1112 # relsrc: ossep
1117 1113 # otarget: ossep
1118 1114 def copyfile(abssrc, relsrc, otarget, exact):
1119 1115 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
1120 1116 if '/' in abstarget:
1121 1117 # We cannot normalize abstarget itself, this would prevent
1122 1118 # case only renames, like a => A.
1123 1119 abspath, absname = abstarget.rsplit('/', 1)
1124 1120 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
1125 1121 reltarget = repo.pathto(abstarget, cwd)
1126 1122 target = repo.wjoin(abstarget)
1127 1123 src = repo.wjoin(abssrc)
1128 1124 state = repo.dirstate[abstarget]
1129 1125
1130 1126 scmutil.checkportable(ui, abstarget)
1131 1127
1132 1128 # check for collisions
1133 1129 prevsrc = targets.get(abstarget)
1134 1130 if prevsrc is not None:
1135 1131 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
1136 1132 (reltarget, repo.pathto(abssrc, cwd),
1137 1133 repo.pathto(prevsrc, cwd)))
1138 1134 return
1139 1135
1140 1136 # check for overwrites
1141 1137 exists = os.path.lexists(target)
1142 1138 samefile = False
1143 1139 if exists and abssrc != abstarget:
1144 1140 if (repo.dirstate.normalize(abssrc) ==
1145 1141 repo.dirstate.normalize(abstarget)):
1146 1142 if not rename:
1147 1143 ui.warn(_("%s: can't copy - same file\n") % reltarget)
1148 1144 return
1149 1145 exists = False
1150 1146 samefile = True
1151 1147
1152 1148 if not after and exists or after and state in 'mn':
1153 1149 if not opts['force']:
1154 1150 if state in 'mn':
1155 1151 msg = _('%s: not overwriting - file already committed\n')
1156 1152 if after:
1157 1153 flags = '--after --force'
1158 1154 else:
1159 1155 flags = '--force'
1160 1156 if rename:
1161 1157 hint = _('(hg rename %s to replace the file by '
1162 1158 'recording a rename)\n') % flags
1163 1159 else:
1164 1160 hint = _('(hg copy %s to replace the file by '
1165 1161 'recording a copy)\n') % flags
1166 1162 else:
1167 1163 msg = _('%s: not overwriting - file exists\n')
1168 1164 if rename:
1169 1165 hint = _('(hg rename --after to record the rename)\n')
1170 1166 else:
1171 1167 hint = _('(hg copy --after to record the copy)\n')
1172 1168 ui.warn(msg % reltarget)
1173 1169 ui.warn(hint)
1174 1170 return
1175 1171
1176 1172 if after:
1177 1173 if not exists:
1178 1174 if rename:
1179 1175 ui.warn(_('%s: not recording move - %s does not exist\n') %
1180 1176 (relsrc, reltarget))
1181 1177 else:
1182 1178 ui.warn(_('%s: not recording copy - %s does not exist\n') %
1183 1179 (relsrc, reltarget))
1184 1180 return
1185 1181 elif not dryrun:
1186 1182 try:
1187 1183 if exists:
1188 1184 os.unlink(target)
1189 1185 targetdir = os.path.dirname(target) or '.'
1190 1186 if not os.path.isdir(targetdir):
1191 1187 os.makedirs(targetdir)
1192 1188 if samefile:
1193 1189 tmp = target + "~hgrename"
1194 1190 os.rename(src, tmp)
1195 1191 os.rename(tmp, target)
1196 1192 else:
1197 1193 # Preserve stat info on renames, not on copies; this matches
1198 1194 # Linux CLI behavior.
1199 1195 util.copyfile(src, target, copystat=rename)
1200 1196 srcexists = True
1201 1197 except IOError as inst:
1202 1198 if inst.errno == errno.ENOENT:
1203 1199 ui.warn(_('%s: deleted in working directory\n') % relsrc)
1204 1200 srcexists = False
1205 1201 else:
1206 1202 ui.warn(_('%s: cannot copy - %s\n') %
1207 1203 (relsrc, encoding.strtolocal(inst.strerror)))
1208 1204 return True # report a failure
1209 1205
1210 1206 if ui.verbose or not exact:
1211 1207 if rename:
1212 1208 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
1213 1209 else:
1214 1210 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
1215 1211
1216 1212 targets[abstarget] = abssrc
1217 1213
1218 1214 # fix up dirstate
1219 1215 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
1220 1216 dryrun=dryrun, cwd=cwd)
1221 1217 if rename and not dryrun:
1222 1218 if not after and srcexists and not samefile:
1223 1219 repo.wvfs.unlinkpath(abssrc)
1224 1220 wctx.forget([abssrc])
1225 1221
1226 1222 # pat: ossep
1227 1223 # dest ossep
1228 1224 # srcs: list of (hgsep, hgsep, ossep, bool)
1229 1225 # return: function that takes hgsep and returns ossep
1230 1226 def targetpathfn(pat, dest, srcs):
1231 1227 if os.path.isdir(pat):
1232 1228 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1233 1229 abspfx = util.localpath(abspfx)
1234 1230 if destdirexists:
1235 1231 striplen = len(os.path.split(abspfx)[0])
1236 1232 else:
1237 1233 striplen = len(abspfx)
1238 1234 if striplen:
1239 1235 striplen += len(pycompat.ossep)
1240 1236 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1241 1237 elif destdirexists:
1242 1238 res = lambda p: os.path.join(dest,
1243 1239 os.path.basename(util.localpath(p)))
1244 1240 else:
1245 1241 res = lambda p: dest
1246 1242 return res
1247 1243
1248 1244 # pat: ossep
1249 1245 # dest ossep
1250 1246 # srcs: list of (hgsep, hgsep, ossep, bool)
1251 1247 # return: function that takes hgsep and returns ossep
1252 1248 def targetpathafterfn(pat, dest, srcs):
1253 1249 if matchmod.patkind(pat):
1254 1250 # a mercurial pattern
1255 1251 res = lambda p: os.path.join(dest,
1256 1252 os.path.basename(util.localpath(p)))
1257 1253 else:
1258 1254 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1259 1255 if len(abspfx) < len(srcs[0][0]):
1260 1256 # A directory. Either the target path contains the last
1261 1257 # component of the source path or it does not.
1262 1258 def evalpath(striplen):
1263 1259 score = 0
1264 1260 for s in srcs:
1265 1261 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1266 1262 if os.path.lexists(t):
1267 1263 score += 1
1268 1264 return score
1269 1265
1270 1266 abspfx = util.localpath(abspfx)
1271 1267 striplen = len(abspfx)
1272 1268 if striplen:
1273 1269 striplen += len(pycompat.ossep)
1274 1270 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1275 1271 score = evalpath(striplen)
1276 1272 striplen1 = len(os.path.split(abspfx)[0])
1277 1273 if striplen1:
1278 1274 striplen1 += len(pycompat.ossep)
1279 1275 if evalpath(striplen1) > score:
1280 1276 striplen = striplen1
1281 1277 res = lambda p: os.path.join(dest,
1282 1278 util.localpath(p)[striplen:])
1283 1279 else:
1284 1280 # a file
1285 1281 if destdirexists:
1286 1282 res = lambda p: os.path.join(dest,
1287 1283 os.path.basename(util.localpath(p)))
1288 1284 else:
1289 1285 res = lambda p: dest
1290 1286 return res
1291 1287
1292 1288 pats = scmutil.expandpats(pats)
1293 1289 if not pats:
1294 1290 raise error.Abort(_('no source or destination specified'))
1295 1291 if len(pats) == 1:
1296 1292 raise error.Abort(_('no destination specified'))
1297 1293 dest = pats.pop()
1298 1294 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1299 1295 if not destdirexists:
1300 1296 if len(pats) > 1 or matchmod.patkind(pats[0]):
1301 1297 raise error.Abort(_('with multiple sources, destination must be an '
1302 1298 'existing directory'))
1303 1299 if util.endswithsep(dest):
1304 1300 raise error.Abort(_('destination %s is not a directory') % dest)
1305 1301
1306 1302 tfn = targetpathfn
1307 1303 if after:
1308 1304 tfn = targetpathafterfn
1309 1305 copylist = []
1310 1306 for pat in pats:
1311 1307 srcs = walkpat(pat)
1312 1308 if not srcs:
1313 1309 continue
1314 1310 copylist.append((tfn(pat, dest, srcs), srcs))
1315 1311 if not copylist:
1316 1312 raise error.Abort(_('no files to copy'))
1317 1313
1318 1314 errors = 0
1319 1315 for targetpath, srcs in copylist:
1320 1316 for abssrc, relsrc, exact in srcs:
1321 1317 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1322 1318 errors += 1
1323 1319
1324 1320 if errors:
1325 1321 ui.warn(_('(consider using --after)\n'))
1326 1322
1327 1323 return errors != 0
1328 1324
1329 1325 ## facility to let extension process additional data into an import patch
1330 1326 # list of identifier to be executed in order
1331 1327 extrapreimport = [] # run before commit
1332 1328 extrapostimport = [] # run after commit
1333 1329 # mapping from identifier to actual import function
1334 1330 #
1335 1331 # 'preimport' are run before the commit is made and are provided the following
1336 1332 # arguments:
1337 1333 # - repo: the localrepository instance,
1338 1334 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1339 1335 # - extra: the future extra dictionary of the changeset, please mutate it,
1340 1336 # - opts: the import options.
1341 1337 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
1342 1338 # mutation of in memory commit and more. Feel free to rework the code to get
1343 1339 # there.
1344 1340 extrapreimportmap = {}
1345 1341 # 'postimport' are run after the commit is made and are provided the following
1346 1342 # argument:
1347 1343 # - ctx: the changectx created by import.
1348 1344 extrapostimportmap = {}
1349 1345
1350 1346 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
1351 1347 """Utility function used by commands.import to import a single patch
1352 1348
1353 1349 This function is explicitly defined here to help the evolve extension to
1354 1350 wrap this part of the import logic.
1355 1351
1356 1352 The API is currently a bit ugly because it a simple code translation from
1357 1353 the import command. Feel free to make it better.
1358 1354
1359 1355 :hunk: a patch (as a binary string)
1360 1356 :parents: nodes that will be parent of the created commit
1361 1357 :opts: the full dict of option passed to the import command
1362 1358 :msgs: list to save commit message to.
1363 1359 (used in case we need to save it when failing)
1364 1360 :updatefunc: a function that update a repo to a given node
1365 1361 updatefunc(<repo>, <node>)
1366 1362 """
1367 1363 # avoid cycle context -> subrepo -> cmdutil
1368 1364 from . import context
1369 1365 extractdata = patch.extract(ui, hunk)
1370 1366 tmpname = extractdata.get('filename')
1371 1367 message = extractdata.get('message')
1372 1368 user = opts.get('user') or extractdata.get('user')
1373 1369 date = opts.get('date') or extractdata.get('date')
1374 1370 branch = extractdata.get('branch')
1375 1371 nodeid = extractdata.get('nodeid')
1376 1372 p1 = extractdata.get('p1')
1377 1373 p2 = extractdata.get('p2')
1378 1374
1379 1375 nocommit = opts.get('no_commit')
1380 1376 importbranch = opts.get('import_branch')
1381 1377 update = not opts.get('bypass')
1382 1378 strip = opts["strip"]
1383 1379 prefix = opts["prefix"]
1384 1380 sim = float(opts.get('similarity') or 0)
1385 1381 if not tmpname:
1386 1382 return (None, None, False)
1387 1383
1388 1384 rejects = False
1389 1385
1390 1386 try:
1391 1387 cmdline_message = logmessage(ui, opts)
1392 1388 if cmdline_message:
1393 1389 # pickup the cmdline msg
1394 1390 message = cmdline_message
1395 1391 elif message:
1396 1392 # pickup the patch msg
1397 1393 message = message.strip()
1398 1394 else:
1399 1395 # launch the editor
1400 1396 message = None
1401 1397 ui.debug('message:\n%s\n' % (message or ''))
1402 1398
1403 1399 if len(parents) == 1:
1404 1400 parents.append(repo[nullid])
1405 1401 if opts.get('exact'):
1406 1402 if not nodeid or not p1:
1407 1403 raise error.Abort(_('not a Mercurial patch'))
1408 1404 p1 = repo[p1]
1409 1405 p2 = repo[p2 or nullid]
1410 1406 elif p2:
1411 1407 try:
1412 1408 p1 = repo[p1]
1413 1409 p2 = repo[p2]
1414 1410 # Without any options, consider p2 only if the
1415 1411 # patch is being applied on top of the recorded
1416 1412 # first parent.
1417 1413 if p1 != parents[0]:
1418 1414 p1 = parents[0]
1419 1415 p2 = repo[nullid]
1420 1416 except error.RepoError:
1421 1417 p1, p2 = parents
1422 1418 if p2.node() == nullid:
1423 1419 ui.warn(_("warning: import the patch as a normal revision\n"
1424 1420 "(use --exact to import the patch as a merge)\n"))
1425 1421 else:
1426 1422 p1, p2 = parents
1427 1423
1428 1424 n = None
1429 1425 if update:
1430 1426 if p1 != parents[0]:
1431 1427 updatefunc(repo, p1.node())
1432 1428 if p2 != parents[1]:
1433 1429 repo.setparents(p1.node(), p2.node())
1434 1430
1435 1431 if opts.get('exact') or importbranch:
1436 1432 repo.dirstate.setbranch(branch or 'default')
1437 1433
1438 1434 partial = opts.get('partial', False)
1439 1435 files = set()
1440 1436 try:
1441 1437 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
1442 1438 files=files, eolmode=None, similarity=sim / 100.0)
1443 1439 except error.PatchError as e:
1444 1440 if not partial:
1445 1441 raise error.Abort(pycompat.bytestr(e))
1446 1442 if partial:
1447 1443 rejects = True
1448 1444
1449 1445 files = list(files)
1450 1446 if nocommit:
1451 1447 if message:
1452 1448 msgs.append(message)
1453 1449 else:
1454 1450 if opts.get('exact') or p2:
1455 1451 # If you got here, you either use --force and know what
1456 1452 # you are doing or used --exact or a merge patch while
1457 1453 # being updated to its first parent.
1458 1454 m = None
1459 1455 else:
1460 1456 m = scmutil.matchfiles(repo, files or [])
1461 1457 editform = mergeeditform(repo[None], 'import.normal')
1462 1458 if opts.get('exact'):
1463 1459 editor = None
1464 1460 else:
1465 1461 editor = getcommiteditor(editform=editform,
1466 1462 **pycompat.strkwargs(opts))
1467 1463 extra = {}
1468 1464 for idfunc in extrapreimport:
1469 1465 extrapreimportmap[idfunc](repo, extractdata, extra, opts)
1470 1466 overrides = {}
1471 1467 if partial:
1472 1468 overrides[('ui', 'allowemptycommit')] = True
1473 1469 with repo.ui.configoverride(overrides, 'import'):
1474 1470 n = repo.commit(message, user,
1475 1471 date, match=m,
1476 1472 editor=editor, extra=extra)
1477 1473 for idfunc in extrapostimport:
1478 1474 extrapostimportmap[idfunc](repo[n])
1479 1475 else:
1480 1476 if opts.get('exact') or importbranch:
1481 1477 branch = branch or 'default'
1482 1478 else:
1483 1479 branch = p1.branch()
1484 1480 store = patch.filestore()
1485 1481 try:
1486 1482 files = set()
1487 1483 try:
1488 1484 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
1489 1485 files, eolmode=None)
1490 1486 except error.PatchError as e:
1491 1487 raise error.Abort(stringutil.forcebytestr(e))
1492 1488 if opts.get('exact'):
1493 1489 editor = None
1494 1490 else:
1495 1491 editor = getcommiteditor(editform='import.bypass')
1496 1492 memctx = context.memctx(repo, (p1.node(), p2.node()),
1497 1493 message,
1498 1494 files=files,
1499 1495 filectxfn=store,
1500 1496 user=user,
1501 1497 date=date,
1502 1498 branch=branch,
1503 1499 editor=editor)
1504 1500 n = memctx.commit()
1505 1501 finally:
1506 1502 store.close()
1507 1503 if opts.get('exact') and nocommit:
1508 1504 # --exact with --no-commit is still useful in that it does merge
1509 1505 # and branch bits
1510 1506 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1511 1507 elif opts.get('exact') and hex(n) != nodeid:
1512 1508 raise error.Abort(_('patch is damaged or loses information'))
1513 1509 msg = _('applied to working directory')
1514 1510 if n:
1515 1511 # i18n: refers to a short changeset id
1516 1512 msg = _('created %s') % short(n)
1517 1513 return (msg, n, rejects)
1518 1514 finally:
1519 1515 os.unlink(tmpname)
1520 1516
1521 1517 # facility to let extensions include additional data in an exported patch
1522 1518 # list of identifiers to be executed in order
1523 1519 extraexport = []
1524 1520 # mapping from identifier to actual export function
1525 1521 # function as to return a string to be added to the header or None
1526 1522 # it is given two arguments (sequencenumber, changectx)
1527 1523 extraexportmap = {}
1528 1524
1529 1525 def _exportsingle(repo, ctx, match, switch_parent, seqno, write, diffopts):
1530 1526 node = scmutil.binnode(ctx)
1531 1527 parents = [p.node() for p in ctx.parents() if p]
1532 1528 branch = ctx.branch()
1533 1529 if switch_parent:
1534 1530 parents.reverse()
1535 1531
1536 1532 if parents:
1537 1533 prev = parents[0]
1538 1534 else:
1539 1535 prev = nullid
1540 1536
1541 1537 write("# HG changeset patch\n")
1542 1538 write("# User %s\n" % ctx.user())
1543 1539 write("# Date %d %d\n" % ctx.date())
1544 1540 write("# %s\n" % dateutil.datestr(ctx.date()))
1545 1541 if branch and branch != 'default':
1546 1542 write("# Branch %s\n" % branch)
1547 1543 write("# Node ID %s\n" % hex(node))
1548 1544 write("# Parent %s\n" % hex(prev))
1549 1545 if len(parents) > 1:
1550 1546 write("# Parent %s\n" % hex(parents[1]))
1551 1547
1552 1548 for headerid in extraexport:
1553 1549 header = extraexportmap[headerid](seqno, ctx)
1554 1550 if header is not None:
1555 1551 write('# %s\n' % header)
1556 1552 write(ctx.description().rstrip())
1557 1553 write("\n\n")
1558 1554
1559 1555 for chunk, label in patch.diffui(repo, prev, node, match, opts=diffopts):
1560 1556 write(chunk, label=label)
1561 1557
1562 1558 def _exportfile(repo, revs, fp, switch_parent, diffopts, match):
1563 1559 """Export changesets to stdout or a single file"""
1564 1560 dest = '<unnamed>'
1565 1561 if fp:
1566 1562 dest = getattr(fp, 'name', dest)
1567 1563 def write(s, **kw):
1568 1564 fp.write(s)
1569 1565 else:
1570 1566 write = repo.ui.write
1571 1567
1572 1568 for seqno, rev in enumerate(revs, 1):
1573 1569 ctx = repo[rev]
1574 1570 if not dest.startswith('<'):
1575 1571 repo.ui.note("%s\n" % dest)
1576 1572 _exportsingle(repo, ctx, match, switch_parent, seqno, write, diffopts)
1577 1573
1578 1574 def _exportfntemplate(repo, revs, fntemplate, switch_parent, diffopts, match):
1579 1575 """Export changesets to possibly multiple files"""
1580 1576 total = len(revs)
1581 1577 revwidth = max(len(str(rev)) for rev in revs)
1582 filemode = {}
1578 filemap = util.sortdict() # filename: [(seqno, rev), ...]
1583 1579
1584 1580 for seqno, rev in enumerate(revs, 1):
1585 1581 ctx = repo[rev]
1586 fo = makefileobj(ctx, fntemplate, mode='wb', modemap=filemode,
1587 total=total, seqno=seqno, revwidth=revwidth)
1588 dest = fo.name
1589 def write(s, **kw):
1590 fo.write(s)
1591 repo.ui.note("%s\n" % dest)
1592 _exportsingle(repo, ctx, match, switch_parent, seqno, write, diffopts)
1593 fo.close()
1582 dest = makefilename(ctx, fntemplate,
1583 total=total, seqno=seqno, revwidth=revwidth)
1584 filemap.setdefault(dest, []).append((seqno, rev))
1585
1586 for dest in filemap:
1587 with open(dest, 'wb') as fo:
1588 repo.ui.note("%s\n" % dest)
1589 def write(s, **kw):
1590 fo.write(s)
1591 for seqno, rev in filemap[dest]:
1592 ctx = repo[rev]
1593 _exportsingle(repo, ctx, match, switch_parent, seqno, write,
1594 diffopts)
1594 1595
1595 1596 def export(repo, revs, fntemplate='hg-%h.patch', fp=None, switch_parent=False,
1596 1597 opts=None, match=None):
1597 1598 '''export changesets as hg patches
1598 1599
1599 1600 Args:
1600 1601 repo: The repository from which we're exporting revisions.
1601 1602 revs: A list of revisions to export as revision numbers.
1602 1603 fntemplate: An optional string to use for generating patch file names.
1603 1604 fp: An optional file-like object to which patches should be written.
1604 1605 switch_parent: If True, show diffs against second parent when not nullid.
1605 1606 Default is false, which always shows diff against p1.
1606 1607 opts: diff options to use for generating the patch.
1607 1608 match: If specified, only export changes to files matching this matcher.
1608 1609
1609 1610 Returns:
1610 1611 Nothing.
1611 1612
1612 1613 Side Effect:
1613 1614 "HG Changeset Patch" data is emitted to one of the following
1614 1615 destinations:
1615 1616 fp is specified: All revs are written to the specified
1616 1617 file-like object.
1617 1618 fntemplate specified: Each rev is written to a unique file named using
1618 1619 the given template.
1619 1620 Neither fp nor template specified: All revs written to repo.ui.write()
1620 1621 '''
1621 1622 if fp or not fntemplate:
1622 1623 _exportfile(repo, revs, fp, switch_parent, opts, match)
1623 1624 else:
1624 1625 _exportfntemplate(repo, revs, fntemplate, switch_parent, opts, match)
1625 1626
1626 1627 def showmarker(fm, marker, index=None):
1627 1628 """utility function to display obsolescence marker in a readable way
1628 1629
1629 1630 To be used by debug function."""
1630 1631 if index is not None:
1631 1632 fm.write('index', '%i ', index)
1632 1633 fm.write('prednode', '%s ', hex(marker.prednode()))
1633 1634 succs = marker.succnodes()
1634 1635 fm.condwrite(succs, 'succnodes', '%s ',
1635 1636 fm.formatlist(map(hex, succs), name='node'))
1636 1637 fm.write('flag', '%X ', marker.flags())
1637 1638 parents = marker.parentnodes()
1638 1639 if parents is not None:
1639 1640 fm.write('parentnodes', '{%s} ',
1640 1641 fm.formatlist(map(hex, parents), name='node', sep=', '))
1641 1642 fm.write('date', '(%s) ', fm.formatdate(marker.date()))
1642 1643 meta = marker.metadata().copy()
1643 1644 meta.pop('date', None)
1644 1645 smeta = util.rapply(pycompat.maybebytestr, meta)
1645 1646 fm.write('metadata', '{%s}', fm.formatdict(smeta, fmt='%r: %r', sep=', '))
1646 1647 fm.plain('\n')
1647 1648
1648 1649 def finddate(ui, repo, date):
1649 1650 """Find the tipmost changeset that matches the given date spec"""
1650 1651
1651 1652 df = dateutil.matchdate(date)
1652 1653 m = scmutil.matchall(repo)
1653 1654 results = {}
1654 1655
1655 1656 def prep(ctx, fns):
1656 1657 d = ctx.date()
1657 1658 if df(d[0]):
1658 1659 results[ctx.rev()] = d
1659 1660
1660 1661 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1661 1662 rev = ctx.rev()
1662 1663 if rev in results:
1663 1664 ui.status(_("found revision %s from %s\n") %
1664 1665 (rev, dateutil.datestr(results[rev])))
1665 1666 return '%d' % rev
1666 1667
1667 1668 raise error.Abort(_("revision matching date not found"))
1668 1669
1669 1670 def increasingwindows(windowsize=8, sizelimit=512):
1670 1671 while True:
1671 1672 yield windowsize
1672 1673 if windowsize < sizelimit:
1673 1674 windowsize *= 2
1674 1675
1675 1676 def _walkrevs(repo, opts):
1676 1677 # Default --rev value depends on --follow but --follow behavior
1677 1678 # depends on revisions resolved from --rev...
1678 1679 follow = opts.get('follow') or opts.get('follow_first')
1679 1680 if opts.get('rev'):
1680 1681 revs = scmutil.revrange(repo, opts['rev'])
1681 1682 elif follow and repo.dirstate.p1() == nullid:
1682 1683 revs = smartset.baseset()
1683 1684 elif follow:
1684 1685 revs = repo.revs('reverse(:.)')
1685 1686 else:
1686 1687 revs = smartset.spanset(repo)
1687 1688 revs.reverse()
1688 1689 return revs
1689 1690
1690 1691 class FileWalkError(Exception):
1691 1692 pass
1692 1693
1693 1694 def walkfilerevs(repo, match, follow, revs, fncache):
1694 1695 '''Walks the file history for the matched files.
1695 1696
1696 1697 Returns the changeset revs that are involved in the file history.
1697 1698
1698 1699 Throws FileWalkError if the file history can't be walked using
1699 1700 filelogs alone.
1700 1701 '''
1701 1702 wanted = set()
1702 1703 copies = []
1703 1704 minrev, maxrev = min(revs), max(revs)
1704 1705 def filerevgen(filelog, last):
1705 1706 """
1706 1707 Only files, no patterns. Check the history of each file.
1707 1708
1708 1709 Examines filelog entries within minrev, maxrev linkrev range
1709 1710 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1710 1711 tuples in backwards order
1711 1712 """
1712 1713 cl_count = len(repo)
1713 1714 revs = []
1714 1715 for j in xrange(0, last + 1):
1715 1716 linkrev = filelog.linkrev(j)
1716 1717 if linkrev < minrev:
1717 1718 continue
1718 1719 # only yield rev for which we have the changelog, it can
1719 1720 # happen while doing "hg log" during a pull or commit
1720 1721 if linkrev >= cl_count:
1721 1722 break
1722 1723
1723 1724 parentlinkrevs = []
1724 1725 for p in filelog.parentrevs(j):
1725 1726 if p != nullrev:
1726 1727 parentlinkrevs.append(filelog.linkrev(p))
1727 1728 n = filelog.node(j)
1728 1729 revs.append((linkrev, parentlinkrevs,
1729 1730 follow and filelog.renamed(n)))
1730 1731
1731 1732 return reversed(revs)
1732 1733 def iterfiles():
1733 1734 pctx = repo['.']
1734 1735 for filename in match.files():
1735 1736 if follow:
1736 1737 if filename not in pctx:
1737 1738 raise error.Abort(_('cannot follow file not in parent '
1738 1739 'revision: "%s"') % filename)
1739 1740 yield filename, pctx[filename].filenode()
1740 1741 else:
1741 1742 yield filename, None
1742 1743 for filename_node in copies:
1743 1744 yield filename_node
1744 1745
1745 1746 for file_, node in iterfiles():
1746 1747 filelog = repo.file(file_)
1747 1748 if not len(filelog):
1748 1749 if node is None:
1749 1750 # A zero count may be a directory or deleted file, so
1750 1751 # try to find matching entries on the slow path.
1751 1752 if follow:
1752 1753 raise error.Abort(
1753 1754 _('cannot follow nonexistent file: "%s"') % file_)
1754 1755 raise FileWalkError("Cannot walk via filelog")
1755 1756 else:
1756 1757 continue
1757 1758
1758 1759 if node is None:
1759 1760 last = len(filelog) - 1
1760 1761 else:
1761 1762 last = filelog.rev(node)
1762 1763
1763 1764 # keep track of all ancestors of the file
1764 1765 ancestors = {filelog.linkrev(last)}
1765 1766
1766 1767 # iterate from latest to oldest revision
1767 1768 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1768 1769 if not follow:
1769 1770 if rev > maxrev:
1770 1771 continue
1771 1772 else:
1772 1773 # Note that last might not be the first interesting
1773 1774 # rev to us:
1774 1775 # if the file has been changed after maxrev, we'll
1775 1776 # have linkrev(last) > maxrev, and we still need
1776 1777 # to explore the file graph
1777 1778 if rev not in ancestors:
1778 1779 continue
1779 1780 # XXX insert 1327 fix here
1780 1781 if flparentlinkrevs:
1781 1782 ancestors.update(flparentlinkrevs)
1782 1783
1783 1784 fncache.setdefault(rev, []).append(file_)
1784 1785 wanted.add(rev)
1785 1786 if copied:
1786 1787 copies.append(copied)
1787 1788
1788 1789 return wanted
1789 1790
1790 1791 class _followfilter(object):
1791 1792 def __init__(self, repo, onlyfirst=False):
1792 1793 self.repo = repo
1793 1794 self.startrev = nullrev
1794 1795 self.roots = set()
1795 1796 self.onlyfirst = onlyfirst
1796 1797
1797 1798 def match(self, rev):
1798 1799 def realparents(rev):
1799 1800 if self.onlyfirst:
1800 1801 return self.repo.changelog.parentrevs(rev)[0:1]
1801 1802 else:
1802 1803 return filter(lambda x: x != nullrev,
1803 1804 self.repo.changelog.parentrevs(rev))
1804 1805
1805 1806 if self.startrev == nullrev:
1806 1807 self.startrev = rev
1807 1808 return True
1808 1809
1809 1810 if rev > self.startrev:
1810 1811 # forward: all descendants
1811 1812 if not self.roots:
1812 1813 self.roots.add(self.startrev)
1813 1814 for parent in realparents(rev):
1814 1815 if parent in self.roots:
1815 1816 self.roots.add(rev)
1816 1817 return True
1817 1818 else:
1818 1819 # backwards: all parents
1819 1820 if not self.roots:
1820 1821 self.roots.update(realparents(self.startrev))
1821 1822 if rev in self.roots:
1822 1823 self.roots.remove(rev)
1823 1824 self.roots.update(realparents(rev))
1824 1825 return True
1825 1826
1826 1827 return False
1827 1828
1828 1829 def walkchangerevs(repo, match, opts, prepare):
1829 1830 '''Iterate over files and the revs in which they changed.
1830 1831
1831 1832 Callers most commonly need to iterate backwards over the history
1832 1833 in which they are interested. Doing so has awful (quadratic-looking)
1833 1834 performance, so we use iterators in a "windowed" way.
1834 1835
1835 1836 We walk a window of revisions in the desired order. Within the
1836 1837 window, we first walk forwards to gather data, then in the desired
1837 1838 order (usually backwards) to display it.
1838 1839
1839 1840 This function returns an iterator yielding contexts. Before
1840 1841 yielding each context, the iterator will first call the prepare
1841 1842 function on each context in the window in forward order.'''
1842 1843
1843 1844 follow = opts.get('follow') or opts.get('follow_first')
1844 1845 revs = _walkrevs(repo, opts)
1845 1846 if not revs:
1846 1847 return []
1847 1848 wanted = set()
1848 1849 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
1849 1850 fncache = {}
1850 1851 change = repo.__getitem__
1851 1852
1852 1853 # First step is to fill wanted, the set of revisions that we want to yield.
1853 1854 # When it does not induce extra cost, we also fill fncache for revisions in
1854 1855 # wanted: a cache of filenames that were changed (ctx.files()) and that
1855 1856 # match the file filtering conditions.
1856 1857
1857 1858 if match.always():
1858 1859 # No files, no patterns. Display all revs.
1859 1860 wanted = revs
1860 1861 elif not slowpath:
1861 1862 # We only have to read through the filelog to find wanted revisions
1862 1863
1863 1864 try:
1864 1865 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1865 1866 except FileWalkError:
1866 1867 slowpath = True
1867 1868
1868 1869 # We decided to fall back to the slowpath because at least one
1869 1870 # of the paths was not a file. Check to see if at least one of them
1870 1871 # existed in history, otherwise simply return
1871 1872 for path in match.files():
1872 1873 if path == '.' or path in repo.store:
1873 1874 break
1874 1875 else:
1875 1876 return []
1876 1877
1877 1878 if slowpath:
1878 1879 # We have to read the changelog to match filenames against
1879 1880 # changed files
1880 1881
1881 1882 if follow:
1882 1883 raise error.Abort(_('can only follow copies/renames for explicit '
1883 1884 'filenames'))
1884 1885
1885 1886 # The slow path checks files modified in every changeset.
1886 1887 # This is really slow on large repos, so compute the set lazily.
1887 1888 class lazywantedset(object):
1888 1889 def __init__(self):
1889 1890 self.set = set()
1890 1891 self.revs = set(revs)
1891 1892
1892 1893 # No need to worry about locality here because it will be accessed
1893 1894 # in the same order as the increasing window below.
1894 1895 def __contains__(self, value):
1895 1896 if value in self.set:
1896 1897 return True
1897 1898 elif not value in self.revs:
1898 1899 return False
1899 1900 else:
1900 1901 self.revs.discard(value)
1901 1902 ctx = change(value)
1902 1903 matches = [f for f in ctx.files() if match(f)]
1903 1904 if matches:
1904 1905 fncache[value] = matches
1905 1906 self.set.add(value)
1906 1907 return True
1907 1908 return False
1908 1909
1909 1910 def discard(self, value):
1910 1911 self.revs.discard(value)
1911 1912 self.set.discard(value)
1912 1913
1913 1914 wanted = lazywantedset()
1914 1915
1915 1916 # it might be worthwhile to do this in the iterator if the rev range
1916 1917 # is descending and the prune args are all within that range
1917 1918 for rev in opts.get('prune', ()):
1918 1919 rev = repo[rev].rev()
1919 1920 ff = _followfilter(repo)
1920 1921 stop = min(revs[0], revs[-1])
1921 1922 for x in xrange(rev, stop - 1, -1):
1922 1923 if ff.match(x):
1923 1924 wanted = wanted - [x]
1924 1925
1925 1926 # Now that wanted is correctly initialized, we can iterate over the
1926 1927 # revision range, yielding only revisions in wanted.
1927 1928 def iterate():
1928 1929 if follow and match.always():
1929 1930 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
1930 1931 def want(rev):
1931 1932 return ff.match(rev) and rev in wanted
1932 1933 else:
1933 1934 def want(rev):
1934 1935 return rev in wanted
1935 1936
1936 1937 it = iter(revs)
1937 1938 stopiteration = False
1938 1939 for windowsize in increasingwindows():
1939 1940 nrevs = []
1940 1941 for i in xrange(windowsize):
1941 1942 rev = next(it, None)
1942 1943 if rev is None:
1943 1944 stopiteration = True
1944 1945 break
1945 1946 elif want(rev):
1946 1947 nrevs.append(rev)
1947 1948 for rev in sorted(nrevs):
1948 1949 fns = fncache.get(rev)
1949 1950 ctx = change(rev)
1950 1951 if not fns:
1951 1952 def fns_generator():
1952 1953 for f in ctx.files():
1953 1954 if match(f):
1954 1955 yield f
1955 1956 fns = fns_generator()
1956 1957 prepare(ctx, fns)
1957 1958 for rev in nrevs:
1958 1959 yield change(rev)
1959 1960
1960 1961 if stopiteration:
1961 1962 break
1962 1963
1963 1964 return iterate()
1964 1965
1965 1966 def add(ui, repo, match, prefix, explicitonly, **opts):
1966 1967 join = lambda f: os.path.join(prefix, f)
1967 1968 bad = []
1968 1969
1969 1970 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
1970 1971 names = []
1971 1972 wctx = repo[None]
1972 1973 cca = None
1973 1974 abort, warn = scmutil.checkportabilityalert(ui)
1974 1975 if abort or warn:
1975 1976 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
1976 1977
1977 1978 badmatch = matchmod.badmatch(match, badfn)
1978 1979 dirstate = repo.dirstate
1979 1980 # We don't want to just call wctx.walk here, since it would return a lot of
1980 1981 # clean files, which we aren't interested in and takes time.
1981 1982 for f in sorted(dirstate.walk(badmatch, subrepos=sorted(wctx.substate),
1982 1983 unknown=True, ignored=False, full=False)):
1983 1984 exact = match.exact(f)
1984 1985 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
1985 1986 if cca:
1986 1987 cca(f)
1987 1988 names.append(f)
1988 1989 if ui.verbose or not exact:
1989 1990 ui.status(_('adding %s\n') % match.rel(f))
1990 1991
1991 1992 for subpath in sorted(wctx.substate):
1992 1993 sub = wctx.sub(subpath)
1993 1994 try:
1994 1995 submatch = matchmod.subdirmatcher(subpath, match)
1995 1996 if opts.get(r'subrepos'):
1996 1997 bad.extend(sub.add(ui, submatch, prefix, False, **opts))
1997 1998 else:
1998 1999 bad.extend(sub.add(ui, submatch, prefix, True, **opts))
1999 2000 except error.LookupError:
2000 2001 ui.status(_("skipping missing subrepository: %s\n")
2001 2002 % join(subpath))
2002 2003
2003 2004 if not opts.get(r'dry_run'):
2004 2005 rejected = wctx.add(names, prefix)
2005 2006 bad.extend(f for f in rejected if f in match.files())
2006 2007 return bad
2007 2008
2008 2009 def addwebdirpath(repo, serverpath, webconf):
2009 2010 webconf[serverpath] = repo.root
2010 2011 repo.ui.debug('adding %s = %s\n' % (serverpath, repo.root))
2011 2012
2012 2013 for r in repo.revs('filelog("path:.hgsub")'):
2013 2014 ctx = repo[r]
2014 2015 for subpath in ctx.substate:
2015 2016 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2016 2017
2017 2018 def forget(ui, repo, match, prefix, explicitonly, dryrun):
2018 2019 join = lambda f: os.path.join(prefix, f)
2019 2020 bad = []
2020 2021 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2021 2022 wctx = repo[None]
2022 2023 forgot = []
2023 2024
2024 2025 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2025 2026 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2026 2027 if explicitonly:
2027 2028 forget = [f for f in forget if match.exact(f)]
2028 2029
2029 2030 for subpath in sorted(wctx.substate):
2030 2031 sub = wctx.sub(subpath)
2031 2032 try:
2032 2033 submatch = matchmod.subdirmatcher(subpath, match)
2033 2034 subbad, subforgot = sub.forget(submatch, prefix, dryrun=dryrun)
2034 2035 bad.extend([subpath + '/' + f for f in subbad])
2035 2036 forgot.extend([subpath + '/' + f for f in subforgot])
2036 2037 except error.LookupError:
2037 2038 ui.status(_("skipping missing subrepository: %s\n")
2038 2039 % join(subpath))
2039 2040
2040 2041 if not explicitonly:
2041 2042 for f in match.files():
2042 2043 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2043 2044 if f not in forgot:
2044 2045 if repo.wvfs.exists(f):
2045 2046 # Don't complain if the exact case match wasn't given.
2046 2047 # But don't do this until after checking 'forgot', so
2047 2048 # that subrepo files aren't normalized, and this op is
2048 2049 # purely from data cached by the status walk above.
2049 2050 if repo.dirstate.normalize(f) in repo.dirstate:
2050 2051 continue
2051 2052 ui.warn(_('not removing %s: '
2052 2053 'file is already untracked\n')
2053 2054 % match.rel(f))
2054 2055 bad.append(f)
2055 2056
2056 2057 for f in forget:
2057 2058 if ui.verbose or not match.exact(f):
2058 2059 ui.status(_('removing %s\n') % match.rel(f))
2059 2060
2060 2061 if not dryrun:
2061 2062 rejected = wctx.forget(forget, prefix)
2062 2063 bad.extend(f for f in rejected if f in match.files())
2063 2064 forgot.extend(f for f in forget if f not in rejected)
2064 2065 return bad, forgot
2065 2066
2066 2067 def files(ui, ctx, m, fm, fmt, subrepos):
2067 2068 rev = ctx.rev()
2068 2069 ret = 1
2069 2070 ds = ctx.repo().dirstate
2070 2071
2071 2072 for f in ctx.matches(m):
2072 2073 if rev is None and ds[f] == 'r':
2073 2074 continue
2074 2075 fm.startitem()
2075 2076 if ui.verbose:
2076 2077 fc = ctx[f]
2077 2078 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2078 2079 fm.data(abspath=f)
2079 2080 fm.write('path', fmt, m.rel(f))
2080 2081 ret = 0
2081 2082
2082 2083 for subpath in sorted(ctx.substate):
2083 2084 submatch = matchmod.subdirmatcher(subpath, m)
2084 2085 if (subrepos or m.exact(subpath) or any(submatch.files())):
2085 2086 sub = ctx.sub(subpath)
2086 2087 try:
2087 2088 recurse = m.exact(subpath) or subrepos
2088 2089 if sub.printfiles(ui, submatch, fm, fmt, recurse) == 0:
2089 2090 ret = 0
2090 2091 except error.LookupError:
2091 2092 ui.status(_("skipping missing subrepository: %s\n")
2092 2093 % m.abs(subpath))
2093 2094
2094 2095 return ret
2095 2096
2096 2097 def remove(ui, repo, m, prefix, after, force, subrepos, dryrun, warnings=None):
2097 2098 join = lambda f: os.path.join(prefix, f)
2098 2099 ret = 0
2099 2100 s = repo.status(match=m, clean=True)
2100 2101 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2101 2102
2102 2103 wctx = repo[None]
2103 2104
2104 2105 if warnings is None:
2105 2106 warnings = []
2106 2107 warn = True
2107 2108 else:
2108 2109 warn = False
2109 2110
2110 2111 subs = sorted(wctx.substate)
2111 2112 total = len(subs)
2112 2113 count = 0
2113 2114 for subpath in subs:
2114 2115 count += 1
2115 2116 submatch = matchmod.subdirmatcher(subpath, m)
2116 2117 if subrepos or m.exact(subpath) or any(submatch.files()):
2117 2118 ui.progress(_('searching'), count, total=total, unit=_('subrepos'))
2118 2119 sub = wctx.sub(subpath)
2119 2120 try:
2120 2121 if sub.removefiles(submatch, prefix, after, force, subrepos,
2121 2122 dryrun, warnings):
2122 2123 ret = 1
2123 2124 except error.LookupError:
2124 2125 warnings.append(_("skipping missing subrepository: %s\n")
2125 2126 % join(subpath))
2126 2127 ui.progress(_('searching'), None)
2127 2128
2128 2129 # warn about failure to delete explicit files/dirs
2129 2130 deleteddirs = util.dirs(deleted)
2130 2131 files = m.files()
2131 2132 total = len(files)
2132 2133 count = 0
2133 2134 for f in files:
2134 2135 def insubrepo():
2135 2136 for subpath in wctx.substate:
2136 2137 if f.startswith(subpath + '/'):
2137 2138 return True
2138 2139 return False
2139 2140
2140 2141 count += 1
2141 2142 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2142 2143 isdir = f in deleteddirs or wctx.hasdir(f)
2143 2144 if (f in repo.dirstate or isdir or f == '.'
2144 2145 or insubrepo() or f in subs):
2145 2146 continue
2146 2147
2147 2148 if repo.wvfs.exists(f):
2148 2149 if repo.wvfs.isdir(f):
2149 2150 warnings.append(_('not removing %s: no tracked files\n')
2150 2151 % m.rel(f))
2151 2152 else:
2152 2153 warnings.append(_('not removing %s: file is untracked\n')
2153 2154 % m.rel(f))
2154 2155 # missing files will generate a warning elsewhere
2155 2156 ret = 1
2156 2157 ui.progress(_('deleting'), None)
2157 2158
2158 2159 if force:
2159 2160 list = modified + deleted + clean + added
2160 2161 elif after:
2161 2162 list = deleted
2162 2163 remaining = modified + added + clean
2163 2164 total = len(remaining)
2164 2165 count = 0
2165 2166 for f in remaining:
2166 2167 count += 1
2167 2168 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2168 2169 if ui.verbose or (f in files):
2169 2170 warnings.append(_('not removing %s: file still exists\n')
2170 2171 % m.rel(f))
2171 2172 ret = 1
2172 2173 ui.progress(_('skipping'), None)
2173 2174 else:
2174 2175 list = deleted + clean
2175 2176 total = len(modified) + len(added)
2176 2177 count = 0
2177 2178 for f in modified:
2178 2179 count += 1
2179 2180 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2180 2181 warnings.append(_('not removing %s: file is modified (use -f'
2181 2182 ' to force removal)\n') % m.rel(f))
2182 2183 ret = 1
2183 2184 for f in added:
2184 2185 count += 1
2185 2186 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2186 2187 warnings.append(_("not removing %s: file has been marked for add"
2187 2188 " (use 'hg forget' to undo add)\n") % m.rel(f))
2188 2189 ret = 1
2189 2190 ui.progress(_('skipping'), None)
2190 2191
2191 2192 list = sorted(list)
2192 2193 total = len(list)
2193 2194 count = 0
2194 2195 for f in list:
2195 2196 count += 1
2196 2197 if ui.verbose or not m.exact(f):
2197 2198 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2198 2199 ui.status(_('removing %s\n') % m.rel(f))
2199 2200 ui.progress(_('deleting'), None)
2200 2201
2201 2202 if not dryrun:
2202 2203 with repo.wlock():
2203 2204 if not after:
2204 2205 for f in list:
2205 2206 if f in added:
2206 2207 continue # we never unlink added files on remove
2207 2208 repo.wvfs.unlinkpath(f, ignoremissing=True)
2208 2209 repo[None].forget(list)
2209 2210
2210 2211 if warn:
2211 2212 for warning in warnings:
2212 2213 ui.warn(warning)
2213 2214
2214 2215 return ret
2215 2216
2216 2217 def _updatecatformatter(fm, ctx, matcher, path, decode):
2217 2218 """Hook for adding data to the formatter used by ``hg cat``.
2218 2219
2219 2220 Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
2220 2221 this method first."""
2221 2222 data = ctx[path].data()
2222 2223 if decode:
2223 2224 data = ctx.repo().wwritedata(path, data)
2224 2225 fm.startitem()
2225 2226 fm.write('data', '%s', data)
2226 2227 fm.data(abspath=path, path=matcher.rel(path))
2227 2228
2228 2229 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2229 2230 err = 1
2230 2231 opts = pycompat.byteskwargs(opts)
2231 2232
2232 2233 def write(path):
2233 2234 filename = None
2234 2235 if fntemplate:
2235 2236 filename = makefilename(ctx, fntemplate,
2236 2237 pathname=os.path.join(prefix, path))
2237 2238 # attempt to create the directory if it does not already exist
2238 2239 try:
2239 2240 os.makedirs(os.path.dirname(filename))
2240 2241 except OSError:
2241 2242 pass
2242 2243 with formatter.maybereopen(basefm, filename) as fm:
2243 2244 _updatecatformatter(fm, ctx, matcher, path, opts.get('decode'))
2244 2245
2245 2246 # Automation often uses hg cat on single files, so special case it
2246 2247 # for performance to avoid the cost of parsing the manifest.
2247 2248 if len(matcher.files()) == 1 and not matcher.anypats():
2248 2249 file = matcher.files()[0]
2249 2250 mfl = repo.manifestlog
2250 2251 mfnode = ctx.manifestnode()
2251 2252 try:
2252 2253 if mfnode and mfl[mfnode].find(file)[0]:
2253 2254 scmutil.fileprefetchhooks(repo, ctx, [file])
2254 2255 write(file)
2255 2256 return 0
2256 2257 except KeyError:
2257 2258 pass
2258 2259
2259 2260 files = [f for f in ctx.walk(matcher)]
2260 2261 scmutil.fileprefetchhooks(repo, ctx, files)
2261 2262
2262 2263 for abs in files:
2263 2264 write(abs)
2264 2265 err = 0
2265 2266
2266 2267 for subpath in sorted(ctx.substate):
2267 2268 sub = ctx.sub(subpath)
2268 2269 try:
2269 2270 submatch = matchmod.subdirmatcher(subpath, matcher)
2270 2271
2271 2272 if not sub.cat(submatch, basefm, fntemplate,
2272 2273 os.path.join(prefix, sub._path),
2273 2274 **pycompat.strkwargs(opts)):
2274 2275 err = 0
2275 2276 except error.RepoLookupError:
2276 2277 ui.status(_("skipping missing subrepository: %s\n")
2277 2278 % os.path.join(prefix, subpath))
2278 2279
2279 2280 return err
2280 2281
2281 2282 def commit(ui, repo, commitfunc, pats, opts):
2282 2283 '''commit the specified files or all outstanding changes'''
2283 2284 date = opts.get('date')
2284 2285 if date:
2285 2286 opts['date'] = dateutil.parsedate(date)
2286 2287 message = logmessage(ui, opts)
2287 2288 matcher = scmutil.match(repo[None], pats, opts)
2288 2289
2289 2290 dsguard = None
2290 2291 # extract addremove carefully -- this function can be called from a command
2291 2292 # that doesn't support addremove
2292 2293 if opts.get('addremove'):
2293 2294 dsguard = dirstateguard.dirstateguard(repo, 'commit')
2294 2295 with dsguard or util.nullcontextmanager():
2295 2296 if dsguard:
2296 2297 if scmutil.addremove(repo, matcher, "", opts) != 0:
2297 2298 raise error.Abort(
2298 2299 _("failed to mark all new/missing files as added/removed"))
2299 2300
2300 2301 return commitfunc(ui, repo, message, matcher, opts)
2301 2302
2302 2303 def samefile(f, ctx1, ctx2):
2303 2304 if f in ctx1.manifest():
2304 2305 a = ctx1.filectx(f)
2305 2306 if f in ctx2.manifest():
2306 2307 b = ctx2.filectx(f)
2307 2308 return (not a.cmp(b)
2308 2309 and a.flags() == b.flags())
2309 2310 else:
2310 2311 return False
2311 2312 else:
2312 2313 return f not in ctx2.manifest()
2313 2314
2314 2315 def amend(ui, repo, old, extra, pats, opts):
2315 2316 # avoid cycle context -> subrepo -> cmdutil
2316 2317 from . import context
2317 2318
2318 2319 # amend will reuse the existing user if not specified, but the obsolete
2319 2320 # marker creation requires that the current user's name is specified.
2320 2321 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2321 2322 ui.username() # raise exception if username not set
2322 2323
2323 2324 ui.note(_('amending changeset %s\n') % old)
2324 2325 base = old.p1()
2325 2326
2326 2327 with repo.wlock(), repo.lock(), repo.transaction('amend'):
2327 2328 # Participating changesets:
2328 2329 #
2329 2330 # wctx o - workingctx that contains changes from working copy
2330 2331 # | to go into amending commit
2331 2332 # |
2332 2333 # old o - changeset to amend
2333 2334 # |
2334 2335 # base o - first parent of the changeset to amend
2335 2336 wctx = repo[None]
2336 2337
2337 2338 # Copy to avoid mutating input
2338 2339 extra = extra.copy()
2339 2340 # Update extra dict from amended commit (e.g. to preserve graft
2340 2341 # source)
2341 2342 extra.update(old.extra())
2342 2343
2343 2344 # Also update it from the from the wctx
2344 2345 extra.update(wctx.extra())
2345 2346
2346 2347 user = opts.get('user') or old.user()
2347 2348 date = opts.get('date') or old.date()
2348 2349
2349 2350 # Parse the date to allow comparison between date and old.date()
2350 2351 date = dateutil.parsedate(date)
2351 2352
2352 2353 if len(old.parents()) > 1:
2353 2354 # ctx.files() isn't reliable for merges, so fall back to the
2354 2355 # slower repo.status() method
2355 2356 files = set([fn for st in repo.status(base, old)[:3]
2356 2357 for fn in st])
2357 2358 else:
2358 2359 files = set(old.files())
2359 2360
2360 2361 # add/remove the files to the working copy if the "addremove" option
2361 2362 # was specified.
2362 2363 matcher = scmutil.match(wctx, pats, opts)
2363 2364 if (opts.get('addremove')
2364 2365 and scmutil.addremove(repo, matcher, "", opts)):
2365 2366 raise error.Abort(
2366 2367 _("failed to mark all new/missing files as added/removed"))
2367 2368
2368 2369 # Check subrepos. This depends on in-place wctx._status update in
2369 2370 # subrepo.precommit(). To minimize the risk of this hack, we do
2370 2371 # nothing if .hgsub does not exist.
2371 2372 if '.hgsub' in wctx or '.hgsub' in old:
2372 2373 subs, commitsubs, newsubstate = subrepoutil.precommit(
2373 2374 ui, wctx, wctx._status, matcher)
2374 2375 # amend should abort if commitsubrepos is enabled
2375 2376 assert not commitsubs
2376 2377 if subs:
2377 2378 subrepoutil.writestate(repo, newsubstate)
2378 2379
2379 2380 ms = mergemod.mergestate.read(repo)
2380 2381 mergeutil.checkunresolved(ms)
2381 2382
2382 2383 filestoamend = set(f for f in wctx.files() if matcher(f))
2383 2384
2384 2385 changes = (len(filestoamend) > 0)
2385 2386 if changes:
2386 2387 # Recompute copies (avoid recording a -> b -> a)
2387 2388 copied = copies.pathcopies(base, wctx, matcher)
2388 2389 if old.p2:
2389 2390 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
2390 2391
2391 2392 # Prune files which were reverted by the updates: if old
2392 2393 # introduced file X and the file was renamed in the working
2393 2394 # copy, then those two files are the same and
2394 2395 # we can discard X from our list of files. Likewise if X
2395 2396 # was removed, it's no longer relevant. If X is missing (aka
2396 2397 # deleted), old X must be preserved.
2397 2398 files.update(filestoamend)
2398 2399 files = [f for f in files if (not samefile(f, wctx, base)
2399 2400 or f in wctx.deleted())]
2400 2401
2401 2402 def filectxfn(repo, ctx_, path):
2402 2403 try:
2403 2404 # If the file being considered is not amongst the files
2404 2405 # to be amended, we should return the file context from the
2405 2406 # old changeset. This avoids issues when only some files in
2406 2407 # the working copy are being amended but there are also
2407 2408 # changes to other files from the old changeset.
2408 2409 if path not in filestoamend:
2409 2410 return old.filectx(path)
2410 2411
2411 2412 # Return None for removed files.
2412 2413 if path in wctx.removed():
2413 2414 return None
2414 2415
2415 2416 fctx = wctx[path]
2416 2417 flags = fctx.flags()
2417 2418 mctx = context.memfilectx(repo, ctx_,
2418 2419 fctx.path(), fctx.data(),
2419 2420 islink='l' in flags,
2420 2421 isexec='x' in flags,
2421 2422 copied=copied.get(path))
2422 2423 return mctx
2423 2424 except KeyError:
2424 2425 return None
2425 2426 else:
2426 2427 ui.note(_('copying changeset %s to %s\n') % (old, base))
2427 2428
2428 2429 # Use version of files as in the old cset
2429 2430 def filectxfn(repo, ctx_, path):
2430 2431 try:
2431 2432 return old.filectx(path)
2432 2433 except KeyError:
2433 2434 return None
2434 2435
2435 2436 # See if we got a message from -m or -l, if not, open the editor with
2436 2437 # the message of the changeset to amend.
2437 2438 message = logmessage(ui, opts)
2438 2439
2439 2440 editform = mergeeditform(old, 'commit.amend')
2440 2441 editor = getcommiteditor(editform=editform,
2441 2442 **pycompat.strkwargs(opts))
2442 2443
2443 2444 if not message:
2444 2445 editor = getcommiteditor(edit=True, editform=editform)
2445 2446 message = old.description()
2446 2447
2447 2448 pureextra = extra.copy()
2448 2449 extra['amend_source'] = old.hex()
2449 2450
2450 2451 new = context.memctx(repo,
2451 2452 parents=[base.node(), old.p2().node()],
2452 2453 text=message,
2453 2454 files=files,
2454 2455 filectxfn=filectxfn,
2455 2456 user=user,
2456 2457 date=date,
2457 2458 extra=extra,
2458 2459 editor=editor)
2459 2460
2460 2461 newdesc = changelog.stripdesc(new.description())
2461 2462 if ((not changes)
2462 2463 and newdesc == old.description()
2463 2464 and user == old.user()
2464 2465 and date == old.date()
2465 2466 and pureextra == old.extra()):
2466 2467 # nothing changed. continuing here would create a new node
2467 2468 # anyway because of the amend_source noise.
2468 2469 #
2469 2470 # This not what we expect from amend.
2470 2471 return old.node()
2471 2472
2472 2473 if opts.get('secret'):
2473 2474 commitphase = 'secret'
2474 2475 else:
2475 2476 commitphase = old.phase()
2476 2477 overrides = {('phases', 'new-commit'): commitphase}
2477 2478 with ui.configoverride(overrides, 'amend'):
2478 2479 newid = repo.commitctx(new)
2479 2480
2480 2481 # Reroute the working copy parent to the new changeset
2481 2482 repo.setparents(newid, nullid)
2482 2483 mapping = {old.node(): (newid,)}
2483 2484 obsmetadata = None
2484 2485 if opts.get('note'):
2485 2486 obsmetadata = {'note': opts['note']}
2486 2487 scmutil.cleanupnodes(repo, mapping, 'amend', metadata=obsmetadata)
2487 2488
2488 2489 # Fixing the dirstate because localrepo.commitctx does not update
2489 2490 # it. This is rather convenient because we did not need to update
2490 2491 # the dirstate for all the files in the new commit which commitctx
2491 2492 # could have done if it updated the dirstate. Now, we can
2492 2493 # selectively update the dirstate only for the amended files.
2493 2494 dirstate = repo.dirstate
2494 2495
2495 2496 # Update the state of the files which were added and
2496 2497 # and modified in the amend to "normal" in the dirstate.
2497 2498 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
2498 2499 for f in normalfiles:
2499 2500 dirstate.normal(f)
2500 2501
2501 2502 # Update the state of files which were removed in the amend
2502 2503 # to "removed" in the dirstate.
2503 2504 removedfiles = set(wctx.removed()) & filestoamend
2504 2505 for f in removedfiles:
2505 2506 dirstate.drop(f)
2506 2507
2507 2508 return newid
2508 2509
2509 2510 def commiteditor(repo, ctx, subs, editform=''):
2510 2511 if ctx.description():
2511 2512 return ctx.description()
2512 2513 return commitforceeditor(repo, ctx, subs, editform=editform,
2513 2514 unchangedmessagedetection=True)
2514 2515
2515 2516 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2516 2517 editform='', unchangedmessagedetection=False):
2517 2518 if not extramsg:
2518 2519 extramsg = _("Leave message empty to abort commit.")
2519 2520
2520 2521 forms = [e for e in editform.split('.') if e]
2521 2522 forms.insert(0, 'changeset')
2522 2523 templatetext = None
2523 2524 while forms:
2524 2525 ref = '.'.join(forms)
2525 2526 if repo.ui.config('committemplate', ref):
2526 2527 templatetext = committext = buildcommittemplate(
2527 2528 repo, ctx, subs, extramsg, ref)
2528 2529 break
2529 2530 forms.pop()
2530 2531 else:
2531 2532 committext = buildcommittext(repo, ctx, subs, extramsg)
2532 2533
2533 2534 # run editor in the repository root
2534 2535 olddir = pycompat.getcwd()
2535 2536 os.chdir(repo.root)
2536 2537
2537 2538 # make in-memory changes visible to external process
2538 2539 tr = repo.currenttransaction()
2539 2540 repo.dirstate.write(tr)
2540 2541 pending = tr and tr.writepending() and repo.root
2541 2542
2542 2543 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
2543 2544 editform=editform, pending=pending,
2544 2545 repopath=repo.path, action='commit')
2545 2546 text = editortext
2546 2547
2547 2548 # strip away anything below this special string (used for editors that want
2548 2549 # to display the diff)
2549 2550 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
2550 2551 if stripbelow:
2551 2552 text = text[:stripbelow.start()]
2552 2553
2553 2554 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2554 2555 os.chdir(olddir)
2555 2556
2556 2557 if finishdesc:
2557 2558 text = finishdesc(text)
2558 2559 if not text.strip():
2559 2560 raise error.Abort(_("empty commit message"))
2560 2561 if unchangedmessagedetection and editortext == templatetext:
2561 2562 raise error.Abort(_("commit message unchanged"))
2562 2563
2563 2564 return text
2564 2565
2565 2566 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
2566 2567 ui = repo.ui
2567 2568 spec = formatter.templatespec(ref, None, None)
2568 2569 t = logcmdutil.changesettemplater(ui, repo, spec)
2569 2570 t.t.cache.update((k, templater.unquotestring(v))
2570 2571 for k, v in repo.ui.configitems('committemplate'))
2571 2572
2572 2573 if not extramsg:
2573 2574 extramsg = '' # ensure that extramsg is string
2574 2575
2575 2576 ui.pushbuffer()
2576 2577 t.show(ctx, extramsg=extramsg)
2577 2578 return ui.popbuffer()
2578 2579
2579 2580 def hgprefix(msg):
2580 2581 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
2581 2582
2582 2583 def buildcommittext(repo, ctx, subs, extramsg):
2583 2584 edittext = []
2584 2585 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2585 2586 if ctx.description():
2586 2587 edittext.append(ctx.description())
2587 2588 edittext.append("")
2588 2589 edittext.append("") # Empty line between message and comments.
2589 2590 edittext.append(hgprefix(_("Enter commit message."
2590 2591 " Lines beginning with 'HG:' are removed.")))
2591 2592 edittext.append(hgprefix(extramsg))
2592 2593 edittext.append("HG: --")
2593 2594 edittext.append(hgprefix(_("user: %s") % ctx.user()))
2594 2595 if ctx.p2():
2595 2596 edittext.append(hgprefix(_("branch merge")))
2596 2597 if ctx.branch():
2597 2598 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
2598 2599 if bookmarks.isactivewdirparent(repo):
2599 2600 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
2600 2601 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
2601 2602 edittext.extend([hgprefix(_("added %s") % f) for f in added])
2602 2603 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
2603 2604 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
2604 2605 if not added and not modified and not removed:
2605 2606 edittext.append(hgprefix(_("no files changed")))
2606 2607 edittext.append("")
2607 2608
2608 2609 return "\n".join(edittext)
2609 2610
2610 2611 def commitstatus(repo, node, branch, bheads=None, opts=None):
2611 2612 if opts is None:
2612 2613 opts = {}
2613 2614 ctx = repo[node]
2614 2615 parents = ctx.parents()
2615 2616
2616 2617 if (not opts.get('amend') and bheads and node not in bheads and not
2617 2618 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2618 2619 repo.ui.status(_('created new head\n'))
2619 2620 # The message is not printed for initial roots. For the other
2620 2621 # changesets, it is printed in the following situations:
2621 2622 #
2622 2623 # Par column: for the 2 parents with ...
2623 2624 # N: null or no parent
2624 2625 # B: parent is on another named branch
2625 2626 # C: parent is a regular non head changeset
2626 2627 # H: parent was a branch head of the current branch
2627 2628 # Msg column: whether we print "created new head" message
2628 2629 # In the following, it is assumed that there already exists some
2629 2630 # initial branch heads of the current branch, otherwise nothing is
2630 2631 # printed anyway.
2631 2632 #
2632 2633 # Par Msg Comment
2633 2634 # N N y additional topo root
2634 2635 #
2635 2636 # B N y additional branch root
2636 2637 # C N y additional topo head
2637 2638 # H N n usual case
2638 2639 #
2639 2640 # B B y weird additional branch root
2640 2641 # C B y branch merge
2641 2642 # H B n merge with named branch
2642 2643 #
2643 2644 # C C y additional head from merge
2644 2645 # C H n merge with a head
2645 2646 #
2646 2647 # H H n head merge: head count decreases
2647 2648
2648 2649 if not opts.get('close_branch'):
2649 2650 for r in parents:
2650 2651 if r.closesbranch() and r.branch() == branch:
2651 2652 repo.ui.status(_('reopening closed branch head %d\n') % r.rev())
2652 2653
2653 2654 if repo.ui.debugflag:
2654 2655 repo.ui.write(_('committed changeset %d:%s\n') % (ctx.rev(), ctx.hex()))
2655 2656 elif repo.ui.verbose:
2656 2657 repo.ui.write(_('committed changeset %d:%s\n') % (ctx.rev(), ctx))
2657 2658
2658 2659 def postcommitstatus(repo, pats, opts):
2659 2660 return repo.status(match=scmutil.match(repo[None], pats, opts))
2660 2661
2661 2662 def revert(ui, repo, ctx, parents, *pats, **opts):
2662 2663 opts = pycompat.byteskwargs(opts)
2663 2664 parent, p2 = parents
2664 2665 node = ctx.node()
2665 2666
2666 2667 mf = ctx.manifest()
2667 2668 if node == p2:
2668 2669 parent = p2
2669 2670
2670 2671 # need all matching names in dirstate and manifest of target rev,
2671 2672 # so have to walk both. do not print errors if files exist in one
2672 2673 # but not other. in both cases, filesets should be evaluated against
2673 2674 # workingctx to get consistent result (issue4497). this means 'set:**'
2674 2675 # cannot be used to select missing files from target rev.
2675 2676
2676 2677 # `names` is a mapping for all elements in working copy and target revision
2677 2678 # The mapping is in the form:
2678 2679 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2679 2680 names = {}
2680 2681
2681 2682 with repo.wlock():
2682 2683 ## filling of the `names` mapping
2683 2684 # walk dirstate to fill `names`
2684 2685
2685 2686 interactive = opts.get('interactive', False)
2686 2687 wctx = repo[None]
2687 2688 m = scmutil.match(wctx, pats, opts)
2688 2689
2689 2690 # we'll need this later
2690 2691 targetsubs = sorted(s for s in wctx.substate if m(s))
2691 2692
2692 2693 if not m.always():
2693 2694 matcher = matchmod.badmatch(m, lambda x, y: False)
2694 2695 for abs in wctx.walk(matcher):
2695 2696 names[abs] = m.rel(abs), m.exact(abs)
2696 2697
2697 2698 # walk target manifest to fill `names`
2698 2699
2699 2700 def badfn(path, msg):
2700 2701 if path in names:
2701 2702 return
2702 2703 if path in ctx.substate:
2703 2704 return
2704 2705 path_ = path + '/'
2705 2706 for f in names:
2706 2707 if f.startswith(path_):
2707 2708 return
2708 2709 ui.warn("%s: %s\n" % (m.rel(path), msg))
2709 2710
2710 2711 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
2711 2712 if abs not in names:
2712 2713 names[abs] = m.rel(abs), m.exact(abs)
2713 2714
2714 2715 # Find status of all file in `names`.
2715 2716 m = scmutil.matchfiles(repo, names)
2716 2717
2717 2718 changes = repo.status(node1=node, match=m,
2718 2719 unknown=True, ignored=True, clean=True)
2719 2720 else:
2720 2721 changes = repo.status(node1=node, match=m)
2721 2722 for kind in changes:
2722 2723 for abs in kind:
2723 2724 names[abs] = m.rel(abs), m.exact(abs)
2724 2725
2725 2726 m = scmutil.matchfiles(repo, names)
2726 2727
2727 2728 modified = set(changes.modified)
2728 2729 added = set(changes.added)
2729 2730 removed = set(changes.removed)
2730 2731 _deleted = set(changes.deleted)
2731 2732 unknown = set(changes.unknown)
2732 2733 unknown.update(changes.ignored)
2733 2734 clean = set(changes.clean)
2734 2735 modadded = set()
2735 2736
2736 2737 # We need to account for the state of the file in the dirstate,
2737 2738 # even when we revert against something else than parent. This will
2738 2739 # slightly alter the behavior of revert (doing back up or not, delete
2739 2740 # or just forget etc).
2740 2741 if parent == node:
2741 2742 dsmodified = modified
2742 2743 dsadded = added
2743 2744 dsremoved = removed
2744 2745 # store all local modifications, useful later for rename detection
2745 2746 localchanges = dsmodified | dsadded
2746 2747 modified, added, removed = set(), set(), set()
2747 2748 else:
2748 2749 changes = repo.status(node1=parent, match=m)
2749 2750 dsmodified = set(changes.modified)
2750 2751 dsadded = set(changes.added)
2751 2752 dsremoved = set(changes.removed)
2752 2753 # store all local modifications, useful later for rename detection
2753 2754 localchanges = dsmodified | dsadded
2754 2755
2755 2756 # only take into account for removes between wc and target
2756 2757 clean |= dsremoved - removed
2757 2758 dsremoved &= removed
2758 2759 # distinct between dirstate remove and other
2759 2760 removed -= dsremoved
2760 2761
2761 2762 modadded = added & dsmodified
2762 2763 added -= modadded
2763 2764
2764 2765 # tell newly modified apart.
2765 2766 dsmodified &= modified
2766 2767 dsmodified |= modified & dsadded # dirstate added may need backup
2767 2768 modified -= dsmodified
2768 2769
2769 2770 # We need to wait for some post-processing to update this set
2770 2771 # before making the distinction. The dirstate will be used for
2771 2772 # that purpose.
2772 2773 dsadded = added
2773 2774
2774 2775 # in case of merge, files that are actually added can be reported as
2775 2776 # modified, we need to post process the result
2776 2777 if p2 != nullid:
2777 2778 mergeadd = set(dsmodified)
2778 2779 for path in dsmodified:
2779 2780 if path in mf:
2780 2781 mergeadd.remove(path)
2781 2782 dsadded |= mergeadd
2782 2783 dsmodified -= mergeadd
2783 2784
2784 2785 # if f is a rename, update `names` to also revert the source
2785 2786 cwd = repo.getcwd()
2786 2787 for f in localchanges:
2787 2788 src = repo.dirstate.copied(f)
2788 2789 # XXX should we check for rename down to target node?
2789 2790 if src and src not in names and repo.dirstate[src] == 'r':
2790 2791 dsremoved.add(src)
2791 2792 names[src] = (repo.pathto(src, cwd), True)
2792 2793
2793 2794 # determine the exact nature of the deleted changesets
2794 2795 deladded = set(_deleted)
2795 2796 for path in _deleted:
2796 2797 if path in mf:
2797 2798 deladded.remove(path)
2798 2799 deleted = _deleted - deladded
2799 2800
2800 2801 # distinguish between file to forget and the other
2801 2802 added = set()
2802 2803 for abs in dsadded:
2803 2804 if repo.dirstate[abs] != 'a':
2804 2805 added.add(abs)
2805 2806 dsadded -= added
2806 2807
2807 2808 for abs in deladded:
2808 2809 if repo.dirstate[abs] == 'a':
2809 2810 dsadded.add(abs)
2810 2811 deladded -= dsadded
2811 2812
2812 2813 # For files marked as removed, we check if an unknown file is present at
2813 2814 # the same path. If a such file exists it may need to be backed up.
2814 2815 # Making the distinction at this stage helps have simpler backup
2815 2816 # logic.
2816 2817 removunk = set()
2817 2818 for abs in removed:
2818 2819 target = repo.wjoin(abs)
2819 2820 if os.path.lexists(target):
2820 2821 removunk.add(abs)
2821 2822 removed -= removunk
2822 2823
2823 2824 dsremovunk = set()
2824 2825 for abs in dsremoved:
2825 2826 target = repo.wjoin(abs)
2826 2827 if os.path.lexists(target):
2827 2828 dsremovunk.add(abs)
2828 2829 dsremoved -= dsremovunk
2829 2830
2830 2831 # action to be actually performed by revert
2831 2832 # (<list of file>, message>) tuple
2832 2833 actions = {'revert': ([], _('reverting %s\n')),
2833 2834 'add': ([], _('adding %s\n')),
2834 2835 'remove': ([], _('removing %s\n')),
2835 2836 'drop': ([], _('removing %s\n')),
2836 2837 'forget': ([], _('forgetting %s\n')),
2837 2838 'undelete': ([], _('undeleting %s\n')),
2838 2839 'noop': (None, _('no changes needed to %s\n')),
2839 2840 'unknown': (None, _('file not managed: %s\n')),
2840 2841 }
2841 2842
2842 2843 # "constant" that convey the backup strategy.
2843 2844 # All set to `discard` if `no-backup` is set do avoid checking
2844 2845 # no_backup lower in the code.
2845 2846 # These values are ordered for comparison purposes
2846 2847 backupinteractive = 3 # do backup if interactively modified
2847 2848 backup = 2 # unconditionally do backup
2848 2849 check = 1 # check if the existing file differs from target
2849 2850 discard = 0 # never do backup
2850 2851 if opts.get('no_backup'):
2851 2852 backupinteractive = backup = check = discard
2852 2853 if interactive:
2853 2854 dsmodifiedbackup = backupinteractive
2854 2855 else:
2855 2856 dsmodifiedbackup = backup
2856 2857 tobackup = set()
2857 2858
2858 2859 backupanddel = actions['remove']
2859 2860 if not opts.get('no_backup'):
2860 2861 backupanddel = actions['drop']
2861 2862
2862 2863 disptable = (
2863 2864 # dispatch table:
2864 2865 # file state
2865 2866 # action
2866 2867 # make backup
2867 2868
2868 2869 ## Sets that results that will change file on disk
2869 2870 # Modified compared to target, no local change
2870 2871 (modified, actions['revert'], discard),
2871 2872 # Modified compared to target, but local file is deleted
2872 2873 (deleted, actions['revert'], discard),
2873 2874 # Modified compared to target, local change
2874 2875 (dsmodified, actions['revert'], dsmodifiedbackup),
2875 2876 # Added since target
2876 2877 (added, actions['remove'], discard),
2877 2878 # Added in working directory
2878 2879 (dsadded, actions['forget'], discard),
2879 2880 # Added since target, have local modification
2880 2881 (modadded, backupanddel, backup),
2881 2882 # Added since target but file is missing in working directory
2882 2883 (deladded, actions['drop'], discard),
2883 2884 # Removed since target, before working copy parent
2884 2885 (removed, actions['add'], discard),
2885 2886 # Same as `removed` but an unknown file exists at the same path
2886 2887 (removunk, actions['add'], check),
2887 2888 # Removed since targe, marked as such in working copy parent
2888 2889 (dsremoved, actions['undelete'], discard),
2889 2890 # Same as `dsremoved` but an unknown file exists at the same path
2890 2891 (dsremovunk, actions['undelete'], check),
2891 2892 ## the following sets does not result in any file changes
2892 2893 # File with no modification
2893 2894 (clean, actions['noop'], discard),
2894 2895 # Existing file, not tracked anywhere
2895 2896 (unknown, actions['unknown'], discard),
2896 2897 )
2897 2898
2898 2899 for abs, (rel, exact) in sorted(names.items()):
2899 2900 # target file to be touch on disk (relative to cwd)
2900 2901 target = repo.wjoin(abs)
2901 2902 # search the entry in the dispatch table.
2902 2903 # if the file is in any of these sets, it was touched in the working
2903 2904 # directory parent and we are sure it needs to be reverted.
2904 2905 for table, (xlist, msg), dobackup in disptable:
2905 2906 if abs not in table:
2906 2907 continue
2907 2908 if xlist is not None:
2908 2909 xlist.append(abs)
2909 2910 if dobackup:
2910 2911 # If in interactive mode, don't automatically create
2911 2912 # .orig files (issue4793)
2912 2913 if dobackup == backupinteractive:
2913 2914 tobackup.add(abs)
2914 2915 elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
2915 2916 bakname = scmutil.origpath(ui, repo, rel)
2916 2917 ui.note(_('saving current version of %s as %s\n') %
2917 2918 (rel, bakname))
2918 2919 if not opts.get('dry_run'):
2919 2920 if interactive:
2920 2921 util.copyfile(target, bakname)
2921 2922 else:
2922 2923 util.rename(target, bakname)
2923 2924 if ui.verbose or not exact:
2924 2925 if not isinstance(msg, bytes):
2925 2926 msg = msg(abs)
2926 2927 ui.status(msg % rel)
2927 2928 elif exact:
2928 2929 ui.warn(msg % rel)
2929 2930 break
2930 2931
2931 2932 if not opts.get('dry_run'):
2932 2933 needdata = ('revert', 'add', 'undelete')
2933 2934 if _revertprefetch is not _revertprefetchstub:
2934 2935 ui.deprecwarn("'cmdutil._revertprefetch' is deprecated, "
2935 2936 "add a callback to 'scmutil.fileprefetchhooks'",
2936 2937 '4.6', stacklevel=1)
2937 2938 _revertprefetch(repo, ctx,
2938 2939 *[actions[name][0] for name in needdata])
2939 2940 oplist = [actions[name][0] for name in needdata]
2940 2941 prefetch = scmutil.fileprefetchhooks
2941 2942 prefetch(repo, ctx, [f for sublist in oplist for f in sublist])
2942 2943 _performrevert(repo, parents, ctx, actions, interactive, tobackup)
2943 2944
2944 2945 if targetsubs:
2945 2946 # Revert the subrepos on the revert list
2946 2947 for sub in targetsubs:
2947 2948 try:
2948 2949 wctx.sub(sub).revert(ctx.substate[sub], *pats,
2949 2950 **pycompat.strkwargs(opts))
2950 2951 except KeyError:
2951 2952 raise error.Abort("subrepository '%s' does not exist in %s!"
2952 2953 % (sub, short(ctx.node())))
2953 2954
2954 2955 def _revertprefetchstub(repo, ctx, *files):
2955 2956 """Stub method for detecting extension wrapping of _revertprefetch(), to
2956 2957 issue a deprecation warning."""
2957 2958
2958 2959 _revertprefetch = _revertprefetchstub
2959 2960
2960 2961 def _performrevert(repo, parents, ctx, actions, interactive=False,
2961 2962 tobackup=None):
2962 2963 """function that actually perform all the actions computed for revert
2963 2964
2964 2965 This is an independent function to let extension to plug in and react to
2965 2966 the imminent revert.
2966 2967
2967 2968 Make sure you have the working directory locked when calling this function.
2968 2969 """
2969 2970 parent, p2 = parents
2970 2971 node = ctx.node()
2971 2972 excluded_files = []
2972 2973
2973 2974 def checkout(f):
2974 2975 fc = ctx[f]
2975 2976 repo.wwrite(f, fc.data(), fc.flags())
2976 2977
2977 2978 def doremove(f):
2978 2979 try:
2979 2980 repo.wvfs.unlinkpath(f)
2980 2981 except OSError:
2981 2982 pass
2982 2983 repo.dirstate.remove(f)
2983 2984
2984 2985 audit_path = pathutil.pathauditor(repo.root, cached=True)
2985 2986 for f in actions['forget'][0]:
2986 2987 if interactive:
2987 2988 choice = repo.ui.promptchoice(
2988 2989 _("forget added file %s (Yn)?$$ &Yes $$ &No") % f)
2989 2990 if choice == 0:
2990 2991 repo.dirstate.drop(f)
2991 2992 else:
2992 2993 excluded_files.append(f)
2993 2994 else:
2994 2995 repo.dirstate.drop(f)
2995 2996 for f in actions['remove'][0]:
2996 2997 audit_path(f)
2997 2998 if interactive:
2998 2999 choice = repo.ui.promptchoice(
2999 3000 _("remove added file %s (Yn)?$$ &Yes $$ &No") % f)
3000 3001 if choice == 0:
3001 3002 doremove(f)
3002 3003 else:
3003 3004 excluded_files.append(f)
3004 3005 else:
3005 3006 doremove(f)
3006 3007 for f in actions['drop'][0]:
3007 3008 audit_path(f)
3008 3009 repo.dirstate.remove(f)
3009 3010
3010 3011 normal = None
3011 3012 if node == parent:
3012 3013 # We're reverting to our parent. If possible, we'd like status
3013 3014 # to report the file as clean. We have to use normallookup for
3014 3015 # merges to avoid losing information about merged/dirty files.
3015 3016 if p2 != nullid:
3016 3017 normal = repo.dirstate.normallookup
3017 3018 else:
3018 3019 normal = repo.dirstate.normal
3019 3020
3020 3021 newlyaddedandmodifiedfiles = set()
3021 3022 if interactive:
3022 3023 # Prompt the user for changes to revert
3023 3024 torevert = [f for f in actions['revert'][0] if f not in excluded_files]
3024 3025 m = scmutil.matchfiles(repo, torevert)
3025 3026 diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
3026 3027 diffopts.nodates = True
3027 3028 diffopts.git = True
3028 3029 operation = 'discard'
3029 3030 reversehunks = True
3030 3031 if node != parent:
3031 3032 operation = 'apply'
3032 3033 reversehunks = False
3033 3034 if reversehunks:
3034 3035 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3035 3036 else:
3036 3037 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3037 3038 originalchunks = patch.parsepatch(diff)
3038 3039
3039 3040 try:
3040 3041
3041 3042 chunks, opts = recordfilter(repo.ui, originalchunks,
3042 3043 operation=operation)
3043 3044 if reversehunks:
3044 3045 chunks = patch.reversehunks(chunks)
3045 3046
3046 3047 except error.PatchError as err:
3047 3048 raise error.Abort(_('error parsing patch: %s') % err)
3048 3049
3049 3050 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
3050 3051 if tobackup is None:
3051 3052 tobackup = set()
3052 3053 # Apply changes
3053 3054 fp = stringio()
3054 3055 for c in chunks:
3055 3056 # Create a backup file only if this hunk should be backed up
3056 3057 if ishunk(c) and c.header.filename() in tobackup:
3057 3058 abs = c.header.filename()
3058 3059 target = repo.wjoin(abs)
3059 3060 bakname = scmutil.origpath(repo.ui, repo, m.rel(abs))
3060 3061 util.copyfile(target, bakname)
3061 3062 tobackup.remove(abs)
3062 3063 c.write(fp)
3063 3064 dopatch = fp.tell()
3064 3065 fp.seek(0)
3065 3066 if dopatch:
3066 3067 try:
3067 3068 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3068 3069 except error.PatchError as err:
3069 3070 raise error.Abort(pycompat.bytestr(err))
3070 3071 del fp
3071 3072 else:
3072 3073 for f in actions['revert'][0]:
3073 3074 checkout(f)
3074 3075 if normal:
3075 3076 normal(f)
3076 3077
3077 3078 for f in actions['add'][0]:
3078 3079 # Don't checkout modified files, they are already created by the diff
3079 3080 if f not in newlyaddedandmodifiedfiles:
3080 3081 checkout(f)
3081 3082 repo.dirstate.add(f)
3082 3083
3083 3084 normal = repo.dirstate.normallookup
3084 3085 if node == parent and p2 == nullid:
3085 3086 normal = repo.dirstate.normal
3086 3087 for f in actions['undelete'][0]:
3087 3088 checkout(f)
3088 3089 normal(f)
3089 3090
3090 3091 copied = copies.pathcopies(repo[parent], ctx)
3091 3092
3092 3093 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3093 3094 if f in copied:
3094 3095 repo.dirstate.copy(copied[f], f)
3095 3096
3096 3097 class command(registrar.command):
3097 3098 """deprecated: used registrar.command instead"""
3098 3099 def _doregister(self, func, name, *args, **kwargs):
3099 3100 func._deprecatedregistrar = True # flag for deprecwarn in extensions.py
3100 3101 return super(command, self)._doregister(func, name, *args, **kwargs)
3101 3102
3102 3103 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3103 3104 # commands.outgoing. "missing" is "missing" of the result of
3104 3105 # "findcommonoutgoing()"
3105 3106 outgoinghooks = util.hooks()
3106 3107
3107 3108 # a list of (ui, repo) functions called by commands.summary
3108 3109 summaryhooks = util.hooks()
3109 3110
3110 3111 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3111 3112 #
3112 3113 # functions should return tuple of booleans below, if 'changes' is None:
3113 3114 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3114 3115 #
3115 3116 # otherwise, 'changes' is a tuple of tuples below:
3116 3117 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3117 3118 # - (desturl, destbranch, destpeer, outgoing)
3118 3119 summaryremotehooks = util.hooks()
3119 3120
3120 3121 # A list of state files kept by multistep operations like graft.
3121 3122 # Since graft cannot be aborted, it is considered 'clearable' by update.
3122 3123 # note: bisect is intentionally excluded
3123 3124 # (state file, clearable, allowcommit, error, hint)
3124 3125 unfinishedstates = [
3125 3126 ('graftstate', True, False, _('graft in progress'),
3126 3127 _("use 'hg graft --continue' or 'hg update' to abort")),
3127 3128 ('updatestate', True, False, _('last update was interrupted'),
3128 3129 _("use 'hg update' to get a consistent checkout"))
3129 3130 ]
3130 3131
3131 3132 def checkunfinished(repo, commit=False):
3132 3133 '''Look for an unfinished multistep operation, like graft, and abort
3133 3134 if found. It's probably good to check this right before
3134 3135 bailifchanged().
3135 3136 '''
3136 3137 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3137 3138 if commit and allowcommit:
3138 3139 continue
3139 3140 if repo.vfs.exists(f):
3140 3141 raise error.Abort(msg, hint=hint)
3141 3142
3142 3143 def clearunfinished(repo):
3143 3144 '''Check for unfinished operations (as above), and clear the ones
3144 3145 that are clearable.
3145 3146 '''
3146 3147 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3147 3148 if not clearable and repo.vfs.exists(f):
3148 3149 raise error.Abort(msg, hint=hint)
3149 3150 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3150 3151 if clearable and repo.vfs.exists(f):
3151 3152 util.unlink(repo.vfs.join(f))
3152 3153
3153 3154 afterresolvedstates = [
3154 3155 ('graftstate',
3155 3156 _('hg graft --continue')),
3156 3157 ]
3157 3158
3158 3159 def howtocontinue(repo):
3159 3160 '''Check for an unfinished operation and return the command to finish
3160 3161 it.
3161 3162
3162 3163 afterresolvedstates tuples define a .hg/{file} and the corresponding
3163 3164 command needed to finish it.
3164 3165
3165 3166 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3166 3167 a boolean.
3167 3168 '''
3168 3169 contmsg = _("continue: %s")
3169 3170 for f, msg in afterresolvedstates:
3170 3171 if repo.vfs.exists(f):
3171 3172 return contmsg % msg, True
3172 3173 if repo[None].dirty(missing=True, merge=False, branch=False):
3173 3174 return contmsg % _("hg commit"), False
3174 3175 return None, None
3175 3176
3176 3177 def checkafterresolved(repo):
3177 3178 '''Inform the user about the next action after completing hg resolve
3178 3179
3179 3180 If there's a matching afterresolvedstates, howtocontinue will yield
3180 3181 repo.ui.warn as the reporter.
3181 3182
3182 3183 Otherwise, it will yield repo.ui.note.
3183 3184 '''
3184 3185 msg, warning = howtocontinue(repo)
3185 3186 if msg is not None:
3186 3187 if warning:
3187 3188 repo.ui.warn("%s\n" % msg)
3188 3189 else:
3189 3190 repo.ui.note("%s\n" % msg)
3190 3191
3191 3192 def wrongtooltocontinue(repo, task):
3192 3193 '''Raise an abort suggesting how to properly continue if there is an
3193 3194 active task.
3194 3195
3195 3196 Uses howtocontinue() to find the active task.
3196 3197
3197 3198 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3198 3199 a hint.
3199 3200 '''
3200 3201 after = howtocontinue(repo)
3201 3202 hint = None
3202 3203 if after[1]:
3203 3204 hint = after[0]
3204 3205 raise error.Abort(_('no %s in progress') % task, hint=hint)
3205 3206
3206 3207 class changeset_printer(logcmdutil.changesetprinter):
3207 3208
3208 3209 def __init__(self, ui, *args, **kwargs):
3209 3210 msg = ("'cmdutil.changeset_printer' is deprecated, "
3210 3211 "use 'logcmdutil.logcmdutil'")
3211 3212 ui.deprecwarn(msg, "4.6")
3212 3213 super(changeset_printer, self).__init__(ui, *args, **kwargs)
3213 3214
3214 3215 def displaygraph(ui, *args, **kwargs):
3215 3216 msg = ("'cmdutil.displaygraph' is deprecated, "
3216 3217 "use 'logcmdutil.displaygraph'")
3217 3218 ui.deprecwarn(msg, "4.6")
3218 3219 return logcmdutil.displaygraph(ui, *args, **kwargs)
3219 3220
3220 3221 def show_changeset(ui, *args, **kwargs):
3221 3222 msg = ("'cmdutil.show_changeset' is deprecated, "
3222 3223 "use 'logcmdutil.changesetdisplayer'")
3223 3224 ui.deprecwarn(msg, "4.6")
3224 3225 return logcmdutil.changesetdisplayer(ui, *args, **kwargs)
General Comments 0
You need to be logged in to leave comments. Login now