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