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