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