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