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