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