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