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