##// END OF EJS Templates
cmdutil: migrate previously missed instances to uipathfn...
Martin von Zweigbergk -
r41813:0cbf491d default
parent child Browse files
Show More
@@ -1,3348 +1,3348 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 section='commands',
287 287 configprefix='commit.interactive.')
288 288 diffopts.nodates = True
289 289 diffopts.git = True
290 290 diffopts.showfunc = True
291 291 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
292 292 originalchunks = patch.parsepatch(originaldiff)
293 293
294 294 # 1. filter patch, since we are intending to apply subset of it
295 295 try:
296 296 chunks, newopts = filterfn(ui, originalchunks)
297 297 except error.PatchError as err:
298 298 raise error.Abort(_('error parsing patch: %s') % err)
299 299 opts.update(newopts)
300 300
301 301 # We need to keep a backup of files that have been newly added and
302 302 # modified during the recording process because there is a previous
303 303 # version without the edit in the workdir
304 304 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
305 305 contenders = set()
306 306 for h in chunks:
307 307 try:
308 308 contenders.update(set(h.files()))
309 309 except AttributeError:
310 310 pass
311 311
312 312 changed = status.modified + status.added + status.removed
313 313 newfiles = [f for f in changed if f in contenders]
314 314 if not newfiles:
315 315 ui.status(_('no changes to record\n'))
316 316 return 0
317 317
318 318 modified = set(status.modified)
319 319
320 320 # 2. backup changed files, so we can restore them in the end
321 321
322 322 if backupall:
323 323 tobackup = changed
324 324 else:
325 325 tobackup = [f for f in newfiles if f in modified or f in \
326 326 newlyaddedandmodifiedfiles]
327 327 backups = {}
328 328 if tobackup:
329 329 backupdir = repo.vfs.join('record-backups')
330 330 try:
331 331 os.mkdir(backupdir)
332 332 except OSError as err:
333 333 if err.errno != errno.EEXIST:
334 334 raise
335 335 try:
336 336 # backup continues
337 337 for f in tobackup:
338 338 fd, tmpname = pycompat.mkstemp(prefix=f.replace('/', '_') + '.',
339 339 dir=backupdir)
340 340 os.close(fd)
341 341 ui.debug('backup %r as %r\n' % (f, tmpname))
342 342 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
343 343 backups[f] = tmpname
344 344
345 345 fp = stringio()
346 346 for c in chunks:
347 347 fname = c.filename()
348 348 if fname in backups:
349 349 c.write(fp)
350 350 dopatch = fp.tell()
351 351 fp.seek(0)
352 352
353 353 # 2.5 optionally review / modify patch in text editor
354 354 if opts.get('review', False):
355 355 patchtext = (crecordmod.diffhelptext
356 356 + crecordmod.patchhelptext
357 357 + fp.read())
358 358 reviewedpatch = ui.edit(patchtext, "",
359 359 action="diff",
360 360 repopath=repo.path)
361 361 fp.truncate(0)
362 362 fp.write(reviewedpatch)
363 363 fp.seek(0)
364 364
365 365 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
366 366 # 3a. apply filtered patch to clean repo (clean)
367 367 if backups:
368 368 # Equivalent to hg.revert
369 369 m = scmutil.matchfiles(repo, backups.keys())
370 370 mergemod.update(repo, repo.dirstate.p1(), branchmerge=False,
371 371 force=True, matcher=m)
372 372
373 373 # 3b. (apply)
374 374 if dopatch:
375 375 try:
376 376 ui.debug('applying patch\n')
377 377 ui.debug(fp.getvalue())
378 378 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
379 379 except error.PatchError as err:
380 380 raise error.Abort(pycompat.bytestr(err))
381 381 del fp
382 382
383 383 # 4. We prepared working directory according to filtered
384 384 # patch. Now is the time to delegate the job to
385 385 # commit/qrefresh or the like!
386 386
387 387 # Make all of the pathnames absolute.
388 388 newfiles = [repo.wjoin(nf) for nf in newfiles]
389 389 return commitfunc(ui, repo, *newfiles, **pycompat.strkwargs(opts))
390 390 finally:
391 391 # 5. finally restore backed-up files
392 392 try:
393 393 dirstate = repo.dirstate
394 394 for realname, tmpname in backups.iteritems():
395 395 ui.debug('restoring %r to %r\n' % (tmpname, realname))
396 396
397 397 if dirstate[realname] == 'n':
398 398 # without normallookup, restoring timestamp
399 399 # may cause partially committed files
400 400 # to be treated as unmodified
401 401 dirstate.normallookup(realname)
402 402
403 403 # copystat=True here and above are a hack to trick any
404 404 # editors that have f open that we haven't modified them.
405 405 #
406 406 # Also note that this racy as an editor could notice the
407 407 # file's mtime before we've finished writing it.
408 408 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
409 409 os.unlink(tmpname)
410 410 if tobackup:
411 411 os.rmdir(backupdir)
412 412 except OSError:
413 413 pass
414 414
415 415 def recordinwlock(ui, repo, message, match, opts):
416 416 with repo.wlock():
417 417 return recordfunc(ui, repo, message, match, opts)
418 418
419 419 return commit(ui, repo, recordinwlock, pats, opts)
420 420
421 421 class dirnode(object):
422 422 """
423 423 Represent a directory in user working copy with information required for
424 424 the purpose of tersing its status.
425 425
426 426 path is the path to the directory, without a trailing '/'
427 427
428 428 statuses is a set of statuses of all files in this directory (this includes
429 429 all the files in all the subdirectories too)
430 430
431 431 files is a list of files which are direct child of this directory
432 432
433 433 subdirs is a dictionary of sub-directory name as the key and it's own
434 434 dirnode object as the value
435 435 """
436 436
437 437 def __init__(self, dirpath):
438 438 self.path = dirpath
439 439 self.statuses = set([])
440 440 self.files = []
441 441 self.subdirs = {}
442 442
443 443 def _addfileindir(self, filename, status):
444 444 """Add a file in this directory as a direct child."""
445 445 self.files.append((filename, status))
446 446
447 447 def addfile(self, filename, status):
448 448 """
449 449 Add a file to this directory or to its direct parent directory.
450 450
451 451 If the file is not direct child of this directory, we traverse to the
452 452 directory of which this file is a direct child of and add the file
453 453 there.
454 454 """
455 455
456 456 # the filename contains a path separator, it means it's not the direct
457 457 # child of this directory
458 458 if '/' in filename:
459 459 subdir, filep = filename.split('/', 1)
460 460
461 461 # does the dirnode object for subdir exists
462 462 if subdir not in self.subdirs:
463 463 subdirpath = pathutil.join(self.path, subdir)
464 464 self.subdirs[subdir] = dirnode(subdirpath)
465 465
466 466 # try adding the file in subdir
467 467 self.subdirs[subdir].addfile(filep, status)
468 468
469 469 else:
470 470 self._addfileindir(filename, status)
471 471
472 472 if status not in self.statuses:
473 473 self.statuses.add(status)
474 474
475 475 def iterfilepaths(self):
476 476 """Yield (status, path) for files directly under this directory."""
477 477 for f, st in self.files:
478 478 yield st, pathutil.join(self.path, f)
479 479
480 480 def tersewalk(self, terseargs):
481 481 """
482 482 Yield (status, path) obtained by processing the status of this
483 483 dirnode.
484 484
485 485 terseargs is the string of arguments passed by the user with `--terse`
486 486 flag.
487 487
488 488 Following are the cases which can happen:
489 489
490 490 1) All the files in the directory (including all the files in its
491 491 subdirectories) share the same status and the user has asked us to terse
492 492 that status. -> yield (status, dirpath). dirpath will end in '/'.
493 493
494 494 2) Otherwise, we do following:
495 495
496 496 a) Yield (status, filepath) for all the files which are in this
497 497 directory (only the ones in this directory, not the subdirs)
498 498
499 499 b) Recurse the function on all the subdirectories of this
500 500 directory
501 501 """
502 502
503 503 if len(self.statuses) == 1:
504 504 onlyst = self.statuses.pop()
505 505
506 506 # Making sure we terse only when the status abbreviation is
507 507 # passed as terse argument
508 508 if onlyst in terseargs:
509 509 yield onlyst, self.path + '/'
510 510 return
511 511
512 512 # add the files to status list
513 513 for st, fpath in self.iterfilepaths():
514 514 yield st, fpath
515 515
516 516 #recurse on the subdirs
517 517 for dirobj in self.subdirs.values():
518 518 for st, fpath in dirobj.tersewalk(terseargs):
519 519 yield st, fpath
520 520
521 521 def tersedir(statuslist, terseargs):
522 522 """
523 523 Terse the status if all the files in a directory shares the same status.
524 524
525 525 statuslist is scmutil.status() object which contains a list of files for
526 526 each status.
527 527 terseargs is string which is passed by the user as the argument to `--terse`
528 528 flag.
529 529
530 530 The function makes a tree of objects of dirnode class, and at each node it
531 531 stores the information required to know whether we can terse a certain
532 532 directory or not.
533 533 """
534 534 # the order matters here as that is used to produce final list
535 535 allst = ('m', 'a', 'r', 'd', 'u', 'i', 'c')
536 536
537 537 # checking the argument validity
538 538 for s in pycompat.bytestr(terseargs):
539 539 if s not in allst:
540 540 raise error.Abort(_("'%s' not recognized") % s)
541 541
542 542 # creating a dirnode object for the root of the repo
543 543 rootobj = dirnode('')
544 544 pstatus = ('modified', 'added', 'deleted', 'clean', 'unknown',
545 545 'ignored', 'removed')
546 546
547 547 tersedict = {}
548 548 for attrname in pstatus:
549 549 statuschar = attrname[0:1]
550 550 for f in getattr(statuslist, attrname):
551 551 rootobj.addfile(f, statuschar)
552 552 tersedict[statuschar] = []
553 553
554 554 # we won't be tersing the root dir, so add files in it
555 555 for st, fpath in rootobj.iterfilepaths():
556 556 tersedict[st].append(fpath)
557 557
558 558 # process each sub-directory and build tersedict
559 559 for subdir in rootobj.subdirs.values():
560 560 for st, f in subdir.tersewalk(terseargs):
561 561 tersedict[st].append(f)
562 562
563 563 tersedlist = []
564 564 for st in allst:
565 565 tersedict[st].sort()
566 566 tersedlist.append(tersedict[st])
567 567
568 568 return tersedlist
569 569
570 570 def _commentlines(raw):
571 571 '''Surround lineswith a comment char and a new line'''
572 572 lines = raw.splitlines()
573 573 commentedlines = ['# %s' % line for line in lines]
574 574 return '\n'.join(commentedlines) + '\n'
575 575
576 576 def _conflictsmsg(repo):
577 577 mergestate = mergemod.mergestate.read(repo)
578 578 if not mergestate.active():
579 579 return
580 580
581 581 m = scmutil.match(repo[None])
582 582 unresolvedlist = [f for f in mergestate.unresolved() if m(f)]
583 583 if unresolvedlist:
584 584 mergeliststr = '\n'.join(
585 585 [' %s' % util.pathto(repo.root, encoding.getcwd(), path)
586 586 for path in sorted(unresolvedlist)])
587 587 msg = _('''Unresolved merge conflicts:
588 588
589 589 %s
590 590
591 591 To mark files as resolved: hg resolve --mark FILE''') % mergeliststr
592 592 else:
593 593 msg = _('No unresolved merge conflicts.')
594 594
595 595 return _commentlines(msg)
596 596
597 597 def _helpmessage(continuecmd, abortcmd):
598 598 msg = _('To continue: %s\n'
599 599 'To abort: %s') % (continuecmd, abortcmd)
600 600 return _commentlines(msg)
601 601
602 602 def _rebasemsg():
603 603 return _helpmessage('hg rebase --continue', 'hg rebase --abort')
604 604
605 605 def _histeditmsg():
606 606 return _helpmessage('hg histedit --continue', 'hg histedit --abort')
607 607
608 608 def _unshelvemsg():
609 609 return _helpmessage('hg unshelve --continue', 'hg unshelve --abort')
610 610
611 611 def _graftmsg():
612 612 return _helpmessage('hg graft --continue', 'hg graft --abort')
613 613
614 614 def _mergemsg():
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 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
1139 1139 def walkpat(pat):
1140 1140 srcs = []
1141 1141 if after:
1142 1142 badstates = '?'
1143 1143 else:
1144 1144 badstates = '?r'
1145 1145 m = scmutil.match(wctx, [pat], opts, globbed=True)
1146 1146 for abs in wctx.walk(m):
1147 1147 state = repo.dirstate[abs]
1148 1148 rel = uipathfn(abs)
1149 1149 exact = m.exact(abs)
1150 1150 if state in badstates:
1151 1151 if exact and state == '?':
1152 1152 ui.warn(_('%s: not copying - file is not managed\n') % rel)
1153 1153 if exact and state == 'r':
1154 1154 ui.warn(_('%s: not copying - file has been marked for'
1155 1155 ' remove\n') % rel)
1156 1156 continue
1157 1157 # abs: hgsep
1158 1158 # rel: ossep
1159 1159 srcs.append((abs, rel, exact))
1160 1160 return srcs
1161 1161
1162 1162 # abssrc: hgsep
1163 1163 # relsrc: ossep
1164 1164 # otarget: ossep
1165 1165 def copyfile(abssrc, relsrc, otarget, exact):
1166 1166 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
1167 1167 if '/' in abstarget:
1168 1168 # We cannot normalize abstarget itself, this would prevent
1169 1169 # case only renames, like a => A.
1170 1170 abspath, absname = abstarget.rsplit('/', 1)
1171 1171 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
1172 1172 reltarget = repo.pathto(abstarget, cwd)
1173 1173 target = repo.wjoin(abstarget)
1174 1174 src = repo.wjoin(abssrc)
1175 1175 state = repo.dirstate[abstarget]
1176 1176
1177 1177 scmutil.checkportable(ui, abstarget)
1178 1178
1179 1179 # check for collisions
1180 1180 prevsrc = targets.get(abstarget)
1181 1181 if prevsrc is not None:
1182 1182 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
1183 1183 (reltarget, repo.pathto(abssrc, cwd),
1184 1184 repo.pathto(prevsrc, cwd)))
1185 1185 return True # report a failure
1186 1186
1187 1187 # check for overwrites
1188 1188 exists = os.path.lexists(target)
1189 1189 samefile = False
1190 1190 if exists and abssrc != abstarget:
1191 1191 if (repo.dirstate.normalize(abssrc) ==
1192 1192 repo.dirstate.normalize(abstarget)):
1193 1193 if not rename:
1194 1194 ui.warn(_("%s: can't copy - same file\n") % reltarget)
1195 1195 return True # report a failure
1196 1196 exists = False
1197 1197 samefile = True
1198 1198
1199 1199 if not after and exists or after and state in 'mn':
1200 1200 if not opts['force']:
1201 1201 if state in 'mn':
1202 1202 msg = _('%s: not overwriting - file already committed\n')
1203 1203 if after:
1204 1204 flags = '--after --force'
1205 1205 else:
1206 1206 flags = '--force'
1207 1207 if rename:
1208 1208 hint = _("('hg rename %s' to replace the file by "
1209 1209 'recording a rename)\n') % flags
1210 1210 else:
1211 1211 hint = _("('hg copy %s' to replace the file by "
1212 1212 'recording a copy)\n') % flags
1213 1213 else:
1214 1214 msg = _('%s: not overwriting - file exists\n')
1215 1215 if rename:
1216 1216 hint = _("('hg rename --after' to record the rename)\n")
1217 1217 else:
1218 1218 hint = _("('hg copy --after' to record the copy)\n")
1219 1219 ui.warn(msg % reltarget)
1220 1220 ui.warn(hint)
1221 1221 return True # report a failure
1222 1222
1223 1223 if after:
1224 1224 if not exists:
1225 1225 if rename:
1226 1226 ui.warn(_('%s: not recording move - %s does not exist\n') %
1227 1227 (relsrc, reltarget))
1228 1228 else:
1229 1229 ui.warn(_('%s: not recording copy - %s does not exist\n') %
1230 1230 (relsrc, reltarget))
1231 1231 return True # report a failure
1232 1232 elif not dryrun:
1233 1233 try:
1234 1234 if exists:
1235 1235 os.unlink(target)
1236 1236 targetdir = os.path.dirname(target) or '.'
1237 1237 if not os.path.isdir(targetdir):
1238 1238 os.makedirs(targetdir)
1239 1239 if samefile:
1240 1240 tmp = target + "~hgrename"
1241 1241 os.rename(src, tmp)
1242 1242 os.rename(tmp, target)
1243 1243 else:
1244 1244 # Preserve stat info on renames, not on copies; this matches
1245 1245 # Linux CLI behavior.
1246 1246 util.copyfile(src, target, copystat=rename)
1247 1247 srcexists = True
1248 1248 except IOError as inst:
1249 1249 if inst.errno == errno.ENOENT:
1250 1250 ui.warn(_('%s: deleted in working directory\n') % relsrc)
1251 1251 srcexists = False
1252 1252 else:
1253 1253 ui.warn(_('%s: cannot copy - %s\n') %
1254 1254 (relsrc, encoding.strtolocal(inst.strerror)))
1255 1255 return True # report a failure
1256 1256
1257 1257 if ui.verbose or not exact:
1258 1258 if rename:
1259 1259 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
1260 1260 else:
1261 1261 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
1262 1262
1263 1263 targets[abstarget] = abssrc
1264 1264
1265 1265 # fix up dirstate
1266 1266 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
1267 1267 dryrun=dryrun, cwd=cwd)
1268 1268 if rename and not dryrun:
1269 1269 if not after and srcexists and not samefile:
1270 1270 rmdir = repo.ui.configbool('experimental', 'removeemptydirs')
1271 1271 repo.wvfs.unlinkpath(abssrc, rmdir=rmdir)
1272 1272 wctx.forget([abssrc])
1273 1273
1274 1274 # pat: ossep
1275 1275 # dest ossep
1276 1276 # srcs: list of (hgsep, hgsep, ossep, bool)
1277 1277 # return: function that takes hgsep and returns ossep
1278 1278 def targetpathfn(pat, dest, srcs):
1279 1279 if os.path.isdir(pat):
1280 1280 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1281 1281 abspfx = util.localpath(abspfx)
1282 1282 if destdirexists:
1283 1283 striplen = len(os.path.split(abspfx)[0])
1284 1284 else:
1285 1285 striplen = len(abspfx)
1286 1286 if striplen:
1287 1287 striplen += len(pycompat.ossep)
1288 1288 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1289 1289 elif destdirexists:
1290 1290 res = lambda p: os.path.join(dest,
1291 1291 os.path.basename(util.localpath(p)))
1292 1292 else:
1293 1293 res = lambda p: dest
1294 1294 return res
1295 1295
1296 1296 # pat: ossep
1297 1297 # dest ossep
1298 1298 # srcs: list of (hgsep, hgsep, ossep, bool)
1299 1299 # return: function that takes hgsep and returns ossep
1300 1300 def targetpathafterfn(pat, dest, srcs):
1301 1301 if matchmod.patkind(pat):
1302 1302 # a mercurial pattern
1303 1303 res = lambda p: os.path.join(dest,
1304 1304 os.path.basename(util.localpath(p)))
1305 1305 else:
1306 1306 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1307 1307 if len(abspfx) < len(srcs[0][0]):
1308 1308 # A directory. Either the target path contains the last
1309 1309 # component of the source path or it does not.
1310 1310 def evalpath(striplen):
1311 1311 score = 0
1312 1312 for s in srcs:
1313 1313 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1314 1314 if os.path.lexists(t):
1315 1315 score += 1
1316 1316 return score
1317 1317
1318 1318 abspfx = util.localpath(abspfx)
1319 1319 striplen = len(abspfx)
1320 1320 if striplen:
1321 1321 striplen += len(pycompat.ossep)
1322 1322 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1323 1323 score = evalpath(striplen)
1324 1324 striplen1 = len(os.path.split(abspfx)[0])
1325 1325 if striplen1:
1326 1326 striplen1 += len(pycompat.ossep)
1327 1327 if evalpath(striplen1) > score:
1328 1328 striplen = striplen1
1329 1329 res = lambda p: os.path.join(dest,
1330 1330 util.localpath(p)[striplen:])
1331 1331 else:
1332 1332 # a file
1333 1333 if destdirexists:
1334 1334 res = lambda p: os.path.join(dest,
1335 1335 os.path.basename(util.localpath(p)))
1336 1336 else:
1337 1337 res = lambda p: dest
1338 1338 return res
1339 1339
1340 1340 pats = scmutil.expandpats(pats)
1341 1341 if not pats:
1342 1342 raise error.Abort(_('no source or destination specified'))
1343 1343 if len(pats) == 1:
1344 1344 raise error.Abort(_('no destination specified'))
1345 1345 dest = pats.pop()
1346 1346 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1347 1347 if not destdirexists:
1348 1348 if len(pats) > 1 or matchmod.patkind(pats[0]):
1349 1349 raise error.Abort(_('with multiple sources, destination must be an '
1350 1350 'existing directory'))
1351 1351 if util.endswithsep(dest):
1352 1352 raise error.Abort(_('destination %s is not a directory') % dest)
1353 1353
1354 1354 tfn = targetpathfn
1355 1355 if after:
1356 1356 tfn = targetpathafterfn
1357 1357 copylist = []
1358 1358 for pat in pats:
1359 1359 srcs = walkpat(pat)
1360 1360 if not srcs:
1361 1361 continue
1362 1362 copylist.append((tfn(pat, dest, srcs), srcs))
1363 1363 if not copylist:
1364 1364 raise error.Abort(_('no files to copy'))
1365 1365
1366 1366 errors = 0
1367 1367 for targetpath, srcs in copylist:
1368 1368 for abssrc, relsrc, exact in srcs:
1369 1369 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1370 1370 errors += 1
1371 1371
1372 1372 return errors != 0
1373 1373
1374 1374 ## facility to let extension process additional data into an import patch
1375 1375 # list of identifier to be executed in order
1376 1376 extrapreimport = [] # run before commit
1377 1377 extrapostimport = [] # run after commit
1378 1378 # mapping from identifier to actual import function
1379 1379 #
1380 1380 # 'preimport' are run before the commit is made and are provided the following
1381 1381 # arguments:
1382 1382 # - repo: the localrepository instance,
1383 1383 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1384 1384 # - extra: the future extra dictionary of the changeset, please mutate it,
1385 1385 # - opts: the import options.
1386 1386 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
1387 1387 # mutation of in memory commit and more. Feel free to rework the code to get
1388 1388 # there.
1389 1389 extrapreimportmap = {}
1390 1390 # 'postimport' are run after the commit is made and are provided the following
1391 1391 # argument:
1392 1392 # - ctx: the changectx created by import.
1393 1393 extrapostimportmap = {}
1394 1394
1395 1395 def tryimportone(ui, repo, patchdata, parents, opts, msgs, updatefunc):
1396 1396 """Utility function used by commands.import to import a single patch
1397 1397
1398 1398 This function is explicitly defined here to help the evolve extension to
1399 1399 wrap this part of the import logic.
1400 1400
1401 1401 The API is currently a bit ugly because it a simple code translation from
1402 1402 the import command. Feel free to make it better.
1403 1403
1404 1404 :patchdata: a dictionary containing parsed patch data (such as from
1405 1405 ``patch.extract()``)
1406 1406 :parents: nodes that will be parent of the created commit
1407 1407 :opts: the full dict of option passed to the import command
1408 1408 :msgs: list to save commit message to.
1409 1409 (used in case we need to save it when failing)
1410 1410 :updatefunc: a function that update a repo to a given node
1411 1411 updatefunc(<repo>, <node>)
1412 1412 """
1413 1413 # avoid cycle context -> subrepo -> cmdutil
1414 1414 from . import context
1415 1415
1416 1416 tmpname = patchdata.get('filename')
1417 1417 message = patchdata.get('message')
1418 1418 user = opts.get('user') or patchdata.get('user')
1419 1419 date = opts.get('date') or patchdata.get('date')
1420 1420 branch = patchdata.get('branch')
1421 1421 nodeid = patchdata.get('nodeid')
1422 1422 p1 = patchdata.get('p1')
1423 1423 p2 = patchdata.get('p2')
1424 1424
1425 1425 nocommit = opts.get('no_commit')
1426 1426 importbranch = opts.get('import_branch')
1427 1427 update = not opts.get('bypass')
1428 1428 strip = opts["strip"]
1429 1429 prefix = opts["prefix"]
1430 1430 sim = float(opts.get('similarity') or 0)
1431 1431
1432 1432 if not tmpname:
1433 1433 return None, None, False
1434 1434
1435 1435 rejects = False
1436 1436
1437 1437 cmdline_message = logmessage(ui, opts)
1438 1438 if cmdline_message:
1439 1439 # pickup the cmdline msg
1440 1440 message = cmdline_message
1441 1441 elif message:
1442 1442 # pickup the patch msg
1443 1443 message = message.strip()
1444 1444 else:
1445 1445 # launch the editor
1446 1446 message = None
1447 1447 ui.debug('message:\n%s\n' % (message or ''))
1448 1448
1449 1449 if len(parents) == 1:
1450 1450 parents.append(repo[nullid])
1451 1451 if opts.get('exact'):
1452 1452 if not nodeid or not p1:
1453 1453 raise error.Abort(_('not a Mercurial patch'))
1454 1454 p1 = repo[p1]
1455 1455 p2 = repo[p2 or nullid]
1456 1456 elif p2:
1457 1457 try:
1458 1458 p1 = repo[p1]
1459 1459 p2 = repo[p2]
1460 1460 # Without any options, consider p2 only if the
1461 1461 # patch is being applied on top of the recorded
1462 1462 # first parent.
1463 1463 if p1 != parents[0]:
1464 1464 p1 = parents[0]
1465 1465 p2 = repo[nullid]
1466 1466 except error.RepoError:
1467 1467 p1, p2 = parents
1468 1468 if p2.node() == nullid:
1469 1469 ui.warn(_("warning: import the patch as a normal revision\n"
1470 1470 "(use --exact to import the patch as a merge)\n"))
1471 1471 else:
1472 1472 p1, p2 = parents
1473 1473
1474 1474 n = None
1475 1475 if update:
1476 1476 if p1 != parents[0]:
1477 1477 updatefunc(repo, p1.node())
1478 1478 if p2 != parents[1]:
1479 1479 repo.setparents(p1.node(), p2.node())
1480 1480
1481 1481 if opts.get('exact') or importbranch:
1482 1482 repo.dirstate.setbranch(branch or 'default')
1483 1483
1484 1484 partial = opts.get('partial', False)
1485 1485 files = set()
1486 1486 try:
1487 1487 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
1488 1488 files=files, eolmode=None, similarity=sim / 100.0)
1489 1489 except error.PatchError as e:
1490 1490 if not partial:
1491 1491 raise error.Abort(pycompat.bytestr(e))
1492 1492 if partial:
1493 1493 rejects = True
1494 1494
1495 1495 files = list(files)
1496 1496 if nocommit:
1497 1497 if message:
1498 1498 msgs.append(message)
1499 1499 else:
1500 1500 if opts.get('exact') or p2:
1501 1501 # If you got here, you either use --force and know what
1502 1502 # you are doing or used --exact or a merge patch while
1503 1503 # being updated to its first parent.
1504 1504 m = None
1505 1505 else:
1506 1506 m = scmutil.matchfiles(repo, files or [])
1507 1507 editform = mergeeditform(repo[None], 'import.normal')
1508 1508 if opts.get('exact'):
1509 1509 editor = None
1510 1510 else:
1511 1511 editor = getcommiteditor(editform=editform,
1512 1512 **pycompat.strkwargs(opts))
1513 1513 extra = {}
1514 1514 for idfunc in extrapreimport:
1515 1515 extrapreimportmap[idfunc](repo, patchdata, extra, opts)
1516 1516 overrides = {}
1517 1517 if partial:
1518 1518 overrides[('ui', 'allowemptycommit')] = True
1519 1519 with repo.ui.configoverride(overrides, 'import'):
1520 1520 n = repo.commit(message, user,
1521 1521 date, match=m,
1522 1522 editor=editor, extra=extra)
1523 1523 for idfunc in extrapostimport:
1524 1524 extrapostimportmap[idfunc](repo[n])
1525 1525 else:
1526 1526 if opts.get('exact') or importbranch:
1527 1527 branch = branch or 'default'
1528 1528 else:
1529 1529 branch = p1.branch()
1530 1530 store = patch.filestore()
1531 1531 try:
1532 1532 files = set()
1533 1533 try:
1534 1534 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
1535 1535 files, eolmode=None)
1536 1536 except error.PatchError as e:
1537 1537 raise error.Abort(stringutil.forcebytestr(e))
1538 1538 if opts.get('exact'):
1539 1539 editor = None
1540 1540 else:
1541 1541 editor = getcommiteditor(editform='import.bypass')
1542 1542 memctx = context.memctx(repo, (p1.node(), p2.node()),
1543 1543 message,
1544 1544 files=files,
1545 1545 filectxfn=store,
1546 1546 user=user,
1547 1547 date=date,
1548 1548 branch=branch,
1549 1549 editor=editor)
1550 1550 n = memctx.commit()
1551 1551 finally:
1552 1552 store.close()
1553 1553 if opts.get('exact') and nocommit:
1554 1554 # --exact with --no-commit is still useful in that it does merge
1555 1555 # and branch bits
1556 1556 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1557 1557 elif opts.get('exact') and (not n or hex(n) != nodeid):
1558 1558 raise error.Abort(_('patch is damaged or loses information'))
1559 1559 msg = _('applied to working directory')
1560 1560 if n:
1561 1561 # i18n: refers to a short changeset id
1562 1562 msg = _('created %s') % short(n)
1563 1563 return msg, n, rejects
1564 1564
1565 1565 # facility to let extensions include additional data in an exported patch
1566 1566 # list of identifiers to be executed in order
1567 1567 extraexport = []
1568 1568 # mapping from identifier to actual export function
1569 1569 # function as to return a string to be added to the header or None
1570 1570 # it is given two arguments (sequencenumber, changectx)
1571 1571 extraexportmap = {}
1572 1572
1573 1573 def _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts):
1574 1574 node = scmutil.binnode(ctx)
1575 1575 parents = [p.node() for p in ctx.parents() if p]
1576 1576 branch = ctx.branch()
1577 1577 if switch_parent:
1578 1578 parents.reverse()
1579 1579
1580 1580 if parents:
1581 1581 prev = parents[0]
1582 1582 else:
1583 1583 prev = nullid
1584 1584
1585 1585 fm.context(ctx=ctx)
1586 1586 fm.plain('# HG changeset patch\n')
1587 1587 fm.write('user', '# User %s\n', ctx.user())
1588 1588 fm.plain('# Date %d %d\n' % ctx.date())
1589 1589 fm.write('date', '# %s\n', fm.formatdate(ctx.date()))
1590 1590 fm.condwrite(branch and branch != 'default',
1591 1591 'branch', '# Branch %s\n', branch)
1592 1592 fm.write('node', '# Node ID %s\n', hex(node))
1593 1593 fm.plain('# Parent %s\n' % hex(prev))
1594 1594 if len(parents) > 1:
1595 1595 fm.plain('# Parent %s\n' % hex(parents[1]))
1596 1596 fm.data(parents=fm.formatlist(pycompat.maplist(hex, parents), name='node'))
1597 1597
1598 1598 # TODO: redesign extraexportmap function to support formatter
1599 1599 for headerid in extraexport:
1600 1600 header = extraexportmap[headerid](seqno, ctx)
1601 1601 if header is not None:
1602 1602 fm.plain('# %s\n' % header)
1603 1603
1604 1604 fm.write('desc', '%s\n', ctx.description().rstrip())
1605 1605 fm.plain('\n')
1606 1606
1607 1607 if fm.isplain():
1608 1608 chunkiter = patch.diffui(repo, prev, node, match, opts=diffopts)
1609 1609 for chunk, label in chunkiter:
1610 1610 fm.plain(chunk, label=label)
1611 1611 else:
1612 1612 chunkiter = patch.diff(repo, prev, node, match, opts=diffopts)
1613 1613 # TODO: make it structured?
1614 1614 fm.data(diff=b''.join(chunkiter))
1615 1615
1616 1616 def _exportfile(repo, revs, fm, dest, switch_parent, diffopts, match):
1617 1617 """Export changesets to stdout or a single file"""
1618 1618 for seqno, rev in enumerate(revs, 1):
1619 1619 ctx = repo[rev]
1620 1620 if not dest.startswith('<'):
1621 1621 repo.ui.note("%s\n" % dest)
1622 1622 fm.startitem()
1623 1623 _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts)
1624 1624
1625 1625 def _exportfntemplate(repo, revs, basefm, fntemplate, switch_parent, diffopts,
1626 1626 match):
1627 1627 """Export changesets to possibly multiple files"""
1628 1628 total = len(revs)
1629 1629 revwidth = max(len(str(rev)) for rev in revs)
1630 1630 filemap = util.sortdict() # filename: [(seqno, rev), ...]
1631 1631
1632 1632 for seqno, rev in enumerate(revs, 1):
1633 1633 ctx = repo[rev]
1634 1634 dest = makefilename(ctx, fntemplate,
1635 1635 total=total, seqno=seqno, revwidth=revwidth)
1636 1636 filemap.setdefault(dest, []).append((seqno, rev))
1637 1637
1638 1638 for dest in filemap:
1639 1639 with formatter.maybereopen(basefm, dest) as fm:
1640 1640 repo.ui.note("%s\n" % dest)
1641 1641 for seqno, rev in filemap[dest]:
1642 1642 fm.startitem()
1643 1643 ctx = repo[rev]
1644 1644 _exportsingle(repo, ctx, fm, match, switch_parent, seqno,
1645 1645 diffopts)
1646 1646
1647 1647 def export(repo, revs, basefm, fntemplate='hg-%h.patch', switch_parent=False,
1648 1648 opts=None, match=None):
1649 1649 '''export changesets as hg patches
1650 1650
1651 1651 Args:
1652 1652 repo: The repository from which we're exporting revisions.
1653 1653 revs: A list of revisions to export as revision numbers.
1654 1654 basefm: A formatter to which patches should be written.
1655 1655 fntemplate: An optional string to use for generating patch file names.
1656 1656 switch_parent: If True, show diffs against second parent when not nullid.
1657 1657 Default is false, which always shows diff against p1.
1658 1658 opts: diff options to use for generating the patch.
1659 1659 match: If specified, only export changes to files matching this matcher.
1660 1660
1661 1661 Returns:
1662 1662 Nothing.
1663 1663
1664 1664 Side Effect:
1665 1665 "HG Changeset Patch" data is emitted to one of the following
1666 1666 destinations:
1667 1667 fntemplate specified: Each rev is written to a unique file named using
1668 1668 the given template.
1669 1669 Otherwise: All revs will be written to basefm.
1670 1670 '''
1671 1671 scmutil.prefetchfiles(repo, revs, match)
1672 1672
1673 1673 if not fntemplate:
1674 1674 _exportfile(repo, revs, basefm, '<unnamed>', switch_parent, opts, match)
1675 1675 else:
1676 1676 _exportfntemplate(repo, revs, basefm, fntemplate, switch_parent, opts,
1677 1677 match)
1678 1678
1679 1679 def exportfile(repo, revs, fp, switch_parent=False, opts=None, match=None):
1680 1680 """Export changesets to the given file stream"""
1681 1681 scmutil.prefetchfiles(repo, revs, match)
1682 1682
1683 1683 dest = getattr(fp, 'name', '<unnamed>')
1684 1684 with formatter.formatter(repo.ui, fp, 'export', {}) as fm:
1685 1685 _exportfile(repo, revs, fm, dest, switch_parent, opts, match)
1686 1686
1687 1687 def showmarker(fm, marker, index=None):
1688 1688 """utility function to display obsolescence marker in a readable way
1689 1689
1690 1690 To be used by debug function."""
1691 1691 if index is not None:
1692 1692 fm.write('index', '%i ', index)
1693 1693 fm.write('prednode', '%s ', hex(marker.prednode()))
1694 1694 succs = marker.succnodes()
1695 1695 fm.condwrite(succs, 'succnodes', '%s ',
1696 1696 fm.formatlist(map(hex, succs), name='node'))
1697 1697 fm.write('flag', '%X ', marker.flags())
1698 1698 parents = marker.parentnodes()
1699 1699 if parents is not None:
1700 1700 fm.write('parentnodes', '{%s} ',
1701 1701 fm.formatlist(map(hex, parents), name='node', sep=', '))
1702 1702 fm.write('date', '(%s) ', fm.formatdate(marker.date()))
1703 1703 meta = marker.metadata().copy()
1704 1704 meta.pop('date', None)
1705 1705 smeta = pycompat.rapply(pycompat.maybebytestr, meta)
1706 1706 fm.write('metadata', '{%s}', fm.formatdict(smeta, fmt='%r: %r', sep=', '))
1707 1707 fm.plain('\n')
1708 1708
1709 1709 def finddate(ui, repo, date):
1710 1710 """Find the tipmost changeset that matches the given date spec"""
1711 1711
1712 1712 df = dateutil.matchdate(date)
1713 1713 m = scmutil.matchall(repo)
1714 1714 results = {}
1715 1715
1716 1716 def prep(ctx, fns):
1717 1717 d = ctx.date()
1718 1718 if df(d[0]):
1719 1719 results[ctx.rev()] = d
1720 1720
1721 1721 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1722 1722 rev = ctx.rev()
1723 1723 if rev in results:
1724 1724 ui.status(_("found revision %s from %s\n") %
1725 1725 (rev, dateutil.datestr(results[rev])))
1726 1726 return '%d' % rev
1727 1727
1728 1728 raise error.Abort(_("revision matching date not found"))
1729 1729
1730 1730 def increasingwindows(windowsize=8, sizelimit=512):
1731 1731 while True:
1732 1732 yield windowsize
1733 1733 if windowsize < sizelimit:
1734 1734 windowsize *= 2
1735 1735
1736 1736 def _walkrevs(repo, opts):
1737 1737 # Default --rev value depends on --follow but --follow behavior
1738 1738 # depends on revisions resolved from --rev...
1739 1739 follow = opts.get('follow') or opts.get('follow_first')
1740 1740 if opts.get('rev'):
1741 1741 revs = scmutil.revrange(repo, opts['rev'])
1742 1742 elif follow and repo.dirstate.p1() == nullid:
1743 1743 revs = smartset.baseset()
1744 1744 elif follow:
1745 1745 revs = repo.revs('reverse(:.)')
1746 1746 else:
1747 1747 revs = smartset.spanset(repo)
1748 1748 revs.reverse()
1749 1749 return revs
1750 1750
1751 1751 class FileWalkError(Exception):
1752 1752 pass
1753 1753
1754 1754 def walkfilerevs(repo, match, follow, revs, fncache):
1755 1755 '''Walks the file history for the matched files.
1756 1756
1757 1757 Returns the changeset revs that are involved in the file history.
1758 1758
1759 1759 Throws FileWalkError if the file history can't be walked using
1760 1760 filelogs alone.
1761 1761 '''
1762 1762 wanted = set()
1763 1763 copies = []
1764 1764 minrev, maxrev = min(revs), max(revs)
1765 1765 def filerevgen(filelog, last):
1766 1766 """
1767 1767 Only files, no patterns. Check the history of each file.
1768 1768
1769 1769 Examines filelog entries within minrev, maxrev linkrev range
1770 1770 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1771 1771 tuples in backwards order
1772 1772 """
1773 1773 cl_count = len(repo)
1774 1774 revs = []
1775 1775 for j in pycompat.xrange(0, last + 1):
1776 1776 linkrev = filelog.linkrev(j)
1777 1777 if linkrev < minrev:
1778 1778 continue
1779 1779 # only yield rev for which we have the changelog, it can
1780 1780 # happen while doing "hg log" during a pull or commit
1781 1781 if linkrev >= cl_count:
1782 1782 break
1783 1783
1784 1784 parentlinkrevs = []
1785 1785 for p in filelog.parentrevs(j):
1786 1786 if p != nullrev:
1787 1787 parentlinkrevs.append(filelog.linkrev(p))
1788 1788 n = filelog.node(j)
1789 1789 revs.append((linkrev, parentlinkrevs,
1790 1790 follow and filelog.renamed(n)))
1791 1791
1792 1792 return reversed(revs)
1793 1793 def iterfiles():
1794 1794 pctx = repo['.']
1795 1795 for filename in match.files():
1796 1796 if follow:
1797 1797 if filename not in pctx:
1798 1798 raise error.Abort(_('cannot follow file not in parent '
1799 1799 'revision: "%s"') % filename)
1800 1800 yield filename, pctx[filename].filenode()
1801 1801 else:
1802 1802 yield filename, None
1803 1803 for filename_node in copies:
1804 1804 yield filename_node
1805 1805
1806 1806 for file_, node in iterfiles():
1807 1807 filelog = repo.file(file_)
1808 1808 if not len(filelog):
1809 1809 if node is None:
1810 1810 # A zero count may be a directory or deleted file, so
1811 1811 # try to find matching entries on the slow path.
1812 1812 if follow:
1813 1813 raise error.Abort(
1814 1814 _('cannot follow nonexistent file: "%s"') % file_)
1815 1815 raise FileWalkError("Cannot walk via filelog")
1816 1816 else:
1817 1817 continue
1818 1818
1819 1819 if node is None:
1820 1820 last = len(filelog) - 1
1821 1821 else:
1822 1822 last = filelog.rev(node)
1823 1823
1824 1824 # keep track of all ancestors of the file
1825 1825 ancestors = {filelog.linkrev(last)}
1826 1826
1827 1827 # iterate from latest to oldest revision
1828 1828 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1829 1829 if not follow:
1830 1830 if rev > maxrev:
1831 1831 continue
1832 1832 else:
1833 1833 # Note that last might not be the first interesting
1834 1834 # rev to us:
1835 1835 # if the file has been changed after maxrev, we'll
1836 1836 # have linkrev(last) > maxrev, and we still need
1837 1837 # to explore the file graph
1838 1838 if rev not in ancestors:
1839 1839 continue
1840 1840 # XXX insert 1327 fix here
1841 1841 if flparentlinkrevs:
1842 1842 ancestors.update(flparentlinkrevs)
1843 1843
1844 1844 fncache.setdefault(rev, []).append(file_)
1845 1845 wanted.add(rev)
1846 1846 if copied:
1847 1847 copies.append(copied)
1848 1848
1849 1849 return wanted
1850 1850
1851 1851 class _followfilter(object):
1852 1852 def __init__(self, repo, onlyfirst=False):
1853 1853 self.repo = repo
1854 1854 self.startrev = nullrev
1855 1855 self.roots = set()
1856 1856 self.onlyfirst = onlyfirst
1857 1857
1858 1858 def match(self, rev):
1859 1859 def realparents(rev):
1860 1860 if self.onlyfirst:
1861 1861 return self.repo.changelog.parentrevs(rev)[0:1]
1862 1862 else:
1863 1863 return filter(lambda x: x != nullrev,
1864 1864 self.repo.changelog.parentrevs(rev))
1865 1865
1866 1866 if self.startrev == nullrev:
1867 1867 self.startrev = rev
1868 1868 return True
1869 1869
1870 1870 if rev > self.startrev:
1871 1871 # forward: all descendants
1872 1872 if not self.roots:
1873 1873 self.roots.add(self.startrev)
1874 1874 for parent in realparents(rev):
1875 1875 if parent in self.roots:
1876 1876 self.roots.add(rev)
1877 1877 return True
1878 1878 else:
1879 1879 # backwards: all parents
1880 1880 if not self.roots:
1881 1881 self.roots.update(realparents(self.startrev))
1882 1882 if rev in self.roots:
1883 1883 self.roots.remove(rev)
1884 1884 self.roots.update(realparents(rev))
1885 1885 return True
1886 1886
1887 1887 return False
1888 1888
1889 1889 def walkchangerevs(repo, match, opts, prepare):
1890 1890 '''Iterate over files and the revs in which they changed.
1891 1891
1892 1892 Callers most commonly need to iterate backwards over the history
1893 1893 in which they are interested. Doing so has awful (quadratic-looking)
1894 1894 performance, so we use iterators in a "windowed" way.
1895 1895
1896 1896 We walk a window of revisions in the desired order. Within the
1897 1897 window, we first walk forwards to gather data, then in the desired
1898 1898 order (usually backwards) to display it.
1899 1899
1900 1900 This function returns an iterator yielding contexts. Before
1901 1901 yielding each context, the iterator will first call the prepare
1902 1902 function on each context in the window in forward order.'''
1903 1903
1904 1904 allfiles = opts.get('all_files')
1905 1905 follow = opts.get('follow') or opts.get('follow_first')
1906 1906 revs = _walkrevs(repo, opts)
1907 1907 if not revs:
1908 1908 return []
1909 1909 wanted = set()
1910 1910 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
1911 1911 fncache = {}
1912 1912 change = repo.__getitem__
1913 1913
1914 1914 # First step is to fill wanted, the set of revisions that we want to yield.
1915 1915 # When it does not induce extra cost, we also fill fncache for revisions in
1916 1916 # wanted: a cache of filenames that were changed (ctx.files()) and that
1917 1917 # match the file filtering conditions.
1918 1918
1919 1919 if match.always() or allfiles:
1920 1920 # No files, no patterns. Display all revs.
1921 1921 wanted = revs
1922 1922 elif not slowpath:
1923 1923 # We only have to read through the filelog to find wanted revisions
1924 1924
1925 1925 try:
1926 1926 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1927 1927 except FileWalkError:
1928 1928 slowpath = True
1929 1929
1930 1930 # We decided to fall back to the slowpath because at least one
1931 1931 # of the paths was not a file. Check to see if at least one of them
1932 1932 # existed in history, otherwise simply return
1933 1933 for path in match.files():
1934 1934 if path == '.' or path in repo.store:
1935 1935 break
1936 1936 else:
1937 1937 return []
1938 1938
1939 1939 if slowpath:
1940 1940 # We have to read the changelog to match filenames against
1941 1941 # changed files
1942 1942
1943 1943 if follow:
1944 1944 raise error.Abort(_('can only follow copies/renames for explicit '
1945 1945 'filenames'))
1946 1946
1947 1947 # The slow path checks files modified in every changeset.
1948 1948 # This is really slow on large repos, so compute the set lazily.
1949 1949 class lazywantedset(object):
1950 1950 def __init__(self):
1951 1951 self.set = set()
1952 1952 self.revs = set(revs)
1953 1953
1954 1954 # No need to worry about locality here because it will be accessed
1955 1955 # in the same order as the increasing window below.
1956 1956 def __contains__(self, value):
1957 1957 if value in self.set:
1958 1958 return True
1959 1959 elif not value in self.revs:
1960 1960 return False
1961 1961 else:
1962 1962 self.revs.discard(value)
1963 1963 ctx = change(value)
1964 1964 matches = [f for f in ctx.files() if match(f)]
1965 1965 if matches:
1966 1966 fncache[value] = matches
1967 1967 self.set.add(value)
1968 1968 return True
1969 1969 return False
1970 1970
1971 1971 def discard(self, value):
1972 1972 self.revs.discard(value)
1973 1973 self.set.discard(value)
1974 1974
1975 1975 wanted = lazywantedset()
1976 1976
1977 1977 # it might be worthwhile to do this in the iterator if the rev range
1978 1978 # is descending and the prune args are all within that range
1979 1979 for rev in opts.get('prune', ()):
1980 1980 rev = repo[rev].rev()
1981 1981 ff = _followfilter(repo)
1982 1982 stop = min(revs[0], revs[-1])
1983 1983 for x in pycompat.xrange(rev, stop - 1, -1):
1984 1984 if ff.match(x):
1985 1985 wanted = wanted - [x]
1986 1986
1987 1987 # Now that wanted is correctly initialized, we can iterate over the
1988 1988 # revision range, yielding only revisions in wanted.
1989 1989 def iterate():
1990 1990 if follow and match.always():
1991 1991 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
1992 1992 def want(rev):
1993 1993 return ff.match(rev) and rev in wanted
1994 1994 else:
1995 1995 def want(rev):
1996 1996 return rev in wanted
1997 1997
1998 1998 it = iter(revs)
1999 1999 stopiteration = False
2000 2000 for windowsize in increasingwindows():
2001 2001 nrevs = []
2002 2002 for i in pycompat.xrange(windowsize):
2003 2003 rev = next(it, None)
2004 2004 if rev is None:
2005 2005 stopiteration = True
2006 2006 break
2007 2007 elif want(rev):
2008 2008 nrevs.append(rev)
2009 2009 for rev in sorted(nrevs):
2010 2010 fns = fncache.get(rev)
2011 2011 ctx = change(rev)
2012 2012 if not fns:
2013 2013 def fns_generator():
2014 2014 if allfiles:
2015 2015 fiter = iter(ctx)
2016 2016 else:
2017 2017 fiter = ctx.files()
2018 2018 for f in fiter:
2019 2019 if match(f):
2020 2020 yield f
2021 2021 fns = fns_generator()
2022 2022 prepare(ctx, fns)
2023 2023 for rev in nrevs:
2024 2024 yield change(rev)
2025 2025
2026 2026 if stopiteration:
2027 2027 break
2028 2028
2029 2029 return iterate()
2030 2030
2031 2031 def add(ui, repo, match, prefix, uipathfn, explicitonly, **opts):
2032 2032 bad = []
2033 2033
2034 2034 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2035 2035 names = []
2036 2036 wctx = repo[None]
2037 2037 cca = None
2038 2038 abort, warn = scmutil.checkportabilityalert(ui)
2039 2039 if abort or warn:
2040 2040 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2041 2041
2042 2042 match = repo.narrowmatch(match, includeexact=True)
2043 2043 badmatch = matchmod.badmatch(match, badfn)
2044 2044 dirstate = repo.dirstate
2045 2045 # We don't want to just call wctx.walk here, since it would return a lot of
2046 2046 # clean files, which we aren't interested in and takes time.
2047 2047 for f in sorted(dirstate.walk(badmatch, subrepos=sorted(wctx.substate),
2048 2048 unknown=True, ignored=False, full=False)):
2049 2049 exact = match.exact(f)
2050 2050 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2051 2051 if cca:
2052 2052 cca(f)
2053 2053 names.append(f)
2054 2054 if ui.verbose or not exact:
2055 2055 ui.status(_('adding %s\n') % uipathfn(f),
2056 2056 label='ui.addremove.added')
2057 2057
2058 2058 for subpath in sorted(wctx.substate):
2059 2059 sub = wctx.sub(subpath)
2060 2060 try:
2061 2061 submatch = matchmod.subdirmatcher(subpath, match)
2062 2062 subprefix = repo.wvfs.reljoin(prefix, subpath)
2063 2063 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2064 2064 if opts.get(r'subrepos'):
2065 2065 bad.extend(sub.add(ui, submatch, subprefix, subuipathfn, False,
2066 2066 **opts))
2067 2067 else:
2068 2068 bad.extend(sub.add(ui, submatch, subprefix, subuipathfn, True,
2069 2069 **opts))
2070 2070 except error.LookupError:
2071 2071 ui.status(_("skipping missing subrepository: %s\n")
2072 2072 % uipathfn(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, uipathfn, explicitonly, dryrun,
2089 2089 interactive):
2090 2090 if dryrun and interactive:
2091 2091 raise error.Abort(_("cannot specify both --dry-run and --interactive"))
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 submatch = matchmod.subdirmatcher(subpath, match)
2105 2105 subprefix = repo.wvfs.reljoin(prefix, subpath)
2106 2106 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2107 2107 try:
2108 2108 subbad, subforgot = sub.forget(submatch, subprefix, subuipathfn,
2109 2109 dryrun=dryrun,
2110 2110 interactive=interactive)
2111 2111 bad.extend([subpath + '/' + f for f in subbad])
2112 2112 forgot.extend([subpath + '/' + f for f in subforgot])
2113 2113 except error.LookupError:
2114 2114 ui.status(_("skipping missing subrepository: %s\n")
2115 2115 % uipathfn(subpath))
2116 2116
2117 2117 if not explicitonly:
2118 2118 for f in match.files():
2119 2119 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2120 2120 if f not in forgot:
2121 2121 if repo.wvfs.exists(f):
2122 2122 # Don't complain if the exact case match wasn't given.
2123 2123 # But don't do this until after checking 'forgot', so
2124 2124 # that subrepo files aren't normalized, and this op is
2125 2125 # purely from data cached by the status walk above.
2126 2126 if repo.dirstate.normalize(f) in repo.dirstate:
2127 2127 continue
2128 2128 ui.warn(_('not removing %s: '
2129 2129 'file is already untracked\n')
2130 2130 % uipathfn(f))
2131 2131 bad.append(f)
2132 2132
2133 2133 if interactive:
2134 2134 responses = _('[Ynsa?]'
2135 2135 '$$ &Yes, forget this file'
2136 2136 '$$ &No, skip this file'
2137 2137 '$$ &Skip remaining files'
2138 2138 '$$ Include &all remaining files'
2139 2139 '$$ &? (display help)')
2140 2140 for filename in forget[:]:
2141 2141 r = ui.promptchoice(_('forget %s %s') %
2142 2142 (uipathfn(filename), responses))
2143 2143 if r == 4: # ?
2144 2144 while r == 4:
2145 2145 for c, t in ui.extractchoices(responses)[1]:
2146 2146 ui.write('%s - %s\n' % (c, encoding.lower(t)))
2147 2147 r = ui.promptchoice(_('forget %s %s') %
2148 2148 (uipathfn(filename), responses))
2149 2149 if r == 0: # yes
2150 2150 continue
2151 2151 elif r == 1: # no
2152 2152 forget.remove(filename)
2153 2153 elif r == 2: # Skip
2154 2154 fnindex = forget.index(filename)
2155 2155 del forget[fnindex:]
2156 2156 break
2157 2157 elif r == 3: # All
2158 2158 break
2159 2159
2160 2160 for f in forget:
2161 2161 if ui.verbose or not match.exact(f) or interactive:
2162 2162 ui.status(_('removing %s\n') % uipathfn(f),
2163 2163 label='ui.addremove.removed')
2164 2164
2165 2165 if not dryrun:
2166 2166 rejected = wctx.forget(forget, prefix)
2167 2167 bad.extend(f for f in rejected if f in match.files())
2168 2168 forgot.extend(f for f in forget if f not in rejected)
2169 2169 return bad, forgot
2170 2170
2171 2171 def files(ui, ctx, m, fm, fmt, subrepos):
2172 2172 ret = 1
2173 2173
2174 2174 needsfctx = ui.verbose or {'size', 'flags'} & fm.datahint()
2175 2175 uipathfn = scmutil.getuipathfn(ctx.repo(), legacyrelativevalue=True)
2176 2176 for f in ctx.matches(m):
2177 2177 fm.startitem()
2178 2178 fm.context(ctx=ctx)
2179 2179 if needsfctx:
2180 2180 fc = ctx[f]
2181 2181 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2182 2182 fm.data(path=f)
2183 2183 fm.plain(fmt % uipathfn(f))
2184 2184 ret = 0
2185 2185
2186 2186 for subpath in sorted(ctx.substate):
2187 2187 submatch = matchmod.subdirmatcher(subpath, m)
2188 2188 if (subrepos or m.exact(subpath) or any(submatch.files())):
2189 2189 sub = ctx.sub(subpath)
2190 2190 try:
2191 2191 recurse = m.exact(subpath) or subrepos
2192 2192 if sub.printfiles(ui, submatch, fm, fmt, recurse) == 0:
2193 2193 ret = 0
2194 2194 except error.LookupError:
2195 2195 ui.status(_("skipping missing subrepository: %s\n")
2196 % m.rel(subpath))
2196 % uipathfn(subpath))
2197 2197
2198 2198 return ret
2199 2199
2200 2200 def remove(ui, repo, m, prefix, uipathfn, after, force, subrepos, dryrun,
2201 2201 warnings=None):
2202 2202 ret = 0
2203 2203 s = repo.status(match=m, clean=True)
2204 2204 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2205 2205
2206 2206 wctx = repo[None]
2207 2207
2208 2208 if warnings is None:
2209 2209 warnings = []
2210 2210 warn = True
2211 2211 else:
2212 2212 warn = False
2213 2213
2214 2214 subs = sorted(wctx.substate)
2215 2215 progress = ui.makeprogress(_('searching'), total=len(subs),
2216 2216 unit=_('subrepos'))
2217 2217 for subpath in subs:
2218 2218 submatch = matchmod.subdirmatcher(subpath, m)
2219 2219 subprefix = repo.wvfs.reljoin(prefix, subpath)
2220 2220 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2221 2221 if subrepos or m.exact(subpath) or any(submatch.files()):
2222 2222 progress.increment()
2223 2223 sub = wctx.sub(subpath)
2224 2224 try:
2225 2225 if sub.removefiles(submatch, subprefix, subuipathfn, after,
2226 2226 force, subrepos, dryrun, warnings):
2227 2227 ret = 1
2228 2228 except error.LookupError:
2229 2229 warnings.append(_("skipping missing subrepository: %s\n")
2230 2230 % uipathfn(subpath))
2231 2231 progress.complete()
2232 2232
2233 2233 # warn about failure to delete explicit files/dirs
2234 2234 deleteddirs = util.dirs(deleted)
2235 2235 files = m.files()
2236 2236 progress = ui.makeprogress(_('deleting'), total=len(files),
2237 2237 unit=_('files'))
2238 2238 for f in files:
2239 2239 def insubrepo():
2240 2240 for subpath in wctx.substate:
2241 2241 if f.startswith(subpath + '/'):
2242 2242 return True
2243 2243 return False
2244 2244
2245 2245 progress.increment()
2246 2246 isdir = f in deleteddirs or wctx.hasdir(f)
2247 2247 if (f in repo.dirstate or isdir or f == '.'
2248 2248 or insubrepo() or f in subs):
2249 2249 continue
2250 2250
2251 2251 if repo.wvfs.exists(f):
2252 2252 if repo.wvfs.isdir(f):
2253 2253 warnings.append(_('not removing %s: no tracked files\n')
2254 2254 % uipathfn(f))
2255 2255 else:
2256 2256 warnings.append(_('not removing %s: file is untracked\n')
2257 2257 % uipathfn(f))
2258 2258 # missing files will generate a warning elsewhere
2259 2259 ret = 1
2260 2260 progress.complete()
2261 2261
2262 2262 if force:
2263 2263 list = modified + deleted + clean + added
2264 2264 elif after:
2265 2265 list = deleted
2266 2266 remaining = modified + added + clean
2267 2267 progress = ui.makeprogress(_('skipping'), total=len(remaining),
2268 2268 unit=_('files'))
2269 2269 for f in remaining:
2270 2270 progress.increment()
2271 2271 if ui.verbose or (f in files):
2272 2272 warnings.append(_('not removing %s: file still exists\n')
2273 2273 % uipathfn(f))
2274 2274 ret = 1
2275 2275 progress.complete()
2276 2276 else:
2277 2277 list = deleted + clean
2278 2278 progress = ui.makeprogress(_('skipping'),
2279 2279 total=(len(modified) + len(added)),
2280 2280 unit=_('files'))
2281 2281 for f in modified:
2282 2282 progress.increment()
2283 2283 warnings.append(_('not removing %s: file is modified (use -f'
2284 2284 ' to force removal)\n') % uipathfn(f))
2285 2285 ret = 1
2286 2286 for f in added:
2287 2287 progress.increment()
2288 2288 warnings.append(_("not removing %s: file has been marked for add"
2289 2289 " (use 'hg forget' to undo add)\n") % uipathfn(f))
2290 2290 ret = 1
2291 2291 progress.complete()
2292 2292
2293 2293 list = sorted(list)
2294 2294 progress = ui.makeprogress(_('deleting'), total=len(list),
2295 2295 unit=_('files'))
2296 2296 for f in list:
2297 2297 if ui.verbose or not m.exact(f):
2298 2298 progress.increment()
2299 2299 ui.status(_('removing %s\n') % uipathfn(f),
2300 2300 label='ui.addremove.removed')
2301 2301 progress.complete()
2302 2302
2303 2303 if not dryrun:
2304 2304 with repo.wlock():
2305 2305 if not after:
2306 2306 for f in list:
2307 2307 if f in added:
2308 2308 continue # we never unlink added files on remove
2309 2309 rmdir = repo.ui.configbool('experimental',
2310 2310 'removeemptydirs')
2311 2311 repo.wvfs.unlinkpath(f, ignoremissing=True, rmdir=rmdir)
2312 2312 repo[None].forget(list)
2313 2313
2314 2314 if warn:
2315 2315 for warning in warnings:
2316 2316 ui.warn(warning)
2317 2317
2318 2318 return ret
2319 2319
2320 2320 def _updatecatformatter(fm, ctx, matcher, path, decode):
2321 2321 """Hook for adding data to the formatter used by ``hg cat``.
2322 2322
2323 2323 Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
2324 2324 this method first."""
2325 2325 data = ctx[path].data()
2326 2326 if decode:
2327 2327 data = ctx.repo().wwritedata(path, data)
2328 2328 fm.startitem()
2329 2329 fm.context(ctx=ctx)
2330 2330 fm.write('data', '%s', data)
2331 2331 fm.data(path=path)
2332 2332
2333 2333 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2334 2334 err = 1
2335 2335 opts = pycompat.byteskwargs(opts)
2336 2336
2337 2337 def write(path):
2338 2338 filename = None
2339 2339 if fntemplate:
2340 2340 filename = makefilename(ctx, fntemplate,
2341 2341 pathname=os.path.join(prefix, path))
2342 2342 # attempt to create the directory if it does not already exist
2343 2343 try:
2344 2344 os.makedirs(os.path.dirname(filename))
2345 2345 except OSError:
2346 2346 pass
2347 2347 with formatter.maybereopen(basefm, filename) as fm:
2348 2348 _updatecatformatter(fm, ctx, matcher, path, opts.get('decode'))
2349 2349
2350 2350 # Automation often uses hg cat on single files, so special case it
2351 2351 # for performance to avoid the cost of parsing the manifest.
2352 2352 if len(matcher.files()) == 1 and not matcher.anypats():
2353 2353 file = matcher.files()[0]
2354 2354 mfl = repo.manifestlog
2355 2355 mfnode = ctx.manifestnode()
2356 2356 try:
2357 2357 if mfnode and mfl[mfnode].find(file)[0]:
2358 2358 scmutil.prefetchfiles(repo, [ctx.rev()], matcher)
2359 2359 write(file)
2360 2360 return 0
2361 2361 except KeyError:
2362 2362 pass
2363 2363
2364 2364 scmutil.prefetchfiles(repo, [ctx.rev()], matcher)
2365 2365
2366 2366 for abs in ctx.walk(matcher):
2367 2367 write(abs)
2368 2368 err = 0
2369 2369
2370 2370 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2371 2371 for subpath in sorted(ctx.substate):
2372 2372 sub = ctx.sub(subpath)
2373 2373 try:
2374 2374 submatch = matchmod.subdirmatcher(subpath, matcher)
2375 2375 subprefix = os.path.join(prefix, subpath)
2376 2376 if not sub.cat(submatch, basefm, fntemplate, subprefix,
2377 2377 **pycompat.strkwargs(opts)):
2378 2378 err = 0
2379 2379 except error.RepoLookupError:
2380 2380 ui.status(_("skipping missing subrepository: %s\n") %
2381 2381 uipathfn(subpath))
2382 2382
2383 2383 return err
2384 2384
2385 2385 def commit(ui, repo, commitfunc, pats, opts):
2386 2386 '''commit the specified files or all outstanding changes'''
2387 2387 date = opts.get('date')
2388 2388 if date:
2389 2389 opts['date'] = dateutil.parsedate(date)
2390 2390 message = logmessage(ui, opts)
2391 2391 matcher = scmutil.match(repo[None], pats, opts)
2392 2392
2393 2393 dsguard = None
2394 2394 # extract addremove carefully -- this function can be called from a command
2395 2395 # that doesn't support addremove
2396 2396 if opts.get('addremove'):
2397 2397 dsguard = dirstateguard.dirstateguard(repo, 'commit')
2398 2398 with dsguard or util.nullcontextmanager():
2399 2399 if dsguard:
2400 2400 relative = scmutil.anypats(pats, opts)
2401 2401 uipathfn = scmutil.getuipathfn(repo, forcerelativevalue=relative)
2402 2402 if scmutil.addremove(repo, matcher, "", uipathfn, opts) != 0:
2403 2403 raise error.Abort(
2404 2404 _("failed to mark all new/missing files as added/removed"))
2405 2405
2406 2406 return commitfunc(ui, repo, message, matcher, opts)
2407 2407
2408 2408 def samefile(f, ctx1, ctx2):
2409 2409 if f in ctx1.manifest():
2410 2410 a = ctx1.filectx(f)
2411 2411 if f in ctx2.manifest():
2412 2412 b = ctx2.filectx(f)
2413 2413 return (not a.cmp(b)
2414 2414 and a.flags() == b.flags())
2415 2415 else:
2416 2416 return False
2417 2417 else:
2418 2418 return f not in ctx2.manifest()
2419 2419
2420 2420 def amend(ui, repo, old, extra, pats, opts):
2421 2421 # avoid cycle context -> subrepo -> cmdutil
2422 2422 from . import context
2423 2423
2424 2424 # amend will reuse the existing user if not specified, but the obsolete
2425 2425 # marker creation requires that the current user's name is specified.
2426 2426 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2427 2427 ui.username() # raise exception if username not set
2428 2428
2429 2429 ui.note(_('amending changeset %s\n') % old)
2430 2430 base = old.p1()
2431 2431
2432 2432 with repo.wlock(), repo.lock(), repo.transaction('amend'):
2433 2433 # Participating changesets:
2434 2434 #
2435 2435 # wctx o - workingctx that contains changes from working copy
2436 2436 # | to go into amending commit
2437 2437 # |
2438 2438 # old o - changeset to amend
2439 2439 # |
2440 2440 # base o - first parent of the changeset to amend
2441 2441 wctx = repo[None]
2442 2442
2443 2443 # Copy to avoid mutating input
2444 2444 extra = extra.copy()
2445 2445 # Update extra dict from amended commit (e.g. to preserve graft
2446 2446 # source)
2447 2447 extra.update(old.extra())
2448 2448
2449 2449 # Also update it from the from the wctx
2450 2450 extra.update(wctx.extra())
2451 2451
2452 2452 user = opts.get('user') or old.user()
2453 2453
2454 2454 datemaydiffer = False # date-only change should be ignored?
2455 2455 if opts.get('date') and opts.get('currentdate'):
2456 2456 raise error.Abort(_('--date and --currentdate are mutually '
2457 2457 'exclusive'))
2458 2458 if opts.get('date'):
2459 2459 date = dateutil.parsedate(opts.get('date'))
2460 2460 elif opts.get('currentdate'):
2461 2461 date = dateutil.makedate()
2462 2462 elif (ui.configbool('rewrite', 'update-timestamp')
2463 2463 and opts.get('currentdate') is None):
2464 2464 date = dateutil.makedate()
2465 2465 datemaydiffer = True
2466 2466 else:
2467 2467 date = old.date()
2468 2468
2469 2469 if len(old.parents()) > 1:
2470 2470 # ctx.files() isn't reliable for merges, so fall back to the
2471 2471 # slower repo.status() method
2472 2472 files = set([fn for st in base.status(old)[:3]
2473 2473 for fn in st])
2474 2474 else:
2475 2475 files = set(old.files())
2476 2476
2477 2477 # add/remove the files to the working copy if the "addremove" option
2478 2478 # was specified.
2479 2479 matcher = scmutil.match(wctx, pats, opts)
2480 2480 relative = scmutil.anypats(pats, opts)
2481 2481 uipathfn = scmutil.getuipathfn(repo, forcerelativevalue=relative)
2482 2482 if (opts.get('addremove')
2483 2483 and scmutil.addremove(repo, matcher, "", uipathfn, opts)):
2484 2484 raise error.Abort(
2485 2485 _("failed to mark all new/missing files as added/removed"))
2486 2486
2487 2487 # Check subrepos. This depends on in-place wctx._status update in
2488 2488 # subrepo.precommit(). To minimize the risk of this hack, we do
2489 2489 # nothing if .hgsub does not exist.
2490 2490 if '.hgsub' in wctx or '.hgsub' in old:
2491 2491 subs, commitsubs, newsubstate = subrepoutil.precommit(
2492 2492 ui, wctx, wctx._status, matcher)
2493 2493 # amend should abort if commitsubrepos is enabled
2494 2494 assert not commitsubs
2495 2495 if subs:
2496 2496 subrepoutil.writestate(repo, newsubstate)
2497 2497
2498 2498 ms = mergemod.mergestate.read(repo)
2499 2499 mergeutil.checkunresolved(ms)
2500 2500
2501 2501 filestoamend = set(f for f in wctx.files() if matcher(f))
2502 2502
2503 2503 changes = (len(filestoamend) > 0)
2504 2504 if changes:
2505 2505 # Recompute copies (avoid recording a -> b -> a)
2506 2506 copied = copies.pathcopies(base, wctx, matcher)
2507 2507 if old.p2:
2508 2508 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
2509 2509
2510 2510 # Prune files which were reverted by the updates: if old
2511 2511 # introduced file X and the file was renamed in the working
2512 2512 # copy, then those two files are the same and
2513 2513 # we can discard X from our list of files. Likewise if X
2514 2514 # was removed, it's no longer relevant. If X is missing (aka
2515 2515 # deleted), old X must be preserved.
2516 2516 files.update(filestoamend)
2517 2517 files = [f for f in files if (not samefile(f, wctx, base)
2518 2518 or f in wctx.deleted())]
2519 2519
2520 2520 def filectxfn(repo, ctx_, path):
2521 2521 try:
2522 2522 # If the file being considered is not amongst the files
2523 2523 # to be amended, we should return the file context from the
2524 2524 # old changeset. This avoids issues when only some files in
2525 2525 # the working copy are being amended but there are also
2526 2526 # changes to other files from the old changeset.
2527 2527 if path not in filestoamend:
2528 2528 return old.filectx(path)
2529 2529
2530 2530 # Return None for removed files.
2531 2531 if path in wctx.removed():
2532 2532 return None
2533 2533
2534 2534 fctx = wctx[path]
2535 2535 flags = fctx.flags()
2536 2536 mctx = context.memfilectx(repo, ctx_,
2537 2537 fctx.path(), fctx.data(),
2538 2538 islink='l' in flags,
2539 2539 isexec='x' in flags,
2540 2540 copied=copied.get(path))
2541 2541 return mctx
2542 2542 except KeyError:
2543 2543 return None
2544 2544 else:
2545 2545 ui.note(_('copying changeset %s to %s\n') % (old, base))
2546 2546
2547 2547 # Use version of files as in the old cset
2548 2548 def filectxfn(repo, ctx_, path):
2549 2549 try:
2550 2550 return old.filectx(path)
2551 2551 except KeyError:
2552 2552 return None
2553 2553
2554 2554 # See if we got a message from -m or -l, if not, open the editor with
2555 2555 # the message of the changeset to amend.
2556 2556 message = logmessage(ui, opts)
2557 2557
2558 2558 editform = mergeeditform(old, 'commit.amend')
2559 2559 editor = getcommiteditor(editform=editform,
2560 2560 **pycompat.strkwargs(opts))
2561 2561
2562 2562 if not message:
2563 2563 editor = getcommiteditor(edit=True, editform=editform)
2564 2564 message = old.description()
2565 2565
2566 2566 pureextra = extra.copy()
2567 2567 extra['amend_source'] = old.hex()
2568 2568
2569 2569 new = context.memctx(repo,
2570 2570 parents=[base.node(), old.p2().node()],
2571 2571 text=message,
2572 2572 files=files,
2573 2573 filectxfn=filectxfn,
2574 2574 user=user,
2575 2575 date=date,
2576 2576 extra=extra,
2577 2577 editor=editor)
2578 2578
2579 2579 newdesc = changelog.stripdesc(new.description())
2580 2580 if ((not changes)
2581 2581 and newdesc == old.description()
2582 2582 and user == old.user()
2583 2583 and (date == old.date() or datemaydiffer)
2584 2584 and pureextra == old.extra()):
2585 2585 # nothing changed. continuing here would create a new node
2586 2586 # anyway because of the amend_source noise.
2587 2587 #
2588 2588 # This not what we expect from amend.
2589 2589 return old.node()
2590 2590
2591 2591 commitphase = None
2592 2592 if opts.get('secret'):
2593 2593 commitphase = phases.secret
2594 2594 newid = repo.commitctx(new)
2595 2595
2596 2596 # Reroute the working copy parent to the new changeset
2597 2597 repo.setparents(newid, nullid)
2598 2598 mapping = {old.node(): (newid,)}
2599 2599 obsmetadata = None
2600 2600 if opts.get('note'):
2601 2601 obsmetadata = {'note': encoding.fromlocal(opts['note'])}
2602 2602 backup = ui.configbool('rewrite', 'backup-bundle')
2603 2603 scmutil.cleanupnodes(repo, mapping, 'amend', metadata=obsmetadata,
2604 2604 fixphase=True, targetphase=commitphase,
2605 2605 backup=backup)
2606 2606
2607 2607 # Fixing the dirstate because localrepo.commitctx does not update
2608 2608 # it. This is rather convenient because we did not need to update
2609 2609 # the dirstate for all the files in the new commit which commitctx
2610 2610 # could have done if it updated the dirstate. Now, we can
2611 2611 # selectively update the dirstate only for the amended files.
2612 2612 dirstate = repo.dirstate
2613 2613
2614 2614 # Update the state of the files which were added and
2615 2615 # and modified in the amend to "normal" in the dirstate.
2616 2616 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
2617 2617 for f in normalfiles:
2618 2618 dirstate.normal(f)
2619 2619
2620 2620 # Update the state of files which were removed in the amend
2621 2621 # to "removed" in the dirstate.
2622 2622 removedfiles = set(wctx.removed()) & filestoamend
2623 2623 for f in removedfiles:
2624 2624 dirstate.drop(f)
2625 2625
2626 2626 return newid
2627 2627
2628 2628 def commiteditor(repo, ctx, subs, editform=''):
2629 2629 if ctx.description():
2630 2630 return ctx.description()
2631 2631 return commitforceeditor(repo, ctx, subs, editform=editform,
2632 2632 unchangedmessagedetection=True)
2633 2633
2634 2634 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2635 2635 editform='', unchangedmessagedetection=False):
2636 2636 if not extramsg:
2637 2637 extramsg = _("Leave message empty to abort commit.")
2638 2638
2639 2639 forms = [e for e in editform.split('.') if e]
2640 2640 forms.insert(0, 'changeset')
2641 2641 templatetext = None
2642 2642 while forms:
2643 2643 ref = '.'.join(forms)
2644 2644 if repo.ui.config('committemplate', ref):
2645 2645 templatetext = committext = buildcommittemplate(
2646 2646 repo, ctx, subs, extramsg, ref)
2647 2647 break
2648 2648 forms.pop()
2649 2649 else:
2650 2650 committext = buildcommittext(repo, ctx, subs, extramsg)
2651 2651
2652 2652 # run editor in the repository root
2653 2653 olddir = encoding.getcwd()
2654 2654 os.chdir(repo.root)
2655 2655
2656 2656 # make in-memory changes visible to external process
2657 2657 tr = repo.currenttransaction()
2658 2658 repo.dirstate.write(tr)
2659 2659 pending = tr and tr.writepending() and repo.root
2660 2660
2661 2661 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
2662 2662 editform=editform, pending=pending,
2663 2663 repopath=repo.path, action='commit')
2664 2664 text = editortext
2665 2665
2666 2666 # strip away anything below this special string (used for editors that want
2667 2667 # to display the diff)
2668 2668 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
2669 2669 if stripbelow:
2670 2670 text = text[:stripbelow.start()]
2671 2671
2672 2672 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2673 2673 os.chdir(olddir)
2674 2674
2675 2675 if finishdesc:
2676 2676 text = finishdesc(text)
2677 2677 if not text.strip():
2678 2678 raise error.Abort(_("empty commit message"))
2679 2679 if unchangedmessagedetection and editortext == templatetext:
2680 2680 raise error.Abort(_("commit message unchanged"))
2681 2681
2682 2682 return text
2683 2683
2684 2684 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
2685 2685 ui = repo.ui
2686 2686 spec = formatter.templatespec(ref, None, None)
2687 2687 t = logcmdutil.changesettemplater(ui, repo, spec)
2688 2688 t.t.cache.update((k, templater.unquotestring(v))
2689 2689 for k, v in repo.ui.configitems('committemplate'))
2690 2690
2691 2691 if not extramsg:
2692 2692 extramsg = '' # ensure that extramsg is string
2693 2693
2694 2694 ui.pushbuffer()
2695 2695 t.show(ctx, extramsg=extramsg)
2696 2696 return ui.popbuffer()
2697 2697
2698 2698 def hgprefix(msg):
2699 2699 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
2700 2700
2701 2701 def buildcommittext(repo, ctx, subs, extramsg):
2702 2702 edittext = []
2703 2703 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2704 2704 if ctx.description():
2705 2705 edittext.append(ctx.description())
2706 2706 edittext.append("")
2707 2707 edittext.append("") # Empty line between message and comments.
2708 2708 edittext.append(hgprefix(_("Enter commit message."
2709 2709 " Lines beginning with 'HG:' are removed.")))
2710 2710 edittext.append(hgprefix(extramsg))
2711 2711 edittext.append("HG: --")
2712 2712 edittext.append(hgprefix(_("user: %s") % ctx.user()))
2713 2713 if ctx.p2():
2714 2714 edittext.append(hgprefix(_("branch merge")))
2715 2715 if ctx.branch():
2716 2716 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
2717 2717 if bookmarks.isactivewdirparent(repo):
2718 2718 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
2719 2719 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
2720 2720 edittext.extend([hgprefix(_("added %s") % f) for f in added])
2721 2721 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
2722 2722 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
2723 2723 if not added and not modified and not removed:
2724 2724 edittext.append(hgprefix(_("no files changed")))
2725 2725 edittext.append("")
2726 2726
2727 2727 return "\n".join(edittext)
2728 2728
2729 2729 def commitstatus(repo, node, branch, bheads=None, opts=None):
2730 2730 if opts is None:
2731 2731 opts = {}
2732 2732 ctx = repo[node]
2733 2733 parents = ctx.parents()
2734 2734
2735 2735 if (not opts.get('amend') and bheads and node not in bheads and not
2736 2736 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2737 2737 repo.ui.status(_('created new head\n'))
2738 2738 # The message is not printed for initial roots. For the other
2739 2739 # changesets, it is printed in the following situations:
2740 2740 #
2741 2741 # Par column: for the 2 parents with ...
2742 2742 # N: null or no parent
2743 2743 # B: parent is on another named branch
2744 2744 # C: parent is a regular non head changeset
2745 2745 # H: parent was a branch head of the current branch
2746 2746 # Msg column: whether we print "created new head" message
2747 2747 # In the following, it is assumed that there already exists some
2748 2748 # initial branch heads of the current branch, otherwise nothing is
2749 2749 # printed anyway.
2750 2750 #
2751 2751 # Par Msg Comment
2752 2752 # N N y additional topo root
2753 2753 #
2754 2754 # B N y additional branch root
2755 2755 # C N y additional topo head
2756 2756 # H N n usual case
2757 2757 #
2758 2758 # B B y weird additional branch root
2759 2759 # C B y branch merge
2760 2760 # H B n merge with named branch
2761 2761 #
2762 2762 # C C y additional head from merge
2763 2763 # C H n merge with a head
2764 2764 #
2765 2765 # H H n head merge: head count decreases
2766 2766
2767 2767 if not opts.get('close_branch'):
2768 2768 for r in parents:
2769 2769 if r.closesbranch() and r.branch() == branch:
2770 2770 repo.ui.status(_('reopening closed branch head %d\n') % r.rev())
2771 2771
2772 2772 if repo.ui.debugflag:
2773 2773 repo.ui.write(_('committed changeset %d:%s\n') % (ctx.rev(), ctx.hex()))
2774 2774 elif repo.ui.verbose:
2775 2775 repo.ui.write(_('committed changeset %d:%s\n') % (ctx.rev(), ctx))
2776 2776
2777 2777 def postcommitstatus(repo, pats, opts):
2778 2778 return repo.status(match=scmutil.match(repo[None], pats, opts))
2779 2779
2780 2780 def revert(ui, repo, ctx, parents, *pats, **opts):
2781 2781 opts = pycompat.byteskwargs(opts)
2782 2782 parent, p2 = parents
2783 2783 node = ctx.node()
2784 2784
2785 2785 mf = ctx.manifest()
2786 2786 if node == p2:
2787 2787 parent = p2
2788 2788
2789 2789 # need all matching names in dirstate and manifest of target rev,
2790 2790 # so have to walk both. do not print errors if files exist in one
2791 2791 # but not other. in both cases, filesets should be evaluated against
2792 2792 # workingctx to get consistent result (issue4497). this means 'set:**'
2793 2793 # cannot be used to select missing files from target rev.
2794 2794
2795 2795 # `names` is a mapping for all elements in working copy and target revision
2796 2796 # The mapping is in the form:
2797 2797 # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2798 2798 names = {}
2799 2799 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2800 2800
2801 2801 with repo.wlock():
2802 2802 ## filling of the `names` mapping
2803 2803 # walk dirstate to fill `names`
2804 2804
2805 2805 interactive = opts.get('interactive', False)
2806 2806 wctx = repo[None]
2807 2807 m = scmutil.match(wctx, pats, opts)
2808 2808
2809 2809 # we'll need this later
2810 2810 targetsubs = sorted(s for s in wctx.substate if m(s))
2811 2811
2812 2812 if not m.always():
2813 2813 matcher = matchmod.badmatch(m, lambda x, y: False)
2814 2814 for abs in wctx.walk(matcher):
2815 2815 names[abs] = m.exact(abs)
2816 2816
2817 2817 # walk target manifest to fill `names`
2818 2818
2819 2819 def badfn(path, msg):
2820 2820 if path in names:
2821 2821 return
2822 2822 if path in ctx.substate:
2823 2823 return
2824 2824 path_ = path + '/'
2825 2825 for f in names:
2826 2826 if f.startswith(path_):
2827 2827 return
2828 ui.warn("%s: %s\n" % (m.rel(path), msg))
2828 ui.warn("%s: %s\n" % (uipathfn(path), msg))
2829 2829
2830 2830 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
2831 2831 if abs not in names:
2832 2832 names[abs] = m.exact(abs)
2833 2833
2834 2834 # Find status of all file in `names`.
2835 2835 m = scmutil.matchfiles(repo, names)
2836 2836
2837 2837 changes = repo.status(node1=node, match=m,
2838 2838 unknown=True, ignored=True, clean=True)
2839 2839 else:
2840 2840 changes = repo.status(node1=node, match=m)
2841 2841 for kind in changes:
2842 2842 for abs in kind:
2843 2843 names[abs] = m.exact(abs)
2844 2844
2845 2845 m = scmutil.matchfiles(repo, names)
2846 2846
2847 2847 modified = set(changes.modified)
2848 2848 added = set(changes.added)
2849 2849 removed = set(changes.removed)
2850 2850 _deleted = set(changes.deleted)
2851 2851 unknown = set(changes.unknown)
2852 2852 unknown.update(changes.ignored)
2853 2853 clean = set(changes.clean)
2854 2854 modadded = set()
2855 2855
2856 2856 # We need to account for the state of the file in the dirstate,
2857 2857 # even when we revert against something else than parent. This will
2858 2858 # slightly alter the behavior of revert (doing back up or not, delete
2859 2859 # or just forget etc).
2860 2860 if parent == node:
2861 2861 dsmodified = modified
2862 2862 dsadded = added
2863 2863 dsremoved = removed
2864 2864 # store all local modifications, useful later for rename detection
2865 2865 localchanges = dsmodified | dsadded
2866 2866 modified, added, removed = set(), set(), set()
2867 2867 else:
2868 2868 changes = repo.status(node1=parent, match=m)
2869 2869 dsmodified = set(changes.modified)
2870 2870 dsadded = set(changes.added)
2871 2871 dsremoved = set(changes.removed)
2872 2872 # store all local modifications, useful later for rename detection
2873 2873 localchanges = dsmodified | dsadded
2874 2874
2875 2875 # only take into account for removes between wc and target
2876 2876 clean |= dsremoved - removed
2877 2877 dsremoved &= removed
2878 2878 # distinct between dirstate remove and other
2879 2879 removed -= dsremoved
2880 2880
2881 2881 modadded = added & dsmodified
2882 2882 added -= modadded
2883 2883
2884 2884 # tell newly modified apart.
2885 2885 dsmodified &= modified
2886 2886 dsmodified |= modified & dsadded # dirstate added may need backup
2887 2887 modified -= dsmodified
2888 2888
2889 2889 # We need to wait for some post-processing to update this set
2890 2890 # before making the distinction. The dirstate will be used for
2891 2891 # that purpose.
2892 2892 dsadded = added
2893 2893
2894 2894 # in case of merge, files that are actually added can be reported as
2895 2895 # modified, we need to post process the result
2896 2896 if p2 != nullid:
2897 2897 mergeadd = set(dsmodified)
2898 2898 for path in dsmodified:
2899 2899 if path in mf:
2900 2900 mergeadd.remove(path)
2901 2901 dsadded |= mergeadd
2902 2902 dsmodified -= mergeadd
2903 2903
2904 2904 # if f is a rename, update `names` to also revert the source
2905 2905 for f in localchanges:
2906 2906 src = repo.dirstate.copied(f)
2907 2907 # XXX should we check for rename down to target node?
2908 2908 if src and src not in names and repo.dirstate[src] == 'r':
2909 2909 dsremoved.add(src)
2910 2910 names[src] = True
2911 2911
2912 2912 # determine the exact nature of the deleted changesets
2913 2913 deladded = set(_deleted)
2914 2914 for path in _deleted:
2915 2915 if path in mf:
2916 2916 deladded.remove(path)
2917 2917 deleted = _deleted - deladded
2918 2918
2919 2919 # distinguish between file to forget and the other
2920 2920 added = set()
2921 2921 for abs in dsadded:
2922 2922 if repo.dirstate[abs] != 'a':
2923 2923 added.add(abs)
2924 2924 dsadded -= added
2925 2925
2926 2926 for abs in deladded:
2927 2927 if repo.dirstate[abs] == 'a':
2928 2928 dsadded.add(abs)
2929 2929 deladded -= dsadded
2930 2930
2931 2931 # For files marked as removed, we check if an unknown file is present at
2932 2932 # the same path. If a such file exists it may need to be backed up.
2933 2933 # Making the distinction at this stage helps have simpler backup
2934 2934 # logic.
2935 2935 removunk = set()
2936 2936 for abs in removed:
2937 2937 target = repo.wjoin(abs)
2938 2938 if os.path.lexists(target):
2939 2939 removunk.add(abs)
2940 2940 removed -= removunk
2941 2941
2942 2942 dsremovunk = set()
2943 2943 for abs in dsremoved:
2944 2944 target = repo.wjoin(abs)
2945 2945 if os.path.lexists(target):
2946 2946 dsremovunk.add(abs)
2947 2947 dsremoved -= dsremovunk
2948 2948
2949 2949 # action to be actually performed by revert
2950 2950 # (<list of file>, message>) tuple
2951 2951 actions = {'revert': ([], _('reverting %s\n')),
2952 2952 'add': ([], _('adding %s\n')),
2953 2953 'remove': ([], _('removing %s\n')),
2954 2954 'drop': ([], _('removing %s\n')),
2955 2955 'forget': ([], _('forgetting %s\n')),
2956 2956 'undelete': ([], _('undeleting %s\n')),
2957 2957 'noop': (None, _('no changes needed to %s\n')),
2958 2958 'unknown': (None, _('file not managed: %s\n')),
2959 2959 }
2960 2960
2961 2961 # "constant" that convey the backup strategy.
2962 2962 # All set to `discard` if `no-backup` is set do avoid checking
2963 2963 # no_backup lower in the code.
2964 2964 # These values are ordered for comparison purposes
2965 2965 backupinteractive = 3 # do backup if interactively modified
2966 2966 backup = 2 # unconditionally do backup
2967 2967 check = 1 # check if the existing file differs from target
2968 2968 discard = 0 # never do backup
2969 2969 if opts.get('no_backup'):
2970 2970 backupinteractive = backup = check = discard
2971 2971 if interactive:
2972 2972 dsmodifiedbackup = backupinteractive
2973 2973 else:
2974 2974 dsmodifiedbackup = backup
2975 2975 tobackup = set()
2976 2976
2977 2977 backupanddel = actions['remove']
2978 2978 if not opts.get('no_backup'):
2979 2979 backupanddel = actions['drop']
2980 2980
2981 2981 disptable = (
2982 2982 # dispatch table:
2983 2983 # file state
2984 2984 # action
2985 2985 # make backup
2986 2986
2987 2987 ## Sets that results that will change file on disk
2988 2988 # Modified compared to target, no local change
2989 2989 (modified, actions['revert'], discard),
2990 2990 # Modified compared to target, but local file is deleted
2991 2991 (deleted, actions['revert'], discard),
2992 2992 # Modified compared to target, local change
2993 2993 (dsmodified, actions['revert'], dsmodifiedbackup),
2994 2994 # Added since target
2995 2995 (added, actions['remove'], discard),
2996 2996 # Added in working directory
2997 2997 (dsadded, actions['forget'], discard),
2998 2998 # Added since target, have local modification
2999 2999 (modadded, backupanddel, backup),
3000 3000 # Added since target but file is missing in working directory
3001 3001 (deladded, actions['drop'], discard),
3002 3002 # Removed since target, before working copy parent
3003 3003 (removed, actions['add'], discard),
3004 3004 # Same as `removed` but an unknown file exists at the same path
3005 3005 (removunk, actions['add'], check),
3006 3006 # Removed since targe, marked as such in working copy parent
3007 3007 (dsremoved, actions['undelete'], discard),
3008 3008 # Same as `dsremoved` but an unknown file exists at the same path
3009 3009 (dsremovunk, actions['undelete'], check),
3010 3010 ## the following sets does not result in any file changes
3011 3011 # File with no modification
3012 3012 (clean, actions['noop'], discard),
3013 3013 # Existing file, not tracked anywhere
3014 3014 (unknown, actions['unknown'], discard),
3015 3015 )
3016 3016
3017 3017 for abs, exact in sorted(names.items()):
3018 3018 # target file to be touch on disk (relative to cwd)
3019 3019 target = repo.wjoin(abs)
3020 3020 # search the entry in the dispatch table.
3021 3021 # if the file is in any of these sets, it was touched in the working
3022 3022 # directory parent and we are sure it needs to be reverted.
3023 3023 for table, (xlist, msg), dobackup in disptable:
3024 3024 if abs not in table:
3025 3025 continue
3026 3026 if xlist is not None:
3027 3027 xlist.append(abs)
3028 3028 if dobackup:
3029 3029 # If in interactive mode, don't automatically create
3030 3030 # .orig files (issue4793)
3031 3031 if dobackup == backupinteractive:
3032 3032 tobackup.add(abs)
3033 3033 elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
3034 3034 absbakname = scmutil.backuppath(ui, repo, abs)
3035 3035 bakname = os.path.relpath(absbakname,
3036 3036 start=repo.root)
3037 3037 ui.note(_('saving current version of %s as %s\n') %
3038 3038 (uipathfn(abs), uipathfn(bakname)))
3039 3039 if not opts.get('dry_run'):
3040 3040 if interactive:
3041 3041 util.copyfile(target, absbakname)
3042 3042 else:
3043 3043 util.rename(target, absbakname)
3044 3044 if opts.get('dry_run'):
3045 3045 if ui.verbose or not exact:
3046 3046 ui.status(msg % uipathfn(abs))
3047 3047 elif exact:
3048 3048 ui.warn(msg % uipathfn(abs))
3049 3049 break
3050 3050
3051 3051 if not opts.get('dry_run'):
3052 3052 needdata = ('revert', 'add', 'undelete')
3053 3053 oplist = [actions[name][0] for name in needdata]
3054 3054 prefetch = scmutil.prefetchfiles
3055 3055 matchfiles = scmutil.matchfiles
3056 3056 prefetch(repo, [ctx.rev()],
3057 3057 matchfiles(repo,
3058 3058 [f for sublist in oplist for f in sublist]))
3059 3059 _performrevert(repo, parents, ctx, names, uipathfn, actions,
3060 3060 interactive, tobackup)
3061 3061
3062 3062 if targetsubs:
3063 3063 # Revert the subrepos on the revert list
3064 3064 for sub in targetsubs:
3065 3065 try:
3066 3066 wctx.sub(sub).revert(ctx.substate[sub], *pats,
3067 3067 **pycompat.strkwargs(opts))
3068 3068 except KeyError:
3069 3069 raise error.Abort("subrepository '%s' does not exist in %s!"
3070 3070 % (sub, short(ctx.node())))
3071 3071
3072 3072 def _performrevert(repo, parents, ctx, names, uipathfn, actions,
3073 3073 interactive=False, tobackup=None):
3074 3074 """function that actually perform all the actions computed for revert
3075 3075
3076 3076 This is an independent function to let extension to plug in and react to
3077 3077 the imminent revert.
3078 3078
3079 3079 Make sure you have the working directory locked when calling this function.
3080 3080 """
3081 3081 parent, p2 = parents
3082 3082 node = ctx.node()
3083 3083 excluded_files = []
3084 3084
3085 3085 def checkout(f):
3086 3086 fc = ctx[f]
3087 3087 repo.wwrite(f, fc.data(), fc.flags())
3088 3088
3089 3089 def doremove(f):
3090 3090 try:
3091 3091 rmdir = repo.ui.configbool('experimental', 'removeemptydirs')
3092 3092 repo.wvfs.unlinkpath(f, rmdir=rmdir)
3093 3093 except OSError:
3094 3094 pass
3095 3095 repo.dirstate.remove(f)
3096 3096
3097 3097 def prntstatusmsg(action, f):
3098 3098 exact = names[f]
3099 3099 if repo.ui.verbose or not exact:
3100 3100 repo.ui.status(actions[action][1] % uipathfn(f))
3101 3101
3102 3102 audit_path = pathutil.pathauditor(repo.root, cached=True)
3103 3103 for f in actions['forget'][0]:
3104 3104 if interactive:
3105 3105 choice = repo.ui.promptchoice(
3106 3106 _("forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f))
3107 3107 if choice == 0:
3108 3108 prntstatusmsg('forget', f)
3109 3109 repo.dirstate.drop(f)
3110 3110 else:
3111 3111 excluded_files.append(f)
3112 3112 else:
3113 3113 prntstatusmsg('forget', f)
3114 3114 repo.dirstate.drop(f)
3115 3115 for f in actions['remove'][0]:
3116 3116 audit_path(f)
3117 3117 if interactive:
3118 3118 choice = repo.ui.promptchoice(
3119 3119 _("remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f))
3120 3120 if choice == 0:
3121 3121 prntstatusmsg('remove', f)
3122 3122 doremove(f)
3123 3123 else:
3124 3124 excluded_files.append(f)
3125 3125 else:
3126 3126 prntstatusmsg('remove', f)
3127 3127 doremove(f)
3128 3128 for f in actions['drop'][0]:
3129 3129 audit_path(f)
3130 3130 prntstatusmsg('drop', f)
3131 3131 repo.dirstate.remove(f)
3132 3132
3133 3133 normal = None
3134 3134 if node == parent:
3135 3135 # We're reverting to our parent. If possible, we'd like status
3136 3136 # to report the file as clean. We have to use normallookup for
3137 3137 # merges to avoid losing information about merged/dirty files.
3138 3138 if p2 != nullid:
3139 3139 normal = repo.dirstate.normallookup
3140 3140 else:
3141 3141 normal = repo.dirstate.normal
3142 3142
3143 3143 newlyaddedandmodifiedfiles = set()
3144 3144 if interactive:
3145 3145 # Prompt the user for changes to revert
3146 3146 torevert = [f for f in actions['revert'][0] if f not in excluded_files]
3147 3147 m = scmutil.matchfiles(repo, torevert)
3148 3148 diffopts = patch.difffeatureopts(repo.ui, whitespace=True,
3149 3149 section='commands',
3150 3150 configprefix='revert.interactive.')
3151 3151 diffopts.nodates = True
3152 3152 diffopts.git = True
3153 3153 operation = 'discard'
3154 3154 reversehunks = True
3155 3155 if node != parent:
3156 3156 operation = 'apply'
3157 3157 reversehunks = False
3158 3158 if reversehunks:
3159 3159 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3160 3160 else:
3161 3161 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3162 3162 originalchunks = patch.parsepatch(diff)
3163 3163
3164 3164 try:
3165 3165
3166 3166 chunks, opts = recordfilter(repo.ui, originalchunks,
3167 3167 operation=operation)
3168 3168 if reversehunks:
3169 3169 chunks = patch.reversehunks(chunks)
3170 3170
3171 3171 except error.PatchError as err:
3172 3172 raise error.Abort(_('error parsing patch: %s') % err)
3173 3173
3174 3174 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
3175 3175 if tobackup is None:
3176 3176 tobackup = set()
3177 3177 # Apply changes
3178 3178 fp = stringio()
3179 3179 # chunks are serialized per file, but files aren't sorted
3180 3180 for f in sorted(set(c.header.filename() for c in chunks if ishunk(c))):
3181 3181 prntstatusmsg('revert', f)
3182 3182 for c in chunks:
3183 3183 if ishunk(c):
3184 3184 abs = c.header.filename()
3185 3185 # Create a backup file only if this hunk should be backed up
3186 3186 if c.header.filename() in tobackup:
3187 3187 target = repo.wjoin(abs)
3188 3188 bakname = scmutil.backuppath(repo.ui, repo, abs)
3189 3189 util.copyfile(target, bakname)
3190 3190 tobackup.remove(abs)
3191 3191 c.write(fp)
3192 3192 dopatch = fp.tell()
3193 3193 fp.seek(0)
3194 3194 if dopatch:
3195 3195 try:
3196 3196 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3197 3197 except error.PatchError as err:
3198 3198 raise error.Abort(pycompat.bytestr(err))
3199 3199 del fp
3200 3200 else:
3201 3201 for f in actions['revert'][0]:
3202 3202 prntstatusmsg('revert', f)
3203 3203 checkout(f)
3204 3204 if normal:
3205 3205 normal(f)
3206 3206
3207 3207 for f in actions['add'][0]:
3208 3208 # Don't checkout modified files, they are already created by the diff
3209 3209 if f not in newlyaddedandmodifiedfiles:
3210 3210 prntstatusmsg('add', f)
3211 3211 checkout(f)
3212 3212 repo.dirstate.add(f)
3213 3213
3214 3214 normal = repo.dirstate.normallookup
3215 3215 if node == parent and p2 == nullid:
3216 3216 normal = repo.dirstate.normal
3217 3217 for f in actions['undelete'][0]:
3218 3218 if interactive:
3219 3219 choice = repo.ui.promptchoice(
3220 3220 _("add back removed file %s (Yn)?$$ &Yes $$ &No") % f)
3221 3221 if choice == 0:
3222 3222 prntstatusmsg('undelete', f)
3223 3223 checkout(f)
3224 3224 normal(f)
3225 3225 else:
3226 3226 excluded_files.append(f)
3227 3227 else:
3228 3228 prntstatusmsg('undelete', f)
3229 3229 checkout(f)
3230 3230 normal(f)
3231 3231
3232 3232 copied = copies.pathcopies(repo[parent], ctx)
3233 3233
3234 3234 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3235 3235 if f in copied:
3236 3236 repo.dirstate.copy(copied[f], f)
3237 3237
3238 3238 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3239 3239 # commands.outgoing. "missing" is "missing" of the result of
3240 3240 # "findcommonoutgoing()"
3241 3241 outgoinghooks = util.hooks()
3242 3242
3243 3243 # a list of (ui, repo) functions called by commands.summary
3244 3244 summaryhooks = util.hooks()
3245 3245
3246 3246 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3247 3247 #
3248 3248 # functions should return tuple of booleans below, if 'changes' is None:
3249 3249 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3250 3250 #
3251 3251 # otherwise, 'changes' is a tuple of tuples below:
3252 3252 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3253 3253 # - (desturl, destbranch, destpeer, outgoing)
3254 3254 summaryremotehooks = util.hooks()
3255 3255
3256 3256 # A list of state files kept by multistep operations like graft.
3257 3257 # Since graft cannot be aborted, it is considered 'clearable' by update.
3258 3258 # note: bisect is intentionally excluded
3259 3259 # (state file, clearable, allowcommit, error, hint)
3260 3260 unfinishedstates = [
3261 3261 ('graftstate', True, False, _('graft in progress'),
3262 3262 _("use 'hg graft --continue' or 'hg graft --stop' to stop")),
3263 3263 ('updatestate', True, False, _('last update was interrupted'),
3264 3264 _("use 'hg update' to get a consistent checkout"))
3265 3265 ]
3266 3266
3267 3267 def checkunfinished(repo, commit=False):
3268 3268 '''Look for an unfinished multistep operation, like graft, and abort
3269 3269 if found. It's probably good to check this right before
3270 3270 bailifchanged().
3271 3271 '''
3272 3272 # Check for non-clearable states first, so things like rebase will take
3273 3273 # precedence over update.
3274 3274 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3275 3275 if clearable or (commit and allowcommit):
3276 3276 continue
3277 3277 if repo.vfs.exists(f):
3278 3278 raise error.Abort(msg, hint=hint)
3279 3279
3280 3280 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3281 3281 if not clearable or (commit and allowcommit):
3282 3282 continue
3283 3283 if repo.vfs.exists(f):
3284 3284 raise error.Abort(msg, hint=hint)
3285 3285
3286 3286 def clearunfinished(repo):
3287 3287 '''Check for unfinished operations (as above), and clear the ones
3288 3288 that are clearable.
3289 3289 '''
3290 3290 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3291 3291 if not clearable and repo.vfs.exists(f):
3292 3292 raise error.Abort(msg, hint=hint)
3293 3293 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3294 3294 if clearable and repo.vfs.exists(f):
3295 3295 util.unlink(repo.vfs.join(f))
3296 3296
3297 3297 afterresolvedstates = [
3298 3298 ('graftstate',
3299 3299 _('hg graft --continue')),
3300 3300 ]
3301 3301
3302 3302 def howtocontinue(repo):
3303 3303 '''Check for an unfinished operation and return the command to finish
3304 3304 it.
3305 3305
3306 3306 afterresolvedstates tuples define a .hg/{file} and the corresponding
3307 3307 command needed to finish it.
3308 3308
3309 3309 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3310 3310 a boolean.
3311 3311 '''
3312 3312 contmsg = _("continue: %s")
3313 3313 for f, msg in afterresolvedstates:
3314 3314 if repo.vfs.exists(f):
3315 3315 return contmsg % msg, True
3316 3316 if repo[None].dirty(missing=True, merge=False, branch=False):
3317 3317 return contmsg % _("hg commit"), False
3318 3318 return None, None
3319 3319
3320 3320 def checkafterresolved(repo):
3321 3321 '''Inform the user about the next action after completing hg resolve
3322 3322
3323 3323 If there's a matching afterresolvedstates, howtocontinue will yield
3324 3324 repo.ui.warn as the reporter.
3325 3325
3326 3326 Otherwise, it will yield repo.ui.note.
3327 3327 '''
3328 3328 msg, warning = howtocontinue(repo)
3329 3329 if msg is not None:
3330 3330 if warning:
3331 3331 repo.ui.warn("%s\n" % msg)
3332 3332 else:
3333 3333 repo.ui.note("%s\n" % msg)
3334 3334
3335 3335 def wrongtooltocontinue(repo, task):
3336 3336 '''Raise an abort suggesting how to properly continue if there is an
3337 3337 active task.
3338 3338
3339 3339 Uses howtocontinue() to find the active task.
3340 3340
3341 3341 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3342 3342 a hint.
3343 3343 '''
3344 3344 after = howtocontinue(repo)
3345 3345 hint = None
3346 3346 if after[1]:
3347 3347 hint = after[0]
3348 3348 raise error.Abort(_('no %s in progress') % task, hint=hint)
General Comments 0
You need to be logged in to leave comments. Login now