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