##// END OF EJS Templates
remove: queue warnings until after status messages (issue5140) (API)...
timeless -
r28607:a88959ae default
parent child Browse files
Show More
@@ -1,3489 +1,3500 b''
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import cStringIO
11 11 import errno
12 12 import os
13 13 import re
14 14 import sys
15 15 import tempfile
16 16
17 17 from .i18n import _
18 18 from .node import (
19 19 bin,
20 20 hex,
21 21 nullid,
22 22 nullrev,
23 23 short,
24 24 )
25 25
26 26 from . import (
27 27 bookmarks,
28 28 changelog,
29 29 copies,
30 30 crecord as crecordmod,
31 31 encoding,
32 32 error,
33 33 formatter,
34 34 graphmod,
35 35 lock as lockmod,
36 36 match as matchmod,
37 37 obsolete,
38 38 patch,
39 39 pathutil,
40 40 phases,
41 41 repair,
42 42 revlog,
43 43 revset,
44 44 scmutil,
45 45 templatekw,
46 46 templater,
47 47 util,
48 48 )
49 49
50 50 def ishunk(x):
51 51 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
52 52 return isinstance(x, hunkclasses)
53 53
54 54 def newandmodified(chunks, originalchunks):
55 55 newlyaddedandmodifiedfiles = set()
56 56 for chunk in chunks:
57 57 if ishunk(chunk) and chunk.header.isnewfile() and chunk not in \
58 58 originalchunks:
59 59 newlyaddedandmodifiedfiles.add(chunk.header.filename())
60 60 return newlyaddedandmodifiedfiles
61 61
62 62 def parsealiases(cmd):
63 63 return cmd.lstrip("^").split("|")
64 64
65 65 def setupwrapcolorwrite(ui):
66 66 # wrap ui.write so diff output can be labeled/colorized
67 67 def wrapwrite(orig, *args, **kw):
68 68 label = kw.pop('label', '')
69 69 for chunk, l in patch.difflabel(lambda: args):
70 70 orig(chunk, label=label + l)
71 71
72 72 oldwrite = ui.write
73 73 def wrap(*args, **kwargs):
74 74 return wrapwrite(oldwrite, *args, **kwargs)
75 75 setattr(ui, 'write', wrap)
76 76 return oldwrite
77 77
78 78 def filterchunks(ui, originalhunks, usecurses, testfile, operation=None):
79 79 if usecurses:
80 80 if testfile:
81 81 recordfn = crecordmod.testdecorator(testfile,
82 82 crecordmod.testchunkselector)
83 83 else:
84 84 recordfn = crecordmod.chunkselector
85 85
86 86 return crecordmod.filterpatch(ui, originalhunks, recordfn, operation)
87 87
88 88 else:
89 89 return patch.filterpatch(ui, originalhunks, operation)
90 90
91 91 def recordfilter(ui, originalhunks, operation=None):
92 92 """ Prompts the user to filter the originalhunks and return a list of
93 93 selected hunks.
94 94 *operation* is used for ui purposes to indicate the user
95 95 what kind of filtering they are doing: reverting, committing, shelving, etc.
96 96 *operation* has to be a translated string.
97 97 """
98 98 usecurses = crecordmod.checkcurses(ui)
99 99 testfile = ui.config('experimental', 'crecordtest', None)
100 100 oldwrite = setupwrapcolorwrite(ui)
101 101 try:
102 102 newchunks, newopts = filterchunks(ui, originalhunks, usecurses,
103 103 testfile, operation)
104 104 finally:
105 105 ui.write = oldwrite
106 106 return newchunks, newopts
107 107
108 108 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
109 109 filterfn, *pats, **opts):
110 110 from . import merge as mergemod
111 111 if not ui.interactive():
112 112 if cmdsuggest:
113 113 msg = _('running non-interactively, use %s instead') % cmdsuggest
114 114 else:
115 115 msg = _('running non-interactively')
116 116 raise error.Abort(msg)
117 117
118 118 # make sure username is set before going interactive
119 119 if not opts.get('user'):
120 120 ui.username() # raise exception, username not provided
121 121
122 122 def recordfunc(ui, repo, message, match, opts):
123 123 """This is generic record driver.
124 124
125 125 Its job is to interactively filter local changes, and
126 126 accordingly prepare working directory into a state in which the
127 127 job can be delegated to a non-interactive commit command such as
128 128 'commit' or 'qrefresh'.
129 129
130 130 After the actual job is done by non-interactive command, the
131 131 working directory is restored to its original state.
132 132
133 133 In the end we'll record interesting changes, and everything else
134 134 will be left in place, so the user can continue working.
135 135 """
136 136
137 137 checkunfinished(repo, commit=True)
138 138 merge = len(repo[None].parents()) > 1
139 139 if merge:
140 140 raise error.Abort(_('cannot partially commit a merge '
141 141 '(use "hg commit" instead)'))
142 142
143 143 status = repo.status(match=match)
144 144 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
145 145 diffopts.nodates = True
146 146 diffopts.git = True
147 147 diffopts.showfunc = True
148 148 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
149 149 originalchunks = patch.parsepatch(originaldiff)
150 150
151 151 # 1. filter patch, since we are intending to apply subset of it
152 152 try:
153 153 chunks, newopts = filterfn(ui, originalchunks)
154 154 except patch.PatchError as err:
155 155 raise error.Abort(_('error parsing patch: %s') % err)
156 156 opts.update(newopts)
157 157
158 158 # We need to keep a backup of files that have been newly added and
159 159 # modified during the recording process because there is a previous
160 160 # version without the edit in the workdir
161 161 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
162 162 contenders = set()
163 163 for h in chunks:
164 164 try:
165 165 contenders.update(set(h.files()))
166 166 except AttributeError:
167 167 pass
168 168
169 169 changed = status.modified + status.added + status.removed
170 170 newfiles = [f for f in changed if f in contenders]
171 171 if not newfiles:
172 172 ui.status(_('no changes to record\n'))
173 173 return 0
174 174
175 175 modified = set(status.modified)
176 176
177 177 # 2. backup changed files, so we can restore them in the end
178 178
179 179 if backupall:
180 180 tobackup = changed
181 181 else:
182 182 tobackup = [f for f in newfiles if f in modified or f in \
183 183 newlyaddedandmodifiedfiles]
184 184 backups = {}
185 185 if tobackup:
186 186 backupdir = repo.join('record-backups')
187 187 try:
188 188 os.mkdir(backupdir)
189 189 except OSError as err:
190 190 if err.errno != errno.EEXIST:
191 191 raise
192 192 try:
193 193 # backup continues
194 194 for f in tobackup:
195 195 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
196 196 dir=backupdir)
197 197 os.close(fd)
198 198 ui.debug('backup %r as %r\n' % (f, tmpname))
199 199 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
200 200 backups[f] = tmpname
201 201
202 202 fp = cStringIO.StringIO()
203 203 for c in chunks:
204 204 fname = c.filename()
205 205 if fname in backups:
206 206 c.write(fp)
207 207 dopatch = fp.tell()
208 208 fp.seek(0)
209 209
210 210 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
211 211 # 3a. apply filtered patch to clean repo (clean)
212 212 if backups:
213 213 # Equivalent to hg.revert
214 214 m = scmutil.matchfiles(repo, backups.keys())
215 215 mergemod.update(repo, repo.dirstate.p1(),
216 216 False, True, matcher=m)
217 217
218 218 # 3b. (apply)
219 219 if dopatch:
220 220 try:
221 221 ui.debug('applying patch\n')
222 222 ui.debug(fp.getvalue())
223 223 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
224 224 except patch.PatchError as err:
225 225 raise error.Abort(str(err))
226 226 del fp
227 227
228 228 # 4. We prepared working directory according to filtered
229 229 # patch. Now is the time to delegate the job to
230 230 # commit/qrefresh or the like!
231 231
232 232 # Make all of the pathnames absolute.
233 233 newfiles = [repo.wjoin(nf) for nf in newfiles]
234 234 return commitfunc(ui, repo, *newfiles, **opts)
235 235 finally:
236 236 # 5. finally restore backed-up files
237 237 try:
238 238 dirstate = repo.dirstate
239 239 for realname, tmpname in backups.iteritems():
240 240 ui.debug('restoring %r to %r\n' % (tmpname, realname))
241 241
242 242 if dirstate[realname] == 'n':
243 243 # without normallookup, restoring timestamp
244 244 # may cause partially committed files
245 245 # to be treated as unmodified
246 246 dirstate.normallookup(realname)
247 247
248 248 # copystat=True here and above are a hack to trick any
249 249 # editors that have f open that we haven't modified them.
250 250 #
251 251 # Also note that this racy as an editor could notice the
252 252 # file's mtime before we've finished writing it.
253 253 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
254 254 os.unlink(tmpname)
255 255 if tobackup:
256 256 os.rmdir(backupdir)
257 257 except OSError:
258 258 pass
259 259
260 260 def recordinwlock(ui, repo, message, match, opts):
261 261 with repo.wlock():
262 262 return recordfunc(ui, repo, message, match, opts)
263 263
264 264 return commit(ui, repo, recordinwlock, pats, opts)
265 265
266 266 def findpossible(cmd, table, strict=False):
267 267 """
268 268 Return cmd -> (aliases, command table entry)
269 269 for each matching command.
270 270 Return debug commands (or their aliases) only if no normal command matches.
271 271 """
272 272 choice = {}
273 273 debugchoice = {}
274 274
275 275 if cmd in table:
276 276 # short-circuit exact matches, "log" alias beats "^log|history"
277 277 keys = [cmd]
278 278 else:
279 279 keys = table.keys()
280 280
281 281 allcmds = []
282 282 for e in keys:
283 283 aliases = parsealiases(e)
284 284 allcmds.extend(aliases)
285 285 found = None
286 286 if cmd in aliases:
287 287 found = cmd
288 288 elif not strict:
289 289 for a in aliases:
290 290 if a.startswith(cmd):
291 291 found = a
292 292 break
293 293 if found is not None:
294 294 if aliases[0].startswith("debug") or found.startswith("debug"):
295 295 debugchoice[found] = (aliases, table[e])
296 296 else:
297 297 choice[found] = (aliases, table[e])
298 298
299 299 if not choice and debugchoice:
300 300 choice = debugchoice
301 301
302 302 return choice, allcmds
303 303
304 304 def findcmd(cmd, table, strict=True):
305 305 """Return (aliases, command table entry) for command string."""
306 306 choice, allcmds = findpossible(cmd, table, strict)
307 307
308 308 if cmd in choice:
309 309 return choice[cmd]
310 310
311 311 if len(choice) > 1:
312 312 clist = choice.keys()
313 313 clist.sort()
314 314 raise error.AmbiguousCommand(cmd, clist)
315 315
316 316 if choice:
317 317 return choice.values()[0]
318 318
319 319 raise error.UnknownCommand(cmd, allcmds)
320 320
321 321 def findrepo(p):
322 322 while not os.path.isdir(os.path.join(p, ".hg")):
323 323 oldp, p = p, os.path.dirname(p)
324 324 if p == oldp:
325 325 return None
326 326
327 327 return p
328 328
329 329 def bailifchanged(repo, merge=True):
330 330 if merge and repo.dirstate.p2() != nullid:
331 331 raise error.Abort(_('outstanding uncommitted merge'))
332 332 modified, added, removed, deleted = repo.status()[:4]
333 333 if modified or added or removed or deleted:
334 334 raise error.Abort(_('uncommitted changes'))
335 335 ctx = repo[None]
336 336 for s in sorted(ctx.substate):
337 337 ctx.sub(s).bailifchanged()
338 338
339 339 def logmessage(ui, opts):
340 340 """ get the log message according to -m and -l option """
341 341 message = opts.get('message')
342 342 logfile = opts.get('logfile')
343 343
344 344 if message and logfile:
345 345 raise error.Abort(_('options --message and --logfile are mutually '
346 346 'exclusive'))
347 347 if not message and logfile:
348 348 try:
349 349 if logfile == '-':
350 350 message = ui.fin.read()
351 351 else:
352 352 message = '\n'.join(util.readfile(logfile).splitlines())
353 353 except IOError as inst:
354 354 raise error.Abort(_("can't read commit message '%s': %s") %
355 355 (logfile, inst.strerror))
356 356 return message
357 357
358 358 def mergeeditform(ctxorbool, baseformname):
359 359 """return appropriate editform name (referencing a committemplate)
360 360
361 361 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
362 362 merging is committed.
363 363
364 364 This returns baseformname with '.merge' appended if it is a merge,
365 365 otherwise '.normal' is appended.
366 366 """
367 367 if isinstance(ctxorbool, bool):
368 368 if ctxorbool:
369 369 return baseformname + ".merge"
370 370 elif 1 < len(ctxorbool.parents()):
371 371 return baseformname + ".merge"
372 372
373 373 return baseformname + ".normal"
374 374
375 375 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
376 376 editform='', **opts):
377 377 """get appropriate commit message editor according to '--edit' option
378 378
379 379 'finishdesc' is a function to be called with edited commit message
380 380 (= 'description' of the new changeset) just after editing, but
381 381 before checking empty-ness. It should return actual text to be
382 382 stored into history. This allows to change description before
383 383 storing.
384 384
385 385 'extramsg' is a extra message to be shown in the editor instead of
386 386 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
387 387 is automatically added.
388 388
389 389 'editform' is a dot-separated list of names, to distinguish
390 390 the purpose of commit text editing.
391 391
392 392 'getcommiteditor' returns 'commitforceeditor' regardless of
393 393 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
394 394 they are specific for usage in MQ.
395 395 """
396 396 if edit or finishdesc or extramsg:
397 397 return lambda r, c, s: commitforceeditor(r, c, s,
398 398 finishdesc=finishdesc,
399 399 extramsg=extramsg,
400 400 editform=editform)
401 401 elif editform:
402 402 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
403 403 else:
404 404 return commiteditor
405 405
406 406 def loglimit(opts):
407 407 """get the log limit according to option -l/--limit"""
408 408 limit = opts.get('limit')
409 409 if limit:
410 410 try:
411 411 limit = int(limit)
412 412 except ValueError:
413 413 raise error.Abort(_('limit must be a positive integer'))
414 414 if limit <= 0:
415 415 raise error.Abort(_('limit must be positive'))
416 416 else:
417 417 limit = None
418 418 return limit
419 419
420 420 def makefilename(repo, pat, node, desc=None,
421 421 total=None, seqno=None, revwidth=None, pathname=None):
422 422 node_expander = {
423 423 'H': lambda: hex(node),
424 424 'R': lambda: str(repo.changelog.rev(node)),
425 425 'h': lambda: short(node),
426 426 'm': lambda: re.sub('[^\w]', '_', str(desc))
427 427 }
428 428 expander = {
429 429 '%': lambda: '%',
430 430 'b': lambda: os.path.basename(repo.root),
431 431 }
432 432
433 433 try:
434 434 if node:
435 435 expander.update(node_expander)
436 436 if node:
437 437 expander['r'] = (lambda:
438 438 str(repo.changelog.rev(node)).zfill(revwidth or 0))
439 439 if total is not None:
440 440 expander['N'] = lambda: str(total)
441 441 if seqno is not None:
442 442 expander['n'] = lambda: str(seqno)
443 443 if total is not None and seqno is not None:
444 444 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
445 445 if pathname is not None:
446 446 expander['s'] = lambda: os.path.basename(pathname)
447 447 expander['d'] = lambda: os.path.dirname(pathname) or '.'
448 448 expander['p'] = lambda: pathname
449 449
450 450 newname = []
451 451 patlen = len(pat)
452 452 i = 0
453 453 while i < patlen:
454 454 c = pat[i]
455 455 if c == '%':
456 456 i += 1
457 457 c = pat[i]
458 458 c = expander[c]()
459 459 newname.append(c)
460 460 i += 1
461 461 return ''.join(newname)
462 462 except KeyError as inst:
463 463 raise error.Abort(_("invalid format spec '%%%s' in output filename") %
464 464 inst.args[0])
465 465
466 466 class _unclosablefile(object):
467 467 def __init__(self, fp):
468 468 self._fp = fp
469 469
470 470 def close(self):
471 471 pass
472 472
473 473 def __iter__(self):
474 474 return iter(self._fp)
475 475
476 476 def __getattr__(self, attr):
477 477 return getattr(self._fp, attr)
478 478
479 479 def makefileobj(repo, pat, node=None, desc=None, total=None,
480 480 seqno=None, revwidth=None, mode='wb', modemap=None,
481 481 pathname=None):
482 482
483 483 writable = mode not in ('r', 'rb')
484 484
485 485 if not pat or pat == '-':
486 486 if writable:
487 487 fp = repo.ui.fout
488 488 else:
489 489 fp = repo.ui.fin
490 490 return _unclosablefile(fp)
491 491 if util.safehasattr(pat, 'write') and writable:
492 492 return pat
493 493 if util.safehasattr(pat, 'read') and 'r' in mode:
494 494 return pat
495 495 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
496 496 if modemap is not None:
497 497 mode = modemap.get(fn, mode)
498 498 if mode == 'wb':
499 499 modemap[fn] = 'ab'
500 500 return open(fn, mode)
501 501
502 502 def openrevlog(repo, cmd, file_, opts):
503 503 """opens the changelog, manifest, a filelog or a given revlog"""
504 504 cl = opts['changelog']
505 505 mf = opts['manifest']
506 506 dir = opts['dir']
507 507 msg = None
508 508 if cl and mf:
509 509 msg = _('cannot specify --changelog and --manifest at the same time')
510 510 elif cl and dir:
511 511 msg = _('cannot specify --changelog and --dir at the same time')
512 512 elif cl or mf:
513 513 if file_:
514 514 msg = _('cannot specify filename with --changelog or --manifest')
515 515 elif not repo:
516 516 msg = _('cannot specify --changelog or --manifest or --dir '
517 517 'without a repository')
518 518 if msg:
519 519 raise error.Abort(msg)
520 520
521 521 r = None
522 522 if repo:
523 523 if cl:
524 524 r = repo.unfiltered().changelog
525 525 elif dir:
526 526 if 'treemanifest' not in repo.requirements:
527 527 raise error.Abort(_("--dir can only be used on repos with "
528 528 "treemanifest enabled"))
529 529 dirlog = repo.dirlog(file_)
530 530 if len(dirlog):
531 531 r = dirlog
532 532 elif mf:
533 533 r = repo.manifest
534 534 elif file_:
535 535 filelog = repo.file(file_)
536 536 if len(filelog):
537 537 r = filelog
538 538 if not r:
539 539 if not file_:
540 540 raise error.CommandError(cmd, _('invalid arguments'))
541 541 if not os.path.isfile(file_):
542 542 raise error.Abort(_("revlog '%s' not found") % file_)
543 543 r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
544 544 file_[:-2] + ".i")
545 545 return r
546 546
547 547 def copy(ui, repo, pats, opts, rename=False):
548 548 # called with the repo lock held
549 549 #
550 550 # hgsep => pathname that uses "/" to separate directories
551 551 # ossep => pathname that uses os.sep to separate directories
552 552 cwd = repo.getcwd()
553 553 targets = {}
554 554 after = opts.get("after")
555 555 dryrun = opts.get("dry_run")
556 556 wctx = repo[None]
557 557
558 558 def walkpat(pat):
559 559 srcs = []
560 560 if after:
561 561 badstates = '?'
562 562 else:
563 563 badstates = '?r'
564 564 m = scmutil.match(repo[None], [pat], opts, globbed=True)
565 565 for abs in repo.walk(m):
566 566 state = repo.dirstate[abs]
567 567 rel = m.rel(abs)
568 568 exact = m.exact(abs)
569 569 if state in badstates:
570 570 if exact and state == '?':
571 571 ui.warn(_('%s: not copying - file is not managed\n') % rel)
572 572 if exact and state == 'r':
573 573 ui.warn(_('%s: not copying - file has been marked for'
574 574 ' remove\n') % rel)
575 575 continue
576 576 # abs: hgsep
577 577 # rel: ossep
578 578 srcs.append((abs, rel, exact))
579 579 return srcs
580 580
581 581 # abssrc: hgsep
582 582 # relsrc: ossep
583 583 # otarget: ossep
584 584 def copyfile(abssrc, relsrc, otarget, exact):
585 585 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
586 586 if '/' in abstarget:
587 587 # We cannot normalize abstarget itself, this would prevent
588 588 # case only renames, like a => A.
589 589 abspath, absname = abstarget.rsplit('/', 1)
590 590 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
591 591 reltarget = repo.pathto(abstarget, cwd)
592 592 target = repo.wjoin(abstarget)
593 593 src = repo.wjoin(abssrc)
594 594 state = repo.dirstate[abstarget]
595 595
596 596 scmutil.checkportable(ui, abstarget)
597 597
598 598 # check for collisions
599 599 prevsrc = targets.get(abstarget)
600 600 if prevsrc is not None:
601 601 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
602 602 (reltarget, repo.pathto(abssrc, cwd),
603 603 repo.pathto(prevsrc, cwd)))
604 604 return
605 605
606 606 # check for overwrites
607 607 exists = os.path.lexists(target)
608 608 samefile = False
609 609 if exists and abssrc != abstarget:
610 610 if (repo.dirstate.normalize(abssrc) ==
611 611 repo.dirstate.normalize(abstarget)):
612 612 if not rename:
613 613 ui.warn(_("%s: can't copy - same file\n") % reltarget)
614 614 return
615 615 exists = False
616 616 samefile = True
617 617
618 618 if not after and exists or after and state in 'mn':
619 619 if not opts['force']:
620 620 ui.warn(_('%s: not overwriting - file exists\n') %
621 621 reltarget)
622 622 return
623 623
624 624 if after:
625 625 if not exists:
626 626 if rename:
627 627 ui.warn(_('%s: not recording move - %s does not exist\n') %
628 628 (relsrc, reltarget))
629 629 else:
630 630 ui.warn(_('%s: not recording copy - %s does not exist\n') %
631 631 (relsrc, reltarget))
632 632 return
633 633 elif not dryrun:
634 634 try:
635 635 if exists:
636 636 os.unlink(target)
637 637 targetdir = os.path.dirname(target) or '.'
638 638 if not os.path.isdir(targetdir):
639 639 os.makedirs(targetdir)
640 640 if samefile:
641 641 tmp = target + "~hgrename"
642 642 os.rename(src, tmp)
643 643 os.rename(tmp, target)
644 644 else:
645 645 util.copyfile(src, target)
646 646 srcexists = True
647 647 except IOError as inst:
648 648 if inst.errno == errno.ENOENT:
649 649 ui.warn(_('%s: deleted in working directory\n') % relsrc)
650 650 srcexists = False
651 651 else:
652 652 ui.warn(_('%s: cannot copy - %s\n') %
653 653 (relsrc, inst.strerror))
654 654 return True # report a failure
655 655
656 656 if ui.verbose or not exact:
657 657 if rename:
658 658 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
659 659 else:
660 660 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
661 661
662 662 targets[abstarget] = abssrc
663 663
664 664 # fix up dirstate
665 665 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
666 666 dryrun=dryrun, cwd=cwd)
667 667 if rename and not dryrun:
668 668 if not after and srcexists and not samefile:
669 669 util.unlinkpath(repo.wjoin(abssrc))
670 670 wctx.forget([abssrc])
671 671
672 672 # pat: ossep
673 673 # dest ossep
674 674 # srcs: list of (hgsep, hgsep, ossep, bool)
675 675 # return: function that takes hgsep and returns ossep
676 676 def targetpathfn(pat, dest, srcs):
677 677 if os.path.isdir(pat):
678 678 abspfx = pathutil.canonpath(repo.root, cwd, pat)
679 679 abspfx = util.localpath(abspfx)
680 680 if destdirexists:
681 681 striplen = len(os.path.split(abspfx)[0])
682 682 else:
683 683 striplen = len(abspfx)
684 684 if striplen:
685 685 striplen += len(os.sep)
686 686 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
687 687 elif destdirexists:
688 688 res = lambda p: os.path.join(dest,
689 689 os.path.basename(util.localpath(p)))
690 690 else:
691 691 res = lambda p: dest
692 692 return res
693 693
694 694 # pat: ossep
695 695 # dest ossep
696 696 # srcs: list of (hgsep, hgsep, ossep, bool)
697 697 # return: function that takes hgsep and returns ossep
698 698 def targetpathafterfn(pat, dest, srcs):
699 699 if matchmod.patkind(pat):
700 700 # a mercurial pattern
701 701 res = lambda p: os.path.join(dest,
702 702 os.path.basename(util.localpath(p)))
703 703 else:
704 704 abspfx = pathutil.canonpath(repo.root, cwd, pat)
705 705 if len(abspfx) < len(srcs[0][0]):
706 706 # A directory. Either the target path contains the last
707 707 # component of the source path or it does not.
708 708 def evalpath(striplen):
709 709 score = 0
710 710 for s in srcs:
711 711 t = os.path.join(dest, util.localpath(s[0])[striplen:])
712 712 if os.path.lexists(t):
713 713 score += 1
714 714 return score
715 715
716 716 abspfx = util.localpath(abspfx)
717 717 striplen = len(abspfx)
718 718 if striplen:
719 719 striplen += len(os.sep)
720 720 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
721 721 score = evalpath(striplen)
722 722 striplen1 = len(os.path.split(abspfx)[0])
723 723 if striplen1:
724 724 striplen1 += len(os.sep)
725 725 if evalpath(striplen1) > score:
726 726 striplen = striplen1
727 727 res = lambda p: os.path.join(dest,
728 728 util.localpath(p)[striplen:])
729 729 else:
730 730 # a file
731 731 if destdirexists:
732 732 res = lambda p: os.path.join(dest,
733 733 os.path.basename(util.localpath(p)))
734 734 else:
735 735 res = lambda p: dest
736 736 return res
737 737
738 738 pats = scmutil.expandpats(pats)
739 739 if not pats:
740 740 raise error.Abort(_('no source or destination specified'))
741 741 if len(pats) == 1:
742 742 raise error.Abort(_('no destination specified'))
743 743 dest = pats.pop()
744 744 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
745 745 if not destdirexists:
746 746 if len(pats) > 1 or matchmod.patkind(pats[0]):
747 747 raise error.Abort(_('with multiple sources, destination must be an '
748 748 'existing directory'))
749 749 if util.endswithsep(dest):
750 750 raise error.Abort(_('destination %s is not a directory') % dest)
751 751
752 752 tfn = targetpathfn
753 753 if after:
754 754 tfn = targetpathafterfn
755 755 copylist = []
756 756 for pat in pats:
757 757 srcs = walkpat(pat)
758 758 if not srcs:
759 759 continue
760 760 copylist.append((tfn(pat, dest, srcs), srcs))
761 761 if not copylist:
762 762 raise error.Abort(_('no files to copy'))
763 763
764 764 errors = 0
765 765 for targetpath, srcs in copylist:
766 766 for abssrc, relsrc, exact in srcs:
767 767 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
768 768 errors += 1
769 769
770 770 if errors:
771 771 ui.warn(_('(consider using --after)\n'))
772 772
773 773 return errors != 0
774 774
775 775 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
776 776 runargs=None, appendpid=False):
777 777 '''Run a command as a service.'''
778 778
779 779 def writepid(pid):
780 780 if opts['pid_file']:
781 781 if appendpid:
782 782 mode = 'a'
783 783 else:
784 784 mode = 'w'
785 785 fp = open(opts['pid_file'], mode)
786 786 fp.write(str(pid) + '\n')
787 787 fp.close()
788 788
789 789 if opts['daemon'] and not opts['daemon_postexec']:
790 790 # Signal child process startup with file removal
791 791 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
792 792 os.close(lockfd)
793 793 try:
794 794 if not runargs:
795 795 runargs = util.hgcmd() + sys.argv[1:]
796 796 runargs.append('--daemon-postexec=unlink:%s' % lockpath)
797 797 # Don't pass --cwd to the child process, because we've already
798 798 # changed directory.
799 799 for i in xrange(1, len(runargs)):
800 800 if runargs[i].startswith('--cwd='):
801 801 del runargs[i]
802 802 break
803 803 elif runargs[i].startswith('--cwd'):
804 804 del runargs[i:i + 2]
805 805 break
806 806 def condfn():
807 807 return not os.path.exists(lockpath)
808 808 pid = util.rundetached(runargs, condfn)
809 809 if pid < 0:
810 810 raise error.Abort(_('child process failed to start'))
811 811 writepid(pid)
812 812 finally:
813 813 try:
814 814 os.unlink(lockpath)
815 815 except OSError as e:
816 816 if e.errno != errno.ENOENT:
817 817 raise
818 818 if parentfn:
819 819 return parentfn(pid)
820 820 else:
821 821 return
822 822
823 823 if initfn:
824 824 initfn()
825 825
826 826 if not opts['daemon']:
827 827 writepid(util.getpid())
828 828
829 829 if opts['daemon_postexec']:
830 830 try:
831 831 os.setsid()
832 832 except AttributeError:
833 833 pass
834 834 for inst in opts['daemon_postexec']:
835 835 if inst.startswith('unlink:'):
836 836 lockpath = inst[7:]
837 837 os.unlink(lockpath)
838 838 elif inst.startswith('chdir:'):
839 839 os.chdir(inst[6:])
840 840 elif inst != 'none':
841 841 raise error.Abort(_('invalid value for --daemon-postexec: %s')
842 842 % inst)
843 843 util.hidewindow()
844 844 sys.stdout.flush()
845 845 sys.stderr.flush()
846 846
847 847 nullfd = os.open(os.devnull, os.O_RDWR)
848 848 logfilefd = nullfd
849 849 if logfile:
850 850 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
851 851 os.dup2(nullfd, 0)
852 852 os.dup2(logfilefd, 1)
853 853 os.dup2(logfilefd, 2)
854 854 if nullfd not in (0, 1, 2):
855 855 os.close(nullfd)
856 856 if logfile and logfilefd not in (0, 1, 2):
857 857 os.close(logfilefd)
858 858
859 859 if runfn:
860 860 return runfn()
861 861
862 862 ## facility to let extension process additional data into an import patch
863 863 # list of identifier to be executed in order
864 864 extrapreimport = [] # run before commit
865 865 extrapostimport = [] # run after commit
866 866 # mapping from identifier to actual import function
867 867 #
868 868 # 'preimport' are run before the commit is made and are provided the following
869 869 # arguments:
870 870 # - repo: the localrepository instance,
871 871 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
872 872 # - extra: the future extra dictionary of the changeset, please mutate it,
873 873 # - opts: the import options.
874 874 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
875 875 # mutation of in memory commit and more. Feel free to rework the code to get
876 876 # there.
877 877 extrapreimportmap = {}
878 878 # 'postimport' are run after the commit is made and are provided the following
879 879 # argument:
880 880 # - ctx: the changectx created by import.
881 881 extrapostimportmap = {}
882 882
883 883 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
884 884 """Utility function used by commands.import to import a single patch
885 885
886 886 This function is explicitly defined here to help the evolve extension to
887 887 wrap this part of the import logic.
888 888
889 889 The API is currently a bit ugly because it a simple code translation from
890 890 the import command. Feel free to make it better.
891 891
892 892 :hunk: a patch (as a binary string)
893 893 :parents: nodes that will be parent of the created commit
894 894 :opts: the full dict of option passed to the import command
895 895 :msgs: list to save commit message to.
896 896 (used in case we need to save it when failing)
897 897 :updatefunc: a function that update a repo to a given node
898 898 updatefunc(<repo>, <node>)
899 899 """
900 900 # avoid cycle context -> subrepo -> cmdutil
901 901 from . import context
902 902 extractdata = patch.extract(ui, hunk)
903 903 tmpname = extractdata.get('filename')
904 904 message = extractdata.get('message')
905 905 user = opts.get('user') or extractdata.get('user')
906 906 date = opts.get('date') or extractdata.get('date')
907 907 branch = extractdata.get('branch')
908 908 nodeid = extractdata.get('nodeid')
909 909 p1 = extractdata.get('p1')
910 910 p2 = extractdata.get('p2')
911 911
912 912 nocommit = opts.get('no_commit')
913 913 importbranch = opts.get('import_branch')
914 914 update = not opts.get('bypass')
915 915 strip = opts["strip"]
916 916 prefix = opts["prefix"]
917 917 sim = float(opts.get('similarity') or 0)
918 918 if not tmpname:
919 919 return (None, None, False)
920 920
921 921 rejects = False
922 922
923 923 try:
924 924 cmdline_message = logmessage(ui, opts)
925 925 if cmdline_message:
926 926 # pickup the cmdline msg
927 927 message = cmdline_message
928 928 elif message:
929 929 # pickup the patch msg
930 930 message = message.strip()
931 931 else:
932 932 # launch the editor
933 933 message = None
934 934 ui.debug('message:\n%s\n' % message)
935 935
936 936 if len(parents) == 1:
937 937 parents.append(repo[nullid])
938 938 if opts.get('exact'):
939 939 if not nodeid or not p1:
940 940 raise error.Abort(_('not a Mercurial patch'))
941 941 p1 = repo[p1]
942 942 p2 = repo[p2 or nullid]
943 943 elif p2:
944 944 try:
945 945 p1 = repo[p1]
946 946 p2 = repo[p2]
947 947 # Without any options, consider p2 only if the
948 948 # patch is being applied on top of the recorded
949 949 # first parent.
950 950 if p1 != parents[0]:
951 951 p1 = parents[0]
952 952 p2 = repo[nullid]
953 953 except error.RepoError:
954 954 p1, p2 = parents
955 955 if p2.node() == nullid:
956 956 ui.warn(_("warning: import the patch as a normal revision\n"
957 957 "(use --exact to import the patch as a merge)\n"))
958 958 else:
959 959 p1, p2 = parents
960 960
961 961 n = None
962 962 if update:
963 963 if p1 != parents[0]:
964 964 updatefunc(repo, p1.node())
965 965 if p2 != parents[1]:
966 966 repo.setparents(p1.node(), p2.node())
967 967
968 968 if opts.get('exact') or importbranch:
969 969 repo.dirstate.setbranch(branch or 'default')
970 970
971 971 partial = opts.get('partial', False)
972 972 files = set()
973 973 try:
974 974 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
975 975 files=files, eolmode=None, similarity=sim / 100.0)
976 976 except patch.PatchError as e:
977 977 if not partial:
978 978 raise error.Abort(str(e))
979 979 if partial:
980 980 rejects = True
981 981
982 982 files = list(files)
983 983 if nocommit:
984 984 if message:
985 985 msgs.append(message)
986 986 else:
987 987 if opts.get('exact') or p2:
988 988 # If you got here, you either use --force and know what
989 989 # you are doing or used --exact or a merge patch while
990 990 # being updated to its first parent.
991 991 m = None
992 992 else:
993 993 m = scmutil.matchfiles(repo, files or [])
994 994 editform = mergeeditform(repo[None], 'import.normal')
995 995 if opts.get('exact'):
996 996 editor = None
997 997 else:
998 998 editor = getcommiteditor(editform=editform, **opts)
999 999 allowemptyback = repo.ui.backupconfig('ui', 'allowemptycommit')
1000 1000 extra = {}
1001 1001 for idfunc in extrapreimport:
1002 1002 extrapreimportmap[idfunc](repo, extractdata, extra, opts)
1003 1003 try:
1004 1004 if partial:
1005 1005 repo.ui.setconfig('ui', 'allowemptycommit', True)
1006 1006 n = repo.commit(message, user,
1007 1007 date, match=m,
1008 1008 editor=editor, extra=extra)
1009 1009 for idfunc in extrapostimport:
1010 1010 extrapostimportmap[idfunc](repo[n])
1011 1011 finally:
1012 1012 repo.ui.restoreconfig(allowemptyback)
1013 1013 else:
1014 1014 if opts.get('exact') or importbranch:
1015 1015 branch = branch or 'default'
1016 1016 else:
1017 1017 branch = p1.branch()
1018 1018 store = patch.filestore()
1019 1019 try:
1020 1020 files = set()
1021 1021 try:
1022 1022 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
1023 1023 files, eolmode=None)
1024 1024 except patch.PatchError as e:
1025 1025 raise error.Abort(str(e))
1026 1026 if opts.get('exact'):
1027 1027 editor = None
1028 1028 else:
1029 1029 editor = getcommiteditor(editform='import.bypass')
1030 1030 memctx = context.makememctx(repo, (p1.node(), p2.node()),
1031 1031 message,
1032 1032 user,
1033 1033 date,
1034 1034 branch, files, store,
1035 1035 editor=editor)
1036 1036 n = memctx.commit()
1037 1037 finally:
1038 1038 store.close()
1039 1039 if opts.get('exact') and nocommit:
1040 1040 # --exact with --no-commit is still useful in that it does merge
1041 1041 # and branch bits
1042 1042 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1043 1043 elif opts.get('exact') and hex(n) != nodeid:
1044 1044 raise error.Abort(_('patch is damaged or loses information'))
1045 1045 msg = _('applied to working directory')
1046 1046 if n:
1047 1047 # i18n: refers to a short changeset id
1048 1048 msg = _('created %s') % short(n)
1049 1049 return (msg, n, rejects)
1050 1050 finally:
1051 1051 os.unlink(tmpname)
1052 1052
1053 1053 # facility to let extensions include additional data in an exported patch
1054 1054 # list of identifiers to be executed in order
1055 1055 extraexport = []
1056 1056 # mapping from identifier to actual export function
1057 1057 # function as to return a string to be added to the header or None
1058 1058 # it is given two arguments (sequencenumber, changectx)
1059 1059 extraexportmap = {}
1060 1060
1061 1061 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1062 1062 opts=None, match=None):
1063 1063 '''export changesets as hg patches.'''
1064 1064
1065 1065 total = len(revs)
1066 1066 revwidth = max([len(str(rev)) for rev in revs])
1067 1067 filemode = {}
1068 1068
1069 1069 def single(rev, seqno, fp):
1070 1070 ctx = repo[rev]
1071 1071 node = ctx.node()
1072 1072 parents = [p.node() for p in ctx.parents() if p]
1073 1073 branch = ctx.branch()
1074 1074 if switch_parent:
1075 1075 parents.reverse()
1076 1076
1077 1077 if parents:
1078 1078 prev = parents[0]
1079 1079 else:
1080 1080 prev = nullid
1081 1081
1082 1082 shouldclose = False
1083 1083 if not fp and len(template) > 0:
1084 1084 desc_lines = ctx.description().rstrip().split('\n')
1085 1085 desc = desc_lines[0] #Commit always has a first line.
1086 1086 fp = makefileobj(repo, template, node, desc=desc, total=total,
1087 1087 seqno=seqno, revwidth=revwidth, mode='wb',
1088 1088 modemap=filemode)
1089 1089 shouldclose = True
1090 1090 if fp and not getattr(fp, 'name', '<unnamed>').startswith('<'):
1091 1091 repo.ui.note("%s\n" % fp.name)
1092 1092
1093 1093 if not fp:
1094 1094 write = repo.ui.write
1095 1095 else:
1096 1096 def write(s, **kw):
1097 1097 fp.write(s)
1098 1098
1099 1099 write("# HG changeset patch\n")
1100 1100 write("# User %s\n" % ctx.user())
1101 1101 write("# Date %d %d\n" % ctx.date())
1102 1102 write("# %s\n" % util.datestr(ctx.date()))
1103 1103 if branch and branch != 'default':
1104 1104 write("# Branch %s\n" % branch)
1105 1105 write("# Node ID %s\n" % hex(node))
1106 1106 write("# Parent %s\n" % hex(prev))
1107 1107 if len(parents) > 1:
1108 1108 write("# Parent %s\n" % hex(parents[1]))
1109 1109
1110 1110 for headerid in extraexport:
1111 1111 header = extraexportmap[headerid](seqno, ctx)
1112 1112 if header is not None:
1113 1113 write('# %s\n' % header)
1114 1114 write(ctx.description().rstrip())
1115 1115 write("\n\n")
1116 1116
1117 1117 for chunk, label in patch.diffui(repo, prev, node, match, opts=opts):
1118 1118 write(chunk, label=label)
1119 1119
1120 1120 if shouldclose:
1121 1121 fp.close()
1122 1122
1123 1123 for seqno, rev in enumerate(revs):
1124 1124 single(rev, seqno + 1, fp)
1125 1125
1126 1126 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
1127 1127 changes=None, stat=False, fp=None, prefix='',
1128 1128 root='', listsubrepos=False):
1129 1129 '''show diff or diffstat.'''
1130 1130 if fp is None:
1131 1131 write = ui.write
1132 1132 else:
1133 1133 def write(s, **kw):
1134 1134 fp.write(s)
1135 1135
1136 1136 if root:
1137 1137 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
1138 1138 else:
1139 1139 relroot = ''
1140 1140 if relroot != '':
1141 1141 # XXX relative roots currently don't work if the root is within a
1142 1142 # subrepo
1143 1143 uirelroot = match.uipath(relroot)
1144 1144 relroot += '/'
1145 1145 for matchroot in match.files():
1146 1146 if not matchroot.startswith(relroot):
1147 1147 ui.warn(_('warning: %s not inside relative root %s\n') % (
1148 1148 match.uipath(matchroot), uirelroot))
1149 1149
1150 1150 if stat:
1151 1151 diffopts = diffopts.copy(context=0)
1152 1152 width = 80
1153 1153 if not ui.plain():
1154 1154 width = ui.termwidth()
1155 1155 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
1156 1156 prefix=prefix, relroot=relroot)
1157 1157 for chunk, label in patch.diffstatui(util.iterlines(chunks),
1158 1158 width=width,
1159 1159 git=diffopts.git):
1160 1160 write(chunk, label=label)
1161 1161 else:
1162 1162 for chunk, label in patch.diffui(repo, node1, node2, match,
1163 1163 changes, diffopts, prefix=prefix,
1164 1164 relroot=relroot):
1165 1165 write(chunk, label=label)
1166 1166
1167 1167 if listsubrepos:
1168 1168 ctx1 = repo[node1]
1169 1169 ctx2 = repo[node2]
1170 1170 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
1171 1171 tempnode2 = node2
1172 1172 try:
1173 1173 if node2 is not None:
1174 1174 tempnode2 = ctx2.substate[subpath][1]
1175 1175 except KeyError:
1176 1176 # A subrepo that existed in node1 was deleted between node1 and
1177 1177 # node2 (inclusive). Thus, ctx2's substate won't contain that
1178 1178 # subpath. The best we can do is to ignore it.
1179 1179 tempnode2 = None
1180 1180 submatch = matchmod.subdirmatcher(subpath, match)
1181 1181 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
1182 1182 stat=stat, fp=fp, prefix=prefix)
1183 1183
1184 1184 class changeset_printer(object):
1185 1185 '''show changeset information when templating not requested.'''
1186 1186
1187 1187 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1188 1188 self.ui = ui
1189 1189 self.repo = repo
1190 1190 self.buffered = buffered
1191 1191 self.matchfn = matchfn
1192 1192 self.diffopts = diffopts
1193 1193 self.header = {}
1194 1194 self.hunk = {}
1195 1195 self.lastheader = None
1196 1196 self.footer = None
1197 1197
1198 1198 def flush(self, ctx):
1199 1199 rev = ctx.rev()
1200 1200 if rev in self.header:
1201 1201 h = self.header[rev]
1202 1202 if h != self.lastheader:
1203 1203 self.lastheader = h
1204 1204 self.ui.write(h)
1205 1205 del self.header[rev]
1206 1206 if rev in self.hunk:
1207 1207 self.ui.write(self.hunk[rev])
1208 1208 del self.hunk[rev]
1209 1209 return 1
1210 1210 return 0
1211 1211
1212 1212 def close(self):
1213 1213 if self.footer:
1214 1214 self.ui.write(self.footer)
1215 1215
1216 1216 def show(self, ctx, copies=None, matchfn=None, **props):
1217 1217 if self.buffered:
1218 1218 self.ui.pushbuffer(labeled=True)
1219 1219 self._show(ctx, copies, matchfn, props)
1220 1220 self.hunk[ctx.rev()] = self.ui.popbuffer()
1221 1221 else:
1222 1222 self._show(ctx, copies, matchfn, props)
1223 1223
1224 1224 def _show(self, ctx, copies, matchfn, props):
1225 1225 '''show a single changeset or file revision'''
1226 1226 changenode = ctx.node()
1227 1227 rev = ctx.rev()
1228 1228 if self.ui.debugflag:
1229 1229 hexfunc = hex
1230 1230 else:
1231 1231 hexfunc = short
1232 1232 # as of now, wctx.node() and wctx.rev() return None, but we want to
1233 1233 # show the same values as {node} and {rev} templatekw
1234 1234 revnode = (scmutil.intrev(rev), hexfunc(bin(ctx.hex())))
1235 1235
1236 1236 if self.ui.quiet:
1237 1237 self.ui.write("%d:%s\n" % revnode, label='log.node')
1238 1238 return
1239 1239
1240 1240 date = util.datestr(ctx.date())
1241 1241
1242 1242 # i18n: column positioning for "hg log"
1243 1243 self.ui.write(_("changeset: %d:%s\n") % revnode,
1244 1244 label='log.changeset changeset.%s' % ctx.phasestr())
1245 1245
1246 1246 # branches are shown first before any other names due to backwards
1247 1247 # compatibility
1248 1248 branch = ctx.branch()
1249 1249 # don't show the default branch name
1250 1250 if branch != 'default':
1251 1251 # i18n: column positioning for "hg log"
1252 1252 self.ui.write(_("branch: %s\n") % branch,
1253 1253 label='log.branch')
1254 1254
1255 1255 for name, ns in self.repo.names.iteritems():
1256 1256 # branches has special logic already handled above, so here we just
1257 1257 # skip it
1258 1258 if name == 'branches':
1259 1259 continue
1260 1260 # we will use the templatename as the color name since those two
1261 1261 # should be the same
1262 1262 for name in ns.names(self.repo, changenode):
1263 1263 self.ui.write(ns.logfmt % name,
1264 1264 label='log.%s' % ns.colorname)
1265 1265 if self.ui.debugflag:
1266 1266 # i18n: column positioning for "hg log"
1267 1267 self.ui.write(_("phase: %s\n") % ctx.phasestr(),
1268 1268 label='log.phase')
1269 1269 for pctx in scmutil.meaningfulparents(self.repo, ctx):
1270 1270 label = 'log.parent changeset.%s' % pctx.phasestr()
1271 1271 # i18n: column positioning for "hg log"
1272 1272 self.ui.write(_("parent: %d:%s\n")
1273 1273 % (pctx.rev(), hexfunc(pctx.node())),
1274 1274 label=label)
1275 1275
1276 1276 if self.ui.debugflag and rev is not None:
1277 1277 mnode = ctx.manifestnode()
1278 1278 # i18n: column positioning for "hg log"
1279 1279 self.ui.write(_("manifest: %d:%s\n") %
1280 1280 (self.repo.manifest.rev(mnode), hex(mnode)),
1281 1281 label='ui.debug log.manifest')
1282 1282 # i18n: column positioning for "hg log"
1283 1283 self.ui.write(_("user: %s\n") % ctx.user(),
1284 1284 label='log.user')
1285 1285 # i18n: column positioning for "hg log"
1286 1286 self.ui.write(_("date: %s\n") % date,
1287 1287 label='log.date')
1288 1288
1289 1289 if self.ui.debugflag:
1290 1290 files = ctx.p1().status(ctx)[:3]
1291 1291 for key, value in zip([# i18n: column positioning for "hg log"
1292 1292 _("files:"),
1293 1293 # i18n: column positioning for "hg log"
1294 1294 _("files+:"),
1295 1295 # i18n: column positioning for "hg log"
1296 1296 _("files-:")], files):
1297 1297 if value:
1298 1298 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
1299 1299 label='ui.debug log.files')
1300 1300 elif ctx.files() and self.ui.verbose:
1301 1301 # i18n: column positioning for "hg log"
1302 1302 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
1303 1303 label='ui.note log.files')
1304 1304 if copies and self.ui.verbose:
1305 1305 copies = ['%s (%s)' % c for c in copies]
1306 1306 # i18n: column positioning for "hg log"
1307 1307 self.ui.write(_("copies: %s\n") % ' '.join(copies),
1308 1308 label='ui.note log.copies')
1309 1309
1310 1310 extra = ctx.extra()
1311 1311 if extra and self.ui.debugflag:
1312 1312 for key, value in sorted(extra.items()):
1313 1313 # i18n: column positioning for "hg log"
1314 1314 self.ui.write(_("extra: %s=%s\n")
1315 1315 % (key, value.encode('string_escape')),
1316 1316 label='ui.debug log.extra')
1317 1317
1318 1318 description = ctx.description().strip()
1319 1319 if description:
1320 1320 if self.ui.verbose:
1321 1321 self.ui.write(_("description:\n"),
1322 1322 label='ui.note log.description')
1323 1323 self.ui.write(description,
1324 1324 label='ui.note log.description')
1325 1325 self.ui.write("\n\n")
1326 1326 else:
1327 1327 # i18n: column positioning for "hg log"
1328 1328 self.ui.write(_("summary: %s\n") %
1329 1329 description.splitlines()[0],
1330 1330 label='log.summary')
1331 1331 self.ui.write("\n")
1332 1332
1333 1333 self.showpatch(ctx, matchfn)
1334 1334
1335 1335 def showpatch(self, ctx, matchfn):
1336 1336 if not matchfn:
1337 1337 matchfn = self.matchfn
1338 1338 if matchfn:
1339 1339 stat = self.diffopts.get('stat')
1340 1340 diff = self.diffopts.get('patch')
1341 1341 diffopts = patch.diffallopts(self.ui, self.diffopts)
1342 1342 node = ctx.node()
1343 1343 prev = ctx.p1().node()
1344 1344 if stat:
1345 1345 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1346 1346 match=matchfn, stat=True)
1347 1347 if diff:
1348 1348 if stat:
1349 1349 self.ui.write("\n")
1350 1350 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1351 1351 match=matchfn, stat=False)
1352 1352 self.ui.write("\n")
1353 1353
1354 1354 class jsonchangeset(changeset_printer):
1355 1355 '''format changeset information.'''
1356 1356
1357 1357 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1358 1358 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1359 1359 self.cache = {}
1360 1360 self._first = True
1361 1361
1362 1362 def close(self):
1363 1363 if not self._first:
1364 1364 self.ui.write("\n]\n")
1365 1365 else:
1366 1366 self.ui.write("[]\n")
1367 1367
1368 1368 def _show(self, ctx, copies, matchfn, props):
1369 1369 '''show a single changeset or file revision'''
1370 1370 rev = ctx.rev()
1371 1371 if rev is None:
1372 1372 jrev = jnode = 'null'
1373 1373 else:
1374 1374 jrev = str(rev)
1375 1375 jnode = '"%s"' % hex(ctx.node())
1376 1376 j = encoding.jsonescape
1377 1377
1378 1378 if self._first:
1379 1379 self.ui.write("[\n {")
1380 1380 self._first = False
1381 1381 else:
1382 1382 self.ui.write(",\n {")
1383 1383
1384 1384 if self.ui.quiet:
1385 1385 self.ui.write('\n "rev": %s' % jrev)
1386 1386 self.ui.write(',\n "node": %s' % jnode)
1387 1387 self.ui.write('\n }')
1388 1388 return
1389 1389
1390 1390 self.ui.write('\n "rev": %s' % jrev)
1391 1391 self.ui.write(',\n "node": %s' % jnode)
1392 1392 self.ui.write(',\n "branch": "%s"' % j(ctx.branch()))
1393 1393 self.ui.write(',\n "phase": "%s"' % ctx.phasestr())
1394 1394 self.ui.write(',\n "user": "%s"' % j(ctx.user()))
1395 1395 self.ui.write(',\n "date": [%d, %d]' % ctx.date())
1396 1396 self.ui.write(',\n "desc": "%s"' % j(ctx.description()))
1397 1397
1398 1398 self.ui.write(',\n "bookmarks": [%s]' %
1399 1399 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
1400 1400 self.ui.write(',\n "tags": [%s]' %
1401 1401 ", ".join('"%s"' % j(t) for t in ctx.tags()))
1402 1402 self.ui.write(',\n "parents": [%s]' %
1403 1403 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
1404 1404
1405 1405 if self.ui.debugflag:
1406 1406 if rev is None:
1407 1407 jmanifestnode = 'null'
1408 1408 else:
1409 1409 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
1410 1410 self.ui.write(',\n "manifest": %s' % jmanifestnode)
1411 1411
1412 1412 self.ui.write(',\n "extra": {%s}' %
1413 1413 ", ".join('"%s": "%s"' % (j(k), j(v))
1414 1414 for k, v in ctx.extra().items()))
1415 1415
1416 1416 files = ctx.p1().status(ctx)
1417 1417 self.ui.write(',\n "modified": [%s]' %
1418 1418 ", ".join('"%s"' % j(f) for f in files[0]))
1419 1419 self.ui.write(',\n "added": [%s]' %
1420 1420 ", ".join('"%s"' % j(f) for f in files[1]))
1421 1421 self.ui.write(',\n "removed": [%s]' %
1422 1422 ", ".join('"%s"' % j(f) for f in files[2]))
1423 1423
1424 1424 elif self.ui.verbose:
1425 1425 self.ui.write(',\n "files": [%s]' %
1426 1426 ", ".join('"%s"' % j(f) for f in ctx.files()))
1427 1427
1428 1428 if copies:
1429 1429 self.ui.write(',\n "copies": {%s}' %
1430 1430 ", ".join('"%s": "%s"' % (j(k), j(v))
1431 1431 for k, v in copies))
1432 1432
1433 1433 matchfn = self.matchfn
1434 1434 if matchfn:
1435 1435 stat = self.diffopts.get('stat')
1436 1436 diff = self.diffopts.get('patch')
1437 1437 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
1438 1438 node, prev = ctx.node(), ctx.p1().node()
1439 1439 if stat:
1440 1440 self.ui.pushbuffer()
1441 1441 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1442 1442 match=matchfn, stat=True)
1443 1443 self.ui.write(',\n "diffstat": "%s"' % j(self.ui.popbuffer()))
1444 1444 if diff:
1445 1445 self.ui.pushbuffer()
1446 1446 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1447 1447 match=matchfn, stat=False)
1448 1448 self.ui.write(',\n "diff": "%s"' % j(self.ui.popbuffer()))
1449 1449
1450 1450 self.ui.write("\n }")
1451 1451
1452 1452 class changeset_templater(changeset_printer):
1453 1453 '''format changeset information.'''
1454 1454
1455 1455 def __init__(self, ui, repo, matchfn, diffopts, tmpl, mapfile, buffered):
1456 1456 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1457 1457 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
1458 1458 defaulttempl = {
1459 1459 'parent': '{rev}:{node|formatnode} ',
1460 1460 'manifest': '{rev}:{node|formatnode}',
1461 1461 'file_copy': '{name} ({source})',
1462 1462 'extra': '{key}={value|stringescape}'
1463 1463 }
1464 1464 # filecopy is preserved for compatibility reasons
1465 1465 defaulttempl['filecopy'] = defaulttempl['file_copy']
1466 1466 self.t = templater.templater(mapfile, {'formatnode': formatnode},
1467 1467 cache=defaulttempl)
1468 1468 if tmpl:
1469 1469 self.t.cache['changeset'] = tmpl
1470 1470
1471 1471 self.cache = {}
1472 1472
1473 1473 # find correct templates for current mode
1474 1474 tmplmodes = [
1475 1475 (True, None),
1476 1476 (self.ui.verbose, 'verbose'),
1477 1477 (self.ui.quiet, 'quiet'),
1478 1478 (self.ui.debugflag, 'debug'),
1479 1479 ]
1480 1480
1481 1481 self._parts = {'header': '', 'footer': '', 'changeset': 'changeset',
1482 1482 'docheader': '', 'docfooter': ''}
1483 1483 for mode, postfix in tmplmodes:
1484 1484 for t in self._parts:
1485 1485 cur = t
1486 1486 if postfix:
1487 1487 cur += "_" + postfix
1488 1488 if mode and cur in self.t:
1489 1489 self._parts[t] = cur
1490 1490
1491 1491 if self._parts['docheader']:
1492 1492 self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
1493 1493
1494 1494 def close(self):
1495 1495 if self._parts['docfooter']:
1496 1496 if not self.footer:
1497 1497 self.footer = ""
1498 1498 self.footer += templater.stringify(self.t(self._parts['docfooter']))
1499 1499 return super(changeset_templater, self).close()
1500 1500
1501 1501 def _show(self, ctx, copies, matchfn, props):
1502 1502 '''show a single changeset or file revision'''
1503 1503 props = props.copy()
1504 1504 props.update(templatekw.keywords)
1505 1505 props['templ'] = self.t
1506 1506 props['ctx'] = ctx
1507 1507 props['repo'] = self.repo
1508 1508 props['ui'] = self.repo.ui
1509 1509 props['revcache'] = {'copies': copies}
1510 1510 props['cache'] = self.cache
1511 1511
1512 1512 try:
1513 1513 # write header
1514 1514 if self._parts['header']:
1515 1515 h = templater.stringify(self.t(self._parts['header'], **props))
1516 1516 if self.buffered:
1517 1517 self.header[ctx.rev()] = h
1518 1518 else:
1519 1519 if self.lastheader != h:
1520 1520 self.lastheader = h
1521 1521 self.ui.write(h)
1522 1522
1523 1523 # write changeset metadata, then patch if requested
1524 1524 key = self._parts['changeset']
1525 1525 self.ui.write(templater.stringify(self.t(key, **props)))
1526 1526 self.showpatch(ctx, matchfn)
1527 1527
1528 1528 if self._parts['footer']:
1529 1529 if not self.footer:
1530 1530 self.footer = templater.stringify(
1531 1531 self.t(self._parts['footer'], **props))
1532 1532 except KeyError as inst:
1533 1533 msg = _("%s: no key named '%s'")
1534 1534 raise error.Abort(msg % (self.t.mapfile, inst.args[0]))
1535 1535 except SyntaxError as inst:
1536 1536 raise error.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
1537 1537
1538 1538 def gettemplate(ui, tmpl, style):
1539 1539 """
1540 1540 Find the template matching the given template spec or style.
1541 1541 """
1542 1542
1543 1543 # ui settings
1544 1544 if not tmpl and not style: # template are stronger than style
1545 1545 tmpl = ui.config('ui', 'logtemplate')
1546 1546 if tmpl:
1547 1547 try:
1548 1548 tmpl = templater.unquotestring(tmpl)
1549 1549 except SyntaxError:
1550 1550 pass
1551 1551 return tmpl, None
1552 1552 else:
1553 1553 style = util.expandpath(ui.config('ui', 'style', ''))
1554 1554
1555 1555 if not tmpl and style:
1556 1556 mapfile = style
1557 1557 if not os.path.split(mapfile)[0]:
1558 1558 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1559 1559 or templater.templatepath(mapfile))
1560 1560 if mapname:
1561 1561 mapfile = mapname
1562 1562 return None, mapfile
1563 1563
1564 1564 if not tmpl:
1565 1565 return None, None
1566 1566
1567 1567 return formatter.lookuptemplate(ui, 'changeset', tmpl)
1568 1568
1569 1569 def show_changeset(ui, repo, opts, buffered=False):
1570 1570 """show one changeset using template or regular display.
1571 1571
1572 1572 Display format will be the first non-empty hit of:
1573 1573 1. option 'template'
1574 1574 2. option 'style'
1575 1575 3. [ui] setting 'logtemplate'
1576 1576 4. [ui] setting 'style'
1577 1577 If all of these values are either the unset or the empty string,
1578 1578 regular display via changeset_printer() is done.
1579 1579 """
1580 1580 # options
1581 1581 matchfn = None
1582 1582 if opts.get('patch') or opts.get('stat'):
1583 1583 matchfn = scmutil.matchall(repo)
1584 1584
1585 1585 if opts.get('template') == 'json':
1586 1586 return jsonchangeset(ui, repo, matchfn, opts, buffered)
1587 1587
1588 1588 tmpl, mapfile = gettemplate(ui, opts.get('template'), opts.get('style'))
1589 1589
1590 1590 if not tmpl and not mapfile:
1591 1591 return changeset_printer(ui, repo, matchfn, opts, buffered)
1592 1592
1593 1593 try:
1594 1594 t = changeset_templater(ui, repo, matchfn, opts, tmpl, mapfile,
1595 1595 buffered)
1596 1596 except SyntaxError as inst:
1597 1597 raise error.Abort(inst.args[0])
1598 1598 return t
1599 1599
1600 1600 def showmarker(ui, marker):
1601 1601 """utility function to display obsolescence marker in a readable way
1602 1602
1603 1603 To be used by debug function."""
1604 1604 ui.write(hex(marker.precnode()))
1605 1605 for repl in marker.succnodes():
1606 1606 ui.write(' ')
1607 1607 ui.write(hex(repl))
1608 1608 ui.write(' %X ' % marker.flags())
1609 1609 parents = marker.parentnodes()
1610 1610 if parents is not None:
1611 1611 ui.write('{%s} ' % ', '.join(hex(p) for p in parents))
1612 1612 ui.write('(%s) ' % util.datestr(marker.date()))
1613 1613 ui.write('{%s}' % (', '.join('%r: %r' % t for t in
1614 1614 sorted(marker.metadata().items())
1615 1615 if t[0] != 'date')))
1616 1616 ui.write('\n')
1617 1617
1618 1618 def finddate(ui, repo, date):
1619 1619 """Find the tipmost changeset that matches the given date spec"""
1620 1620
1621 1621 df = util.matchdate(date)
1622 1622 m = scmutil.matchall(repo)
1623 1623 results = {}
1624 1624
1625 1625 def prep(ctx, fns):
1626 1626 d = ctx.date()
1627 1627 if df(d[0]):
1628 1628 results[ctx.rev()] = d
1629 1629
1630 1630 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1631 1631 rev = ctx.rev()
1632 1632 if rev in results:
1633 1633 ui.status(_("found revision %s from %s\n") %
1634 1634 (rev, util.datestr(results[rev])))
1635 1635 return str(rev)
1636 1636
1637 1637 raise error.Abort(_("revision matching date not found"))
1638 1638
1639 1639 def increasingwindows(windowsize=8, sizelimit=512):
1640 1640 while True:
1641 1641 yield windowsize
1642 1642 if windowsize < sizelimit:
1643 1643 windowsize *= 2
1644 1644
1645 1645 class FileWalkError(Exception):
1646 1646 pass
1647 1647
1648 1648 def walkfilerevs(repo, match, follow, revs, fncache):
1649 1649 '''Walks the file history for the matched files.
1650 1650
1651 1651 Returns the changeset revs that are involved in the file history.
1652 1652
1653 1653 Throws FileWalkError if the file history can't be walked using
1654 1654 filelogs alone.
1655 1655 '''
1656 1656 wanted = set()
1657 1657 copies = []
1658 1658 minrev, maxrev = min(revs), max(revs)
1659 1659 def filerevgen(filelog, last):
1660 1660 """
1661 1661 Only files, no patterns. Check the history of each file.
1662 1662
1663 1663 Examines filelog entries within minrev, maxrev linkrev range
1664 1664 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1665 1665 tuples in backwards order
1666 1666 """
1667 1667 cl_count = len(repo)
1668 1668 revs = []
1669 1669 for j in xrange(0, last + 1):
1670 1670 linkrev = filelog.linkrev(j)
1671 1671 if linkrev < minrev:
1672 1672 continue
1673 1673 # only yield rev for which we have the changelog, it can
1674 1674 # happen while doing "hg log" during a pull or commit
1675 1675 if linkrev >= cl_count:
1676 1676 break
1677 1677
1678 1678 parentlinkrevs = []
1679 1679 for p in filelog.parentrevs(j):
1680 1680 if p != nullrev:
1681 1681 parentlinkrevs.append(filelog.linkrev(p))
1682 1682 n = filelog.node(j)
1683 1683 revs.append((linkrev, parentlinkrevs,
1684 1684 follow and filelog.renamed(n)))
1685 1685
1686 1686 return reversed(revs)
1687 1687 def iterfiles():
1688 1688 pctx = repo['.']
1689 1689 for filename in match.files():
1690 1690 if follow:
1691 1691 if filename not in pctx:
1692 1692 raise error.Abort(_('cannot follow file not in parent '
1693 1693 'revision: "%s"') % filename)
1694 1694 yield filename, pctx[filename].filenode()
1695 1695 else:
1696 1696 yield filename, None
1697 1697 for filename_node in copies:
1698 1698 yield filename_node
1699 1699
1700 1700 for file_, node in iterfiles():
1701 1701 filelog = repo.file(file_)
1702 1702 if not len(filelog):
1703 1703 if node is None:
1704 1704 # A zero count may be a directory or deleted file, so
1705 1705 # try to find matching entries on the slow path.
1706 1706 if follow:
1707 1707 raise error.Abort(
1708 1708 _('cannot follow nonexistent file: "%s"') % file_)
1709 1709 raise FileWalkError("Cannot walk via filelog")
1710 1710 else:
1711 1711 continue
1712 1712
1713 1713 if node is None:
1714 1714 last = len(filelog) - 1
1715 1715 else:
1716 1716 last = filelog.rev(node)
1717 1717
1718 1718 # keep track of all ancestors of the file
1719 1719 ancestors = set([filelog.linkrev(last)])
1720 1720
1721 1721 # iterate from latest to oldest revision
1722 1722 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1723 1723 if not follow:
1724 1724 if rev > maxrev:
1725 1725 continue
1726 1726 else:
1727 1727 # Note that last might not be the first interesting
1728 1728 # rev to us:
1729 1729 # if the file has been changed after maxrev, we'll
1730 1730 # have linkrev(last) > maxrev, and we still need
1731 1731 # to explore the file graph
1732 1732 if rev not in ancestors:
1733 1733 continue
1734 1734 # XXX insert 1327 fix here
1735 1735 if flparentlinkrevs:
1736 1736 ancestors.update(flparentlinkrevs)
1737 1737
1738 1738 fncache.setdefault(rev, []).append(file_)
1739 1739 wanted.add(rev)
1740 1740 if copied:
1741 1741 copies.append(copied)
1742 1742
1743 1743 return wanted
1744 1744
1745 1745 class _followfilter(object):
1746 1746 def __init__(self, repo, onlyfirst=False):
1747 1747 self.repo = repo
1748 1748 self.startrev = nullrev
1749 1749 self.roots = set()
1750 1750 self.onlyfirst = onlyfirst
1751 1751
1752 1752 def match(self, rev):
1753 1753 def realparents(rev):
1754 1754 if self.onlyfirst:
1755 1755 return self.repo.changelog.parentrevs(rev)[0:1]
1756 1756 else:
1757 1757 return filter(lambda x: x != nullrev,
1758 1758 self.repo.changelog.parentrevs(rev))
1759 1759
1760 1760 if self.startrev == nullrev:
1761 1761 self.startrev = rev
1762 1762 return True
1763 1763
1764 1764 if rev > self.startrev:
1765 1765 # forward: all descendants
1766 1766 if not self.roots:
1767 1767 self.roots.add(self.startrev)
1768 1768 for parent in realparents(rev):
1769 1769 if parent in self.roots:
1770 1770 self.roots.add(rev)
1771 1771 return True
1772 1772 else:
1773 1773 # backwards: all parents
1774 1774 if not self.roots:
1775 1775 self.roots.update(realparents(self.startrev))
1776 1776 if rev in self.roots:
1777 1777 self.roots.remove(rev)
1778 1778 self.roots.update(realparents(rev))
1779 1779 return True
1780 1780
1781 1781 return False
1782 1782
1783 1783 def walkchangerevs(repo, match, opts, prepare):
1784 1784 '''Iterate over files and the revs in which they changed.
1785 1785
1786 1786 Callers most commonly need to iterate backwards over the history
1787 1787 in which they are interested. Doing so has awful (quadratic-looking)
1788 1788 performance, so we use iterators in a "windowed" way.
1789 1789
1790 1790 We walk a window of revisions in the desired order. Within the
1791 1791 window, we first walk forwards to gather data, then in the desired
1792 1792 order (usually backwards) to display it.
1793 1793
1794 1794 This function returns an iterator yielding contexts. Before
1795 1795 yielding each context, the iterator will first call the prepare
1796 1796 function on each context in the window in forward order.'''
1797 1797
1798 1798 follow = opts.get('follow') or opts.get('follow_first')
1799 1799 revs = _logrevs(repo, opts)
1800 1800 if not revs:
1801 1801 return []
1802 1802 wanted = set()
1803 1803 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
1804 1804 opts.get('removed'))
1805 1805 fncache = {}
1806 1806 change = repo.changectx
1807 1807
1808 1808 # First step is to fill wanted, the set of revisions that we want to yield.
1809 1809 # When it does not induce extra cost, we also fill fncache for revisions in
1810 1810 # wanted: a cache of filenames that were changed (ctx.files()) and that
1811 1811 # match the file filtering conditions.
1812 1812
1813 1813 if match.always():
1814 1814 # No files, no patterns. Display all revs.
1815 1815 wanted = revs
1816 1816 elif not slowpath:
1817 1817 # We only have to read through the filelog to find wanted revisions
1818 1818
1819 1819 try:
1820 1820 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1821 1821 except FileWalkError:
1822 1822 slowpath = True
1823 1823
1824 1824 # We decided to fall back to the slowpath because at least one
1825 1825 # of the paths was not a file. Check to see if at least one of them
1826 1826 # existed in history, otherwise simply return
1827 1827 for path in match.files():
1828 1828 if path == '.' or path in repo.store:
1829 1829 break
1830 1830 else:
1831 1831 return []
1832 1832
1833 1833 if slowpath:
1834 1834 # We have to read the changelog to match filenames against
1835 1835 # changed files
1836 1836
1837 1837 if follow:
1838 1838 raise error.Abort(_('can only follow copies/renames for explicit '
1839 1839 'filenames'))
1840 1840
1841 1841 # The slow path checks files modified in every changeset.
1842 1842 # This is really slow on large repos, so compute the set lazily.
1843 1843 class lazywantedset(object):
1844 1844 def __init__(self):
1845 1845 self.set = set()
1846 1846 self.revs = set(revs)
1847 1847
1848 1848 # No need to worry about locality here because it will be accessed
1849 1849 # in the same order as the increasing window below.
1850 1850 def __contains__(self, value):
1851 1851 if value in self.set:
1852 1852 return True
1853 1853 elif not value in self.revs:
1854 1854 return False
1855 1855 else:
1856 1856 self.revs.discard(value)
1857 1857 ctx = change(value)
1858 1858 matches = filter(match, ctx.files())
1859 1859 if matches:
1860 1860 fncache[value] = matches
1861 1861 self.set.add(value)
1862 1862 return True
1863 1863 return False
1864 1864
1865 1865 def discard(self, value):
1866 1866 self.revs.discard(value)
1867 1867 self.set.discard(value)
1868 1868
1869 1869 wanted = lazywantedset()
1870 1870
1871 1871 # it might be worthwhile to do this in the iterator if the rev range
1872 1872 # is descending and the prune args are all within that range
1873 1873 for rev in opts.get('prune', ()):
1874 1874 rev = repo[rev].rev()
1875 1875 ff = _followfilter(repo)
1876 1876 stop = min(revs[0], revs[-1])
1877 1877 for x in xrange(rev, stop - 1, -1):
1878 1878 if ff.match(x):
1879 1879 wanted = wanted - [x]
1880 1880
1881 1881 # Now that wanted is correctly initialized, we can iterate over the
1882 1882 # revision range, yielding only revisions in wanted.
1883 1883 def iterate():
1884 1884 if follow and match.always():
1885 1885 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
1886 1886 def want(rev):
1887 1887 return ff.match(rev) and rev in wanted
1888 1888 else:
1889 1889 def want(rev):
1890 1890 return rev in wanted
1891 1891
1892 1892 it = iter(revs)
1893 1893 stopiteration = False
1894 1894 for windowsize in increasingwindows():
1895 1895 nrevs = []
1896 1896 for i in xrange(windowsize):
1897 1897 rev = next(it, None)
1898 1898 if rev is None:
1899 1899 stopiteration = True
1900 1900 break
1901 1901 elif want(rev):
1902 1902 nrevs.append(rev)
1903 1903 for rev in sorted(nrevs):
1904 1904 fns = fncache.get(rev)
1905 1905 ctx = change(rev)
1906 1906 if not fns:
1907 1907 def fns_generator():
1908 1908 for f in ctx.files():
1909 1909 if match(f):
1910 1910 yield f
1911 1911 fns = fns_generator()
1912 1912 prepare(ctx, fns)
1913 1913 for rev in nrevs:
1914 1914 yield change(rev)
1915 1915
1916 1916 if stopiteration:
1917 1917 break
1918 1918
1919 1919 return iterate()
1920 1920
1921 1921 def _makefollowlogfilematcher(repo, files, followfirst):
1922 1922 # When displaying a revision with --patch --follow FILE, we have
1923 1923 # to know which file of the revision must be diffed. With
1924 1924 # --follow, we want the names of the ancestors of FILE in the
1925 1925 # revision, stored in "fcache". "fcache" is populated by
1926 1926 # reproducing the graph traversal already done by --follow revset
1927 1927 # and relating linkrevs to file names (which is not "correct" but
1928 1928 # good enough).
1929 1929 fcache = {}
1930 1930 fcacheready = [False]
1931 1931 pctx = repo['.']
1932 1932
1933 1933 def populate():
1934 1934 for fn in files:
1935 1935 for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
1936 1936 for c in i:
1937 1937 fcache.setdefault(c.linkrev(), set()).add(c.path())
1938 1938
1939 1939 def filematcher(rev):
1940 1940 if not fcacheready[0]:
1941 1941 # Lazy initialization
1942 1942 fcacheready[0] = True
1943 1943 populate()
1944 1944 return scmutil.matchfiles(repo, fcache.get(rev, []))
1945 1945
1946 1946 return filematcher
1947 1947
1948 1948 def _makenofollowlogfilematcher(repo, pats, opts):
1949 1949 '''hook for extensions to override the filematcher for non-follow cases'''
1950 1950 return None
1951 1951
1952 1952 def _makelogrevset(repo, pats, opts, revs):
1953 1953 """Return (expr, filematcher) where expr is a revset string built
1954 1954 from log options and file patterns or None. If --stat or --patch
1955 1955 are not passed filematcher is None. Otherwise it is a callable
1956 1956 taking a revision number and returning a match objects filtering
1957 1957 the files to be detailed when displaying the revision.
1958 1958 """
1959 1959 opt2revset = {
1960 1960 'no_merges': ('not merge()', None),
1961 1961 'only_merges': ('merge()', None),
1962 1962 '_ancestors': ('ancestors(%(val)s)', None),
1963 1963 '_fancestors': ('_firstancestors(%(val)s)', None),
1964 1964 '_descendants': ('descendants(%(val)s)', None),
1965 1965 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1966 1966 '_matchfiles': ('_matchfiles(%(val)s)', None),
1967 1967 'date': ('date(%(val)r)', None),
1968 1968 'branch': ('branch(%(val)r)', ' or '),
1969 1969 '_patslog': ('filelog(%(val)r)', ' or '),
1970 1970 '_patsfollow': ('follow(%(val)r)', ' or '),
1971 1971 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1972 1972 'keyword': ('keyword(%(val)r)', ' or '),
1973 1973 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1974 1974 'user': ('user(%(val)r)', ' or '),
1975 1975 }
1976 1976
1977 1977 opts = dict(opts)
1978 1978 # follow or not follow?
1979 1979 follow = opts.get('follow') or opts.get('follow_first')
1980 1980 if opts.get('follow_first'):
1981 1981 followfirst = 1
1982 1982 else:
1983 1983 followfirst = 0
1984 1984 # --follow with FILE behavior depends on revs...
1985 1985 it = iter(revs)
1986 1986 startrev = it.next()
1987 1987 followdescendants = startrev < next(it, startrev)
1988 1988
1989 1989 # branch and only_branch are really aliases and must be handled at
1990 1990 # the same time
1991 1991 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1992 1992 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1993 1993 # pats/include/exclude are passed to match.match() directly in
1994 1994 # _matchfiles() revset but walkchangerevs() builds its matcher with
1995 1995 # scmutil.match(). The difference is input pats are globbed on
1996 1996 # platforms without shell expansion (windows).
1997 1997 wctx = repo[None]
1998 1998 match, pats = scmutil.matchandpats(wctx, pats, opts)
1999 1999 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
2000 2000 opts.get('removed'))
2001 2001 if not slowpath:
2002 2002 for f in match.files():
2003 2003 if follow and f not in wctx:
2004 2004 # If the file exists, it may be a directory, so let it
2005 2005 # take the slow path.
2006 2006 if os.path.exists(repo.wjoin(f)):
2007 2007 slowpath = True
2008 2008 continue
2009 2009 else:
2010 2010 raise error.Abort(_('cannot follow file not in parent '
2011 2011 'revision: "%s"') % f)
2012 2012 filelog = repo.file(f)
2013 2013 if not filelog:
2014 2014 # A zero count may be a directory or deleted file, so
2015 2015 # try to find matching entries on the slow path.
2016 2016 if follow:
2017 2017 raise error.Abort(
2018 2018 _('cannot follow nonexistent file: "%s"') % f)
2019 2019 slowpath = True
2020 2020
2021 2021 # We decided to fall back to the slowpath because at least one
2022 2022 # of the paths was not a file. Check to see if at least one of them
2023 2023 # existed in history - in that case, we'll continue down the
2024 2024 # slowpath; otherwise, we can turn off the slowpath
2025 2025 if slowpath:
2026 2026 for path in match.files():
2027 2027 if path == '.' or path in repo.store:
2028 2028 break
2029 2029 else:
2030 2030 slowpath = False
2031 2031
2032 2032 fpats = ('_patsfollow', '_patsfollowfirst')
2033 2033 fnopats = (('_ancestors', '_fancestors'),
2034 2034 ('_descendants', '_fdescendants'))
2035 2035 if slowpath:
2036 2036 # See walkchangerevs() slow path.
2037 2037 #
2038 2038 # pats/include/exclude cannot be represented as separate
2039 2039 # revset expressions as their filtering logic applies at file
2040 2040 # level. For instance "-I a -X a" matches a revision touching
2041 2041 # "a" and "b" while "file(a) and not file(b)" does
2042 2042 # not. Besides, filesets are evaluated against the working
2043 2043 # directory.
2044 2044 matchargs = ['r:', 'd:relpath']
2045 2045 for p in pats:
2046 2046 matchargs.append('p:' + p)
2047 2047 for p in opts.get('include', []):
2048 2048 matchargs.append('i:' + p)
2049 2049 for p in opts.get('exclude', []):
2050 2050 matchargs.append('x:' + p)
2051 2051 matchargs = ','.join(('%r' % p) for p in matchargs)
2052 2052 opts['_matchfiles'] = matchargs
2053 2053 if follow:
2054 2054 opts[fnopats[0][followfirst]] = '.'
2055 2055 else:
2056 2056 if follow:
2057 2057 if pats:
2058 2058 # follow() revset interprets its file argument as a
2059 2059 # manifest entry, so use match.files(), not pats.
2060 2060 opts[fpats[followfirst]] = list(match.files())
2061 2061 else:
2062 2062 op = fnopats[followdescendants][followfirst]
2063 2063 opts[op] = 'rev(%d)' % startrev
2064 2064 else:
2065 2065 opts['_patslog'] = list(pats)
2066 2066
2067 2067 filematcher = None
2068 2068 if opts.get('patch') or opts.get('stat'):
2069 2069 # When following files, track renames via a special matcher.
2070 2070 # If we're forced to take the slowpath it means we're following
2071 2071 # at least one pattern/directory, so don't bother with rename tracking.
2072 2072 if follow and not match.always() and not slowpath:
2073 2073 # _makefollowlogfilematcher expects its files argument to be
2074 2074 # relative to the repo root, so use match.files(), not pats.
2075 2075 filematcher = _makefollowlogfilematcher(repo, match.files(),
2076 2076 followfirst)
2077 2077 else:
2078 2078 filematcher = _makenofollowlogfilematcher(repo, pats, opts)
2079 2079 if filematcher is None:
2080 2080 filematcher = lambda rev: match
2081 2081
2082 2082 expr = []
2083 2083 for op, val in sorted(opts.iteritems()):
2084 2084 if not val:
2085 2085 continue
2086 2086 if op not in opt2revset:
2087 2087 continue
2088 2088 revop, andor = opt2revset[op]
2089 2089 if '%(val)' not in revop:
2090 2090 expr.append(revop)
2091 2091 else:
2092 2092 if not isinstance(val, list):
2093 2093 e = revop % {'val': val}
2094 2094 else:
2095 2095 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
2096 2096 expr.append(e)
2097 2097
2098 2098 if expr:
2099 2099 expr = '(' + ' and '.join(expr) + ')'
2100 2100 else:
2101 2101 expr = None
2102 2102 return expr, filematcher
2103 2103
2104 2104 def _logrevs(repo, opts):
2105 2105 # Default --rev value depends on --follow but --follow behavior
2106 2106 # depends on revisions resolved from --rev...
2107 2107 follow = opts.get('follow') or opts.get('follow_first')
2108 2108 if opts.get('rev'):
2109 2109 revs = scmutil.revrange(repo, opts['rev'])
2110 2110 elif follow and repo.dirstate.p1() == nullid:
2111 2111 revs = revset.baseset()
2112 2112 elif follow:
2113 2113 revs = repo.revs('reverse(:.)')
2114 2114 else:
2115 2115 revs = revset.spanset(repo)
2116 2116 revs.reverse()
2117 2117 return revs
2118 2118
2119 2119 def getgraphlogrevs(repo, pats, opts):
2120 2120 """Return (revs, expr, filematcher) where revs is an iterable of
2121 2121 revision numbers, expr is a revset string built from log options
2122 2122 and file patterns or None, and used to filter 'revs'. If --stat or
2123 2123 --patch are not passed filematcher is None. Otherwise it is a
2124 2124 callable taking a revision number and returning a match objects
2125 2125 filtering the files to be detailed when displaying the revision.
2126 2126 """
2127 2127 limit = loglimit(opts)
2128 2128 revs = _logrevs(repo, opts)
2129 2129 if not revs:
2130 2130 return revset.baseset(), None, None
2131 2131 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2132 2132 if opts.get('rev'):
2133 2133 # User-specified revs might be unsorted, but don't sort before
2134 2134 # _makelogrevset because it might depend on the order of revs
2135 2135 revs.sort(reverse=True)
2136 2136 if expr:
2137 2137 # Revset matchers often operate faster on revisions in changelog
2138 2138 # order, because most filters deal with the changelog.
2139 2139 revs.reverse()
2140 2140 matcher = revset.match(repo.ui, expr)
2141 2141 # Revset matches can reorder revisions. "A or B" typically returns
2142 2142 # returns the revision matching A then the revision matching B. Sort
2143 2143 # again to fix that.
2144 2144 revs = matcher(repo, revs)
2145 2145 revs.sort(reverse=True)
2146 2146 if limit is not None:
2147 2147 limitedrevs = []
2148 2148 for idx, rev in enumerate(revs):
2149 2149 if idx >= limit:
2150 2150 break
2151 2151 limitedrevs.append(rev)
2152 2152 revs = revset.baseset(limitedrevs)
2153 2153
2154 2154 return revs, expr, filematcher
2155 2155
2156 2156 def getlogrevs(repo, pats, opts):
2157 2157 """Return (revs, expr, filematcher) where revs is an iterable of
2158 2158 revision numbers, expr is a revset string built from log options
2159 2159 and file patterns or None, and used to filter 'revs'. If --stat or
2160 2160 --patch are not passed filematcher is None. Otherwise it is a
2161 2161 callable taking a revision number and returning a match objects
2162 2162 filtering the files to be detailed when displaying the revision.
2163 2163 """
2164 2164 limit = loglimit(opts)
2165 2165 revs = _logrevs(repo, opts)
2166 2166 if not revs:
2167 2167 return revset.baseset([]), None, None
2168 2168 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2169 2169 if expr:
2170 2170 # Revset matchers often operate faster on revisions in changelog
2171 2171 # order, because most filters deal with the changelog.
2172 2172 if not opts.get('rev'):
2173 2173 revs.reverse()
2174 2174 matcher = revset.match(repo.ui, expr)
2175 2175 # Revset matches can reorder revisions. "A or B" typically returns
2176 2176 # returns the revision matching A then the revision matching B. Sort
2177 2177 # again to fix that.
2178 2178 fixopts = ['branch', 'only_branch', 'keyword', 'user']
2179 2179 oldrevs = revs
2180 2180 revs = matcher(repo, revs)
2181 2181 if not opts.get('rev'):
2182 2182 revs.sort(reverse=True)
2183 2183 elif len(pats) > 1 or any(len(opts.get(op, [])) > 1 for op in fixopts):
2184 2184 # XXX "A or B" is known to change the order; fix it by filtering
2185 2185 # matched set again (issue5100)
2186 2186 revs = oldrevs & revs
2187 2187 if limit is not None:
2188 2188 limitedrevs = []
2189 2189 for idx, r in enumerate(revs):
2190 2190 if limit <= idx:
2191 2191 break
2192 2192 limitedrevs.append(r)
2193 2193 revs = revset.baseset(limitedrevs)
2194 2194
2195 2195 return revs, expr, filematcher
2196 2196
2197 2197 def _graphnodeformatter(ui, displayer):
2198 2198 spec = ui.config('ui', 'graphnodetemplate')
2199 2199 if not spec:
2200 2200 return templatekw.showgraphnode # fast path for "{graphnode}"
2201 2201
2202 2202 templ = formatter.gettemplater(ui, 'graphnode', spec)
2203 2203 cache = {}
2204 2204 if isinstance(displayer, changeset_templater):
2205 2205 cache = displayer.cache # reuse cache of slow templates
2206 2206 props = templatekw.keywords.copy()
2207 2207 props['templ'] = templ
2208 2208 props['cache'] = cache
2209 2209 def formatnode(repo, ctx):
2210 2210 props['ctx'] = ctx
2211 2211 props['repo'] = repo
2212 2212 props['ui'] = repo.ui
2213 2213 props['revcache'] = {}
2214 2214 return templater.stringify(templ('graphnode', **props))
2215 2215 return formatnode
2216 2216
2217 2217 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
2218 2218 filematcher=None):
2219 2219 formatnode = _graphnodeformatter(ui, displayer)
2220 2220 state = graphmod.asciistate()
2221 2221 styles = state['styles']
2222 2222 edgetypes = {
2223 2223 'parent': graphmod.PARENT,
2224 2224 'grandparent': graphmod.GRANDPARENT,
2225 2225 'missing': graphmod.MISSINGPARENT
2226 2226 }
2227 2227 for name, key in edgetypes.items():
2228 2228 # experimental config: ui.graphstyle.*
2229 2229 styles[key] = ui.config('ui', 'graphstyle.%s' % name, styles[key])
2230 2230 if not styles[key]:
2231 2231 styles[key] = None
2232 2232 for rev, type, ctx, parents in dag:
2233 2233 char = formatnode(repo, ctx)
2234 2234 copies = None
2235 2235 if getrenamed and ctx.rev():
2236 2236 copies = []
2237 2237 for fn in ctx.files():
2238 2238 rename = getrenamed(fn, ctx.rev())
2239 2239 if rename:
2240 2240 copies.append((fn, rename[0]))
2241 2241 revmatchfn = None
2242 2242 if filematcher is not None:
2243 2243 revmatchfn = filematcher(ctx.rev())
2244 2244 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
2245 2245 lines = displayer.hunk.pop(rev).split('\n')
2246 2246 if not lines[-1]:
2247 2247 del lines[-1]
2248 2248 displayer.flush(ctx)
2249 2249 edges = edgefn(type, char, lines, state, rev, parents)
2250 2250 for type, char, lines, coldata in edges:
2251 2251 graphmod.ascii(ui, state, type, char, lines, coldata)
2252 2252 displayer.close()
2253 2253
2254 2254 def graphlog(ui, repo, *pats, **opts):
2255 2255 # Parameters are identical to log command ones
2256 2256 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
2257 2257 revdag = graphmod.dagwalker(repo, revs)
2258 2258
2259 2259 getrenamed = None
2260 2260 if opts.get('copies'):
2261 2261 endrev = None
2262 2262 if opts.get('rev'):
2263 2263 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
2264 2264 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
2265 2265 displayer = show_changeset(ui, repo, opts, buffered=True)
2266 2266 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
2267 2267 filematcher)
2268 2268
2269 2269 def checkunsupportedgraphflags(pats, opts):
2270 2270 for op in ["newest_first"]:
2271 2271 if op in opts and opts[op]:
2272 2272 raise error.Abort(_("-G/--graph option is incompatible with --%s")
2273 2273 % op.replace("_", "-"))
2274 2274
2275 2275 def graphrevs(repo, nodes, opts):
2276 2276 limit = loglimit(opts)
2277 2277 nodes.reverse()
2278 2278 if limit is not None:
2279 2279 nodes = nodes[:limit]
2280 2280 return graphmod.nodes(repo, nodes)
2281 2281
2282 2282 def add(ui, repo, match, prefix, explicitonly, **opts):
2283 2283 join = lambda f: os.path.join(prefix, f)
2284 2284 bad = []
2285 2285
2286 2286 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2287 2287 names = []
2288 2288 wctx = repo[None]
2289 2289 cca = None
2290 2290 abort, warn = scmutil.checkportabilityalert(ui)
2291 2291 if abort or warn:
2292 2292 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2293 2293
2294 2294 badmatch = matchmod.badmatch(match, badfn)
2295 2295 dirstate = repo.dirstate
2296 2296 # We don't want to just call wctx.walk here, since it would return a lot of
2297 2297 # clean files, which we aren't interested in and takes time.
2298 2298 for f in sorted(dirstate.walk(badmatch, sorted(wctx.substate),
2299 2299 True, False, full=False)):
2300 2300 exact = match.exact(f)
2301 2301 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2302 2302 if cca:
2303 2303 cca(f)
2304 2304 names.append(f)
2305 2305 if ui.verbose or not exact:
2306 2306 ui.status(_('adding %s\n') % match.rel(f))
2307 2307
2308 2308 for subpath in sorted(wctx.substate):
2309 2309 sub = wctx.sub(subpath)
2310 2310 try:
2311 2311 submatch = matchmod.subdirmatcher(subpath, match)
2312 2312 if opts.get('subrepos'):
2313 2313 bad.extend(sub.add(ui, submatch, prefix, False, **opts))
2314 2314 else:
2315 2315 bad.extend(sub.add(ui, submatch, prefix, True, **opts))
2316 2316 except error.LookupError:
2317 2317 ui.status(_("skipping missing subrepository: %s\n")
2318 2318 % join(subpath))
2319 2319
2320 2320 if not opts.get('dry_run'):
2321 2321 rejected = wctx.add(names, prefix)
2322 2322 bad.extend(f for f in rejected if f in match.files())
2323 2323 return bad
2324 2324
2325 2325 def forget(ui, repo, match, prefix, explicitonly):
2326 2326 join = lambda f: os.path.join(prefix, f)
2327 2327 bad = []
2328 2328 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2329 2329 wctx = repo[None]
2330 2330 forgot = []
2331 2331
2332 2332 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2333 2333 forget = sorted(s[0] + s[1] + s[3] + s[6])
2334 2334 if explicitonly:
2335 2335 forget = [f for f in forget if match.exact(f)]
2336 2336
2337 2337 for subpath in sorted(wctx.substate):
2338 2338 sub = wctx.sub(subpath)
2339 2339 try:
2340 2340 submatch = matchmod.subdirmatcher(subpath, match)
2341 2341 subbad, subforgot = sub.forget(submatch, prefix)
2342 2342 bad.extend([subpath + '/' + f for f in subbad])
2343 2343 forgot.extend([subpath + '/' + f for f in subforgot])
2344 2344 except error.LookupError:
2345 2345 ui.status(_("skipping missing subrepository: %s\n")
2346 2346 % join(subpath))
2347 2347
2348 2348 if not explicitonly:
2349 2349 for f in match.files():
2350 2350 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2351 2351 if f not in forgot:
2352 2352 if repo.wvfs.exists(f):
2353 2353 # Don't complain if the exact case match wasn't given.
2354 2354 # But don't do this until after checking 'forgot', so
2355 2355 # that subrepo files aren't normalized, and this op is
2356 2356 # purely from data cached by the status walk above.
2357 2357 if repo.dirstate.normalize(f) in repo.dirstate:
2358 2358 continue
2359 2359 ui.warn(_('not removing %s: '
2360 2360 'file is already untracked\n')
2361 2361 % match.rel(f))
2362 2362 bad.append(f)
2363 2363
2364 2364 for f in forget:
2365 2365 if ui.verbose or not match.exact(f):
2366 2366 ui.status(_('removing %s\n') % match.rel(f))
2367 2367
2368 2368 rejected = wctx.forget(forget, prefix)
2369 2369 bad.extend(f for f in rejected if f in match.files())
2370 2370 forgot.extend(f for f in forget if f not in rejected)
2371 2371 return bad, forgot
2372 2372
2373 2373 def files(ui, ctx, m, fm, fmt, subrepos):
2374 2374 rev = ctx.rev()
2375 2375 ret = 1
2376 2376 ds = ctx.repo().dirstate
2377 2377
2378 2378 for f in ctx.matches(m):
2379 2379 if rev is None and ds[f] == 'r':
2380 2380 continue
2381 2381 fm.startitem()
2382 2382 if ui.verbose:
2383 2383 fc = ctx[f]
2384 2384 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2385 2385 fm.data(abspath=f)
2386 2386 fm.write('path', fmt, m.rel(f))
2387 2387 ret = 0
2388 2388
2389 2389 for subpath in sorted(ctx.substate):
2390 2390 def matchessubrepo(subpath):
2391 2391 return (m.exact(subpath)
2392 2392 or any(f.startswith(subpath + '/') for f in m.files()))
2393 2393
2394 2394 if subrepos or matchessubrepo(subpath):
2395 2395 sub = ctx.sub(subpath)
2396 2396 try:
2397 2397 submatch = matchmod.subdirmatcher(subpath, m)
2398 2398 recurse = m.exact(subpath) or subrepos
2399 2399 if sub.printfiles(ui, submatch, fm, fmt, recurse) == 0:
2400 2400 ret = 0
2401 2401 except error.LookupError:
2402 2402 ui.status(_("skipping missing subrepository: %s\n")
2403 2403 % m.abs(subpath))
2404 2404
2405 2405 return ret
2406 2406
2407 def remove(ui, repo, m, prefix, after, force, subrepos):
2407 def remove(ui, repo, m, prefix, after, force, subrepos, warnings=None):
2408 2408 join = lambda f: os.path.join(prefix, f)
2409 2409 ret = 0
2410 2410 s = repo.status(match=m, clean=True)
2411 2411 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2412 2412
2413 2413 wctx = repo[None]
2414 2414
2415 if warnings is None:
2416 warnings = []
2417 warn = True
2418 else:
2419 warn = False
2420
2415 2421 for subpath in sorted(wctx.substate):
2416 2422 def matchessubrepo(matcher, subpath):
2417 2423 if matcher.exact(subpath):
2418 2424 return True
2419 2425 for f in matcher.files():
2420 2426 if f.startswith(subpath):
2421 2427 return True
2422 2428 return False
2423 2429
2424 2430 if subrepos or matchessubrepo(m, subpath):
2425 2431 sub = wctx.sub(subpath)
2426 2432 try:
2427 2433 submatch = matchmod.subdirmatcher(subpath, m)
2428 if sub.removefiles(submatch, prefix, after, force, subrepos):
2434 if sub.removefiles(submatch, prefix, after, force, subrepos,
2435 warnings):
2429 2436 ret = 1
2430 2437 except error.LookupError:
2431 ui.status(_("skipping missing subrepository: %s\n")
2438 warnings.append(_("skipping missing subrepository: %s\n")
2432 2439 % join(subpath))
2433 2440
2434 2441 # warn about failure to delete explicit files/dirs
2435 2442 deleteddirs = util.dirs(deleted)
2436 2443 for f in m.files():
2437 2444 def insubrepo():
2438 2445 for subpath in wctx.substate:
2439 2446 if f.startswith(subpath):
2440 2447 return True
2441 2448 return False
2442 2449
2443 2450 isdir = f in deleteddirs or wctx.hasdir(f)
2444 2451 if f in repo.dirstate or isdir or f == '.' or insubrepo():
2445 2452 continue
2446 2453
2447 2454 if repo.wvfs.exists(f):
2448 2455 if repo.wvfs.isdir(f):
2449 ui.warn(_('not removing %s: no tracked files\n')
2456 warnings.append(_('not removing %s: no tracked files\n')
2450 2457 % m.rel(f))
2451 2458 else:
2452 ui.warn(_('not removing %s: file is untracked\n')
2459 warnings.append(_('not removing %s: file is untracked\n')
2453 2460 % m.rel(f))
2454 2461 # missing files will generate a warning elsewhere
2455 2462 ret = 1
2456 2463
2457 2464 if force:
2458 2465 list = modified + deleted + clean + added
2459 2466 elif after:
2460 2467 list = deleted
2461 2468 for f in modified + added + clean:
2462 ui.warn(_('not removing %s: file still exists\n') % m.rel(f))
2469 warnings.append(_('not removing %s: file still exists\n') % m.rel(f))
2463 2470 ret = 1
2464 2471 else:
2465 2472 list = deleted + clean
2466 2473 for f in modified:
2467 ui.warn(_('not removing %s: file is modified (use -f'
2474 warnings.append(_('not removing %s: file is modified (use -f'
2468 2475 ' to force removal)\n') % m.rel(f))
2469 2476 ret = 1
2470 2477 for f in added:
2471 ui.warn(_('not removing %s: file has been marked for add'
2478 warnings.append(_('not removing %s: file has been marked for add'
2472 2479 ' (use forget to undo)\n') % m.rel(f))
2473 2480 ret = 1
2474 2481
2475 2482 for f in sorted(list):
2476 2483 if ui.verbose or not m.exact(f):
2477 2484 ui.status(_('removing %s\n') % m.rel(f))
2478 2485
2479 2486 with repo.wlock():
2480 2487 if not after:
2481 2488 for f in list:
2482 2489 if f in added:
2483 2490 continue # we never unlink added files on remove
2484 2491 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
2485 2492 repo[None].forget(list)
2486 2493
2494 if warn:
2495 for warning in warnings:
2496 ui.warn(warning)
2497
2487 2498 return ret
2488 2499
2489 2500 def cat(ui, repo, ctx, matcher, prefix, **opts):
2490 2501 err = 1
2491 2502
2492 2503 def write(path):
2493 2504 fp = makefileobj(repo, opts.get('output'), ctx.node(),
2494 2505 pathname=os.path.join(prefix, path))
2495 2506 data = ctx[path].data()
2496 2507 if opts.get('decode'):
2497 2508 data = repo.wwritedata(path, data)
2498 2509 fp.write(data)
2499 2510 fp.close()
2500 2511
2501 2512 # Automation often uses hg cat on single files, so special case it
2502 2513 # for performance to avoid the cost of parsing the manifest.
2503 2514 if len(matcher.files()) == 1 and not matcher.anypats():
2504 2515 file = matcher.files()[0]
2505 2516 mf = repo.manifest
2506 2517 mfnode = ctx.manifestnode()
2507 2518 if mfnode and mf.find(mfnode, file)[0]:
2508 2519 write(file)
2509 2520 return 0
2510 2521
2511 2522 # Don't warn about "missing" files that are really in subrepos
2512 2523 def badfn(path, msg):
2513 2524 for subpath in ctx.substate:
2514 2525 if path.startswith(subpath):
2515 2526 return
2516 2527 matcher.bad(path, msg)
2517 2528
2518 2529 for abs in ctx.walk(matchmod.badmatch(matcher, badfn)):
2519 2530 write(abs)
2520 2531 err = 0
2521 2532
2522 2533 for subpath in sorted(ctx.substate):
2523 2534 sub = ctx.sub(subpath)
2524 2535 try:
2525 2536 submatch = matchmod.subdirmatcher(subpath, matcher)
2526 2537
2527 2538 if not sub.cat(submatch, os.path.join(prefix, sub._path),
2528 2539 **opts):
2529 2540 err = 0
2530 2541 except error.RepoLookupError:
2531 2542 ui.status(_("skipping missing subrepository: %s\n")
2532 2543 % os.path.join(prefix, subpath))
2533 2544
2534 2545 return err
2535 2546
2536 2547 def commit(ui, repo, commitfunc, pats, opts):
2537 2548 '''commit the specified files or all outstanding changes'''
2538 2549 date = opts.get('date')
2539 2550 if date:
2540 2551 opts['date'] = util.parsedate(date)
2541 2552 message = logmessage(ui, opts)
2542 2553 matcher = scmutil.match(repo[None], pats, opts)
2543 2554
2544 2555 # extract addremove carefully -- this function can be called from a command
2545 2556 # that doesn't support addremove
2546 2557 if opts.get('addremove'):
2547 2558 if scmutil.addremove(repo, matcher, "", opts) != 0:
2548 2559 raise error.Abort(
2549 2560 _("failed to mark all new/missing files as added/removed"))
2550 2561
2551 2562 return commitfunc(ui, repo, message, matcher, opts)
2552 2563
2553 2564 def amend(ui, repo, commitfunc, old, extra, pats, opts):
2554 2565 # avoid cycle context -> subrepo -> cmdutil
2555 2566 from . import context
2556 2567
2557 2568 # amend will reuse the existing user if not specified, but the obsolete
2558 2569 # marker creation requires that the current user's name is specified.
2559 2570 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2560 2571 ui.username() # raise exception if username not set
2561 2572
2562 2573 ui.note(_('amending changeset %s\n') % old)
2563 2574 base = old.p1()
2564 2575 createmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
2565 2576
2566 2577 wlock = lock = newid = None
2567 2578 try:
2568 2579 wlock = repo.wlock()
2569 2580 lock = repo.lock()
2570 2581 with repo.transaction('amend') as tr:
2571 2582 # See if we got a message from -m or -l, if not, open the editor
2572 2583 # with the message of the changeset to amend
2573 2584 message = logmessage(ui, opts)
2574 2585 # ensure logfile does not conflict with later enforcement of the
2575 2586 # message. potential logfile content has been processed by
2576 2587 # `logmessage` anyway.
2577 2588 opts.pop('logfile')
2578 2589 # First, do a regular commit to record all changes in the working
2579 2590 # directory (if there are any)
2580 2591 ui.callhooks = False
2581 2592 activebookmark = repo._bookmarks.active
2582 2593 try:
2583 2594 repo._bookmarks.active = None
2584 2595 opts['message'] = 'temporary amend commit for %s' % old
2585 2596 node = commit(ui, repo, commitfunc, pats, opts)
2586 2597 finally:
2587 2598 repo._bookmarks.active = activebookmark
2588 2599 repo._bookmarks.recordchange(tr)
2589 2600 ui.callhooks = True
2590 2601 ctx = repo[node]
2591 2602
2592 2603 # Participating changesets:
2593 2604 #
2594 2605 # node/ctx o - new (intermediate) commit that contains changes
2595 2606 # | from working dir to go into amending commit
2596 2607 # | (or a workingctx if there were no changes)
2597 2608 # |
2598 2609 # old o - changeset to amend
2599 2610 # |
2600 2611 # base o - parent of amending changeset
2601 2612
2602 2613 # Update extra dict from amended commit (e.g. to preserve graft
2603 2614 # source)
2604 2615 extra.update(old.extra())
2605 2616
2606 2617 # Also update it from the intermediate commit or from the wctx
2607 2618 extra.update(ctx.extra())
2608 2619
2609 2620 if len(old.parents()) > 1:
2610 2621 # ctx.files() isn't reliable for merges, so fall back to the
2611 2622 # slower repo.status() method
2612 2623 files = set([fn for st in repo.status(base, old)[:3]
2613 2624 for fn in st])
2614 2625 else:
2615 2626 files = set(old.files())
2616 2627
2617 2628 # Second, we use either the commit we just did, or if there were no
2618 2629 # changes the parent of the working directory as the version of the
2619 2630 # files in the final amend commit
2620 2631 if node:
2621 2632 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
2622 2633
2623 2634 user = ctx.user()
2624 2635 date = ctx.date()
2625 2636 # Recompute copies (avoid recording a -> b -> a)
2626 2637 copied = copies.pathcopies(base, ctx)
2627 2638 if old.p2:
2628 2639 copied.update(copies.pathcopies(old.p2(), ctx))
2629 2640
2630 2641 # Prune files which were reverted by the updates: if old
2631 2642 # introduced file X and our intermediate commit, node,
2632 2643 # renamed that file, then those two files are the same and
2633 2644 # we can discard X from our list of files. Likewise if X
2634 2645 # was deleted, it's no longer relevant
2635 2646 files.update(ctx.files())
2636 2647
2637 2648 def samefile(f):
2638 2649 if f in ctx.manifest():
2639 2650 a = ctx.filectx(f)
2640 2651 if f in base.manifest():
2641 2652 b = base.filectx(f)
2642 2653 return (not a.cmp(b)
2643 2654 and a.flags() == b.flags())
2644 2655 else:
2645 2656 return False
2646 2657 else:
2647 2658 return f not in base.manifest()
2648 2659 files = [f for f in files if not samefile(f)]
2649 2660
2650 2661 def filectxfn(repo, ctx_, path):
2651 2662 try:
2652 2663 fctx = ctx[path]
2653 2664 flags = fctx.flags()
2654 2665 mctx = context.memfilectx(repo,
2655 2666 fctx.path(), fctx.data(),
2656 2667 islink='l' in flags,
2657 2668 isexec='x' in flags,
2658 2669 copied=copied.get(path))
2659 2670 return mctx
2660 2671 except KeyError:
2661 2672 return None
2662 2673 else:
2663 2674 ui.note(_('copying changeset %s to %s\n') % (old, base))
2664 2675
2665 2676 # Use version of files as in the old cset
2666 2677 def filectxfn(repo, ctx_, path):
2667 2678 try:
2668 2679 return old.filectx(path)
2669 2680 except KeyError:
2670 2681 return None
2671 2682
2672 2683 user = opts.get('user') or old.user()
2673 2684 date = opts.get('date') or old.date()
2674 2685 editform = mergeeditform(old, 'commit.amend')
2675 2686 editor = getcommiteditor(editform=editform, **opts)
2676 2687 if not message:
2677 2688 editor = getcommiteditor(edit=True, editform=editform)
2678 2689 message = old.description()
2679 2690
2680 2691 pureextra = extra.copy()
2681 2692 extra['amend_source'] = old.hex()
2682 2693
2683 2694 new = context.memctx(repo,
2684 2695 parents=[base.node(), old.p2().node()],
2685 2696 text=message,
2686 2697 files=files,
2687 2698 filectxfn=filectxfn,
2688 2699 user=user,
2689 2700 date=date,
2690 2701 extra=extra,
2691 2702 editor=editor)
2692 2703
2693 2704 newdesc = changelog.stripdesc(new.description())
2694 2705 if ((not node)
2695 2706 and newdesc == old.description()
2696 2707 and user == old.user()
2697 2708 and date == old.date()
2698 2709 and pureextra == old.extra()):
2699 2710 # nothing changed. continuing here would create a new node
2700 2711 # anyway because of the amend_source noise.
2701 2712 #
2702 2713 # This not what we expect from amend.
2703 2714 return old.node()
2704 2715
2705 2716 ph = repo.ui.config('phases', 'new-commit', phases.draft)
2706 2717 try:
2707 2718 if opts.get('secret'):
2708 2719 commitphase = 'secret'
2709 2720 else:
2710 2721 commitphase = old.phase()
2711 2722 repo.ui.setconfig('phases', 'new-commit', commitphase, 'amend')
2712 2723 newid = repo.commitctx(new)
2713 2724 finally:
2714 2725 repo.ui.setconfig('phases', 'new-commit', ph, 'amend')
2715 2726 if newid != old.node():
2716 2727 # Reroute the working copy parent to the new changeset
2717 2728 repo.setparents(newid, nullid)
2718 2729
2719 2730 # Move bookmarks from old parent to amend commit
2720 2731 bms = repo.nodebookmarks(old.node())
2721 2732 if bms:
2722 2733 marks = repo._bookmarks
2723 2734 for bm in bms:
2724 2735 ui.debug('moving bookmarks %r from %s to %s\n' %
2725 2736 (marks, old.hex(), hex(newid)))
2726 2737 marks[bm] = newid
2727 2738 marks.recordchange(tr)
2728 2739 #commit the whole amend process
2729 2740 if createmarkers:
2730 2741 # mark the new changeset as successor of the rewritten one
2731 2742 new = repo[newid]
2732 2743 obs = [(old, (new,))]
2733 2744 if node:
2734 2745 obs.append((ctx, ()))
2735 2746
2736 2747 obsolete.createmarkers(repo, obs)
2737 2748 if not createmarkers and newid != old.node():
2738 2749 # Strip the intermediate commit (if there was one) and the amended
2739 2750 # commit
2740 2751 if node:
2741 2752 ui.note(_('stripping intermediate changeset %s\n') % ctx)
2742 2753 ui.note(_('stripping amended changeset %s\n') % old)
2743 2754 repair.strip(ui, repo, old.node(), topic='amend-backup')
2744 2755 finally:
2745 2756 lockmod.release(lock, wlock)
2746 2757 return newid
2747 2758
2748 2759 def commiteditor(repo, ctx, subs, editform=''):
2749 2760 if ctx.description():
2750 2761 return ctx.description()
2751 2762 return commitforceeditor(repo, ctx, subs, editform=editform,
2752 2763 unchangedmessagedetection=True)
2753 2764
2754 2765 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2755 2766 editform='', unchangedmessagedetection=False):
2756 2767 if not extramsg:
2757 2768 extramsg = _("Leave message empty to abort commit.")
2758 2769
2759 2770 forms = [e for e in editform.split('.') if e]
2760 2771 forms.insert(0, 'changeset')
2761 2772 templatetext = None
2762 2773 while forms:
2763 2774 tmpl = repo.ui.config('committemplate', '.'.join(forms))
2764 2775 if tmpl:
2765 2776 templatetext = committext = buildcommittemplate(
2766 2777 repo, ctx, subs, extramsg, tmpl)
2767 2778 break
2768 2779 forms.pop()
2769 2780 else:
2770 2781 committext = buildcommittext(repo, ctx, subs, extramsg)
2771 2782
2772 2783 # run editor in the repository root
2773 2784 olddir = os.getcwd()
2774 2785 os.chdir(repo.root)
2775 2786
2776 2787 # make in-memory changes visible to external process
2777 2788 tr = repo.currenttransaction()
2778 2789 repo.dirstate.write(tr)
2779 2790 pending = tr and tr.writepending() and repo.root
2780 2791
2781 2792 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
2782 2793 editform=editform, pending=pending)
2783 2794 text = re.sub("(?m)^HG:.*(\n|$)", "", editortext)
2784 2795 os.chdir(olddir)
2785 2796
2786 2797 if finishdesc:
2787 2798 text = finishdesc(text)
2788 2799 if not text.strip():
2789 2800 raise error.Abort(_("empty commit message"))
2790 2801 if unchangedmessagedetection and editortext == templatetext:
2791 2802 raise error.Abort(_("commit message unchanged"))
2792 2803
2793 2804 return text
2794 2805
2795 2806 def buildcommittemplate(repo, ctx, subs, extramsg, tmpl):
2796 2807 ui = repo.ui
2797 2808 tmpl, mapfile = gettemplate(ui, tmpl, None)
2798 2809
2799 2810 try:
2800 2811 t = changeset_templater(ui, repo, None, {}, tmpl, mapfile, False)
2801 2812 except SyntaxError as inst:
2802 2813 raise error.Abort(inst.args[0])
2803 2814
2804 2815 for k, v in repo.ui.configitems('committemplate'):
2805 2816 if k != 'changeset':
2806 2817 t.t.cache[k] = v
2807 2818
2808 2819 if not extramsg:
2809 2820 extramsg = '' # ensure that extramsg is string
2810 2821
2811 2822 ui.pushbuffer()
2812 2823 t.show(ctx, extramsg=extramsg)
2813 2824 return ui.popbuffer()
2814 2825
2815 2826 def hgprefix(msg):
2816 2827 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
2817 2828
2818 2829 def buildcommittext(repo, ctx, subs, extramsg):
2819 2830 edittext = []
2820 2831 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2821 2832 if ctx.description():
2822 2833 edittext.append(ctx.description())
2823 2834 edittext.append("")
2824 2835 edittext.append("") # Empty line between message and comments.
2825 2836 edittext.append(hgprefix(_("Enter commit message."
2826 2837 " Lines beginning with 'HG:' are removed.")))
2827 2838 edittext.append(hgprefix(extramsg))
2828 2839 edittext.append("HG: --")
2829 2840 edittext.append(hgprefix(_("user: %s") % ctx.user()))
2830 2841 if ctx.p2():
2831 2842 edittext.append(hgprefix(_("branch merge")))
2832 2843 if ctx.branch():
2833 2844 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
2834 2845 if bookmarks.isactivewdirparent(repo):
2835 2846 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
2836 2847 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
2837 2848 edittext.extend([hgprefix(_("added %s") % f) for f in added])
2838 2849 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
2839 2850 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
2840 2851 if not added and not modified and not removed:
2841 2852 edittext.append(hgprefix(_("no files changed")))
2842 2853 edittext.append("")
2843 2854
2844 2855 return "\n".join(edittext)
2845 2856
2846 2857 def commitstatus(repo, node, branch, bheads=None, opts=None):
2847 2858 if opts is None:
2848 2859 opts = {}
2849 2860 ctx = repo[node]
2850 2861 parents = ctx.parents()
2851 2862
2852 2863 if (not opts.get('amend') and bheads and node not in bheads and not
2853 2864 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2854 2865 repo.ui.status(_('created new head\n'))
2855 2866 # The message is not printed for initial roots. For the other
2856 2867 # changesets, it is printed in the following situations:
2857 2868 #
2858 2869 # Par column: for the 2 parents with ...
2859 2870 # N: null or no parent
2860 2871 # B: parent is on another named branch
2861 2872 # C: parent is a regular non head changeset
2862 2873 # H: parent was a branch head of the current branch
2863 2874 # Msg column: whether we print "created new head" message
2864 2875 # In the following, it is assumed that there already exists some
2865 2876 # initial branch heads of the current branch, otherwise nothing is
2866 2877 # printed anyway.
2867 2878 #
2868 2879 # Par Msg Comment
2869 2880 # N N y additional topo root
2870 2881 #
2871 2882 # B N y additional branch root
2872 2883 # C N y additional topo head
2873 2884 # H N n usual case
2874 2885 #
2875 2886 # B B y weird additional branch root
2876 2887 # C B y branch merge
2877 2888 # H B n merge with named branch
2878 2889 #
2879 2890 # C C y additional head from merge
2880 2891 # C H n merge with a head
2881 2892 #
2882 2893 # H H n head merge: head count decreases
2883 2894
2884 2895 if not opts.get('close_branch'):
2885 2896 for r in parents:
2886 2897 if r.closesbranch() and r.branch() == branch:
2887 2898 repo.ui.status(_('reopening closed branch head %d\n') % r)
2888 2899
2889 2900 if repo.ui.debugflag:
2890 2901 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
2891 2902 elif repo.ui.verbose:
2892 2903 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
2893 2904
2894 2905 def postcommitstatus(repo, pats, opts):
2895 2906 return repo.status(match=scmutil.match(repo[None], pats, opts))
2896 2907
2897 2908 def revert(ui, repo, ctx, parents, *pats, **opts):
2898 2909 parent, p2 = parents
2899 2910 node = ctx.node()
2900 2911
2901 2912 mf = ctx.manifest()
2902 2913 if node == p2:
2903 2914 parent = p2
2904 2915
2905 2916 # need all matching names in dirstate and manifest of target rev,
2906 2917 # so have to walk both. do not print errors if files exist in one
2907 2918 # but not other. in both cases, filesets should be evaluated against
2908 2919 # workingctx to get consistent result (issue4497). this means 'set:**'
2909 2920 # cannot be used to select missing files from target rev.
2910 2921
2911 2922 # `names` is a mapping for all elements in working copy and target revision
2912 2923 # The mapping is in the form:
2913 2924 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2914 2925 names = {}
2915 2926
2916 2927 with repo.wlock():
2917 2928 ## filling of the `names` mapping
2918 2929 # walk dirstate to fill `names`
2919 2930
2920 2931 interactive = opts.get('interactive', False)
2921 2932 wctx = repo[None]
2922 2933 m = scmutil.match(wctx, pats, opts)
2923 2934
2924 2935 # we'll need this later
2925 2936 targetsubs = sorted(s for s in wctx.substate if m(s))
2926 2937
2927 2938 if not m.always():
2928 2939 for abs in repo.walk(matchmod.badmatch(m, lambda x, y: False)):
2929 2940 names[abs] = m.rel(abs), m.exact(abs)
2930 2941
2931 2942 # walk target manifest to fill `names`
2932 2943
2933 2944 def badfn(path, msg):
2934 2945 if path in names:
2935 2946 return
2936 2947 if path in ctx.substate:
2937 2948 return
2938 2949 path_ = path + '/'
2939 2950 for f in names:
2940 2951 if f.startswith(path_):
2941 2952 return
2942 2953 ui.warn("%s: %s\n" % (m.rel(path), msg))
2943 2954
2944 2955 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
2945 2956 if abs not in names:
2946 2957 names[abs] = m.rel(abs), m.exact(abs)
2947 2958
2948 2959 # Find status of all file in `names`.
2949 2960 m = scmutil.matchfiles(repo, names)
2950 2961
2951 2962 changes = repo.status(node1=node, match=m,
2952 2963 unknown=True, ignored=True, clean=True)
2953 2964 else:
2954 2965 changes = repo.status(node1=node, match=m)
2955 2966 for kind in changes:
2956 2967 for abs in kind:
2957 2968 names[abs] = m.rel(abs), m.exact(abs)
2958 2969
2959 2970 m = scmutil.matchfiles(repo, names)
2960 2971
2961 2972 modified = set(changes.modified)
2962 2973 added = set(changes.added)
2963 2974 removed = set(changes.removed)
2964 2975 _deleted = set(changes.deleted)
2965 2976 unknown = set(changes.unknown)
2966 2977 unknown.update(changes.ignored)
2967 2978 clean = set(changes.clean)
2968 2979 modadded = set()
2969 2980
2970 2981 # split between files known in target manifest and the others
2971 2982 smf = set(mf)
2972 2983
2973 2984 # determine the exact nature of the deleted changesets
2974 2985 deladded = _deleted - smf
2975 2986 deleted = _deleted - deladded
2976 2987
2977 2988 # We need to account for the state of the file in the dirstate,
2978 2989 # even when we revert against something else than parent. This will
2979 2990 # slightly alter the behavior of revert (doing back up or not, delete
2980 2991 # or just forget etc).
2981 2992 if parent == node:
2982 2993 dsmodified = modified
2983 2994 dsadded = added
2984 2995 dsremoved = removed
2985 2996 # store all local modifications, useful later for rename detection
2986 2997 localchanges = dsmodified | dsadded
2987 2998 modified, added, removed = set(), set(), set()
2988 2999 else:
2989 3000 changes = repo.status(node1=parent, match=m)
2990 3001 dsmodified = set(changes.modified)
2991 3002 dsadded = set(changes.added)
2992 3003 dsremoved = set(changes.removed)
2993 3004 # store all local modifications, useful later for rename detection
2994 3005 localchanges = dsmodified | dsadded
2995 3006
2996 3007 # only take into account for removes between wc and target
2997 3008 clean |= dsremoved - removed
2998 3009 dsremoved &= removed
2999 3010 # distinct between dirstate remove and other
3000 3011 removed -= dsremoved
3001 3012
3002 3013 modadded = added & dsmodified
3003 3014 added -= modadded
3004 3015
3005 3016 # tell newly modified apart.
3006 3017 dsmodified &= modified
3007 3018 dsmodified |= modified & dsadded # dirstate added may needs backup
3008 3019 modified -= dsmodified
3009 3020
3010 3021 # We need to wait for some post-processing to update this set
3011 3022 # before making the distinction. The dirstate will be used for
3012 3023 # that purpose.
3013 3024 dsadded = added
3014 3025
3015 3026 # in case of merge, files that are actually added can be reported as
3016 3027 # modified, we need to post process the result
3017 3028 if p2 != nullid:
3018 3029 mergeadd = dsmodified - smf
3019 3030 dsadded |= mergeadd
3020 3031 dsmodified -= mergeadd
3021 3032
3022 3033 # if f is a rename, update `names` to also revert the source
3023 3034 cwd = repo.getcwd()
3024 3035 for f in localchanges:
3025 3036 src = repo.dirstate.copied(f)
3026 3037 # XXX should we check for rename down to target node?
3027 3038 if src and src not in names and repo.dirstate[src] == 'r':
3028 3039 dsremoved.add(src)
3029 3040 names[src] = (repo.pathto(src, cwd), True)
3030 3041
3031 3042 # distinguish between file to forget and the other
3032 3043 added = set()
3033 3044 for abs in dsadded:
3034 3045 if repo.dirstate[abs] != 'a':
3035 3046 added.add(abs)
3036 3047 dsadded -= added
3037 3048
3038 3049 for abs in deladded:
3039 3050 if repo.dirstate[abs] == 'a':
3040 3051 dsadded.add(abs)
3041 3052 deladded -= dsadded
3042 3053
3043 3054 # For files marked as removed, we check if an unknown file is present at
3044 3055 # the same path. If a such file exists it may need to be backed up.
3045 3056 # Making the distinction at this stage helps have simpler backup
3046 3057 # logic.
3047 3058 removunk = set()
3048 3059 for abs in removed:
3049 3060 target = repo.wjoin(abs)
3050 3061 if os.path.lexists(target):
3051 3062 removunk.add(abs)
3052 3063 removed -= removunk
3053 3064
3054 3065 dsremovunk = set()
3055 3066 for abs in dsremoved:
3056 3067 target = repo.wjoin(abs)
3057 3068 if os.path.lexists(target):
3058 3069 dsremovunk.add(abs)
3059 3070 dsremoved -= dsremovunk
3060 3071
3061 3072 # action to be actually performed by revert
3062 3073 # (<list of file>, message>) tuple
3063 3074 actions = {'revert': ([], _('reverting %s\n')),
3064 3075 'add': ([], _('adding %s\n')),
3065 3076 'remove': ([], _('removing %s\n')),
3066 3077 'drop': ([], _('removing %s\n')),
3067 3078 'forget': ([], _('forgetting %s\n')),
3068 3079 'undelete': ([], _('undeleting %s\n')),
3069 3080 'noop': (None, _('no changes needed to %s\n')),
3070 3081 'unknown': (None, _('file not managed: %s\n')),
3071 3082 }
3072 3083
3073 3084 # "constant" that convey the backup strategy.
3074 3085 # All set to `discard` if `no-backup` is set do avoid checking
3075 3086 # no_backup lower in the code.
3076 3087 # These values are ordered for comparison purposes
3077 3088 backup = 2 # unconditionally do backup
3078 3089 check = 1 # check if the existing file differs from target
3079 3090 discard = 0 # never do backup
3080 3091 if opts.get('no_backup'):
3081 3092 backup = check = discard
3082 3093
3083 3094 backupanddel = actions['remove']
3084 3095 if not opts.get('no_backup'):
3085 3096 backupanddel = actions['drop']
3086 3097
3087 3098 disptable = (
3088 3099 # dispatch table:
3089 3100 # file state
3090 3101 # action
3091 3102 # make backup
3092 3103
3093 3104 ## Sets that results that will change file on disk
3094 3105 # Modified compared to target, no local change
3095 3106 (modified, actions['revert'], discard),
3096 3107 # Modified compared to target, but local file is deleted
3097 3108 (deleted, actions['revert'], discard),
3098 3109 # Modified compared to target, local change
3099 3110 (dsmodified, actions['revert'], backup),
3100 3111 # Added since target
3101 3112 (added, actions['remove'], discard),
3102 3113 # Added in working directory
3103 3114 (dsadded, actions['forget'], discard),
3104 3115 # Added since target, have local modification
3105 3116 (modadded, backupanddel, backup),
3106 3117 # Added since target but file is missing in working directory
3107 3118 (deladded, actions['drop'], discard),
3108 3119 # Removed since target, before working copy parent
3109 3120 (removed, actions['add'], discard),
3110 3121 # Same as `removed` but an unknown file exists at the same path
3111 3122 (removunk, actions['add'], check),
3112 3123 # Removed since targe, marked as such in working copy parent
3113 3124 (dsremoved, actions['undelete'], discard),
3114 3125 # Same as `dsremoved` but an unknown file exists at the same path
3115 3126 (dsremovunk, actions['undelete'], check),
3116 3127 ## the following sets does not result in any file changes
3117 3128 # File with no modification
3118 3129 (clean, actions['noop'], discard),
3119 3130 # Existing file, not tracked anywhere
3120 3131 (unknown, actions['unknown'], discard),
3121 3132 )
3122 3133
3123 3134 for abs, (rel, exact) in sorted(names.items()):
3124 3135 # target file to be touch on disk (relative to cwd)
3125 3136 target = repo.wjoin(abs)
3126 3137 # search the entry in the dispatch table.
3127 3138 # if the file is in any of these sets, it was touched in the working
3128 3139 # directory parent and we are sure it needs to be reverted.
3129 3140 for table, (xlist, msg), dobackup in disptable:
3130 3141 if abs not in table:
3131 3142 continue
3132 3143 if xlist is not None:
3133 3144 xlist.append(abs)
3134 3145 if dobackup and (backup <= dobackup
3135 3146 or wctx[abs].cmp(ctx[abs])):
3136 3147 bakname = scmutil.origpath(ui, repo, rel)
3137 3148 ui.note(_('saving current version of %s as %s\n') %
3138 3149 (rel, bakname))
3139 3150 if not opts.get('dry_run'):
3140 3151 if interactive:
3141 3152 util.copyfile(target, bakname)
3142 3153 else:
3143 3154 util.rename(target, bakname)
3144 3155 if ui.verbose or not exact:
3145 3156 if not isinstance(msg, basestring):
3146 3157 msg = msg(abs)
3147 3158 ui.status(msg % rel)
3148 3159 elif exact:
3149 3160 ui.warn(msg % rel)
3150 3161 break
3151 3162
3152 3163 if not opts.get('dry_run'):
3153 3164 needdata = ('revert', 'add', 'undelete')
3154 3165 _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
3155 3166 _performrevert(repo, parents, ctx, actions, interactive)
3156 3167
3157 3168 if targetsubs:
3158 3169 # Revert the subrepos on the revert list
3159 3170 for sub in targetsubs:
3160 3171 try:
3161 3172 wctx.sub(sub).revert(ctx.substate[sub], *pats, **opts)
3162 3173 except KeyError:
3163 3174 raise error.Abort("subrepository '%s' does not exist in %s!"
3164 3175 % (sub, short(ctx.node())))
3165 3176
3166 3177 def _revertprefetch(repo, ctx, *files):
3167 3178 """Let extension changing the storage layer prefetch content"""
3168 3179 pass
3169 3180
3170 3181 def _performrevert(repo, parents, ctx, actions, interactive=False):
3171 3182 """function that actually perform all the actions computed for revert
3172 3183
3173 3184 This is an independent function to let extension to plug in and react to
3174 3185 the imminent revert.
3175 3186
3176 3187 Make sure you have the working directory locked when calling this function.
3177 3188 """
3178 3189 parent, p2 = parents
3179 3190 node = ctx.node()
3180 3191 excluded_files = []
3181 3192 matcher_opts = {"exclude": excluded_files}
3182 3193
3183 3194 def checkout(f):
3184 3195 fc = ctx[f]
3185 3196 repo.wwrite(f, fc.data(), fc.flags())
3186 3197
3187 3198 audit_path = pathutil.pathauditor(repo.root)
3188 3199 for f in actions['forget'][0]:
3189 3200 if interactive:
3190 3201 choice = \
3191 3202 repo.ui.promptchoice(
3192 3203 _("forget added file %s (yn)?$$ &Yes $$ &No")
3193 3204 % f)
3194 3205 if choice == 0:
3195 3206 repo.dirstate.drop(f)
3196 3207 else:
3197 3208 excluded_files.append(repo.wjoin(f))
3198 3209 else:
3199 3210 repo.dirstate.drop(f)
3200 3211 for f in actions['remove'][0]:
3201 3212 audit_path(f)
3202 3213 try:
3203 3214 util.unlinkpath(repo.wjoin(f))
3204 3215 except OSError:
3205 3216 pass
3206 3217 repo.dirstate.remove(f)
3207 3218 for f in actions['drop'][0]:
3208 3219 audit_path(f)
3209 3220 repo.dirstate.remove(f)
3210 3221
3211 3222 normal = None
3212 3223 if node == parent:
3213 3224 # We're reverting to our parent. If possible, we'd like status
3214 3225 # to report the file as clean. We have to use normallookup for
3215 3226 # merges to avoid losing information about merged/dirty files.
3216 3227 if p2 != nullid:
3217 3228 normal = repo.dirstate.normallookup
3218 3229 else:
3219 3230 normal = repo.dirstate.normal
3220 3231
3221 3232 newlyaddedandmodifiedfiles = set()
3222 3233 if interactive:
3223 3234 # Prompt the user for changes to revert
3224 3235 torevert = [repo.wjoin(f) for f in actions['revert'][0]]
3225 3236 m = scmutil.match(ctx, torevert, matcher_opts)
3226 3237 diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
3227 3238 diffopts.nodates = True
3228 3239 diffopts.git = True
3229 3240 reversehunks = repo.ui.configbool('experimental',
3230 3241 'revertalternateinteractivemode',
3231 3242 True)
3232 3243 if reversehunks:
3233 3244 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3234 3245 else:
3235 3246 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3236 3247 originalchunks = patch.parsepatch(diff)
3237 3248
3238 3249 try:
3239 3250
3240 3251 chunks, opts = recordfilter(repo.ui, originalchunks)
3241 3252 if reversehunks:
3242 3253 chunks = patch.reversehunks(chunks)
3243 3254
3244 3255 except patch.PatchError as err:
3245 3256 raise error.Abort(_('error parsing patch: %s') % err)
3246 3257
3247 3258 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
3248 3259 # Apply changes
3249 3260 fp = cStringIO.StringIO()
3250 3261 for c in chunks:
3251 3262 c.write(fp)
3252 3263 dopatch = fp.tell()
3253 3264 fp.seek(0)
3254 3265 if dopatch:
3255 3266 try:
3256 3267 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3257 3268 except patch.PatchError as err:
3258 3269 raise error.Abort(str(err))
3259 3270 del fp
3260 3271 else:
3261 3272 for f in actions['revert'][0]:
3262 3273 checkout(f)
3263 3274 if normal:
3264 3275 normal(f)
3265 3276
3266 3277 for f in actions['add'][0]:
3267 3278 # Don't checkout modified files, they are already created by the diff
3268 3279 if f not in newlyaddedandmodifiedfiles:
3269 3280 checkout(f)
3270 3281 repo.dirstate.add(f)
3271 3282
3272 3283 normal = repo.dirstate.normallookup
3273 3284 if node == parent and p2 == nullid:
3274 3285 normal = repo.dirstate.normal
3275 3286 for f in actions['undelete'][0]:
3276 3287 checkout(f)
3277 3288 normal(f)
3278 3289
3279 3290 copied = copies.pathcopies(repo[parent], ctx)
3280 3291
3281 3292 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3282 3293 if f in copied:
3283 3294 repo.dirstate.copy(copied[f], f)
3284 3295
3285 3296 def command(table):
3286 3297 """Returns a function object to be used as a decorator for making commands.
3287 3298
3288 3299 This function receives a command table as its argument. The table should
3289 3300 be a dict.
3290 3301
3291 3302 The returned function can be used as a decorator for adding commands
3292 3303 to that command table. This function accepts multiple arguments to define
3293 3304 a command.
3294 3305
3295 3306 The first argument is the command name.
3296 3307
3297 3308 The options argument is an iterable of tuples defining command arguments.
3298 3309 See ``mercurial.fancyopts.fancyopts()`` for the format of each tuple.
3299 3310
3300 3311 The synopsis argument defines a short, one line summary of how to use the
3301 3312 command. This shows up in the help output.
3302 3313
3303 3314 The norepo argument defines whether the command does not require a
3304 3315 local repository. Most commands operate against a repository, thus the
3305 3316 default is False.
3306 3317
3307 3318 The optionalrepo argument defines whether the command optionally requires
3308 3319 a local repository.
3309 3320
3310 3321 The inferrepo argument defines whether to try to find a repository from the
3311 3322 command line arguments. If True, arguments will be examined for potential
3312 3323 repository locations. See ``findrepo()``. If a repository is found, it
3313 3324 will be used.
3314 3325 """
3315 3326 def cmd(name, options=(), synopsis=None, norepo=False, optionalrepo=False,
3316 3327 inferrepo=False):
3317 3328 def decorator(func):
3318 3329 func.norepo = norepo
3319 3330 func.optionalrepo = optionalrepo
3320 3331 func.inferrepo = inferrepo
3321 3332 if synopsis:
3322 3333 table[name] = func, list(options), synopsis
3323 3334 else:
3324 3335 table[name] = func, list(options)
3325 3336 return func
3326 3337 return decorator
3327 3338
3328 3339 return cmd
3329 3340
3330 3341 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3331 3342 # commands.outgoing. "missing" is "missing" of the result of
3332 3343 # "findcommonoutgoing()"
3333 3344 outgoinghooks = util.hooks()
3334 3345
3335 3346 # a list of (ui, repo) functions called by commands.summary
3336 3347 summaryhooks = util.hooks()
3337 3348
3338 3349 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3339 3350 #
3340 3351 # functions should return tuple of booleans below, if 'changes' is None:
3341 3352 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3342 3353 #
3343 3354 # otherwise, 'changes' is a tuple of tuples below:
3344 3355 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3345 3356 # - (desturl, destbranch, destpeer, outgoing)
3346 3357 summaryremotehooks = util.hooks()
3347 3358
3348 3359 # A list of state files kept by multistep operations like graft.
3349 3360 # Since graft cannot be aborted, it is considered 'clearable' by update.
3350 3361 # note: bisect is intentionally excluded
3351 3362 # (state file, clearable, allowcommit, error, hint)
3352 3363 unfinishedstates = [
3353 3364 ('graftstate', True, False, _('graft in progress'),
3354 3365 _("use 'hg graft --continue' or 'hg update' to abort")),
3355 3366 ('updatestate', True, False, _('last update was interrupted'),
3356 3367 _("use 'hg update' to get a consistent checkout"))
3357 3368 ]
3358 3369
3359 3370 def checkunfinished(repo, commit=False):
3360 3371 '''Look for an unfinished multistep operation, like graft, and abort
3361 3372 if found. It's probably good to check this right before
3362 3373 bailifchanged().
3363 3374 '''
3364 3375 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3365 3376 if commit and allowcommit:
3366 3377 continue
3367 3378 if repo.vfs.exists(f):
3368 3379 raise error.Abort(msg, hint=hint)
3369 3380
3370 3381 def clearunfinished(repo):
3371 3382 '''Check for unfinished operations (as above), and clear the ones
3372 3383 that are clearable.
3373 3384 '''
3374 3385 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3375 3386 if not clearable and repo.vfs.exists(f):
3376 3387 raise error.Abort(msg, hint=hint)
3377 3388 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3378 3389 if clearable and repo.vfs.exists(f):
3379 3390 util.unlink(repo.join(f))
3380 3391
3381 3392 afterresolvedstates = [
3382 3393 ('graftstate',
3383 3394 _('hg graft --continue')),
3384 3395 ]
3385 3396
3386 3397 def howtocontinue(repo):
3387 3398 '''Check for an unfinished operation and return the command to finish
3388 3399 it.
3389 3400
3390 3401 afterresolvedstates tupples define a .hg/{file} and the corresponding
3391 3402 command needed to finish it.
3392 3403
3393 3404 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3394 3405 a boolean.
3395 3406 '''
3396 3407 contmsg = _("continue: %s")
3397 3408 for f, msg in afterresolvedstates:
3398 3409 if repo.vfs.exists(f):
3399 3410 return contmsg % msg, True
3400 3411 workingctx = repo[None]
3401 3412 dirty = any(repo.status()) or any(workingctx.sub(s).dirty()
3402 3413 for s in workingctx.substate)
3403 3414 if dirty:
3404 3415 return contmsg % _("hg commit"), False
3405 3416 return None, None
3406 3417
3407 3418 def checkafterresolved(repo):
3408 3419 '''Inform the user about the next action after completing hg resolve
3409 3420
3410 3421 If there's a matching afterresolvedstates, howtocontinue will yield
3411 3422 repo.ui.warn as the reporter.
3412 3423
3413 3424 Otherwise, it will yield repo.ui.note.
3414 3425 '''
3415 3426 msg, warning = howtocontinue(repo)
3416 3427 if msg is not None:
3417 3428 if warning:
3418 3429 repo.ui.warn("%s\n" % msg)
3419 3430 else:
3420 3431 repo.ui.note("%s\n" % msg)
3421 3432
3422 3433 def wrongtooltocontinue(repo, task):
3423 3434 '''Raise an abort suggesting how to properly continue if there is an
3424 3435 active task.
3425 3436
3426 3437 Uses howtocontinue() to find the active task.
3427 3438
3428 3439 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3429 3440 a hint.
3430 3441 '''
3431 3442 after = howtocontinue(repo)
3432 3443 hint = None
3433 3444 if after[1]:
3434 3445 hint = after[0]
3435 3446 raise error.Abort(_('no %s in progress') % task, hint=hint)
3436 3447
3437 3448 class dirstateguard(object):
3438 3449 '''Restore dirstate at unexpected failure.
3439 3450
3440 3451 At the construction, this class does:
3441 3452
3442 3453 - write current ``repo.dirstate`` out, and
3443 3454 - save ``.hg/dirstate`` into the backup file
3444 3455
3445 3456 This restores ``.hg/dirstate`` from backup file, if ``release()``
3446 3457 is invoked before ``close()``.
3447 3458
3448 3459 This just removes the backup file at ``close()`` before ``release()``.
3449 3460 '''
3450 3461
3451 3462 def __init__(self, repo, name):
3452 3463 self._repo = repo
3453 3464 self._suffix = '.backup.%s.%d' % (name, id(self))
3454 3465 repo.dirstate._savebackup(repo.currenttransaction(), self._suffix)
3455 3466 self._active = True
3456 3467 self._closed = False
3457 3468
3458 3469 def __del__(self):
3459 3470 if self._active: # still active
3460 3471 # this may occur, even if this class is used correctly:
3461 3472 # for example, releasing other resources like transaction
3462 3473 # may raise exception before ``dirstateguard.release`` in
3463 3474 # ``release(tr, ....)``.
3464 3475 self._abort()
3465 3476
3466 3477 def close(self):
3467 3478 if not self._active: # already inactivated
3468 3479 msg = (_("can't close already inactivated backup: dirstate%s")
3469 3480 % self._suffix)
3470 3481 raise error.Abort(msg)
3471 3482
3472 3483 self._repo.dirstate._clearbackup(self._repo.currenttransaction(),
3473 3484 self._suffix)
3474 3485 self._active = False
3475 3486 self._closed = True
3476 3487
3477 3488 def _abort(self):
3478 3489 self._repo.dirstate._restorebackup(self._repo.currenttransaction(),
3479 3490 self._suffix)
3480 3491 self._active = False
3481 3492
3482 3493 def release(self):
3483 3494 if not self._closed:
3484 3495 if not self._active: # already inactivated
3485 3496 msg = (_("can't release already inactivated backup:"
3486 3497 " dirstate%s")
3487 3498 % self._suffix)
3488 3499 raise error.Abort(msg)
3489 3500 self._abort()
@@ -1,1938 +1,1940 b''
1 1 # subrepo.py - sub-repository handling for Mercurial
2 2 #
3 3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import copy
11 11 import errno
12 12 import os
13 13 import posixpath
14 14 import re
15 15 import stat
16 16 import subprocess
17 17 import sys
18 18 import tarfile
19 19 import xml.dom.minidom
20 20
21 21
22 22 from .i18n import _
23 23 from . import (
24 24 cmdutil,
25 25 config,
26 26 error,
27 27 exchange,
28 28 match as matchmod,
29 29 node,
30 30 pathutil,
31 31 phases,
32 32 scmutil,
33 33 util,
34 34 )
35 35
36 36 hg = None
37 37 propertycache = util.propertycache
38 38
39 39 nullstate = ('', '', 'empty')
40 40
41 41 def _expandedabspath(path):
42 42 '''
43 43 get a path or url and if it is a path expand it and return an absolute path
44 44 '''
45 45 expandedpath = util.urllocalpath(util.expandpath(path))
46 46 u = util.url(expandedpath)
47 47 if not u.scheme:
48 48 path = util.normpath(os.path.abspath(u.path))
49 49 return path
50 50
51 51 def _getstorehashcachename(remotepath):
52 52 '''get a unique filename for the store hash cache of a remote repository'''
53 53 return util.sha1(_expandedabspath(remotepath)).hexdigest()[0:12]
54 54
55 55 class SubrepoAbort(error.Abort):
56 56 """Exception class used to avoid handling a subrepo error more than once"""
57 57 def __init__(self, *args, **kw):
58 58 error.Abort.__init__(self, *args, **kw)
59 59 self.subrepo = kw.get('subrepo')
60 60 self.cause = kw.get('cause')
61 61
62 62 def annotatesubrepoerror(func):
63 63 def decoratedmethod(self, *args, **kargs):
64 64 try:
65 65 res = func(self, *args, **kargs)
66 66 except SubrepoAbort as ex:
67 67 # This exception has already been handled
68 68 raise ex
69 69 except error.Abort as ex:
70 70 subrepo = subrelpath(self)
71 71 errormsg = str(ex) + ' ' + _('(in subrepo %s)') % subrepo
72 72 # avoid handling this exception by raising a SubrepoAbort exception
73 73 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
74 74 cause=sys.exc_info())
75 75 return res
76 76 return decoratedmethod
77 77
78 78 def state(ctx, ui):
79 79 """return a state dict, mapping subrepo paths configured in .hgsub
80 80 to tuple: (source from .hgsub, revision from .hgsubstate, kind
81 81 (key in types dict))
82 82 """
83 83 p = config.config()
84 84 repo = ctx.repo()
85 85 def read(f, sections=None, remap=None):
86 86 if f in ctx:
87 87 try:
88 88 data = ctx[f].data()
89 89 except IOError as err:
90 90 if err.errno != errno.ENOENT:
91 91 raise
92 92 # handle missing subrepo spec files as removed
93 93 ui.warn(_("warning: subrepo spec file \'%s\' not found\n") %
94 94 repo.pathto(f))
95 95 return
96 96 p.parse(f, data, sections, remap, read)
97 97 else:
98 98 raise error.Abort(_("subrepo spec file \'%s\' not found") %
99 99 repo.pathto(f))
100 100 if '.hgsub' in ctx:
101 101 read('.hgsub')
102 102
103 103 for path, src in ui.configitems('subpaths'):
104 104 p.set('subpaths', path, src, ui.configsource('subpaths', path))
105 105
106 106 rev = {}
107 107 if '.hgsubstate' in ctx:
108 108 try:
109 109 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
110 110 l = l.lstrip()
111 111 if not l:
112 112 continue
113 113 try:
114 114 revision, path = l.split(" ", 1)
115 115 except ValueError:
116 116 raise error.Abort(_("invalid subrepository revision "
117 117 "specifier in \'%s\' line %d")
118 118 % (repo.pathto('.hgsubstate'), (i + 1)))
119 119 rev[path] = revision
120 120 except IOError as err:
121 121 if err.errno != errno.ENOENT:
122 122 raise
123 123
124 124 def remap(src):
125 125 for pattern, repl in p.items('subpaths'):
126 126 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
127 127 # does a string decode.
128 128 repl = repl.encode('string-escape')
129 129 # However, we still want to allow back references to go
130 130 # through unharmed, so we turn r'\\1' into r'\1'. Again,
131 131 # extra escapes are needed because re.sub string decodes.
132 132 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
133 133 try:
134 134 src = re.sub(pattern, repl, src, 1)
135 135 except re.error as e:
136 136 raise error.Abort(_("bad subrepository pattern in %s: %s")
137 137 % (p.source('subpaths', pattern), e))
138 138 return src
139 139
140 140 state = {}
141 141 for path, src in p[''].items():
142 142 kind = 'hg'
143 143 if src.startswith('['):
144 144 if ']' not in src:
145 145 raise error.Abort(_('missing ] in subrepo source'))
146 146 kind, src = src.split(']', 1)
147 147 kind = kind[1:]
148 148 src = src.lstrip() # strip any extra whitespace after ']'
149 149
150 150 if not util.url(src).isabs():
151 151 parent = _abssource(repo, abort=False)
152 152 if parent:
153 153 parent = util.url(parent)
154 154 parent.path = posixpath.join(parent.path or '', src)
155 155 parent.path = posixpath.normpath(parent.path)
156 156 joined = str(parent)
157 157 # Remap the full joined path and use it if it changes,
158 158 # else remap the original source.
159 159 remapped = remap(joined)
160 160 if remapped == joined:
161 161 src = remap(src)
162 162 else:
163 163 src = remapped
164 164
165 165 src = remap(src)
166 166 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
167 167
168 168 return state
169 169
170 170 def writestate(repo, state):
171 171 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
172 172 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)
173 173 if state[s][1] != nullstate[1]]
174 174 repo.wwrite('.hgsubstate', ''.join(lines), '')
175 175
176 176 def submerge(repo, wctx, mctx, actx, overwrite):
177 177 """delegated from merge.applyupdates: merging of .hgsubstate file
178 178 in working context, merging context and ancestor context"""
179 179 if mctx == actx: # backwards?
180 180 actx = wctx.p1()
181 181 s1 = wctx.substate
182 182 s2 = mctx.substate
183 183 sa = actx.substate
184 184 sm = {}
185 185
186 186 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
187 187
188 188 def debug(s, msg, r=""):
189 189 if r:
190 190 r = "%s:%s:%s" % r
191 191 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
192 192
193 193 for s, l in sorted(s1.iteritems()):
194 194 a = sa.get(s, nullstate)
195 195 ld = l # local state with possible dirty flag for compares
196 196 if wctx.sub(s).dirty():
197 197 ld = (l[0], l[1] + "+")
198 198 if wctx == actx: # overwrite
199 199 a = ld
200 200
201 201 if s in s2:
202 202 r = s2[s]
203 203 if ld == r or r == a: # no change or local is newer
204 204 sm[s] = l
205 205 continue
206 206 elif ld == a: # other side changed
207 207 debug(s, "other changed, get", r)
208 208 wctx.sub(s).get(r, overwrite)
209 209 sm[s] = r
210 210 elif ld[0] != r[0]: # sources differ
211 211 if repo.ui.promptchoice(
212 212 _(' subrepository sources for %s differ\n'
213 213 'use (l)ocal source (%s) or (r)emote source (%s)?'
214 214 '$$ &Local $$ &Remote') % (s, l[0], r[0]), 0):
215 215 debug(s, "prompt changed, get", r)
216 216 wctx.sub(s).get(r, overwrite)
217 217 sm[s] = r
218 218 elif ld[1] == a[1]: # local side is unchanged
219 219 debug(s, "other side changed, get", r)
220 220 wctx.sub(s).get(r, overwrite)
221 221 sm[s] = r
222 222 else:
223 223 debug(s, "both sides changed")
224 224 srepo = wctx.sub(s)
225 225 option = repo.ui.promptchoice(
226 226 _(' subrepository %s diverged (local revision: %s, '
227 227 'remote revision: %s)\n'
228 228 '(M)erge, keep (l)ocal or keep (r)emote?'
229 229 '$$ &Merge $$ &Local $$ &Remote')
230 230 % (s, srepo.shortid(l[1]), srepo.shortid(r[1])), 0)
231 231 if option == 0:
232 232 wctx.sub(s).merge(r)
233 233 sm[s] = l
234 234 debug(s, "merge with", r)
235 235 elif option == 1:
236 236 sm[s] = l
237 237 debug(s, "keep local subrepo revision", l)
238 238 else:
239 239 wctx.sub(s).get(r, overwrite)
240 240 sm[s] = r
241 241 debug(s, "get remote subrepo revision", r)
242 242 elif ld == a: # remote removed, local unchanged
243 243 debug(s, "remote removed, remove")
244 244 wctx.sub(s).remove()
245 245 elif a == nullstate: # not present in remote or ancestor
246 246 debug(s, "local added, keep")
247 247 sm[s] = l
248 248 continue
249 249 else:
250 250 if repo.ui.promptchoice(
251 251 _(' local changed subrepository %s which remote removed\n'
252 252 'use (c)hanged version or (d)elete?'
253 253 '$$ &Changed $$ &Delete') % s, 0):
254 254 debug(s, "prompt remove")
255 255 wctx.sub(s).remove()
256 256
257 257 for s, r in sorted(s2.items()):
258 258 if s in s1:
259 259 continue
260 260 elif s not in sa:
261 261 debug(s, "remote added, get", r)
262 262 mctx.sub(s).get(r)
263 263 sm[s] = r
264 264 elif r != sa[s]:
265 265 if repo.ui.promptchoice(
266 266 _(' remote changed subrepository %s which local removed\n'
267 267 'use (c)hanged version or (d)elete?'
268 268 '$$ &Changed $$ &Delete') % s, 0) == 0:
269 269 debug(s, "prompt recreate", r)
270 270 mctx.sub(s).get(r)
271 271 sm[s] = r
272 272
273 273 # record merged .hgsubstate
274 274 writestate(repo, sm)
275 275 return sm
276 276
277 277 def _updateprompt(ui, sub, dirty, local, remote):
278 278 if dirty:
279 279 msg = (_(' subrepository sources for %s differ\n'
280 280 'use (l)ocal source (%s) or (r)emote source (%s)?'
281 281 '$$ &Local $$ &Remote')
282 282 % (subrelpath(sub), local, remote))
283 283 else:
284 284 msg = (_(' subrepository sources for %s differ (in checked out '
285 285 'version)\n'
286 286 'use (l)ocal source (%s) or (r)emote source (%s)?'
287 287 '$$ &Local $$ &Remote')
288 288 % (subrelpath(sub), local, remote))
289 289 return ui.promptchoice(msg, 0)
290 290
291 291 def reporelpath(repo):
292 292 """return path to this (sub)repo as seen from outermost repo"""
293 293 parent = repo
294 294 while util.safehasattr(parent, '_subparent'):
295 295 parent = parent._subparent
296 296 return repo.root[len(pathutil.normasprefix(parent.root)):]
297 297
298 298 def subrelpath(sub):
299 299 """return path to this subrepo as seen from outermost repo"""
300 300 return sub._relpath
301 301
302 302 def _abssource(repo, push=False, abort=True):
303 303 """return pull/push path of repo - either based on parent repo .hgsub info
304 304 or on the top repo config. Abort or return None if no source found."""
305 305 if util.safehasattr(repo, '_subparent'):
306 306 source = util.url(repo._subsource)
307 307 if source.isabs():
308 308 return str(source)
309 309 source.path = posixpath.normpath(source.path)
310 310 parent = _abssource(repo._subparent, push, abort=False)
311 311 if parent:
312 312 parent = util.url(util.pconvert(parent))
313 313 parent.path = posixpath.join(parent.path or '', source.path)
314 314 parent.path = posixpath.normpath(parent.path)
315 315 return str(parent)
316 316 else: # recursion reached top repo
317 317 if util.safehasattr(repo, '_subtoppath'):
318 318 return repo._subtoppath
319 319 if push and repo.ui.config('paths', 'default-push'):
320 320 return repo.ui.config('paths', 'default-push')
321 321 if repo.ui.config('paths', 'default'):
322 322 return repo.ui.config('paths', 'default')
323 323 if repo.shared():
324 324 # chop off the .hg component to get the default path form
325 325 return os.path.dirname(repo.sharedpath)
326 326 if abort:
327 327 raise error.Abort(_("default path for subrepository not found"))
328 328
329 329 def _sanitize(ui, vfs, ignore):
330 330 for dirname, dirs, names in vfs.walk():
331 331 for i, d in enumerate(dirs):
332 332 if d.lower() == ignore:
333 333 del dirs[i]
334 334 break
335 335 if vfs.basename(dirname).lower() != '.hg':
336 336 continue
337 337 for f in names:
338 338 if f.lower() == 'hgrc':
339 339 ui.warn(_("warning: removing potentially hostile 'hgrc' "
340 340 "in '%s'\n") % vfs.join(dirname))
341 341 vfs.unlink(vfs.reljoin(dirname, f))
342 342
343 343 def subrepo(ctx, path, allowwdir=False):
344 344 """return instance of the right subrepo class for subrepo in path"""
345 345 # subrepo inherently violates our import layering rules
346 346 # because it wants to make repo objects from deep inside the stack
347 347 # so we manually delay the circular imports to not break
348 348 # scripts that don't use our demand-loading
349 349 global hg
350 350 from . import hg as h
351 351 hg = h
352 352
353 353 pathutil.pathauditor(ctx.repo().root)(path)
354 354 state = ctx.substate[path]
355 355 if state[2] not in types:
356 356 raise error.Abort(_('unknown subrepo type %s') % state[2])
357 357 if allowwdir:
358 358 state = (state[0], ctx.subrev(path), state[2])
359 359 return types[state[2]](ctx, path, state[:2])
360 360
361 361 def nullsubrepo(ctx, path, pctx):
362 362 """return an empty subrepo in pctx for the extant subrepo in ctx"""
363 363 # subrepo inherently violates our import layering rules
364 364 # because it wants to make repo objects from deep inside the stack
365 365 # so we manually delay the circular imports to not break
366 366 # scripts that don't use our demand-loading
367 367 global hg
368 368 from . import hg as h
369 369 hg = h
370 370
371 371 pathutil.pathauditor(ctx.repo().root)(path)
372 372 state = ctx.substate[path]
373 373 if state[2] not in types:
374 374 raise error.Abort(_('unknown subrepo type %s') % state[2])
375 375 subrev = ''
376 376 if state[2] == 'hg':
377 377 subrev = "0" * 40
378 378 return types[state[2]](pctx, path, (state[0], subrev))
379 379
380 380 def newcommitphase(ui, ctx):
381 381 commitphase = phases.newcommitphase(ui)
382 382 substate = getattr(ctx, "substate", None)
383 383 if not substate:
384 384 return commitphase
385 385 check = ui.config('phases', 'checksubrepos', 'follow')
386 386 if check not in ('ignore', 'follow', 'abort'):
387 387 raise error.Abort(_('invalid phases.checksubrepos configuration: %s')
388 388 % (check))
389 389 if check == 'ignore':
390 390 return commitphase
391 391 maxphase = phases.public
392 392 maxsub = None
393 393 for s in sorted(substate):
394 394 sub = ctx.sub(s)
395 395 subphase = sub.phase(substate[s][1])
396 396 if maxphase < subphase:
397 397 maxphase = subphase
398 398 maxsub = s
399 399 if commitphase < maxphase:
400 400 if check == 'abort':
401 401 raise error.Abort(_("can't commit in %s phase"
402 402 " conflicting %s from subrepository %s") %
403 403 (phases.phasenames[commitphase],
404 404 phases.phasenames[maxphase], maxsub))
405 405 ui.warn(_("warning: changes are committed in"
406 406 " %s phase from subrepository %s\n") %
407 407 (phases.phasenames[maxphase], maxsub))
408 408 return maxphase
409 409 return commitphase
410 410
411 411 # subrepo classes need to implement the following abstract class:
412 412
413 413 class abstractsubrepo(object):
414 414
415 415 def __init__(self, ctx, path):
416 416 """Initialize abstractsubrepo part
417 417
418 418 ``ctx`` is the context referring this subrepository in the
419 419 parent repository.
420 420
421 421 ``path`` is the path to this subrepository as seen from
422 422 innermost repository.
423 423 """
424 424 self.ui = ctx.repo().ui
425 425 self._ctx = ctx
426 426 self._path = path
427 427
428 428 def storeclean(self, path):
429 429 """
430 430 returns true if the repository has not changed since it was last
431 431 cloned from or pushed to a given repository.
432 432 """
433 433 return False
434 434
435 435 def dirty(self, ignoreupdate=False):
436 436 """returns true if the dirstate of the subrepo is dirty or does not
437 437 match current stored state. If ignoreupdate is true, only check
438 438 whether the subrepo has uncommitted changes in its dirstate.
439 439 """
440 440 raise NotImplementedError
441 441
442 442 def dirtyreason(self, ignoreupdate=False):
443 443 """return reason string if it is ``dirty()``
444 444
445 445 Returned string should have enough information for the message
446 446 of exception.
447 447
448 448 This returns None, otherwise.
449 449 """
450 450 if self.dirty(ignoreupdate=ignoreupdate):
451 451 return _("uncommitted changes in subrepository '%s'"
452 452 ) % subrelpath(self)
453 453
454 454 def bailifchanged(self, ignoreupdate=False):
455 455 """raise Abort if subrepository is ``dirty()``
456 456 """
457 457 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate)
458 458 if dirtyreason:
459 459 raise error.Abort(dirtyreason)
460 460
461 461 def basestate(self):
462 462 """current working directory base state, disregarding .hgsubstate
463 463 state and working directory modifications"""
464 464 raise NotImplementedError
465 465
466 466 def checknested(self, path):
467 467 """check if path is a subrepository within this repository"""
468 468 return False
469 469
470 470 def commit(self, text, user, date):
471 471 """commit the current changes to the subrepo with the given
472 472 log message. Use given user and date if possible. Return the
473 473 new state of the subrepo.
474 474 """
475 475 raise NotImplementedError
476 476
477 477 def phase(self, state):
478 478 """returns phase of specified state in the subrepository.
479 479 """
480 480 return phases.public
481 481
482 482 def remove(self):
483 483 """remove the subrepo
484 484
485 485 (should verify the dirstate is not dirty first)
486 486 """
487 487 raise NotImplementedError
488 488
489 489 def get(self, state, overwrite=False):
490 490 """run whatever commands are needed to put the subrepo into
491 491 this state
492 492 """
493 493 raise NotImplementedError
494 494
495 495 def merge(self, state):
496 496 """merge currently-saved state with the new state."""
497 497 raise NotImplementedError
498 498
499 499 def push(self, opts):
500 500 """perform whatever action is analogous to 'hg push'
501 501
502 502 This may be a no-op on some systems.
503 503 """
504 504 raise NotImplementedError
505 505
506 506 def add(self, ui, match, prefix, explicitonly, **opts):
507 507 return []
508 508
509 509 def addremove(self, matcher, prefix, opts, dry_run, similarity):
510 510 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
511 511 return 1
512 512
513 513 def cat(self, match, prefix, **opts):
514 514 return 1
515 515
516 516 def status(self, rev2, **opts):
517 517 return scmutil.status([], [], [], [], [], [], [])
518 518
519 519 def diff(self, ui, diffopts, node2, match, prefix, **opts):
520 520 pass
521 521
522 522 def outgoing(self, ui, dest, opts):
523 523 return 1
524 524
525 525 def incoming(self, ui, source, opts):
526 526 return 1
527 527
528 528 def files(self):
529 529 """return filename iterator"""
530 530 raise NotImplementedError
531 531
532 532 def filedata(self, name):
533 533 """return file data"""
534 534 raise NotImplementedError
535 535
536 536 def fileflags(self, name):
537 537 """return file flags"""
538 538 return ''
539 539
540 540 def getfileset(self, expr):
541 541 """Resolve the fileset expression for this repo"""
542 542 return set()
543 543
544 544 def printfiles(self, ui, m, fm, fmt, subrepos):
545 545 """handle the files command for this subrepo"""
546 546 return 1
547 547
548 548 def archive(self, archiver, prefix, match=None):
549 549 if match is not None:
550 550 files = [f for f in self.files() if match(f)]
551 551 else:
552 552 files = self.files()
553 553 total = len(files)
554 554 relpath = subrelpath(self)
555 555 self.ui.progress(_('archiving (%s)') % relpath, 0,
556 556 unit=_('files'), total=total)
557 557 for i, name in enumerate(files):
558 558 flags = self.fileflags(name)
559 559 mode = 'x' in flags and 0o755 or 0o644
560 560 symlink = 'l' in flags
561 561 archiver.addfile(prefix + self._path + '/' + name,
562 562 mode, symlink, self.filedata(name))
563 563 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
564 564 unit=_('files'), total=total)
565 565 self.ui.progress(_('archiving (%s)') % relpath, None)
566 566 return total
567 567
568 568 def walk(self, match):
569 569 '''
570 570 walk recursively through the directory tree, finding all files
571 571 matched by the match function
572 572 '''
573 573 pass
574 574
575 575 def forget(self, match, prefix):
576 576 return ([], [])
577 577
578 def removefiles(self, matcher, prefix, after, force, subrepos):
578 def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
579 579 """remove the matched files from the subrepository and the filesystem,
580 580 possibly by force and/or after the file has been removed from the
581 581 filesystem. Return 0 on success, 1 on any warning.
582 582 """
583 warnings.append(_("warning: removefiles not implemented (%s)")
584 % self._path)
583 585 return 1
584 586
585 587 def revert(self, substate, *pats, **opts):
586 588 self.ui.warn('%s: reverting %s subrepos is unsupported\n' \
587 589 % (substate[0], substate[2]))
588 590 return []
589 591
590 592 def shortid(self, revid):
591 593 return revid
592 594
593 595 def verify(self):
594 596 '''verify the integrity of the repository. Return 0 on success or
595 597 warning, 1 on any error.
596 598 '''
597 599 return 0
598 600
599 601 @propertycache
600 602 def wvfs(self):
601 603 """return vfs to access the working directory of this subrepository
602 604 """
603 605 return scmutil.vfs(self._ctx.repo().wvfs.join(self._path))
604 606
605 607 @propertycache
606 608 def _relpath(self):
607 609 """return path to this subrepository as seen from outermost repository
608 610 """
609 611 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
610 612
611 613 class hgsubrepo(abstractsubrepo):
612 614 def __init__(self, ctx, path, state):
613 615 super(hgsubrepo, self).__init__(ctx, path)
614 616 self._state = state
615 617 r = ctx.repo()
616 618 root = r.wjoin(path)
617 619 create = not r.wvfs.exists('%s/.hg' % path)
618 620 self._repo = hg.repository(r.baseui, root, create=create)
619 621
620 622 # Propagate the parent's --hidden option
621 623 if r is r.unfiltered():
622 624 self._repo = self._repo.unfiltered()
623 625
624 626 self.ui = self._repo.ui
625 627 for s, k in [('ui', 'commitsubrepos')]:
626 628 v = r.ui.config(s, k)
627 629 if v:
628 630 self.ui.setconfig(s, k, v, 'subrepo')
629 631 # internal config: ui._usedassubrepo
630 632 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
631 633 self._initrepo(r, state[0], create)
632 634
633 635 def storeclean(self, path):
634 636 with self._repo.lock():
635 637 return self._storeclean(path)
636 638
637 639 def _storeclean(self, path):
638 640 clean = True
639 641 itercache = self._calcstorehash(path)
640 642 for filehash in self._readstorehashcache(path):
641 643 if filehash != next(itercache, None):
642 644 clean = False
643 645 break
644 646 if clean:
645 647 # if not empty:
646 648 # the cached and current pull states have a different size
647 649 clean = next(itercache, None) is None
648 650 return clean
649 651
650 652 def _calcstorehash(self, remotepath):
651 653 '''calculate a unique "store hash"
652 654
653 655 This method is used to to detect when there are changes that may
654 656 require a push to a given remote path.'''
655 657 # sort the files that will be hashed in increasing (likely) file size
656 658 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
657 659 yield '# %s\n' % _expandedabspath(remotepath)
658 660 vfs = self._repo.vfs
659 661 for relname in filelist:
660 662 filehash = util.sha1(vfs.tryread(relname)).hexdigest()
661 663 yield '%s = %s\n' % (relname, filehash)
662 664
663 665 @propertycache
664 666 def _cachestorehashvfs(self):
665 667 return scmutil.vfs(self._repo.join('cache/storehash'))
666 668
667 669 def _readstorehashcache(self, remotepath):
668 670 '''read the store hash cache for a given remote repository'''
669 671 cachefile = _getstorehashcachename(remotepath)
670 672 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
671 673
672 674 def _cachestorehash(self, remotepath):
673 675 '''cache the current store hash
674 676
675 677 Each remote repo requires its own store hash cache, because a subrepo
676 678 store may be "clean" versus a given remote repo, but not versus another
677 679 '''
678 680 cachefile = _getstorehashcachename(remotepath)
679 681 with self._repo.lock():
680 682 storehash = list(self._calcstorehash(remotepath))
681 683 vfs = self._cachestorehashvfs
682 684 vfs.writelines(cachefile, storehash, mode='w', notindexed=True)
683 685
684 686 def _getctx(self):
685 687 '''fetch the context for this subrepo revision, possibly a workingctx
686 688 '''
687 689 if self._ctx.rev() is None:
688 690 return self._repo[None] # workingctx if parent is workingctx
689 691 else:
690 692 rev = self._state[1]
691 693 return self._repo[rev]
692 694
693 695 @annotatesubrepoerror
694 696 def _initrepo(self, parentrepo, source, create):
695 697 self._repo._subparent = parentrepo
696 698 self._repo._subsource = source
697 699
698 700 if create:
699 701 lines = ['[paths]\n']
700 702
701 703 def addpathconfig(key, value):
702 704 if value:
703 705 lines.append('%s = %s\n' % (key, value))
704 706 self.ui.setconfig('paths', key, value, 'subrepo')
705 707
706 708 defpath = _abssource(self._repo, abort=False)
707 709 defpushpath = _abssource(self._repo, True, abort=False)
708 710 addpathconfig('default', defpath)
709 711 if defpath != defpushpath:
710 712 addpathconfig('default-push', defpushpath)
711 713
712 714 fp = self._repo.vfs("hgrc", "w", text=True)
713 715 try:
714 716 fp.write(''.join(lines))
715 717 finally:
716 718 fp.close()
717 719
718 720 @annotatesubrepoerror
719 721 def add(self, ui, match, prefix, explicitonly, **opts):
720 722 return cmdutil.add(ui, self._repo, match,
721 723 self.wvfs.reljoin(prefix, self._path),
722 724 explicitonly, **opts)
723 725
724 726 @annotatesubrepoerror
725 727 def addremove(self, m, prefix, opts, dry_run, similarity):
726 728 # In the same way as sub directories are processed, once in a subrepo,
727 729 # always entry any of its subrepos. Don't corrupt the options that will
728 730 # be used to process sibling subrepos however.
729 731 opts = copy.copy(opts)
730 732 opts['subrepos'] = True
731 733 return scmutil.addremove(self._repo, m,
732 734 self.wvfs.reljoin(prefix, self._path), opts,
733 735 dry_run, similarity)
734 736
735 737 @annotatesubrepoerror
736 738 def cat(self, match, prefix, **opts):
737 739 rev = self._state[1]
738 740 ctx = self._repo[rev]
739 741 return cmdutil.cat(self.ui, self._repo, ctx, match, prefix, **opts)
740 742
741 743 @annotatesubrepoerror
742 744 def status(self, rev2, **opts):
743 745 try:
744 746 rev1 = self._state[1]
745 747 ctx1 = self._repo[rev1]
746 748 ctx2 = self._repo[rev2]
747 749 return self._repo.status(ctx1, ctx2, **opts)
748 750 except error.RepoLookupError as inst:
749 751 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
750 752 % (inst, subrelpath(self)))
751 753 return scmutil.status([], [], [], [], [], [], [])
752 754
753 755 @annotatesubrepoerror
754 756 def diff(self, ui, diffopts, node2, match, prefix, **opts):
755 757 try:
756 758 node1 = node.bin(self._state[1])
757 759 # We currently expect node2 to come from substate and be
758 760 # in hex format
759 761 if node2 is not None:
760 762 node2 = node.bin(node2)
761 763 cmdutil.diffordiffstat(ui, self._repo, diffopts,
762 764 node1, node2, match,
763 765 prefix=posixpath.join(prefix, self._path),
764 766 listsubrepos=True, **opts)
765 767 except error.RepoLookupError as inst:
766 768 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
767 769 % (inst, subrelpath(self)))
768 770
769 771 @annotatesubrepoerror
770 772 def archive(self, archiver, prefix, match=None):
771 773 self._get(self._state + ('hg',))
772 774 total = abstractsubrepo.archive(self, archiver, prefix, match)
773 775 rev = self._state[1]
774 776 ctx = self._repo[rev]
775 777 for subpath in ctx.substate:
776 778 s = subrepo(ctx, subpath, True)
777 779 submatch = matchmod.subdirmatcher(subpath, match)
778 780 total += s.archive(archiver, prefix + self._path + '/', submatch)
779 781 return total
780 782
781 783 @annotatesubrepoerror
782 784 def dirty(self, ignoreupdate=False):
783 785 r = self._state[1]
784 786 if r == '' and not ignoreupdate: # no state recorded
785 787 return True
786 788 w = self._repo[None]
787 789 if r != w.p1().hex() and not ignoreupdate:
788 790 # different version checked out
789 791 return True
790 792 return w.dirty() # working directory changed
791 793
792 794 def basestate(self):
793 795 return self._repo['.'].hex()
794 796
795 797 def checknested(self, path):
796 798 return self._repo._checknested(self._repo.wjoin(path))
797 799
798 800 @annotatesubrepoerror
799 801 def commit(self, text, user, date):
800 802 # don't bother committing in the subrepo if it's only been
801 803 # updated
802 804 if not self.dirty(True):
803 805 return self._repo['.'].hex()
804 806 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
805 807 n = self._repo.commit(text, user, date)
806 808 if not n:
807 809 return self._repo['.'].hex() # different version checked out
808 810 return node.hex(n)
809 811
810 812 @annotatesubrepoerror
811 813 def phase(self, state):
812 814 return self._repo[state].phase()
813 815
814 816 @annotatesubrepoerror
815 817 def remove(self):
816 818 # we can't fully delete the repository as it may contain
817 819 # local-only history
818 820 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
819 821 hg.clean(self._repo, node.nullid, False)
820 822
821 823 def _get(self, state):
822 824 source, revision, kind = state
823 825 if revision in self._repo.unfiltered():
824 826 return True
825 827 self._repo._subsource = source
826 828 srcurl = _abssource(self._repo)
827 829 other = hg.peer(self._repo, {}, srcurl)
828 830 if len(self._repo) == 0:
829 831 self.ui.status(_('cloning subrepo %s from %s\n')
830 832 % (subrelpath(self), srcurl))
831 833 parentrepo = self._repo._subparent
832 834 # use self._repo.vfs instead of self.wvfs to remove .hg only
833 835 self._repo.vfs.rmtree()
834 836 other, cloned = hg.clone(self._repo._subparent.baseui, {},
835 837 other, self._repo.root,
836 838 update=False)
837 839 self._repo = cloned.local()
838 840 self._initrepo(parentrepo, source, create=True)
839 841 self._cachestorehash(srcurl)
840 842 else:
841 843 self.ui.status(_('pulling subrepo %s from %s\n')
842 844 % (subrelpath(self), srcurl))
843 845 cleansub = self.storeclean(srcurl)
844 846 exchange.pull(self._repo, other)
845 847 if cleansub:
846 848 # keep the repo clean after pull
847 849 self._cachestorehash(srcurl)
848 850 return False
849 851
850 852 @annotatesubrepoerror
851 853 def get(self, state, overwrite=False):
852 854 inrepo = self._get(state)
853 855 source, revision, kind = state
854 856 repo = self._repo
855 857 repo.ui.debug("getting subrepo %s\n" % self._path)
856 858 if inrepo:
857 859 urepo = repo.unfiltered()
858 860 ctx = urepo[revision]
859 861 if ctx.hidden():
860 862 urepo.ui.warn(
861 863 _('revision %s in subrepo %s is hidden\n') \
862 864 % (revision[0:12], self._path))
863 865 repo = urepo
864 866 hg.updaterepo(repo, revision, overwrite)
865 867
866 868 @annotatesubrepoerror
867 869 def merge(self, state):
868 870 self._get(state)
869 871 cur = self._repo['.']
870 872 dst = self._repo[state[1]]
871 873 anc = dst.ancestor(cur)
872 874
873 875 def mergefunc():
874 876 if anc == cur and dst.branch() == cur.branch():
875 877 self.ui.debug("updating subrepo %s\n" % subrelpath(self))
876 878 hg.update(self._repo, state[1])
877 879 elif anc == dst:
878 880 self.ui.debug("skipping subrepo %s\n" % subrelpath(self))
879 881 else:
880 882 self.ui.debug("merging subrepo %s\n" % subrelpath(self))
881 883 hg.merge(self._repo, state[1], remind=False)
882 884
883 885 wctx = self._repo[None]
884 886 if self.dirty():
885 887 if anc != dst:
886 888 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
887 889 mergefunc()
888 890 else:
889 891 mergefunc()
890 892 else:
891 893 mergefunc()
892 894
893 895 @annotatesubrepoerror
894 896 def push(self, opts):
895 897 force = opts.get('force')
896 898 newbranch = opts.get('new_branch')
897 899 ssh = opts.get('ssh')
898 900
899 901 # push subrepos depth-first for coherent ordering
900 902 c = self._repo['']
901 903 subs = c.substate # only repos that are committed
902 904 for s in sorted(subs):
903 905 if c.sub(s).push(opts) == 0:
904 906 return False
905 907
906 908 dsturl = _abssource(self._repo, True)
907 909 if not force:
908 910 if self.storeclean(dsturl):
909 911 self.ui.status(
910 912 _('no changes made to subrepo %s since last push to %s\n')
911 913 % (subrelpath(self), dsturl))
912 914 return None
913 915 self.ui.status(_('pushing subrepo %s to %s\n') %
914 916 (subrelpath(self), dsturl))
915 917 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
916 918 res = exchange.push(self._repo, other, force, newbranch=newbranch)
917 919
918 920 # the repo is now clean
919 921 self._cachestorehash(dsturl)
920 922 return res.cgresult
921 923
922 924 @annotatesubrepoerror
923 925 def outgoing(self, ui, dest, opts):
924 926 if 'rev' in opts or 'branch' in opts:
925 927 opts = copy.copy(opts)
926 928 opts.pop('rev', None)
927 929 opts.pop('branch', None)
928 930 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
929 931
930 932 @annotatesubrepoerror
931 933 def incoming(self, ui, source, opts):
932 934 if 'rev' in opts or 'branch' in opts:
933 935 opts = copy.copy(opts)
934 936 opts.pop('rev', None)
935 937 opts.pop('branch', None)
936 938 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
937 939
938 940 @annotatesubrepoerror
939 941 def files(self):
940 942 rev = self._state[1]
941 943 ctx = self._repo[rev]
942 944 return ctx.manifest().keys()
943 945
944 946 def filedata(self, name):
945 947 rev = self._state[1]
946 948 return self._repo[rev][name].data()
947 949
948 950 def fileflags(self, name):
949 951 rev = self._state[1]
950 952 ctx = self._repo[rev]
951 953 return ctx.flags(name)
952 954
953 955 @annotatesubrepoerror
954 956 def printfiles(self, ui, m, fm, fmt, subrepos):
955 957 # If the parent context is a workingctx, use the workingctx here for
956 958 # consistency.
957 959 if self._ctx.rev() is None:
958 960 ctx = self._repo[None]
959 961 else:
960 962 rev = self._state[1]
961 963 ctx = self._repo[rev]
962 964 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
963 965
964 966 @annotatesubrepoerror
965 967 def getfileset(self, expr):
966 968 if self._ctx.rev() is None:
967 969 ctx = self._repo[None]
968 970 else:
969 971 rev = self._state[1]
970 972 ctx = self._repo[rev]
971 973
972 974 files = ctx.getfileset(expr)
973 975
974 976 for subpath in ctx.substate:
975 977 sub = ctx.sub(subpath)
976 978
977 979 try:
978 980 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
979 981 except error.LookupError:
980 982 self.ui.status(_("skipping missing subrepository: %s\n")
981 983 % self.wvfs.reljoin(reporelpath(self), subpath))
982 984 return files
983 985
984 986 def walk(self, match):
985 987 ctx = self._repo[None]
986 988 return ctx.walk(match)
987 989
988 990 @annotatesubrepoerror
989 991 def forget(self, match, prefix):
990 992 return cmdutil.forget(self.ui, self._repo, match,
991 993 self.wvfs.reljoin(prefix, self._path), True)
992 994
993 995 @annotatesubrepoerror
994 def removefiles(self, matcher, prefix, after, force, subrepos):
996 def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
995 997 return cmdutil.remove(self.ui, self._repo, matcher,
996 998 self.wvfs.reljoin(prefix, self._path),
997 999 after, force, subrepos)
998 1000
999 1001 @annotatesubrepoerror
1000 1002 def revert(self, substate, *pats, **opts):
1001 1003 # reverting a subrepo is a 2 step process:
1002 1004 # 1. if the no_backup is not set, revert all modified
1003 1005 # files inside the subrepo
1004 1006 # 2. update the subrepo to the revision specified in
1005 1007 # the corresponding substate dictionary
1006 1008 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1007 1009 if not opts.get('no_backup'):
1008 1010 # Revert all files on the subrepo, creating backups
1009 1011 # Note that this will not recursively revert subrepos
1010 1012 # We could do it if there was a set:subrepos() predicate
1011 1013 opts = opts.copy()
1012 1014 opts['date'] = None
1013 1015 opts['rev'] = substate[1]
1014 1016
1015 1017 self.filerevert(*pats, **opts)
1016 1018
1017 1019 # Update the repo to the revision specified in the given substate
1018 1020 if not opts.get('dry_run'):
1019 1021 self.get(substate, overwrite=True)
1020 1022
1021 1023 def filerevert(self, *pats, **opts):
1022 1024 ctx = self._repo[opts['rev']]
1023 1025 parents = self._repo.dirstate.parents()
1024 1026 if opts.get('all'):
1025 1027 pats = ['set:modified()']
1026 1028 else:
1027 1029 pats = []
1028 1030 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
1029 1031
1030 1032 def shortid(self, revid):
1031 1033 return revid[:12]
1032 1034
1033 1035 def verify(self):
1034 1036 try:
1035 1037 rev = self._state[1]
1036 1038 ctx = self._repo.unfiltered()[rev]
1037 1039 if ctx.hidden():
1038 1040 # Since hidden revisions aren't pushed/pulled, it seems worth an
1039 1041 # explicit warning.
1040 1042 ui = self._repo.ui
1041 1043 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
1042 1044 (self._relpath, node.short(self._ctx.node())))
1043 1045 return 0
1044 1046 except error.RepoLookupError:
1045 1047 # A missing subrepo revision may be a case of needing to pull it, so
1046 1048 # don't treat this as an error.
1047 1049 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
1048 1050 (self._relpath, node.short(self._ctx.node())))
1049 1051 return 0
1050 1052
1051 1053 @propertycache
1052 1054 def wvfs(self):
1053 1055 """return own wvfs for efficiency and consistency
1054 1056 """
1055 1057 return self._repo.wvfs
1056 1058
1057 1059 @propertycache
1058 1060 def _relpath(self):
1059 1061 """return path to this subrepository as seen from outermost repository
1060 1062 """
1061 1063 # Keep consistent dir separators by avoiding vfs.join(self._path)
1062 1064 return reporelpath(self._repo)
1063 1065
1064 1066 class svnsubrepo(abstractsubrepo):
1065 1067 def __init__(self, ctx, path, state):
1066 1068 super(svnsubrepo, self).__init__(ctx, path)
1067 1069 self._state = state
1068 1070 self._exe = util.findexe('svn')
1069 1071 if not self._exe:
1070 1072 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
1071 1073 % self._path)
1072 1074
1073 1075 def _svncommand(self, commands, filename='', failok=False):
1074 1076 cmd = [self._exe]
1075 1077 extrakw = {}
1076 1078 if not self.ui.interactive():
1077 1079 # Making stdin be a pipe should prevent svn from behaving
1078 1080 # interactively even if we can't pass --non-interactive.
1079 1081 extrakw['stdin'] = subprocess.PIPE
1080 1082 # Starting in svn 1.5 --non-interactive is a global flag
1081 1083 # instead of being per-command, but we need to support 1.4 so
1082 1084 # we have to be intelligent about what commands take
1083 1085 # --non-interactive.
1084 1086 if commands[0] in ('update', 'checkout', 'commit'):
1085 1087 cmd.append('--non-interactive')
1086 1088 cmd.extend(commands)
1087 1089 if filename is not None:
1088 1090 path = self.wvfs.reljoin(self._ctx.repo().origroot,
1089 1091 self._path, filename)
1090 1092 cmd.append(path)
1091 1093 env = dict(os.environ)
1092 1094 # Avoid localized output, preserve current locale for everything else.
1093 1095 lc_all = env.get('LC_ALL')
1094 1096 if lc_all:
1095 1097 env['LANG'] = lc_all
1096 1098 del env['LC_ALL']
1097 1099 env['LC_MESSAGES'] = 'C'
1098 1100 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
1099 1101 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1100 1102 universal_newlines=True, env=env, **extrakw)
1101 1103 stdout, stderr = p.communicate()
1102 1104 stderr = stderr.strip()
1103 1105 if not failok:
1104 1106 if p.returncode:
1105 1107 raise error.Abort(stderr or 'exited with code %d'
1106 1108 % p.returncode)
1107 1109 if stderr:
1108 1110 self.ui.warn(stderr + '\n')
1109 1111 return stdout, stderr
1110 1112
1111 1113 @propertycache
1112 1114 def _svnversion(self):
1113 1115 output, err = self._svncommand(['--version', '--quiet'], filename=None)
1114 1116 m = re.search(r'^(\d+)\.(\d+)', output)
1115 1117 if not m:
1116 1118 raise error.Abort(_('cannot retrieve svn tool version'))
1117 1119 return (int(m.group(1)), int(m.group(2)))
1118 1120
1119 1121 def _wcrevs(self):
1120 1122 # Get the working directory revision as well as the last
1121 1123 # commit revision so we can compare the subrepo state with
1122 1124 # both. We used to store the working directory one.
1123 1125 output, err = self._svncommand(['info', '--xml'])
1124 1126 doc = xml.dom.minidom.parseString(output)
1125 1127 entries = doc.getElementsByTagName('entry')
1126 1128 lastrev, rev = '0', '0'
1127 1129 if entries:
1128 1130 rev = str(entries[0].getAttribute('revision')) or '0'
1129 1131 commits = entries[0].getElementsByTagName('commit')
1130 1132 if commits:
1131 1133 lastrev = str(commits[0].getAttribute('revision')) or '0'
1132 1134 return (lastrev, rev)
1133 1135
1134 1136 def _wcrev(self):
1135 1137 return self._wcrevs()[0]
1136 1138
1137 1139 def _wcchanged(self):
1138 1140 """Return (changes, extchanges, missing) where changes is True
1139 1141 if the working directory was changed, extchanges is
1140 1142 True if any of these changes concern an external entry and missing
1141 1143 is True if any change is a missing entry.
1142 1144 """
1143 1145 output, err = self._svncommand(['status', '--xml'])
1144 1146 externals, changes, missing = [], [], []
1145 1147 doc = xml.dom.minidom.parseString(output)
1146 1148 for e in doc.getElementsByTagName('entry'):
1147 1149 s = e.getElementsByTagName('wc-status')
1148 1150 if not s:
1149 1151 continue
1150 1152 item = s[0].getAttribute('item')
1151 1153 props = s[0].getAttribute('props')
1152 1154 path = e.getAttribute('path')
1153 1155 if item == 'external':
1154 1156 externals.append(path)
1155 1157 elif item == 'missing':
1156 1158 missing.append(path)
1157 1159 if (item not in ('', 'normal', 'unversioned', 'external')
1158 1160 or props not in ('', 'none', 'normal')):
1159 1161 changes.append(path)
1160 1162 for path in changes:
1161 1163 for ext in externals:
1162 1164 if path == ext or path.startswith(ext + os.sep):
1163 1165 return True, True, bool(missing)
1164 1166 return bool(changes), False, bool(missing)
1165 1167
1166 1168 def dirty(self, ignoreupdate=False):
1167 1169 if not self._wcchanged()[0]:
1168 1170 if self._state[1] in self._wcrevs() or ignoreupdate:
1169 1171 return False
1170 1172 return True
1171 1173
1172 1174 def basestate(self):
1173 1175 lastrev, rev = self._wcrevs()
1174 1176 if lastrev != rev:
1175 1177 # Last committed rev is not the same than rev. We would
1176 1178 # like to take lastrev but we do not know if the subrepo
1177 1179 # URL exists at lastrev. Test it and fallback to rev it
1178 1180 # is not there.
1179 1181 try:
1180 1182 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1181 1183 return lastrev
1182 1184 except error.Abort:
1183 1185 pass
1184 1186 return rev
1185 1187
1186 1188 @annotatesubrepoerror
1187 1189 def commit(self, text, user, date):
1188 1190 # user and date are out of our hands since svn is centralized
1189 1191 changed, extchanged, missing = self._wcchanged()
1190 1192 if not changed:
1191 1193 return self.basestate()
1192 1194 if extchanged:
1193 1195 # Do not try to commit externals
1194 1196 raise error.Abort(_('cannot commit svn externals'))
1195 1197 if missing:
1196 1198 # svn can commit with missing entries but aborting like hg
1197 1199 # seems a better approach.
1198 1200 raise error.Abort(_('cannot commit missing svn entries'))
1199 1201 commitinfo, err = self._svncommand(['commit', '-m', text])
1200 1202 self.ui.status(commitinfo)
1201 1203 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1202 1204 if not newrev:
1203 1205 if not commitinfo.strip():
1204 1206 # Sometimes, our definition of "changed" differs from
1205 1207 # svn one. For instance, svn ignores missing files
1206 1208 # when committing. If there are only missing files, no
1207 1209 # commit is made, no output and no error code.
1208 1210 raise error.Abort(_('failed to commit svn changes'))
1209 1211 raise error.Abort(commitinfo.splitlines()[-1])
1210 1212 newrev = newrev.groups()[0]
1211 1213 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1212 1214 return newrev
1213 1215
1214 1216 @annotatesubrepoerror
1215 1217 def remove(self):
1216 1218 if self.dirty():
1217 1219 self.ui.warn(_('not removing repo %s because '
1218 1220 'it has changes.\n') % self._path)
1219 1221 return
1220 1222 self.ui.note(_('removing subrepo %s\n') % self._path)
1221 1223
1222 1224 self.wvfs.rmtree(forcibly=True)
1223 1225 try:
1224 1226 pwvfs = self._ctx.repo().wvfs
1225 1227 pwvfs.removedirs(pwvfs.dirname(self._path))
1226 1228 except OSError:
1227 1229 pass
1228 1230
1229 1231 @annotatesubrepoerror
1230 1232 def get(self, state, overwrite=False):
1231 1233 if overwrite:
1232 1234 self._svncommand(['revert', '--recursive'])
1233 1235 args = ['checkout']
1234 1236 if self._svnversion >= (1, 5):
1235 1237 args.append('--force')
1236 1238 # The revision must be specified at the end of the URL to properly
1237 1239 # update to a directory which has since been deleted and recreated.
1238 1240 args.append('%s@%s' % (state[0], state[1]))
1239 1241 status, err = self._svncommand(args, failok=True)
1240 1242 _sanitize(self.ui, self.wvfs, '.svn')
1241 1243 if not re.search('Checked out revision [0-9]+.', status):
1242 1244 if ('is already a working copy for a different URL' in err
1243 1245 and (self._wcchanged()[:2] == (False, False))):
1244 1246 # obstructed but clean working copy, so just blow it away.
1245 1247 self.remove()
1246 1248 self.get(state, overwrite=False)
1247 1249 return
1248 1250 raise error.Abort((status or err).splitlines()[-1])
1249 1251 self.ui.status(status)
1250 1252
1251 1253 @annotatesubrepoerror
1252 1254 def merge(self, state):
1253 1255 old = self._state[1]
1254 1256 new = state[1]
1255 1257 wcrev = self._wcrev()
1256 1258 if new != wcrev:
1257 1259 dirty = old == wcrev or self._wcchanged()[0]
1258 1260 if _updateprompt(self.ui, self, dirty, wcrev, new):
1259 1261 self.get(state, False)
1260 1262
1261 1263 def push(self, opts):
1262 1264 # push is a no-op for SVN
1263 1265 return True
1264 1266
1265 1267 @annotatesubrepoerror
1266 1268 def files(self):
1267 1269 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1268 1270 doc = xml.dom.minidom.parseString(output)
1269 1271 paths = []
1270 1272 for e in doc.getElementsByTagName('entry'):
1271 1273 kind = str(e.getAttribute('kind'))
1272 1274 if kind != 'file':
1273 1275 continue
1274 1276 name = ''.join(c.data for c
1275 1277 in e.getElementsByTagName('name')[0].childNodes
1276 1278 if c.nodeType == c.TEXT_NODE)
1277 1279 paths.append(name.encode('utf-8'))
1278 1280 return paths
1279 1281
1280 1282 def filedata(self, name):
1281 1283 return self._svncommand(['cat'], name)[0]
1282 1284
1283 1285
1284 1286 class gitsubrepo(abstractsubrepo):
1285 1287 def __init__(self, ctx, path, state):
1286 1288 super(gitsubrepo, self).__init__(ctx, path)
1287 1289 self._state = state
1288 1290 self._abspath = ctx.repo().wjoin(path)
1289 1291 self._subparent = ctx.repo()
1290 1292 self._ensuregit()
1291 1293
1292 1294 def _ensuregit(self):
1293 1295 try:
1294 1296 self._gitexecutable = 'git'
1295 1297 out, err = self._gitnodir(['--version'])
1296 1298 except OSError as e:
1297 1299 genericerror = _("error executing git for subrepo '%s': %s")
1298 1300 notfoundhint = _("check git is installed and in your PATH")
1299 1301 if e.errno != errno.ENOENT:
1300 1302 raise error.Abort(genericerror % (self._path, e.strerror))
1301 1303 elif os.name == 'nt':
1302 1304 try:
1303 1305 self._gitexecutable = 'git.cmd'
1304 1306 out, err = self._gitnodir(['--version'])
1305 1307 except OSError as e2:
1306 1308 if e2.errno == errno.ENOENT:
1307 1309 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1308 1310 " for subrepo '%s'") % self._path,
1309 1311 hint=notfoundhint)
1310 1312 else:
1311 1313 raise error.Abort(genericerror % (self._path,
1312 1314 e2.strerror))
1313 1315 else:
1314 1316 raise error.Abort(_("couldn't find git for subrepo '%s'")
1315 1317 % self._path, hint=notfoundhint)
1316 1318 versionstatus = self._checkversion(out)
1317 1319 if versionstatus == 'unknown':
1318 1320 self.ui.warn(_('cannot retrieve git version\n'))
1319 1321 elif versionstatus == 'abort':
1320 1322 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1321 1323 elif versionstatus == 'warning':
1322 1324 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1323 1325
1324 1326 @staticmethod
1325 1327 def _gitversion(out):
1326 1328 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
1327 1329 if m:
1328 1330 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1329 1331
1330 1332 m = re.search(r'^git version (\d+)\.(\d+)', out)
1331 1333 if m:
1332 1334 return (int(m.group(1)), int(m.group(2)), 0)
1333 1335
1334 1336 return -1
1335 1337
1336 1338 @staticmethod
1337 1339 def _checkversion(out):
1338 1340 '''ensure git version is new enough
1339 1341
1340 1342 >>> _checkversion = gitsubrepo._checkversion
1341 1343 >>> _checkversion('git version 1.6.0')
1342 1344 'ok'
1343 1345 >>> _checkversion('git version 1.8.5')
1344 1346 'ok'
1345 1347 >>> _checkversion('git version 1.4.0')
1346 1348 'abort'
1347 1349 >>> _checkversion('git version 1.5.0')
1348 1350 'warning'
1349 1351 >>> _checkversion('git version 1.9-rc0')
1350 1352 'ok'
1351 1353 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1352 1354 'ok'
1353 1355 >>> _checkversion('git version 1.9.0.GIT')
1354 1356 'ok'
1355 1357 >>> _checkversion('git version 12345')
1356 1358 'unknown'
1357 1359 >>> _checkversion('no')
1358 1360 'unknown'
1359 1361 '''
1360 1362 version = gitsubrepo._gitversion(out)
1361 1363 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1362 1364 # despite the docstring comment. For now, error on 1.4.0, warn on
1363 1365 # 1.5.0 but attempt to continue.
1364 1366 if version == -1:
1365 1367 return 'unknown'
1366 1368 if version < (1, 5, 0):
1367 1369 return 'abort'
1368 1370 elif version < (1, 6, 0):
1369 1371 return 'warning'
1370 1372 return 'ok'
1371 1373
1372 1374 def _gitcommand(self, commands, env=None, stream=False):
1373 1375 return self._gitdir(commands, env=env, stream=stream)[0]
1374 1376
1375 1377 def _gitdir(self, commands, env=None, stream=False):
1376 1378 return self._gitnodir(commands, env=env, stream=stream,
1377 1379 cwd=self._abspath)
1378 1380
1379 1381 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1380 1382 """Calls the git command
1381 1383
1382 1384 The methods tries to call the git command. versions prior to 1.6.0
1383 1385 are not supported and very probably fail.
1384 1386 """
1385 1387 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1386 1388 # unless ui.quiet is set, print git's stderr,
1387 1389 # which is mostly progress and useful info
1388 1390 errpipe = None
1389 1391 if self.ui.quiet:
1390 1392 errpipe = open(os.devnull, 'w')
1391 1393 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1392 1394 cwd=cwd, env=env, close_fds=util.closefds,
1393 1395 stdout=subprocess.PIPE, stderr=errpipe)
1394 1396 if stream:
1395 1397 return p.stdout, None
1396 1398
1397 1399 retdata = p.stdout.read().strip()
1398 1400 # wait for the child to exit to avoid race condition.
1399 1401 p.wait()
1400 1402
1401 1403 if p.returncode != 0 and p.returncode != 1:
1402 1404 # there are certain error codes that are ok
1403 1405 command = commands[0]
1404 1406 if command in ('cat-file', 'symbolic-ref'):
1405 1407 return retdata, p.returncode
1406 1408 # for all others, abort
1407 1409 raise error.Abort('git %s error %d in %s' %
1408 1410 (command, p.returncode, self._relpath))
1409 1411
1410 1412 return retdata, p.returncode
1411 1413
1412 1414 def _gitmissing(self):
1413 1415 return not self.wvfs.exists('.git')
1414 1416
1415 1417 def _gitstate(self):
1416 1418 return self._gitcommand(['rev-parse', 'HEAD'])
1417 1419
1418 1420 def _gitcurrentbranch(self):
1419 1421 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1420 1422 if err:
1421 1423 current = None
1422 1424 return current
1423 1425
1424 1426 def _gitremote(self, remote):
1425 1427 out = self._gitcommand(['remote', 'show', '-n', remote])
1426 1428 line = out.split('\n')[1]
1427 1429 i = line.index('URL: ') + len('URL: ')
1428 1430 return line[i:]
1429 1431
1430 1432 def _githavelocally(self, revision):
1431 1433 out, code = self._gitdir(['cat-file', '-e', revision])
1432 1434 return code == 0
1433 1435
1434 1436 def _gitisancestor(self, r1, r2):
1435 1437 base = self._gitcommand(['merge-base', r1, r2])
1436 1438 return base == r1
1437 1439
1438 1440 def _gitisbare(self):
1439 1441 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1440 1442
1441 1443 def _gitupdatestat(self):
1442 1444 """This must be run before git diff-index.
1443 1445 diff-index only looks at changes to file stat;
1444 1446 this command looks at file contents and updates the stat."""
1445 1447 self._gitcommand(['update-index', '-q', '--refresh'])
1446 1448
1447 1449 def _gitbranchmap(self):
1448 1450 '''returns 2 things:
1449 1451 a map from git branch to revision
1450 1452 a map from revision to branches'''
1451 1453 branch2rev = {}
1452 1454 rev2branch = {}
1453 1455
1454 1456 out = self._gitcommand(['for-each-ref', '--format',
1455 1457 '%(objectname) %(refname)'])
1456 1458 for line in out.split('\n'):
1457 1459 revision, ref = line.split(' ')
1458 1460 if (not ref.startswith('refs/heads/') and
1459 1461 not ref.startswith('refs/remotes/')):
1460 1462 continue
1461 1463 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1462 1464 continue # ignore remote/HEAD redirects
1463 1465 branch2rev[ref] = revision
1464 1466 rev2branch.setdefault(revision, []).append(ref)
1465 1467 return branch2rev, rev2branch
1466 1468
1467 1469 def _gittracking(self, branches):
1468 1470 'return map of remote branch to local tracking branch'
1469 1471 # assumes no more than one local tracking branch for each remote
1470 1472 tracking = {}
1471 1473 for b in branches:
1472 1474 if b.startswith('refs/remotes/'):
1473 1475 continue
1474 1476 bname = b.split('/', 2)[2]
1475 1477 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1476 1478 if remote:
1477 1479 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1478 1480 tracking['refs/remotes/%s/%s' %
1479 1481 (remote, ref.split('/', 2)[2])] = b
1480 1482 return tracking
1481 1483
1482 1484 def _abssource(self, source):
1483 1485 if '://' not in source:
1484 1486 # recognize the scp syntax as an absolute source
1485 1487 colon = source.find(':')
1486 1488 if colon != -1 and '/' not in source[:colon]:
1487 1489 return source
1488 1490 self._subsource = source
1489 1491 return _abssource(self)
1490 1492
1491 1493 def _fetch(self, source, revision):
1492 1494 if self._gitmissing():
1493 1495 source = self._abssource(source)
1494 1496 self.ui.status(_('cloning subrepo %s from %s\n') %
1495 1497 (self._relpath, source))
1496 1498 self._gitnodir(['clone', source, self._abspath])
1497 1499 if self._githavelocally(revision):
1498 1500 return
1499 1501 self.ui.status(_('pulling subrepo %s from %s\n') %
1500 1502 (self._relpath, self._gitremote('origin')))
1501 1503 # try only origin: the originally cloned repo
1502 1504 self._gitcommand(['fetch'])
1503 1505 if not self._githavelocally(revision):
1504 1506 raise error.Abort(_("revision %s does not exist in subrepo %s\n") %
1505 1507 (revision, self._relpath))
1506 1508
1507 1509 @annotatesubrepoerror
1508 1510 def dirty(self, ignoreupdate=False):
1509 1511 if self._gitmissing():
1510 1512 return self._state[1] != ''
1511 1513 if self._gitisbare():
1512 1514 return True
1513 1515 if not ignoreupdate and self._state[1] != self._gitstate():
1514 1516 # different version checked out
1515 1517 return True
1516 1518 # check for staged changes or modified files; ignore untracked files
1517 1519 self._gitupdatestat()
1518 1520 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1519 1521 return code == 1
1520 1522
1521 1523 def basestate(self):
1522 1524 return self._gitstate()
1523 1525
1524 1526 @annotatesubrepoerror
1525 1527 def get(self, state, overwrite=False):
1526 1528 source, revision, kind = state
1527 1529 if not revision:
1528 1530 self.remove()
1529 1531 return
1530 1532 self._fetch(source, revision)
1531 1533 # if the repo was set to be bare, unbare it
1532 1534 if self._gitisbare():
1533 1535 self._gitcommand(['config', 'core.bare', 'false'])
1534 1536 if self._gitstate() == revision:
1535 1537 self._gitcommand(['reset', '--hard', 'HEAD'])
1536 1538 return
1537 1539 elif self._gitstate() == revision:
1538 1540 if overwrite:
1539 1541 # first reset the index to unmark new files for commit, because
1540 1542 # reset --hard will otherwise throw away files added for commit,
1541 1543 # not just unmark them.
1542 1544 self._gitcommand(['reset', 'HEAD'])
1543 1545 self._gitcommand(['reset', '--hard', 'HEAD'])
1544 1546 return
1545 1547 branch2rev, rev2branch = self._gitbranchmap()
1546 1548
1547 1549 def checkout(args):
1548 1550 cmd = ['checkout']
1549 1551 if overwrite:
1550 1552 # first reset the index to unmark new files for commit, because
1551 1553 # the -f option will otherwise throw away files added for
1552 1554 # commit, not just unmark them.
1553 1555 self._gitcommand(['reset', 'HEAD'])
1554 1556 cmd.append('-f')
1555 1557 self._gitcommand(cmd + args)
1556 1558 _sanitize(self.ui, self.wvfs, '.git')
1557 1559
1558 1560 def rawcheckout():
1559 1561 # no branch to checkout, check it out with no branch
1560 1562 self.ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1561 1563 self._relpath)
1562 1564 self.ui.warn(_('check out a git branch if you intend '
1563 1565 'to make changes\n'))
1564 1566 checkout(['-q', revision])
1565 1567
1566 1568 if revision not in rev2branch:
1567 1569 rawcheckout()
1568 1570 return
1569 1571 branches = rev2branch[revision]
1570 1572 firstlocalbranch = None
1571 1573 for b in branches:
1572 1574 if b == 'refs/heads/master':
1573 1575 # master trumps all other branches
1574 1576 checkout(['refs/heads/master'])
1575 1577 return
1576 1578 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1577 1579 firstlocalbranch = b
1578 1580 if firstlocalbranch:
1579 1581 checkout([firstlocalbranch])
1580 1582 return
1581 1583
1582 1584 tracking = self._gittracking(branch2rev.keys())
1583 1585 # choose a remote branch already tracked if possible
1584 1586 remote = branches[0]
1585 1587 if remote not in tracking:
1586 1588 for b in branches:
1587 1589 if b in tracking:
1588 1590 remote = b
1589 1591 break
1590 1592
1591 1593 if remote not in tracking:
1592 1594 # create a new local tracking branch
1593 1595 local = remote.split('/', 3)[3]
1594 1596 checkout(['-b', local, remote])
1595 1597 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1596 1598 # When updating to a tracked remote branch,
1597 1599 # if the local tracking branch is downstream of it,
1598 1600 # a normal `git pull` would have performed a "fast-forward merge"
1599 1601 # which is equivalent to updating the local branch to the remote.
1600 1602 # Since we are only looking at branching at update, we need to
1601 1603 # detect this situation and perform this action lazily.
1602 1604 if tracking[remote] != self._gitcurrentbranch():
1603 1605 checkout([tracking[remote]])
1604 1606 self._gitcommand(['merge', '--ff', remote])
1605 1607 _sanitize(self.ui, self.wvfs, '.git')
1606 1608 else:
1607 1609 # a real merge would be required, just checkout the revision
1608 1610 rawcheckout()
1609 1611
1610 1612 @annotatesubrepoerror
1611 1613 def commit(self, text, user, date):
1612 1614 if self._gitmissing():
1613 1615 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1614 1616 cmd = ['commit', '-a', '-m', text]
1615 1617 env = os.environ.copy()
1616 1618 if user:
1617 1619 cmd += ['--author', user]
1618 1620 if date:
1619 1621 # git's date parser silently ignores when seconds < 1e9
1620 1622 # convert to ISO8601
1621 1623 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1622 1624 '%Y-%m-%dT%H:%M:%S %1%2')
1623 1625 self._gitcommand(cmd, env=env)
1624 1626 # make sure commit works otherwise HEAD might not exist under certain
1625 1627 # circumstances
1626 1628 return self._gitstate()
1627 1629
1628 1630 @annotatesubrepoerror
1629 1631 def merge(self, state):
1630 1632 source, revision, kind = state
1631 1633 self._fetch(source, revision)
1632 1634 base = self._gitcommand(['merge-base', revision, self._state[1]])
1633 1635 self._gitupdatestat()
1634 1636 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1635 1637
1636 1638 def mergefunc():
1637 1639 if base == revision:
1638 1640 self.get(state) # fast forward merge
1639 1641 elif base != self._state[1]:
1640 1642 self._gitcommand(['merge', '--no-commit', revision])
1641 1643 _sanitize(self.ui, self.wvfs, '.git')
1642 1644
1643 1645 if self.dirty():
1644 1646 if self._gitstate() != revision:
1645 1647 dirty = self._gitstate() == self._state[1] or code != 0
1646 1648 if _updateprompt(self.ui, self, dirty,
1647 1649 self._state[1][:7], revision[:7]):
1648 1650 mergefunc()
1649 1651 else:
1650 1652 mergefunc()
1651 1653
1652 1654 @annotatesubrepoerror
1653 1655 def push(self, opts):
1654 1656 force = opts.get('force')
1655 1657
1656 1658 if not self._state[1]:
1657 1659 return True
1658 1660 if self._gitmissing():
1659 1661 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1660 1662 # if a branch in origin contains the revision, nothing to do
1661 1663 branch2rev, rev2branch = self._gitbranchmap()
1662 1664 if self._state[1] in rev2branch:
1663 1665 for b in rev2branch[self._state[1]]:
1664 1666 if b.startswith('refs/remotes/origin/'):
1665 1667 return True
1666 1668 for b, revision in branch2rev.iteritems():
1667 1669 if b.startswith('refs/remotes/origin/'):
1668 1670 if self._gitisancestor(self._state[1], revision):
1669 1671 return True
1670 1672 # otherwise, try to push the currently checked out branch
1671 1673 cmd = ['push']
1672 1674 if force:
1673 1675 cmd.append('--force')
1674 1676
1675 1677 current = self._gitcurrentbranch()
1676 1678 if current:
1677 1679 # determine if the current branch is even useful
1678 1680 if not self._gitisancestor(self._state[1], current):
1679 1681 self.ui.warn(_('unrelated git branch checked out '
1680 1682 'in subrepo %s\n') % self._relpath)
1681 1683 return False
1682 1684 self.ui.status(_('pushing branch %s of subrepo %s\n') %
1683 1685 (current.split('/', 2)[2], self._relpath))
1684 1686 ret = self._gitdir(cmd + ['origin', current])
1685 1687 return ret[1] == 0
1686 1688 else:
1687 1689 self.ui.warn(_('no branch checked out in subrepo %s\n'
1688 1690 'cannot push revision %s\n') %
1689 1691 (self._relpath, self._state[1]))
1690 1692 return False
1691 1693
1692 1694 @annotatesubrepoerror
1693 1695 def add(self, ui, match, prefix, explicitonly, **opts):
1694 1696 if self._gitmissing():
1695 1697 return []
1696 1698
1697 1699 (modified, added, removed,
1698 1700 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1699 1701 clean=True)
1700 1702
1701 1703 tracked = set()
1702 1704 # dirstates 'amn' warn, 'r' is added again
1703 1705 for l in (modified, added, deleted, clean):
1704 1706 tracked.update(l)
1705 1707
1706 1708 # Unknown files not of interest will be rejected by the matcher
1707 1709 files = unknown
1708 1710 files.extend(match.files())
1709 1711
1710 1712 rejected = []
1711 1713
1712 1714 files = [f for f in sorted(set(files)) if match(f)]
1713 1715 for f in files:
1714 1716 exact = match.exact(f)
1715 1717 command = ["add"]
1716 1718 if exact:
1717 1719 command.append("-f") #should be added, even if ignored
1718 1720 if ui.verbose or not exact:
1719 1721 ui.status(_('adding %s\n') % match.rel(f))
1720 1722
1721 1723 if f in tracked: # hg prints 'adding' even if already tracked
1722 1724 if exact:
1723 1725 rejected.append(f)
1724 1726 continue
1725 1727 if not opts.get('dry_run'):
1726 1728 self._gitcommand(command + [f])
1727 1729
1728 1730 for f in rejected:
1729 1731 ui.warn(_("%s already tracked!\n") % match.abs(f))
1730 1732
1731 1733 return rejected
1732 1734
1733 1735 @annotatesubrepoerror
1734 1736 def remove(self):
1735 1737 if self._gitmissing():
1736 1738 return
1737 1739 if self.dirty():
1738 1740 self.ui.warn(_('not removing repo %s because '
1739 1741 'it has changes.\n') % self._relpath)
1740 1742 return
1741 1743 # we can't fully delete the repository as it may contain
1742 1744 # local-only history
1743 1745 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1744 1746 self._gitcommand(['config', 'core.bare', 'true'])
1745 1747 for f, kind in self.wvfs.readdir():
1746 1748 if f == '.git':
1747 1749 continue
1748 1750 if kind == stat.S_IFDIR:
1749 1751 self.wvfs.rmtree(f)
1750 1752 else:
1751 1753 self.wvfs.unlink(f)
1752 1754
1753 1755 def archive(self, archiver, prefix, match=None):
1754 1756 total = 0
1755 1757 source, revision = self._state
1756 1758 if not revision:
1757 1759 return total
1758 1760 self._fetch(source, revision)
1759 1761
1760 1762 # Parse git's native archive command.
1761 1763 # This should be much faster than manually traversing the trees
1762 1764 # and objects with many subprocess calls.
1763 1765 tarstream = self._gitcommand(['archive', revision], stream=True)
1764 1766 tar = tarfile.open(fileobj=tarstream, mode='r|')
1765 1767 relpath = subrelpath(self)
1766 1768 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1767 1769 for i, info in enumerate(tar):
1768 1770 if info.isdir():
1769 1771 continue
1770 1772 if match and not match(info.name):
1771 1773 continue
1772 1774 if info.issym():
1773 1775 data = info.linkname
1774 1776 else:
1775 1777 data = tar.extractfile(info).read()
1776 1778 archiver.addfile(prefix + self._path + '/' + info.name,
1777 1779 info.mode, info.issym(), data)
1778 1780 total += 1
1779 1781 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1780 1782 unit=_('files'))
1781 1783 self.ui.progress(_('archiving (%s)') % relpath, None)
1782 1784 return total
1783 1785
1784 1786
1785 1787 @annotatesubrepoerror
1786 1788 def cat(self, match, prefix, **opts):
1787 1789 rev = self._state[1]
1788 1790 if match.anypats():
1789 1791 return 1 #No support for include/exclude yet
1790 1792
1791 1793 if not match.files():
1792 1794 return 1
1793 1795
1794 1796 for f in match.files():
1795 1797 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1796 1798 fp = cmdutil.makefileobj(self._subparent, opts.get('output'),
1797 1799 self._ctx.node(),
1798 1800 pathname=self.wvfs.reljoin(prefix, f))
1799 1801 fp.write(output)
1800 1802 fp.close()
1801 1803 return 0
1802 1804
1803 1805
1804 1806 @annotatesubrepoerror
1805 1807 def status(self, rev2, **opts):
1806 1808 rev1 = self._state[1]
1807 1809 if self._gitmissing() or not rev1:
1808 1810 # if the repo is missing, return no results
1809 1811 return scmutil.status([], [], [], [], [], [], [])
1810 1812 modified, added, removed = [], [], []
1811 1813 self._gitupdatestat()
1812 1814 if rev2:
1813 1815 command = ['diff-tree', '-r', rev1, rev2]
1814 1816 else:
1815 1817 command = ['diff-index', rev1]
1816 1818 out = self._gitcommand(command)
1817 1819 for line in out.split('\n'):
1818 1820 tab = line.find('\t')
1819 1821 if tab == -1:
1820 1822 continue
1821 1823 status, f = line[tab - 1], line[tab + 1:]
1822 1824 if status == 'M':
1823 1825 modified.append(f)
1824 1826 elif status == 'A':
1825 1827 added.append(f)
1826 1828 elif status == 'D':
1827 1829 removed.append(f)
1828 1830
1829 1831 deleted, unknown, ignored, clean = [], [], [], []
1830 1832
1831 1833 command = ['status', '--porcelain', '-z']
1832 1834 if opts.get('unknown'):
1833 1835 command += ['--untracked-files=all']
1834 1836 if opts.get('ignored'):
1835 1837 command += ['--ignored']
1836 1838 out = self._gitcommand(command)
1837 1839
1838 1840 changedfiles = set()
1839 1841 changedfiles.update(modified)
1840 1842 changedfiles.update(added)
1841 1843 changedfiles.update(removed)
1842 1844 for line in out.split('\0'):
1843 1845 if not line:
1844 1846 continue
1845 1847 st = line[0:2]
1846 1848 #moves and copies show 2 files on one line
1847 1849 if line.find('\0') >= 0:
1848 1850 filename1, filename2 = line[3:].split('\0')
1849 1851 else:
1850 1852 filename1 = line[3:]
1851 1853 filename2 = None
1852 1854
1853 1855 changedfiles.add(filename1)
1854 1856 if filename2:
1855 1857 changedfiles.add(filename2)
1856 1858
1857 1859 if st == '??':
1858 1860 unknown.append(filename1)
1859 1861 elif st == '!!':
1860 1862 ignored.append(filename1)
1861 1863
1862 1864 if opts.get('clean'):
1863 1865 out = self._gitcommand(['ls-files'])
1864 1866 for f in out.split('\n'):
1865 1867 if not f in changedfiles:
1866 1868 clean.append(f)
1867 1869
1868 1870 return scmutil.status(modified, added, removed, deleted,
1869 1871 unknown, ignored, clean)
1870 1872
1871 1873 @annotatesubrepoerror
1872 1874 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1873 1875 node1 = self._state[1]
1874 1876 cmd = ['diff']
1875 1877 if opts['stat']:
1876 1878 cmd.append('--stat')
1877 1879 else:
1878 1880 # for Git, this also implies '-p'
1879 1881 cmd.append('-U%d' % diffopts.context)
1880 1882
1881 1883 gitprefix = self.wvfs.reljoin(prefix, self._path)
1882 1884
1883 1885 if diffopts.noprefix:
1884 1886 cmd.extend(['--src-prefix=%s/' % gitprefix,
1885 1887 '--dst-prefix=%s/' % gitprefix])
1886 1888 else:
1887 1889 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1888 1890 '--dst-prefix=b/%s/' % gitprefix])
1889 1891
1890 1892 if diffopts.ignorews:
1891 1893 cmd.append('--ignore-all-space')
1892 1894 if diffopts.ignorewsamount:
1893 1895 cmd.append('--ignore-space-change')
1894 1896 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1895 1897 and diffopts.ignoreblanklines:
1896 1898 cmd.append('--ignore-blank-lines')
1897 1899
1898 1900 cmd.append(node1)
1899 1901 if node2:
1900 1902 cmd.append(node2)
1901 1903
1902 1904 output = ""
1903 1905 if match.always():
1904 1906 output += self._gitcommand(cmd) + '\n'
1905 1907 else:
1906 1908 st = self.status(node2)[:3]
1907 1909 files = [f for sublist in st for f in sublist]
1908 1910 for f in files:
1909 1911 if match(f):
1910 1912 output += self._gitcommand(cmd + ['--', f]) + '\n'
1911 1913
1912 1914 if output.strip():
1913 1915 ui.write(output)
1914 1916
1915 1917 @annotatesubrepoerror
1916 1918 def revert(self, substate, *pats, **opts):
1917 1919 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1918 1920 if not opts.get('no_backup'):
1919 1921 status = self.status(None)
1920 1922 names = status.modified
1921 1923 for name in names:
1922 1924 bakname = scmutil.origpath(self.ui, self._subparent, name)
1923 1925 self.ui.note(_('saving current version of %s as %s\n') %
1924 1926 (name, bakname))
1925 1927 self.wvfs.rename(name, bakname)
1926 1928
1927 1929 if not opts.get('dry_run'):
1928 1930 self.get(substate, overwrite=True)
1929 1931 return []
1930 1932
1931 1933 def shortid(self, revid):
1932 1934 return revid[:7]
1933 1935
1934 1936 types = {
1935 1937 'hg': hgsubrepo,
1936 1938 'svn': svnsubrepo,
1937 1939 'git': gitsubrepo,
1938 1940 }
@@ -1,350 +1,350 b''
1 1 $ remove() {
2 2 > hg rm $@
3 3 > echo "exit code: $?"
4 4 > hg st
5 5 > # do not use ls -R, which recurses in .hg subdirs on Mac OS X 10.5
6 6 > find . -name .hg -prune -o -type f -print | sort
7 7 > hg up -C
8 8 > }
9 9
10 10 $ cat >> $HGRCPATH <<EOF
11 11 > [progress]
12 12 > disable=False
13 13 > assume-tty = 1
14 14 > delay = 0
15 15 > # set changedelay really large so we don't see nested topics
16 16 > changedelay = 30000
17 17 > format = topic bar number
18 18 > refresh = 0
19 19 > width = 60
20 20 > EOF
21 21
22 22 $ hg init a
23 23 $ cd a
24 24 $ echo a > foo
25 25
26 26 file not managed
27 27
28 28 $ remove foo
29 29 not removing foo: file is untracked
30 30 exit code: 1
31 31 ? foo
32 32 ./foo
33 33 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
34 34
35 35 $ hg add foo
36 36 $ hg commit -m1
37 37
38 38 the table cases
39 39 00 state added, options none
40 40
41 41 $ echo b > bar
42 42 $ hg add bar
43 43 $ remove bar
44 44 not removing bar: file has been marked for add (use forget to undo)
45 45 exit code: 1
46 46 A bar
47 47 ./bar
48 48 ./foo
49 49 \r (no-eol) (esc)
50 50 updating [===========================================>] 1/1\r (no-eol) (esc)
51 51 \r (no-eol) (esc)
52 52 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
53 53
54 54 01 state clean, options none
55 55
56 56 $ remove foo
57 57 exit code: 0
58 58 R foo
59 59 ? bar
60 60 ./bar
61 61 \r (no-eol) (esc)
62 62 updating [===========================================>] 1/1\r (no-eol) (esc)
63 63 \r (no-eol) (esc)
64 64 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
65 65
66 66 02 state modified, options none
67 67
68 68 $ echo b >> foo
69 69 $ remove foo
70 70 not removing foo: file is modified (use -f to force removal)
71 71 exit code: 1
72 72 M foo
73 73 ? bar
74 74 ./bar
75 75 ./foo
76 76 \r (no-eol) (esc)
77 77 updating [===========================================>] 1/1\r (no-eol) (esc)
78 78 \r (no-eol) (esc)
79 79 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
80 80
81 81 03 state missing, options none
82 82
83 83 $ rm foo
84 84 $ remove foo
85 85 exit code: 0
86 86 R foo
87 87 ? bar
88 88 ./bar
89 89 \r (no-eol) (esc)
90 90 updating [===========================================>] 1/1\r (no-eol) (esc)
91 91 \r (no-eol) (esc)
92 92 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
93 93
94 94 10 state added, options -f
95 95
96 96 $ echo b > bar
97 97 $ hg add bar
98 98 $ remove -f bar
99 99 exit code: 0
100 100 ? bar
101 101 ./bar
102 102 ./foo
103 103 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
104 104 $ rm bar
105 105
106 106 11 state clean, options -f
107 107
108 108 $ remove -f foo
109 109 exit code: 0
110 110 R foo
111 111 \r (no-eol) (esc)
112 112 updating [===========================================>] 1/1\r (no-eol) (esc)
113 113 \r (no-eol) (esc)
114 114 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
115 115
116 116 12 state modified, options -f
117 117
118 118 $ echo b >> foo
119 119 $ remove -f foo
120 120 exit code: 0
121 121 R foo
122 122 \r (no-eol) (esc)
123 123 updating [===========================================>] 1/1\r (no-eol) (esc)
124 124 \r (no-eol) (esc)
125 125 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
126 126
127 127 13 state missing, options -f
128 128
129 129 $ rm foo
130 130 $ remove -f foo
131 131 exit code: 0
132 132 R foo
133 133 \r (no-eol) (esc)
134 134 updating [===========================================>] 1/1\r (no-eol) (esc)
135 135 \r (no-eol) (esc)
136 136 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
137 137
138 138 20 state added, options -A
139 139
140 140 $ echo b > bar
141 141 $ hg add bar
142 142 $ remove -A bar
143 143 not removing bar: file still exists
144 144 exit code: 1
145 145 A bar
146 146 ./bar
147 147 ./foo
148 148 \r (no-eol) (esc)
149 149 updating [===========================================>] 1/1\r (no-eol) (esc)
150 150 \r (no-eol) (esc)
151 151 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
152 152
153 153 21 state clean, options -A
154 154
155 155 $ remove -A foo
156 156 not removing foo: file still exists
157 157 exit code: 1
158 158 ? bar
159 159 ./bar
160 160 ./foo
161 161 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
162 162
163 163 22 state modified, options -A
164 164
165 165 $ echo b >> foo
166 166 $ remove -A foo
167 167 not removing foo: file still exists
168 168 exit code: 1
169 169 M foo
170 170 ? bar
171 171 ./bar
172 172 ./foo
173 173 \r (no-eol) (esc)
174 174 updating [===========================================>] 1/1\r (no-eol) (esc)
175 175 \r (no-eol) (esc)
176 176 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
177 177
178 178 23 state missing, options -A
179 179
180 180 $ rm foo
181 181 $ remove -A foo
182 182 exit code: 0
183 183 R foo
184 184 ? bar
185 185 ./bar
186 186 \r (no-eol) (esc)
187 187 updating [===========================================>] 1/1\r (no-eol) (esc)
188 188 \r (no-eol) (esc)
189 189 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
190 190
191 191 30 state added, options -Af
192 192
193 193 $ echo b > bar
194 194 $ hg add bar
195 195 $ remove -Af bar
196 196 exit code: 0
197 197 ? bar
198 198 ./bar
199 199 ./foo
200 200 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
201 201 $ rm bar
202 202
203 203 31 state clean, options -Af
204 204
205 205 $ remove -Af foo
206 206 exit code: 0
207 207 R foo
208 208 ./foo
209 209 \r (no-eol) (esc)
210 210 updating [===========================================>] 1/1\r (no-eol) (esc)
211 211 \r (no-eol) (esc)
212 212 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
213 213
214 214 32 state modified, options -Af
215 215
216 216 $ echo b >> foo
217 217 $ remove -Af foo
218 218 exit code: 0
219 219 R foo
220 220 ./foo
221 221 \r (no-eol) (esc)
222 222 updating [===========================================>] 1/1\r (no-eol) (esc)
223 223 \r (no-eol) (esc)
224 224 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
225 225
226 226 33 state missing, options -Af
227 227
228 228 $ rm foo
229 229 $ remove -Af foo
230 230 exit code: 0
231 231 R foo
232 232 \r (no-eol) (esc)
233 233 updating [===========================================>] 1/1\r (no-eol) (esc)
234 234 \r (no-eol) (esc)
235 235 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
236 236
237 237 test some directory stuff
238 238
239 239 $ mkdir test
240 240 $ echo a > test/foo
241 241 $ echo b > test/bar
242 242 $ hg ci -Am2
243 243 adding test/bar
244 244 adding test/foo
245 245
246 246 dir, options none
247 247
248 248 $ rm test/bar
249 249 $ remove test
250 250 removing test/bar (glob)
251 251 removing test/foo (glob)
252 252 exit code: 0
253 253 R test/bar
254 254 R test/foo
255 255 ./foo
256 256 \r (no-eol) (esc)
257 257 updating [===========================================>] 2/2\r (no-eol) (esc)
258 258 \r (no-eol) (esc)
259 259 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
260 260
261 261 dir, options -f
262 262
263 263 $ rm test/bar
264 264 $ remove -f test
265 265 removing test/bar (glob)
266 266 removing test/foo (glob)
267 267 exit code: 0
268 268 R test/bar
269 269 R test/foo
270 270 ./foo
271 271 \r (no-eol) (esc)
272 272 updating [===========================================>] 2/2\r (no-eol) (esc)
273 273 \r (no-eol) (esc)
274 274 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
275 275
276 276 dir, options -A
277 277
278 278 $ rm test/bar
279 279 $ remove -A test
280 removing test/bar (glob)
280 281 not removing test/foo: file still exists (glob)
281 removing test/bar (glob)
282 282 exit code: 1
283 283 R test/bar
284 284 ./foo
285 285 ./test/foo
286 286 \r (no-eol) (esc)
287 287 updating [===========================================>] 1/1\r (no-eol) (esc)
288 288 \r (no-eol) (esc)
289 289 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
290 290
291 291 dir, options -Af
292 292
293 293 $ rm test/bar
294 294 $ remove -Af test
295 295 removing test/bar (glob)
296 296 removing test/foo (glob)
297 297 exit code: 0
298 298 R test/bar
299 299 R test/foo
300 300 ./foo
301 301 ./test/foo
302 302 \r (no-eol) (esc)
303 303 updating [===========================================>] 2/2\r (no-eol) (esc)
304 304 \r (no-eol) (esc)
305 305 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
306 306
307 307 test remove dropping empty trees (issue1861)
308 308
309 309 $ mkdir -p issue1861/b/c
310 310 $ echo x > issue1861/x
311 311 $ echo y > issue1861/b/c/y
312 312 $ hg ci -Am add
313 313 adding issue1861/b/c/y
314 314 adding issue1861/x
315 315 $ hg rm issue1861/b
316 316 removing issue1861/b/c/y (glob)
317 317 $ hg ci -m remove
318 318 $ ls issue1861
319 319 x
320 320
321 321 test that commit does not crash if the user removes a newly added file
322 322
323 323 $ touch f1
324 324 $ hg add f1
325 325 $ rm f1
326 326 $ hg ci -A -mx
327 327 removing f1
328 328 nothing changed
329 329 [1]
330 330
331 331 handling of untracked directories and missing files
332 332
333 333 $ mkdir d1
334 334 $ echo a > d1/a
335 335 $ hg rm --after d1
336 336 not removing d1: no tracked files
337 337 [1]
338 338 $ hg add d1/a
339 339 $ rm d1/a
340 340 $ hg rm --after d1
341 341 removing d1/a (glob)
342 342 #if windows
343 343 $ hg rm --after nosuch
344 344 nosuch: * (glob)
345 345 [1]
346 346 #else
347 347 $ hg rm --after nosuch
348 348 nosuch: No such file or directory
349 349 [1]
350 350 #endif
General Comments 0
You need to be logged in to leave comments. Login now