comparison mercurial/subrepo.py @ 24803:e89f909edffa stable 3.4-rc

merge default into stable for 3.4 freeze
author Matt Mackall <mpm@selenic.com>
date Thu, 16 Apr 2015 20:57:51 -0500
parents 56e15db9109f
children a99931201d1b
comparison
equal deleted inserted replaced
24753:612ed41ae359 24803:e89f909edffa
4 # 4 #
5 # This software may be used and distributed according to the terms of the 5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version. 6 # GNU General Public License version 2 or any later version.
7 7
8 import copy 8 import copy
9 import errno, os, re, shutil, posixpath, sys 9 import errno, os, re, posixpath, sys
10 import xml.dom.minidom 10 import xml.dom.minidom
11 import stat, subprocess, tarfile 11 import stat, subprocess, tarfile
12 from i18n import _ 12 from i18n import _
13 import config, util, node, error, cmdutil, scmutil, match as matchmod 13 import config, util, node, error, cmdutil, scmutil, match as matchmod
14 import phases 14 import phases
68 data = ctx[f].data() 68 data = ctx[f].data()
69 except IOError, err: 69 except IOError, err:
70 if err.errno != errno.ENOENT: 70 if err.errno != errno.ENOENT:
71 raise 71 raise
72 # handle missing subrepo spec files as removed 72 # handle missing subrepo spec files as removed
73 ui.warn(_("warning: subrepo spec file %s not found\n") % f) 73 ui.warn(_("warning: subrepo spec file \'%s\' not found\n") %
74 util.pathto(ctx.repo().root, ctx.repo().getcwd(), f))
74 return 75 return
75 p.parse(f, data, sections, remap, read) 76 p.parse(f, data, sections, remap, read)
76 else: 77 else:
77 raise util.Abort(_("subrepo spec file %s not found") % f) 78 repo = ctx.repo()
79 raise util.Abort(_("subrepo spec file \'%s\' not found") %
80 util.pathto(repo.root, repo.getcwd(), f))
78 81
79 if '.hgsub' in ctx: 82 if '.hgsub' in ctx:
80 read('.hgsub') 83 read('.hgsub')
81 84
82 for path, src in ui.configitems('subpaths'): 85 for path, src in ui.configitems('subpaths'):
90 if not l: 93 if not l:
91 continue 94 continue
92 try: 95 try:
93 revision, path = l.split(" ", 1) 96 revision, path = l.split(" ", 1)
94 except ValueError: 97 except ValueError:
98 repo = ctx.repo()
95 raise util.Abort(_("invalid subrepository revision " 99 raise util.Abort(_("invalid subrepository revision "
96 "specifier in .hgsubstate line %d") 100 "specifier in \'%s\' line %d")
97 % (i + 1)) 101 % (util.pathto(repo.root, repo.getcwd(),
102 '.hgsubstate'), (i + 1)))
98 rev[path] = revision 103 rev[path] = revision
99 except IOError, err: 104 except IOError, err:
100 if err.errno != errno.ENOENT: 105 if err.errno != errno.ENOENT:
101 raise 106 raise
102 107
125 kind, src = src.split(']', 1) 130 kind, src = src.split(']', 1)
126 kind = kind[1:] 131 kind = kind[1:]
127 src = src.lstrip() # strip any extra whitespace after ']' 132 src = src.lstrip() # strip any extra whitespace after ']'
128 133
129 if not util.url(src).isabs(): 134 if not util.url(src).isabs():
130 parent = _abssource(ctx._repo, abort=False) 135 parent = _abssource(ctx.repo(), abort=False)
131 if parent: 136 if parent:
132 parent = util.url(parent) 137 parent = util.url(parent)
133 parent.path = posixpath.join(parent.path or '', src) 138 parent.path = posixpath.join(parent.path or '', src)
134 parent.path = posixpath.normpath(parent.path) 139 parent.path = posixpath.normpath(parent.path)
135 joined = str(parent) 140 joined = str(parent)
273 parent = parent._subparent 278 parent = parent._subparent
274 return repo.root[len(pathutil.normasprefix(parent.root)):] 279 return repo.root[len(pathutil.normasprefix(parent.root)):]
275 280
276 def subrelpath(sub): 281 def subrelpath(sub):
277 """return path to this subrepo as seen from outermost repo""" 282 """return path to this subrepo as seen from outermost repo"""
278 if util.safehasattr(sub, '_relpath'): 283 return sub._relpath
279 return sub._relpath
280 if not util.safehasattr(sub, '_repo'):
281 return sub._path
282 return reporelpath(sub._repo)
283 284
284 def _abssource(repo, push=False, abort=True): 285 def _abssource(repo, push=False, abort=True):
285 """return pull/push path of repo - either based on parent repo .hgsub info 286 """return pull/push path of repo - either based on parent repo .hgsub info
286 or on the top repo config. Abort or return None if no source found.""" 287 or on the top repo config. Abort or return None if no source found."""
287 if util.safehasattr(repo, '_subparent'): 288 if util.safehasattr(repo, '_subparent'):
306 # chop off the .hg component to get the default path form 307 # chop off the .hg component to get the default path form
307 return os.path.dirname(repo.sharedpath) 308 return os.path.dirname(repo.sharedpath)
308 if abort: 309 if abort:
309 raise util.Abort(_("default path for subrepository not found")) 310 raise util.Abort(_("default path for subrepository not found"))
310 311
311 def _sanitize(ui, path, ignore): 312 def _sanitize(ui, vfs, ignore):
312 for dirname, dirs, names in os.walk(path): 313 for dirname, dirs, names in vfs.walk():
313 for i, d in enumerate(dirs): 314 for i, d in enumerate(dirs):
314 if d.lower() == ignore: 315 if d.lower() == ignore:
315 del dirs[i] 316 del dirs[i]
316 break 317 break
317 if os.path.basename(dirname).lower() != '.hg': 318 if os.path.basename(dirname).lower() != '.hg':
318 continue 319 continue
319 for f in names: 320 for f in names:
320 if f.lower() == 'hgrc': 321 if f.lower() == 'hgrc':
321 ui.warn(_("warning: removing potentially hostile 'hgrc' " 322 ui.warn(_("warning: removing potentially hostile 'hgrc' "
322 "in '%s'\n") % dirname) 323 "in '%s'\n") % vfs.join(dirname))
323 os.unlink(os.path.join(dirname, f)) 324 vfs.unlink(vfs.reljoin(dirname, f))
324 325
325 def subrepo(ctx, path): 326 def subrepo(ctx, path):
326 """return instance of the right subrepo class for subrepo in path""" 327 """return instance of the right subrepo class for subrepo in path"""
327 # subrepo inherently violates our import layering rules 328 # subrepo inherently violates our import layering rules
328 # because it wants to make repo objects from deep inside the stack 329 # because it wants to make repo objects from deep inside the stack
330 # scripts that don't use our demand-loading 331 # scripts that don't use our demand-loading
331 global hg 332 global hg
332 import hg as h 333 import hg as h
333 hg = h 334 hg = h
334 335
335 pathutil.pathauditor(ctx._repo.root)(path) 336 pathutil.pathauditor(ctx.repo().root)(path)
336 state = ctx.substate[path] 337 state = ctx.substate[path]
337 if state[2] not in types: 338 if state[2] not in types:
338 raise util.Abort(_('unknown subrepo type %s') % state[2]) 339 raise util.Abort(_('unknown subrepo type %s') % state[2])
339 return types[state[2]](ctx, path, state[:2]) 340 return types[state[2]](ctx, path, state[:2])
340 341
371 372
372 # subrepo classes need to implement the following abstract class: 373 # subrepo classes need to implement the following abstract class:
373 374
374 class abstractsubrepo(object): 375 class abstractsubrepo(object):
375 376
376 def __init__(self, ui): 377 def __init__(self, ctx, path):
377 self.ui = ui 378 """Initialize abstractsubrepo part
379
380 ``ctx`` is the context referring this subrepository in the
381 parent repository.
382
383 ``path`` is the path to this subrepositiry as seen from
384 innermost repository.
385 """
386 self.ui = ctx.repo().ui
387 self._ctx = ctx
388 self._path = path
378 389
379 def storeclean(self, path): 390 def storeclean(self, path):
380 """ 391 """
381 returns true if the repository has not changed since it was last 392 returns true if the repository has not changed since it was last
382 cloned from or pushed to a given repository. 393 cloned from or pushed to a given repository.
388 match current stored state. If ignoreupdate is true, only check 399 match current stored state. If ignoreupdate is true, only check
389 whether the subrepo has uncommitted changes in its dirstate. 400 whether the subrepo has uncommitted changes in its dirstate.
390 """ 401 """
391 raise NotImplementedError 402 raise NotImplementedError
392 403
404 def dirtyreason(self, ignoreupdate=False):
405 """return reason string if it is ``dirty()``
406
407 Returned string should have enough information for the message
408 of exception.
409
410 This returns None, otherwise.
411 """
412 if self.dirty(ignoreupdate=ignoreupdate):
413 return _("uncommitted changes in subrepository '%s'"
414 ) % subrelpath(self)
415
416 def bailifchanged(self, ignoreupdate=False):
417 """raise Abort if subrepository is ``dirty()``
418 """
419 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate)
420 if dirtyreason:
421 raise util.Abort(dirtyreason)
422
393 def basestate(self): 423 def basestate(self):
394 """current working directory base state, disregarding .hgsubstate 424 """current working directory base state, disregarding .hgsubstate
395 state and working directory modifications""" 425 state and working directory modifications"""
396 raise NotImplementedError 426 raise NotImplementedError
397 427
466 raise NotImplementedError 496 raise NotImplementedError
467 497
468 def fileflags(self, name): 498 def fileflags(self, name):
469 """return file flags""" 499 """return file flags"""
470 return '' 500 return ''
501
502 def printfiles(self, ui, m, fm, fmt):
503 """handle the files command for this subrepo"""
504 return 1
471 505
472 def archive(self, archiver, prefix, match=None): 506 def archive(self, archiver, prefix, match=None):
473 if match is not None: 507 if match is not None:
474 files = [f for f in self.files() if match(f)] 508 files = [f for f in self.files() if match(f)]
475 else: 509 else:
480 unit=_('files'), total=total) 514 unit=_('files'), total=total)
481 for i, name in enumerate(files): 515 for i, name in enumerate(files):
482 flags = self.fileflags(name) 516 flags = self.fileflags(name)
483 mode = 'x' in flags and 0755 or 0644 517 mode = 'x' in flags and 0755 or 0644
484 symlink = 'l' in flags 518 symlink = 'l' in flags
485 archiver.addfile(os.path.join(prefix, self._path, name), 519 archiver.addfile(self.wvfs.reljoin(prefix, self._path, name),
486 mode, symlink, self.filedata(name)) 520 mode, symlink, self.filedata(name))
487 self.ui.progress(_('archiving (%s)') % relpath, i + 1, 521 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
488 unit=_('files'), total=total) 522 unit=_('files'), total=total)
489 self.ui.progress(_('archiving (%s)') % relpath, None) 523 self.ui.progress(_('archiving (%s)') % relpath, None)
490 return total 524 return total
512 return [] 546 return []
513 547
514 def shortid(self, revid): 548 def shortid(self, revid):
515 return revid 549 return revid
516 550
551 @propertycache
552 def wvfs(self):
553 """return vfs to access the working directory of this subrepository
554 """
555 return scmutil.vfs(self._ctx.repo().wvfs.join(self._path))
556
557 @propertycache
558 def _relpath(self):
559 """return path to this subrepository as seen from outermost repository
560 """
561 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
562
517 class hgsubrepo(abstractsubrepo): 563 class hgsubrepo(abstractsubrepo):
518 def __init__(self, ctx, path, state): 564 def __init__(self, ctx, path, state):
519 super(hgsubrepo, self).__init__(ctx._repo.ui) 565 super(hgsubrepo, self).__init__(ctx, path)
520 self._path = path
521 self._state = state 566 self._state = state
522 r = ctx._repo 567 r = ctx.repo()
523 root = r.wjoin(path) 568 root = r.wjoin(path)
524 create = not r.wvfs.exists('%s/.hg' % path) 569 create = not r.wvfs.exists('%s/.hg' % path)
525 self._repo = hg.repository(r.baseui, root, create=create) 570 self._repo = hg.repository(r.baseui, root, create=create)
526 self.ui = self._repo.ui 571 self.ui = self._repo.ui
527 for s, k in [('ui', 'commitsubrepos')]: 572 for s, k in [('ui', 'commitsubrepos')]:
621 fp.close() 666 fp.close()
622 667
623 @annotatesubrepoerror 668 @annotatesubrepoerror
624 def add(self, ui, match, prefix, explicitonly, **opts): 669 def add(self, ui, match, prefix, explicitonly, **opts):
625 return cmdutil.add(ui, self._repo, match, 670 return cmdutil.add(ui, self._repo, match,
626 os.path.join(prefix, self._path), explicitonly, 671 self.wvfs.reljoin(prefix, self._path),
627 **opts) 672 explicitonly, **opts)
628 673
674 @annotatesubrepoerror
629 def addremove(self, m, prefix, opts, dry_run, similarity): 675 def addremove(self, m, prefix, opts, dry_run, similarity):
630 # In the same way as sub directories are processed, once in a subrepo, 676 # In the same way as sub directories are processed, once in a subrepo,
631 # always entry any of its subrepos. Don't corrupt the options that will 677 # always entry any of its subrepos. Don't corrupt the options that will
632 # be used to process sibling subrepos however. 678 # be used to process sibling subrepos however.
633 opts = copy.copy(opts) 679 opts = copy.copy(opts)
634 opts['subrepos'] = True 680 opts['subrepos'] = True
635 return scmutil.addremove(self._repo, m, 681 return scmutil.addremove(self._repo, m,
636 os.path.join(prefix, self._path), opts, 682 self.wvfs.reljoin(prefix, self._path), opts,
637 dry_run, similarity) 683 dry_run, similarity)
638 684
639 @annotatesubrepoerror 685 @annotatesubrepoerror
640 def cat(self, match, prefix, **opts): 686 def cat(self, match, prefix, **opts):
641 rev = self._state[1] 687 rev = self._state[1]
678 ctx = self._repo[rev] 724 ctx = self._repo[rev]
679 for subpath in ctx.substate: 725 for subpath in ctx.substate:
680 s = subrepo(ctx, subpath) 726 s = subrepo(ctx, subpath)
681 submatch = matchmod.narrowmatcher(subpath, match) 727 submatch = matchmod.narrowmatcher(subpath, match)
682 total += s.archive( 728 total += s.archive(
683 archiver, os.path.join(prefix, self._path), submatch) 729 archiver, self.wvfs.reljoin(prefix, self._path), submatch)
684 return total 730 return total
685 731
686 @annotatesubrepoerror 732 @annotatesubrepoerror
687 def dirty(self, ignoreupdate=False): 733 def dirty(self, ignoreupdate=False):
688 r = self._state[1] 734 r = self._state[1]
732 other = hg.peer(self._repo, {}, srcurl) 778 other = hg.peer(self._repo, {}, srcurl)
733 if len(self._repo) == 0: 779 if len(self._repo) == 0:
734 self.ui.status(_('cloning subrepo %s from %s\n') 780 self.ui.status(_('cloning subrepo %s from %s\n')
735 % (subrelpath(self), srcurl)) 781 % (subrelpath(self), srcurl))
736 parentrepo = self._repo._subparent 782 parentrepo = self._repo._subparent
737 shutil.rmtree(self._repo.path) 783 # use self._repo.vfs instead of self.wvfs to remove .hg only
784 self._repo.vfs.rmtree()
738 other, cloned = hg.clone(self._repo._subparent.baseui, {}, 785 other, cloned = hg.clone(self._repo._subparent.baseui, {},
739 other, self._repo.root, 786 other, self._repo.root,
740 update=False) 787 update=False)
741 self._repo = cloned.local() 788 self._repo = cloned.local()
742 self._initrepo(parentrepo, source, create=True) 789 self._initrepo(parentrepo, source, create=True)
833 880
834 @annotatesubrepoerror 881 @annotatesubrepoerror
835 def files(self): 882 def files(self):
836 rev = self._state[1] 883 rev = self._state[1]
837 ctx = self._repo[rev] 884 ctx = self._repo[rev]
838 return ctx.manifest() 885 return ctx.manifest().keys()
839 886
840 def filedata(self, name): 887 def filedata(self, name):
841 rev = self._state[1] 888 rev = self._state[1]
842 return self._repo[rev][name].data() 889 return self._repo[rev][name].data()
843 890
844 def fileflags(self, name): 891 def fileflags(self, name):
845 rev = self._state[1] 892 rev = self._state[1]
846 ctx = self._repo[rev] 893 ctx = self._repo[rev]
847 return ctx.flags(name) 894 return ctx.flags(name)
848 895
896 @annotatesubrepoerror
897 def printfiles(self, ui, m, fm, fmt):
898 # If the parent context is a workingctx, use the workingctx here for
899 # consistency.
900 if self._ctx.rev() is None:
901 ctx = self._repo[None]
902 else:
903 rev = self._state[1]
904 ctx = self._repo[rev]
905 return cmdutil.files(ui, ctx, m, fm, fmt, True)
906
849 def walk(self, match): 907 def walk(self, match):
850 ctx = self._repo[None] 908 ctx = self._repo[None]
851 return ctx.walk(match) 909 return ctx.walk(match)
852 910
853 @annotatesubrepoerror 911 @annotatesubrepoerror
854 def forget(self, match, prefix): 912 def forget(self, match, prefix):
855 return cmdutil.forget(self.ui, self._repo, match, 913 return cmdutil.forget(self.ui, self._repo, match,
856 os.path.join(prefix, self._path), True) 914 self.wvfs.reljoin(prefix, self._path), True)
857 915
858 @annotatesubrepoerror 916 @annotatesubrepoerror
859 def removefiles(self, matcher, prefix, after, force, subrepos): 917 def removefiles(self, matcher, prefix, after, force, subrepos):
860 return cmdutil.remove(self.ui, self._repo, matcher, 918 return cmdutil.remove(self.ui, self._repo, matcher,
861 os.path.join(prefix, self._path), after, force, 919 self.wvfs.reljoin(prefix, self._path),
862 subrepos) 920 after, force, subrepos)
863 921
864 @annotatesubrepoerror 922 @annotatesubrepoerror
865 def revert(self, substate, *pats, **opts): 923 def revert(self, substate, *pats, **opts):
866 # reverting a subrepo is a 2 step process: 924 # reverting a subrepo is a 2 step process:
867 # 1. if the no_backup is not set, revert all modified 925 # 1. if the no_backup is not set, revert all modified
875 # We could do it if there was a set:subrepos() predicate 933 # We could do it if there was a set:subrepos() predicate
876 opts = opts.copy() 934 opts = opts.copy()
877 opts['date'] = None 935 opts['date'] = None
878 opts['rev'] = substate[1] 936 opts['rev'] = substate[1]
879 937
880 pats = []
881 if not opts.get('all'):
882 pats = ['set:modified()']
883 self.filerevert(*pats, **opts) 938 self.filerevert(*pats, **opts)
884 939
885 # Update the repo to the revision specified in the given substate 940 # Update the repo to the revision specified in the given substate
886 self.get(substate, overwrite=True) 941 if not opts.get('dry_run'):
942 self.get(substate, overwrite=True)
887 943
888 def filerevert(self, *pats, **opts): 944 def filerevert(self, *pats, **opts):
889 ctx = self._repo[opts['rev']] 945 ctx = self._repo[opts['rev']]
890 parents = self._repo.dirstate.parents() 946 parents = self._repo.dirstate.parents()
891 if opts.get('all'): 947 if opts.get('all'):
895 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts) 951 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
896 952
897 def shortid(self, revid): 953 def shortid(self, revid):
898 return revid[:12] 954 return revid[:12]
899 955
956 @propertycache
957 def wvfs(self):
958 """return own wvfs for efficiency and consitency
959 """
960 return self._repo.wvfs
961
962 @propertycache
963 def _relpath(self):
964 """return path to this subrepository as seen from outermost repository
965 """
966 # Keep consistent dir separators by avoiding vfs.join(self._path)
967 return reporelpath(self._repo)
968
900 class svnsubrepo(abstractsubrepo): 969 class svnsubrepo(abstractsubrepo):
901 def __init__(self, ctx, path, state): 970 def __init__(self, ctx, path, state):
902 super(svnsubrepo, self).__init__(ctx._repo.ui) 971 super(svnsubrepo, self).__init__(ctx, path)
903 self._path = path
904 self._state = state 972 self._state = state
905 self._ctx = ctx
906 self._exe = util.findexe('svn') 973 self._exe = util.findexe('svn')
907 if not self._exe: 974 if not self._exe:
908 raise util.Abort(_("'svn' executable not found for subrepo '%s'") 975 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
909 % self._path) 976 % self._path)
910 977
921 # --non-interactive. 988 # --non-interactive.
922 if commands[0] in ('update', 'checkout', 'commit'): 989 if commands[0] in ('update', 'checkout', 'commit'):
923 cmd.append('--non-interactive') 990 cmd.append('--non-interactive')
924 cmd.extend(commands) 991 cmd.extend(commands)
925 if filename is not None: 992 if filename is not None:
926 path = os.path.join(self._ctx._repo.origroot, self._path, filename) 993 path = self.wvfs.reljoin(self._ctx.repo().origroot,
994 self._path, filename)
927 cmd.append(path) 995 cmd.append(path)
928 env = dict(os.environ) 996 env = dict(os.environ)
929 # Avoid localized output, preserve current locale for everything else. 997 # Avoid localized output, preserve current locale for everything else.
930 lc_all = env.get('LC_ALL') 998 lc_all = env.get('LC_ALL')
931 if lc_all: 999 if lc_all:
1053 self.ui.warn(_('not removing repo %s because ' 1121 self.ui.warn(_('not removing repo %s because '
1054 'it has changes.\n') % self._path) 1122 'it has changes.\n') % self._path)
1055 return 1123 return
1056 self.ui.note(_('removing subrepo %s\n') % self._path) 1124 self.ui.note(_('removing subrepo %s\n') % self._path)
1057 1125
1058 def onerror(function, path, excinfo): 1126 self.wvfs.rmtree(forcibly=True)
1059 if function is not os.remove:
1060 raise
1061 # read-only files cannot be unlinked under Windows
1062 s = os.stat(path)
1063 if (s.st_mode & stat.S_IWRITE) != 0:
1064 raise
1065 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
1066 os.remove(path)
1067
1068 path = self._ctx._repo.wjoin(self._path)
1069 shutil.rmtree(path, onerror=onerror)
1070 try: 1127 try:
1071 os.removedirs(os.path.dirname(path)) 1128 self._ctx.repo().wvfs.removedirs(os.path.dirname(self._path))
1072 except OSError: 1129 except OSError:
1073 pass 1130 pass
1074 1131
1075 @annotatesubrepoerror 1132 @annotatesubrepoerror
1076 def get(self, state, overwrite=False): 1133 def get(self, state, overwrite=False):
1081 args.append('--force') 1138 args.append('--force')
1082 # The revision must be specified at the end of the URL to properly 1139 # The revision must be specified at the end of the URL to properly
1083 # update to a directory which has since been deleted and recreated. 1140 # update to a directory which has since been deleted and recreated.
1084 args.append('%s@%s' % (state[0], state[1])) 1141 args.append('%s@%s' % (state[0], state[1]))
1085 status, err = self._svncommand(args, failok=True) 1142 status, err = self._svncommand(args, failok=True)
1086 _sanitize(self.ui, self._ctx._repo.wjoin(self._path), '.svn') 1143 _sanitize(self.ui, self.wvfs, '.svn')
1087 if not re.search('Checked out revision [0-9]+.', status): 1144 if not re.search('Checked out revision [0-9]+.', status):
1088 if ('is already a working copy for a different URL' in err 1145 if ('is already a working copy for a different URL' in err
1089 and (self._wcchanged()[:2] == (False, False))): 1146 and (self._wcchanged()[:2] == (False, False))):
1090 # obstructed but clean working copy, so just blow it away. 1147 # obstructed but clean working copy, so just blow it away.
1091 self.remove() 1148 self.remove()
1127 return self._svncommand(['cat'], name)[0] 1184 return self._svncommand(['cat'], name)[0]
1128 1185
1129 1186
1130 class gitsubrepo(abstractsubrepo): 1187 class gitsubrepo(abstractsubrepo):
1131 def __init__(self, ctx, path, state): 1188 def __init__(self, ctx, path, state):
1132 super(gitsubrepo, self).__init__(ctx._repo.ui) 1189 super(gitsubrepo, self).__init__(ctx, path)
1133 self._state = state 1190 self._state = state
1134 self._ctx = ctx 1191 self._abspath = ctx.repo().wjoin(path)
1135 self._path = path 1192 self._subparent = ctx.repo()
1136 self._relpath = os.path.join(reporelpath(ctx._repo), path)
1137 self._abspath = ctx._repo.wjoin(path)
1138 self._subparent = ctx._repo
1139 self._ensuregit() 1193 self._ensuregit()
1140 1194
1141 def _ensuregit(self): 1195 def _ensuregit(self):
1142 try: 1196 try:
1143 self._gitexecutable = 'git' 1197 self._gitexecutable = 'git'
1242 (command, p.returncode, self._relpath)) 1296 (command, p.returncode, self._relpath))
1243 1297
1244 return retdata, p.returncode 1298 return retdata, p.returncode
1245 1299
1246 def _gitmissing(self): 1300 def _gitmissing(self):
1247 return not os.path.exists(os.path.join(self._abspath, '.git')) 1301 return not self.wvfs.exists('.git')
1248 1302
1249 def _gitstate(self): 1303 def _gitstate(self):
1250 return self._gitcommand(['rev-parse', 'HEAD']) 1304 return self._gitcommand(['rev-parse', 'HEAD'])
1251 1305
1252 def _gitcurrentbranch(self): 1306 def _gitcurrentbranch(self):
1385 # the -f option will otherwise throw away files added for 1439 # the -f option will otherwise throw away files added for
1386 # commit, not just unmark them. 1440 # commit, not just unmark them.
1387 self._gitcommand(['reset', 'HEAD']) 1441 self._gitcommand(['reset', 'HEAD'])
1388 cmd.append('-f') 1442 cmd.append('-f')
1389 self._gitcommand(cmd + args) 1443 self._gitcommand(cmd + args)
1390 _sanitize(self.ui, self._abspath, '.git') 1444 _sanitize(self.ui, self.wvfs, '.git')
1391 1445
1392 def rawcheckout(): 1446 def rawcheckout():
1393 # no branch to checkout, check it out with no branch 1447 # no branch to checkout, check it out with no branch
1394 self.ui.warn(_('checking out detached HEAD in subrepo %s\n') % 1448 self.ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1395 self._relpath) 1449 self._relpath)
1434 # Since we are only looking at branching at update, we need to 1488 # Since we are only looking at branching at update, we need to
1435 # detect this situation and perform this action lazily. 1489 # detect this situation and perform this action lazily.
1436 if tracking[remote] != self._gitcurrentbranch(): 1490 if tracking[remote] != self._gitcurrentbranch():
1437 checkout([tracking[remote]]) 1491 checkout([tracking[remote]])
1438 self._gitcommand(['merge', '--ff', remote]) 1492 self._gitcommand(['merge', '--ff', remote])
1439 _sanitize(self.ui, self._abspath, '.git') 1493 _sanitize(self.ui, self.wvfs, '.git')
1440 else: 1494 else:
1441 # a real merge would be required, just checkout the revision 1495 # a real merge would be required, just checkout the revision
1442 rawcheckout() 1496 rawcheckout()
1443 1497
1444 @annotatesubrepoerror 1498 @annotatesubrepoerror
1470 def mergefunc(): 1524 def mergefunc():
1471 if base == revision: 1525 if base == revision:
1472 self.get(state) # fast forward merge 1526 self.get(state) # fast forward merge
1473 elif base != self._state[1]: 1527 elif base != self._state[1]:
1474 self._gitcommand(['merge', '--no-commit', revision]) 1528 self._gitcommand(['merge', '--no-commit', revision])
1475 _sanitize(self.ui, self._abspath, '.git') 1529 _sanitize(self.ui, self.wvfs, '.git')
1476 1530
1477 if self.dirty(): 1531 if self.dirty():
1478 if self._gitstate() != revision: 1532 if self._gitstate() != revision:
1479 dirty = self._gitstate() == self._state[1] or code != 0 1533 dirty = self._gitstate() == self._state[1] or code != 0
1480 if _updateprompt(self.ui, self, dirty, 1534 if _updateprompt(self.ui, self, dirty,
1522 'cannot push revision %s\n') % 1576 'cannot push revision %s\n') %
1523 (self._relpath, self._state[1])) 1577 (self._relpath, self._state[1]))
1524 return False 1578 return False
1525 1579
1526 @annotatesubrepoerror 1580 @annotatesubrepoerror
1581 def add(self, ui, match, prefix, explicitonly, **opts):
1582 if self._gitmissing():
1583 return []
1584
1585 (modified, added, removed,
1586 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1587 clean=True)
1588
1589 tracked = set()
1590 # dirstates 'amn' warn, 'r' is added again
1591 for l in (modified, added, deleted, clean):
1592 tracked.update(l)
1593
1594 # Unknown files not of interest will be rejected by the matcher
1595 files = unknown
1596 files.extend(match.files())
1597
1598 rejected = []
1599
1600 files = [f for f in sorted(set(files)) if match(f)]
1601 for f in files:
1602 exact = match.exact(f)
1603 command = ["add"]
1604 if exact:
1605 command.append("-f") #should be added, even if ignored
1606 if ui.verbose or not exact:
1607 ui.status(_('adding %s\n') % match.rel(f))
1608
1609 if f in tracked: # hg prints 'adding' even if already tracked
1610 if exact:
1611 rejected.append(f)
1612 continue
1613 if not opts.get('dry_run'):
1614 self._gitcommand(command + [f])
1615
1616 for f in rejected:
1617 ui.warn(_("%s already tracked!\n") % match.abs(f))
1618
1619 return rejected
1620
1621 @annotatesubrepoerror
1527 def remove(self): 1622 def remove(self):
1528 if self._gitmissing(): 1623 if self._gitmissing():
1529 return 1624 return
1530 if self.dirty(): 1625 if self.dirty():
1531 self.ui.warn(_('not removing repo %s because ' 1626 self.ui.warn(_('not removing repo %s because '
1533 return 1628 return
1534 # we can't fully delete the repository as it may contain 1629 # we can't fully delete the repository as it may contain
1535 # local-only history 1630 # local-only history
1536 self.ui.note(_('removing subrepo %s\n') % self._relpath) 1631 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1537 self._gitcommand(['config', 'core.bare', 'true']) 1632 self._gitcommand(['config', 'core.bare', 'true'])
1538 for f in os.listdir(self._abspath): 1633 for f, kind in self.wvfs.readdir():
1539 if f == '.git': 1634 if f == '.git':
1540 continue 1635 continue
1541 path = os.path.join(self._abspath, f) 1636 if kind == stat.S_IFDIR:
1542 if os.path.isdir(path) and not os.path.islink(path): 1637 self.wvfs.rmtree(f)
1543 shutil.rmtree(path)
1544 else: 1638 else:
1545 os.remove(path) 1639 self.wvfs.unlink(f)
1546 1640
1547 def archive(self, archiver, prefix, match=None): 1641 def archive(self, archiver, prefix, match=None):
1548 total = 0 1642 total = 0
1549 source, revision = self._state 1643 source, revision = self._state
1550 if not revision: 1644 if not revision:
1565 continue 1659 continue
1566 if info.issym(): 1660 if info.issym():
1567 data = info.linkname 1661 data = info.linkname
1568 else: 1662 else:
1569 data = tar.extractfile(info).read() 1663 data = tar.extractfile(info).read()
1570 archiver.addfile(os.path.join(prefix, self._path, info.name), 1664 archiver.addfile(self.wvfs.reljoin(prefix, self._path, info.name),
1571 info.mode, info.issym(), data) 1665 info.mode, info.issym(), data)
1572 total += 1 1666 total += 1
1573 self.ui.progress(_('archiving (%s)') % relpath, i + 1, 1667 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1574 unit=_('files')) 1668 unit=_('files'))
1575 self.ui.progress(_('archiving (%s)') % relpath, None) 1669 self.ui.progress(_('archiving (%s)') % relpath, None)
1576 return total 1670 return total
1577 1671
1578 1672
1579 @annotatesubrepoerror 1673 @annotatesubrepoerror
1674 def cat(self, match, prefix, **opts):
1675 rev = self._state[1]
1676 if match.anypats():
1677 return 1 #No support for include/exclude yet
1678
1679 if not match.files():
1680 return 1
1681
1682 for f in match.files():
1683 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1684 fp = cmdutil.makefileobj(self._subparent, opts.get('output'),
1685 self._ctx.node(),
1686 pathname=self.wvfs.reljoin(prefix, f))
1687 fp.write(output)
1688 fp.close()
1689 return 0
1690
1691
1692 @annotatesubrepoerror
1580 def status(self, rev2, **opts): 1693 def status(self, rev2, **opts):
1581 rev1 = self._state[1] 1694 rev1 = self._state[1]
1582 if self._gitmissing() or not rev1: 1695 if self._gitmissing() or not rev1:
1583 # if the repo is missing, return no results 1696 # if the repo is missing, return no results
1584 return [], [], [], [], [], [], [] 1697 return scmutil.status([], [], [], [], [], [], [])
1585 modified, added, removed = [], [], [] 1698 modified, added, removed = [], [], []
1586 self._gitupdatestat() 1699 self._gitupdatestat()
1587 if rev2: 1700 if rev2:
1588 command = ['diff-tree', rev1, rev2] 1701 command = ['diff-tree', rev1, rev2]
1589 else: 1702 else:
1601 elif status == 'D': 1714 elif status == 'D':
1602 removed.append(f) 1715 removed.append(f)
1603 1716
1604 deleted, unknown, ignored, clean = [], [], [], [] 1717 deleted, unknown, ignored, clean = [], [], [], []
1605 1718
1606 if not rev2: 1719 command = ['status', '--porcelain', '-z']
1607 command = ['ls-files', '--others', '--exclude-standard'] 1720 if opts.get('unknown'):
1608 out = self._gitcommand(command) 1721 command += ['--untracked-files=all']
1609 for line in out.split('\n'): 1722 if opts.get('ignored'):
1610 if len(line) == 0: 1723 command += ['--ignored']
1611 continue 1724 out = self._gitcommand(command)
1612 unknown.append(line) 1725
1726 changedfiles = set()
1727 changedfiles.update(modified)
1728 changedfiles.update(added)
1729 changedfiles.update(removed)
1730 for line in out.split('\0'):
1731 if not line:
1732 continue
1733 st = line[0:2]
1734 #moves and copies show 2 files on one line
1735 if line.find('\0') >= 0:
1736 filename1, filename2 = line[3:].split('\0')
1737 else:
1738 filename1 = line[3:]
1739 filename2 = None
1740
1741 changedfiles.add(filename1)
1742 if filename2:
1743 changedfiles.add(filename2)
1744
1745 if st == '??':
1746 unknown.append(filename1)
1747 elif st == '!!':
1748 ignored.append(filename1)
1749
1750 if opts.get('clean'):
1751 out = self._gitcommand(['ls-files'])
1752 for f in out.split('\n'):
1753 if not f in changedfiles:
1754 clean.append(f)
1613 1755
1614 return scmutil.status(modified, added, removed, deleted, 1756 return scmutil.status(modified, added, removed, deleted,
1615 unknown, ignored, clean) 1757 unknown, ignored, clean)
1616 1758
1617 @annotatesubrepoerror 1759 @annotatesubrepoerror
1622 cmd.append('--stat') 1764 cmd.append('--stat')
1623 else: 1765 else:
1624 # for Git, this also implies '-p' 1766 # for Git, this also implies '-p'
1625 cmd.append('-U%d' % diffopts.context) 1767 cmd.append('-U%d' % diffopts.context)
1626 1768
1627 gitprefix = os.path.join(prefix, self._path) 1769 gitprefix = self.wvfs.reljoin(prefix, self._path)
1628 1770
1629 if diffopts.noprefix: 1771 if diffopts.noprefix:
1630 cmd.extend(['--src-prefix=%s/' % gitprefix, 1772 cmd.extend(['--src-prefix=%s/' % gitprefix,
1631 '--dst-prefix=%s/' % gitprefix]) 1773 '--dst-prefix=%s/' % gitprefix])
1632 else: 1774 else:
1643 1785
1644 cmd.append(node1) 1786 cmd.append(node1)
1645 if node2: 1787 if node2:
1646 cmd.append(node2) 1788 cmd.append(node2)
1647 1789
1648 if match.anypats():
1649 return #No support for include/exclude yet
1650
1651 output = "" 1790 output = ""
1652 if match.always(): 1791 if match.always():
1653 output += self._gitcommand(cmd) + '\n' 1792 output += self._gitcommand(cmd) + '\n'
1654 elif match.files(): 1793 else:
1655 for f in match.files(): 1794 st = self.status(node2)[:3]
1656 output += self._gitcommand(cmd + [f]) + '\n' 1795 files = [f for sublist in st for f in sublist]
1657 elif match(gitprefix): #Subrepo is matched 1796 for f in files:
1658 output += self._gitcommand(cmd) + '\n' 1797 if match(f):
1798 output += self._gitcommand(cmd + ['--', f]) + '\n'
1659 1799
1660 if output.strip(): 1800 if output.strip():
1661 ui.write(output) 1801 ui.write(output)
1662 1802
1663 @annotatesubrepoerror 1803 @annotatesubrepoerror
1668 names = status.modified 1808 names = status.modified
1669 for name in names: 1809 for name in names:
1670 bakname = "%s.orig" % name 1810 bakname = "%s.orig" % name
1671 self.ui.note(_('saving current version of %s as %s\n') % 1811 self.ui.note(_('saving current version of %s as %s\n') %
1672 (name, bakname)) 1812 (name, bakname))
1673 util.rename(os.path.join(self._abspath, name), 1813 self.wvfs.rename(name, bakname)
1674 os.path.join(self._abspath, bakname)) 1814
1675 1815 if not opts.get('dry_run'):
1676 self.get(substate, overwrite=True) 1816 self.get(substate, overwrite=True)
1677 return [] 1817 return []
1678 1818
1679 def shortid(self, revid): 1819 def shortid(self, revid):
1680 return revid[:7] 1820 return revid[:7]
1681 1821