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