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