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