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