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