##// END OF EJS Templates
subrepo: normalize path in the specific way for problematic encodings...
FUJIWARA Katsunori -
r21568:8dd17b19 stable
parent child Browse files
Show More
@@ -1,144 +1,166 b''
1 import os, errno, stat
1 import os, errno, stat
2
2
3 import util
3 import util
4 from i18n import _
4 from i18n import _
5
5
6 class pathauditor(object):
6 class pathauditor(object):
7 '''ensure that a filesystem path contains no banned components.
7 '''ensure that a filesystem path contains no banned components.
8 the following properties of a path are checked:
8 the following properties of a path are checked:
9
9
10 - ends with a directory separator
10 - ends with a directory separator
11 - under top-level .hg
11 - under top-level .hg
12 - starts at the root of a windows drive
12 - starts at the root of a windows drive
13 - contains ".."
13 - contains ".."
14 - traverses a symlink (e.g. a/symlink_here/b)
14 - traverses a symlink (e.g. a/symlink_here/b)
15 - inside a nested repository (a callback can be used to approve
15 - inside a nested repository (a callback can be used to approve
16 some nested repositories, e.g., subrepositories)
16 some nested repositories, e.g., subrepositories)
17 '''
17 '''
18
18
19 def __init__(self, root, callback=None):
19 def __init__(self, root, callback=None):
20 self.audited = set()
20 self.audited = set()
21 self.auditeddir = set()
21 self.auditeddir = set()
22 self.root = root
22 self.root = root
23 self.callback = callback
23 self.callback = callback
24 if os.path.lexists(root) and not util.checkcase(root):
24 if os.path.lexists(root) and not util.checkcase(root):
25 self.normcase = util.normcase
25 self.normcase = util.normcase
26 else:
26 else:
27 self.normcase = lambda x: x
27 self.normcase = lambda x: x
28
28
29 def __call__(self, path):
29 def __call__(self, path):
30 '''Check the relative path.
30 '''Check the relative path.
31 path may contain a pattern (e.g. foodir/**.txt)'''
31 path may contain a pattern (e.g. foodir/**.txt)'''
32
32
33 path = util.localpath(path)
33 path = util.localpath(path)
34 normpath = self.normcase(path)
34 normpath = self.normcase(path)
35 if normpath in self.audited:
35 if normpath in self.audited:
36 return
36 return
37 # AIX ignores "/" at end of path, others raise EISDIR.
37 # AIX ignores "/" at end of path, others raise EISDIR.
38 if util.endswithsep(path):
38 if util.endswithsep(path):
39 raise util.Abort(_("path ends in directory separator: %s") % path)
39 raise util.Abort(_("path ends in directory separator: %s") % path)
40 parts = util.splitpath(path)
40 parts = util.splitpath(path)
41 if (os.path.splitdrive(path)[0]
41 if (os.path.splitdrive(path)[0]
42 or parts[0].lower() in ('.hg', '.hg.', '')
42 or parts[0].lower() in ('.hg', '.hg.', '')
43 or os.pardir in parts):
43 or os.pardir in parts):
44 raise util.Abort(_("path contains illegal component: %s") % path)
44 raise util.Abort(_("path contains illegal component: %s") % path)
45 if '.hg' in path.lower():
45 if '.hg' in path.lower():
46 lparts = [p.lower() for p in parts]
46 lparts = [p.lower() for p in parts]
47 for p in '.hg', '.hg.':
47 for p in '.hg', '.hg.':
48 if p in lparts[1:]:
48 if p in lparts[1:]:
49 pos = lparts.index(p)
49 pos = lparts.index(p)
50 base = os.path.join(*parts[:pos])
50 base = os.path.join(*parts[:pos])
51 raise util.Abort(_("path '%s' is inside nested repo %r")
51 raise util.Abort(_("path '%s' is inside nested repo %r")
52 % (path, base))
52 % (path, base))
53
53
54 normparts = util.splitpath(normpath)
54 normparts = util.splitpath(normpath)
55 assert len(parts) == len(normparts)
55 assert len(parts) == len(normparts)
56
56
57 parts.pop()
57 parts.pop()
58 normparts.pop()
58 normparts.pop()
59 prefixes = []
59 prefixes = []
60 while parts:
60 while parts:
61 prefix = os.sep.join(parts)
61 prefix = os.sep.join(parts)
62 normprefix = os.sep.join(normparts)
62 normprefix = os.sep.join(normparts)
63 if normprefix in self.auditeddir:
63 if normprefix in self.auditeddir:
64 break
64 break
65 curpath = os.path.join(self.root, prefix)
65 curpath = os.path.join(self.root, prefix)
66 try:
66 try:
67 st = os.lstat(curpath)
67 st = os.lstat(curpath)
68 except OSError, err:
68 except OSError, err:
69 # EINVAL can be raised as invalid path syntax under win32.
69 # EINVAL can be raised as invalid path syntax under win32.
70 # They must be ignored for patterns can be checked too.
70 # They must be ignored for patterns can be checked too.
71 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
71 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
72 raise
72 raise
73 else:
73 else:
74 if stat.S_ISLNK(st.st_mode):
74 if stat.S_ISLNK(st.st_mode):
75 raise util.Abort(
75 raise util.Abort(
76 _('path %r traverses symbolic link %r')
76 _('path %r traverses symbolic link %r')
77 % (path, prefix))
77 % (path, prefix))
78 elif (stat.S_ISDIR(st.st_mode) and
78 elif (stat.S_ISDIR(st.st_mode) and
79 os.path.isdir(os.path.join(curpath, '.hg'))):
79 os.path.isdir(os.path.join(curpath, '.hg'))):
80 if not self.callback or not self.callback(curpath):
80 if not self.callback or not self.callback(curpath):
81 raise util.Abort(_("path '%s' is inside nested "
81 raise util.Abort(_("path '%s' is inside nested "
82 "repo %r")
82 "repo %r")
83 % (path, prefix))
83 % (path, prefix))
84 prefixes.append(normprefix)
84 prefixes.append(normprefix)
85 parts.pop()
85 parts.pop()
86 normparts.pop()
86 normparts.pop()
87
87
88 self.audited.add(normpath)
88 self.audited.add(normpath)
89 # only add prefixes to the cache after checking everything: we don't
89 # only add prefixes to the cache after checking everything: we don't
90 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
90 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
91 self.auditeddir.update(prefixes)
91 self.auditeddir.update(prefixes)
92
92
93 def check(self, path):
93 def check(self, path):
94 try:
94 try:
95 self(path)
95 self(path)
96 return True
96 return True
97 except (OSError, util.Abort):
97 except (OSError, util.Abort):
98 return False
98 return False
99
99
100 def canonpath(root, cwd, myname, auditor=None):
100 def canonpath(root, cwd, myname, auditor=None):
101 '''return the canonical path of myname, given cwd and root'''
101 '''return the canonical path of myname, given cwd and root'''
102 if util.endswithsep(root):
102 if util.endswithsep(root):
103 rootsep = root
103 rootsep = root
104 else:
104 else:
105 rootsep = root + os.sep
105 rootsep = root + os.sep
106 name = myname
106 name = myname
107 if not os.path.isabs(name):
107 if not os.path.isabs(name):
108 name = os.path.join(root, cwd, name)
108 name = os.path.join(root, cwd, name)
109 name = os.path.normpath(name)
109 name = os.path.normpath(name)
110 if auditor is None:
110 if auditor is None:
111 auditor = pathauditor(root)
111 auditor = pathauditor(root)
112 if name != rootsep and name.startswith(rootsep):
112 if name != rootsep and name.startswith(rootsep):
113 name = name[len(rootsep):]
113 name = name[len(rootsep):]
114 auditor(name)
114 auditor(name)
115 return util.pconvert(name)
115 return util.pconvert(name)
116 elif name == root:
116 elif name == root:
117 return ''
117 return ''
118 else:
118 else:
119 # Determine whether `name' is in the hierarchy at or beneath `root',
119 # Determine whether `name' is in the hierarchy at or beneath `root',
120 # by iterating name=dirname(name) until that causes no change (can't
120 # by iterating name=dirname(name) until that causes no change (can't
121 # check name == '/', because that doesn't work on windows). The list
121 # check name == '/', because that doesn't work on windows). The list
122 # `rel' holds the reversed list of components making up the relative
122 # `rel' holds the reversed list of components making up the relative
123 # file name we want.
123 # file name we want.
124 rel = []
124 rel = []
125 while True:
125 while True:
126 try:
126 try:
127 s = util.samefile(name, root)
127 s = util.samefile(name, root)
128 except OSError:
128 except OSError:
129 s = False
129 s = False
130 if s:
130 if s:
131 if not rel:
131 if not rel:
132 # name was actually the same as root (maybe a symlink)
132 # name was actually the same as root (maybe a symlink)
133 return ''
133 return ''
134 rel.reverse()
134 rel.reverse()
135 name = os.path.join(*rel)
135 name = os.path.join(*rel)
136 auditor(name)
136 auditor(name)
137 return util.pconvert(name)
137 return util.pconvert(name)
138 dirname, basename = util.split(name)
138 dirname, basename = util.split(name)
139 rel.append(basename)
139 rel.append(basename)
140 if dirname == name:
140 if dirname == name:
141 break
141 break
142 name = dirname
142 name = dirname
143
143
144 raise util.Abort(_("%s not under root '%s'") % (myname, root))
144 raise util.Abort(_("%s not under root '%s'") % (myname, root))
145
146 def normasprefix(path):
147 '''normalize the specified path as path prefix
148
149 Returned vaule can be used safely for "p.startswith(prefix)",
150 "p[len(prefix):]", and so on.
151
152 For efficiency, this expects "path" argument to be already
153 normalized by "os.path.normpath", "os.path.realpath", and so on.
154
155 See also issue3033 for detail about need of this function.
156
157 >>> normasprefix('/foo/bar').replace(os.sep, '/')
158 '/foo/bar/'
159 >>> normasprefix('/').replace(os.sep, '/')
160 '/'
161 '''
162 d, p = os.path.splitdrive(path)
163 if len(p) != len(os.sep):
164 return path + os.sep
165 else:
166 return path
@@ -1,1571 +1,1570 b''
1 # subrepo.py - sub-repository handling for Mercurial
1 # subrepo.py - sub-repository handling for Mercurial
2 #
2 #
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
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 errno, os, re, shutil, posixpath, sys
8 import errno, os, re, shutil, posixpath, sys
9 import xml.dom.minidom
9 import xml.dom.minidom
10 import stat, subprocess, tarfile
10 import stat, subprocess, tarfile
11 from i18n import _
11 from i18n import _
12 import config, util, node, error, cmdutil, bookmarks, match as matchmod
12 import config, util, node, error, cmdutil, bookmarks, match as matchmod
13 import phases
13 import phases
14 import pathutil
14 import pathutil
15 hg = None
15 hg = None
16 propertycache = util.propertycache
16 propertycache = util.propertycache
17
17
18 nullstate = ('', '', 'empty')
18 nullstate = ('', '', 'empty')
19
19
20 def _expandedabspath(path):
20 def _expandedabspath(path):
21 '''
21 '''
22 get a path or url and if it is a path expand it and return an absolute path
22 get a path or url and if it is a path expand it and return an absolute path
23 '''
23 '''
24 expandedpath = util.urllocalpath(util.expandpath(path))
24 expandedpath = util.urllocalpath(util.expandpath(path))
25 u = util.url(expandedpath)
25 u = util.url(expandedpath)
26 if not u.scheme:
26 if not u.scheme:
27 path = util.normpath(os.path.abspath(u.path))
27 path = util.normpath(os.path.abspath(u.path))
28 return path
28 return path
29
29
30 def _getstorehashcachename(remotepath):
30 def _getstorehashcachename(remotepath):
31 '''get a unique filename for the store hash cache of a remote repository'''
31 '''get a unique filename for the store hash cache of a remote repository'''
32 return util.sha1(_expandedabspath(remotepath)).hexdigest()[0:12]
32 return util.sha1(_expandedabspath(remotepath)).hexdigest()[0:12]
33
33
34 def _calcfilehash(filename):
34 def _calcfilehash(filename):
35 data = ''
35 data = ''
36 if os.path.exists(filename):
36 if os.path.exists(filename):
37 fd = open(filename, 'rb')
37 fd = open(filename, 'rb')
38 data = fd.read()
38 data = fd.read()
39 fd.close()
39 fd.close()
40 return util.sha1(data).hexdigest()
40 return util.sha1(data).hexdigest()
41
41
42 class SubrepoAbort(error.Abort):
42 class SubrepoAbort(error.Abort):
43 """Exception class used to avoid handling a subrepo error more than once"""
43 """Exception class used to avoid handling a subrepo error more than once"""
44 def __init__(self, *args, **kw):
44 def __init__(self, *args, **kw):
45 error.Abort.__init__(self, *args, **kw)
45 error.Abort.__init__(self, *args, **kw)
46 self.subrepo = kw.get('subrepo')
46 self.subrepo = kw.get('subrepo')
47 self.cause = kw.get('cause')
47 self.cause = kw.get('cause')
48
48
49 def annotatesubrepoerror(func):
49 def annotatesubrepoerror(func):
50 def decoratedmethod(self, *args, **kargs):
50 def decoratedmethod(self, *args, **kargs):
51 try:
51 try:
52 res = func(self, *args, **kargs)
52 res = func(self, *args, **kargs)
53 except SubrepoAbort, ex:
53 except SubrepoAbort, ex:
54 # This exception has already been handled
54 # This exception has already been handled
55 raise ex
55 raise ex
56 except error.Abort, ex:
56 except error.Abort, ex:
57 subrepo = subrelpath(self)
57 subrepo = subrelpath(self)
58 errormsg = str(ex) + ' ' + _('(in subrepo %s)') % subrepo
58 errormsg = str(ex) + ' ' + _('(in subrepo %s)') % subrepo
59 # avoid handling this exception by raising a SubrepoAbort exception
59 # avoid handling this exception by raising a SubrepoAbort exception
60 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
60 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
61 cause=sys.exc_info())
61 cause=sys.exc_info())
62 return res
62 return res
63 return decoratedmethod
63 return decoratedmethod
64
64
65 def state(ctx, ui):
65 def state(ctx, ui):
66 """return a state dict, mapping subrepo paths configured in .hgsub
66 """return a state dict, mapping subrepo paths configured in .hgsub
67 to tuple: (source from .hgsub, revision from .hgsubstate, kind
67 to tuple: (source from .hgsub, revision from .hgsubstate, kind
68 (key in types dict))
68 (key in types dict))
69 """
69 """
70 p = config.config()
70 p = config.config()
71 def read(f, sections=None, remap=None):
71 def read(f, sections=None, remap=None):
72 if f in ctx:
72 if f in ctx:
73 try:
73 try:
74 data = ctx[f].data()
74 data = ctx[f].data()
75 except IOError, err:
75 except IOError, err:
76 if err.errno != errno.ENOENT:
76 if err.errno != errno.ENOENT:
77 raise
77 raise
78 # handle missing subrepo spec files as removed
78 # handle missing subrepo spec files as removed
79 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
79 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
80 return
80 return
81 p.parse(f, data, sections, remap, read)
81 p.parse(f, data, sections, remap, read)
82 else:
82 else:
83 raise util.Abort(_("subrepo spec file %s not found") % f)
83 raise util.Abort(_("subrepo spec file %s not found") % f)
84
84
85 if '.hgsub' in ctx:
85 if '.hgsub' in ctx:
86 read('.hgsub')
86 read('.hgsub')
87
87
88 for path, src in ui.configitems('subpaths'):
88 for path, src in ui.configitems('subpaths'):
89 p.set('subpaths', path, src, ui.configsource('subpaths', path))
89 p.set('subpaths', path, src, ui.configsource('subpaths', path))
90
90
91 rev = {}
91 rev = {}
92 if '.hgsubstate' in ctx:
92 if '.hgsubstate' in ctx:
93 try:
93 try:
94 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
94 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
95 l = l.lstrip()
95 l = l.lstrip()
96 if not l:
96 if not l:
97 continue
97 continue
98 try:
98 try:
99 revision, path = l.split(" ", 1)
99 revision, path = l.split(" ", 1)
100 except ValueError:
100 except ValueError:
101 raise util.Abort(_("invalid subrepository revision "
101 raise util.Abort(_("invalid subrepository revision "
102 "specifier in .hgsubstate line %d")
102 "specifier in .hgsubstate line %d")
103 % (i + 1))
103 % (i + 1))
104 rev[path] = revision
104 rev[path] = revision
105 except IOError, err:
105 except IOError, err:
106 if err.errno != errno.ENOENT:
106 if err.errno != errno.ENOENT:
107 raise
107 raise
108
108
109 def remap(src):
109 def remap(src):
110 for pattern, repl in p.items('subpaths'):
110 for pattern, repl in p.items('subpaths'):
111 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
111 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
112 # does a string decode.
112 # does a string decode.
113 repl = repl.encode('string-escape')
113 repl = repl.encode('string-escape')
114 # However, we still want to allow back references to go
114 # However, we still want to allow back references to go
115 # through unharmed, so we turn r'\\1' into r'\1'. Again,
115 # through unharmed, so we turn r'\\1' into r'\1'. Again,
116 # extra escapes are needed because re.sub string decodes.
116 # extra escapes are needed because re.sub string decodes.
117 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
117 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
118 try:
118 try:
119 src = re.sub(pattern, repl, src, 1)
119 src = re.sub(pattern, repl, src, 1)
120 except re.error, e:
120 except re.error, e:
121 raise util.Abort(_("bad subrepository pattern in %s: %s")
121 raise util.Abort(_("bad subrepository pattern in %s: %s")
122 % (p.source('subpaths', pattern), e))
122 % (p.source('subpaths', pattern), e))
123 return src
123 return src
124
124
125 state = {}
125 state = {}
126 for path, src in p[''].items():
126 for path, src in p[''].items():
127 kind = 'hg'
127 kind = 'hg'
128 if src.startswith('['):
128 if src.startswith('['):
129 if ']' not in src:
129 if ']' not in src:
130 raise util.Abort(_('missing ] in subrepo source'))
130 raise util.Abort(_('missing ] in subrepo source'))
131 kind, src = src.split(']', 1)
131 kind, src = src.split(']', 1)
132 kind = kind[1:]
132 kind = kind[1:]
133 src = src.lstrip() # strip any extra whitespace after ']'
133 src = src.lstrip() # strip any extra whitespace after ']'
134
134
135 if not util.url(src).isabs():
135 if not util.url(src).isabs():
136 parent = _abssource(ctx._repo, abort=False)
136 parent = _abssource(ctx._repo, abort=False)
137 if parent:
137 if parent:
138 parent = util.url(parent)
138 parent = util.url(parent)
139 parent.path = posixpath.join(parent.path or '', src)
139 parent.path = posixpath.join(parent.path or '', src)
140 parent.path = posixpath.normpath(parent.path)
140 parent.path = posixpath.normpath(parent.path)
141 joined = str(parent)
141 joined = str(parent)
142 # Remap the full joined path and use it if it changes,
142 # Remap the full joined path and use it if it changes,
143 # else remap the original source.
143 # else remap the original source.
144 remapped = remap(joined)
144 remapped = remap(joined)
145 if remapped == joined:
145 if remapped == joined:
146 src = remap(src)
146 src = remap(src)
147 else:
147 else:
148 src = remapped
148 src = remapped
149
149
150 src = remap(src)
150 src = remap(src)
151 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
151 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
152
152
153 return state
153 return state
154
154
155 def writestate(repo, state):
155 def writestate(repo, state):
156 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
156 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
157 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
157 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
158 repo.wwrite('.hgsubstate', ''.join(lines), '')
158 repo.wwrite('.hgsubstate', ''.join(lines), '')
159
159
160 def submerge(repo, wctx, mctx, actx, overwrite):
160 def submerge(repo, wctx, mctx, actx, overwrite):
161 """delegated from merge.applyupdates: merging of .hgsubstate file
161 """delegated from merge.applyupdates: merging of .hgsubstate file
162 in working context, merging context and ancestor context"""
162 in working context, merging context and ancestor context"""
163 if mctx == actx: # backwards?
163 if mctx == actx: # backwards?
164 actx = wctx.p1()
164 actx = wctx.p1()
165 s1 = wctx.substate
165 s1 = wctx.substate
166 s2 = mctx.substate
166 s2 = mctx.substate
167 sa = actx.substate
167 sa = actx.substate
168 sm = {}
168 sm = {}
169
169
170 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
170 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
171
171
172 def debug(s, msg, r=""):
172 def debug(s, msg, r=""):
173 if r:
173 if r:
174 r = "%s:%s:%s" % r
174 r = "%s:%s:%s" % r
175 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
175 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
176
176
177 for s, l in sorted(s1.iteritems()):
177 for s, l in sorted(s1.iteritems()):
178 a = sa.get(s, nullstate)
178 a = sa.get(s, nullstate)
179 ld = l # local state with possible dirty flag for compares
179 ld = l # local state with possible dirty flag for compares
180 if wctx.sub(s).dirty():
180 if wctx.sub(s).dirty():
181 ld = (l[0], l[1] + "+")
181 ld = (l[0], l[1] + "+")
182 if wctx == actx: # overwrite
182 if wctx == actx: # overwrite
183 a = ld
183 a = ld
184
184
185 if s in s2:
185 if s in s2:
186 r = s2[s]
186 r = s2[s]
187 if ld == r or r == a: # no change or local is newer
187 if ld == r or r == a: # no change or local is newer
188 sm[s] = l
188 sm[s] = l
189 continue
189 continue
190 elif ld == a: # other side changed
190 elif ld == a: # other side changed
191 debug(s, "other changed, get", r)
191 debug(s, "other changed, get", r)
192 wctx.sub(s).get(r, overwrite)
192 wctx.sub(s).get(r, overwrite)
193 sm[s] = r
193 sm[s] = r
194 elif ld[0] != r[0]: # sources differ
194 elif ld[0] != r[0]: # sources differ
195 if repo.ui.promptchoice(
195 if repo.ui.promptchoice(
196 _(' subrepository sources for %s differ\n'
196 _(' subrepository sources for %s differ\n'
197 'use (l)ocal source (%s) or (r)emote source (%s)?'
197 'use (l)ocal source (%s) or (r)emote source (%s)?'
198 '$$ &Local $$ &Remote') % (s, l[0], r[0]), 0):
198 '$$ &Local $$ &Remote') % (s, l[0], r[0]), 0):
199 debug(s, "prompt changed, get", r)
199 debug(s, "prompt changed, get", r)
200 wctx.sub(s).get(r, overwrite)
200 wctx.sub(s).get(r, overwrite)
201 sm[s] = r
201 sm[s] = r
202 elif ld[1] == a[1]: # local side is unchanged
202 elif ld[1] == a[1]: # local side is unchanged
203 debug(s, "other side changed, get", r)
203 debug(s, "other side changed, get", r)
204 wctx.sub(s).get(r, overwrite)
204 wctx.sub(s).get(r, overwrite)
205 sm[s] = r
205 sm[s] = r
206 else:
206 else:
207 debug(s, "both sides changed")
207 debug(s, "both sides changed")
208 option = repo.ui.promptchoice(
208 option = repo.ui.promptchoice(
209 _(' subrepository %s diverged (local revision: %s, '
209 _(' subrepository %s diverged (local revision: %s, '
210 'remote revision: %s)\n'
210 'remote revision: %s)\n'
211 '(M)erge, keep (l)ocal or keep (r)emote?'
211 '(M)erge, keep (l)ocal or keep (r)emote?'
212 '$$ &Merge $$ &Local $$ &Remote')
212 '$$ &Merge $$ &Local $$ &Remote')
213 % (s, l[1][:12], r[1][:12]), 0)
213 % (s, l[1][:12], r[1][:12]), 0)
214 if option == 0:
214 if option == 0:
215 wctx.sub(s).merge(r)
215 wctx.sub(s).merge(r)
216 sm[s] = l
216 sm[s] = l
217 debug(s, "merge with", r)
217 debug(s, "merge with", r)
218 elif option == 1:
218 elif option == 1:
219 sm[s] = l
219 sm[s] = l
220 debug(s, "keep local subrepo revision", l)
220 debug(s, "keep local subrepo revision", l)
221 else:
221 else:
222 wctx.sub(s).get(r, overwrite)
222 wctx.sub(s).get(r, overwrite)
223 sm[s] = r
223 sm[s] = r
224 debug(s, "get remote subrepo revision", r)
224 debug(s, "get remote subrepo revision", r)
225 elif ld == a: # remote removed, local unchanged
225 elif ld == a: # remote removed, local unchanged
226 debug(s, "remote removed, remove")
226 debug(s, "remote removed, remove")
227 wctx.sub(s).remove()
227 wctx.sub(s).remove()
228 elif a == nullstate: # not present in remote or ancestor
228 elif a == nullstate: # not present in remote or ancestor
229 debug(s, "local added, keep")
229 debug(s, "local added, keep")
230 sm[s] = l
230 sm[s] = l
231 continue
231 continue
232 else:
232 else:
233 if repo.ui.promptchoice(
233 if repo.ui.promptchoice(
234 _(' local changed subrepository %s which remote removed\n'
234 _(' local changed subrepository %s which remote removed\n'
235 'use (c)hanged version or (d)elete?'
235 'use (c)hanged version or (d)elete?'
236 '$$ &Changed $$ &Delete') % s, 0):
236 '$$ &Changed $$ &Delete') % s, 0):
237 debug(s, "prompt remove")
237 debug(s, "prompt remove")
238 wctx.sub(s).remove()
238 wctx.sub(s).remove()
239
239
240 for s, r in sorted(s2.items()):
240 for s, r in sorted(s2.items()):
241 if s in s1:
241 if s in s1:
242 continue
242 continue
243 elif s not in sa:
243 elif s not in sa:
244 debug(s, "remote added, get", r)
244 debug(s, "remote added, get", r)
245 mctx.sub(s).get(r)
245 mctx.sub(s).get(r)
246 sm[s] = r
246 sm[s] = r
247 elif r != sa[s]:
247 elif r != sa[s]:
248 if repo.ui.promptchoice(
248 if repo.ui.promptchoice(
249 _(' remote changed subrepository %s which local removed\n'
249 _(' remote changed subrepository %s which local removed\n'
250 'use (c)hanged version or (d)elete?'
250 'use (c)hanged version or (d)elete?'
251 '$$ &Changed $$ &Delete') % s, 0) == 0:
251 '$$ &Changed $$ &Delete') % s, 0) == 0:
252 debug(s, "prompt recreate", r)
252 debug(s, "prompt recreate", r)
253 wctx.sub(s).get(r)
253 wctx.sub(s).get(r)
254 sm[s] = r
254 sm[s] = r
255
255
256 # record merged .hgsubstate
256 # record merged .hgsubstate
257 writestate(repo, sm)
257 writestate(repo, sm)
258 return sm
258 return sm
259
259
260 def _updateprompt(ui, sub, dirty, local, remote):
260 def _updateprompt(ui, sub, dirty, local, remote):
261 if dirty:
261 if dirty:
262 msg = (_(' subrepository sources for %s differ\n'
262 msg = (_(' subrepository sources for %s differ\n'
263 'use (l)ocal source (%s) or (r)emote source (%s)?\n'
263 'use (l)ocal source (%s) or (r)emote source (%s)?\n'
264 '$$ &Local $$ &Remote')
264 '$$ &Local $$ &Remote')
265 % (subrelpath(sub), local, remote))
265 % (subrelpath(sub), local, remote))
266 else:
266 else:
267 msg = (_(' subrepository sources for %s differ (in checked out '
267 msg = (_(' subrepository sources for %s differ (in checked out '
268 'version)\n'
268 'version)\n'
269 'use (l)ocal source (%s) or (r)emote source (%s)?\n'
269 'use (l)ocal source (%s) or (r)emote source (%s)?\n'
270 '$$ &Local $$ &Remote')
270 '$$ &Local $$ &Remote')
271 % (subrelpath(sub), local, remote))
271 % (subrelpath(sub), local, remote))
272 return ui.promptchoice(msg, 0)
272 return ui.promptchoice(msg, 0)
273
273
274 def reporelpath(repo):
274 def reporelpath(repo):
275 """return path to this (sub)repo as seen from outermost repo"""
275 """return path to this (sub)repo as seen from outermost repo"""
276 parent = repo
276 parent = repo
277 while util.safehasattr(parent, '_subparent'):
277 while util.safehasattr(parent, '_subparent'):
278 parent = parent._subparent
278 parent = parent._subparent
279 p = parent.root.rstrip(os.sep)
279 return repo.root[len(pathutil.normasprefix(parent.root)):]
280 return repo.root[len(p) + 1:]
281
280
282 def subrelpath(sub):
281 def subrelpath(sub):
283 """return path to this subrepo as seen from outermost repo"""
282 """return path to this subrepo as seen from outermost repo"""
284 if util.safehasattr(sub, '_relpath'):
283 if util.safehasattr(sub, '_relpath'):
285 return sub._relpath
284 return sub._relpath
286 if not util.safehasattr(sub, '_repo'):
285 if not util.safehasattr(sub, '_repo'):
287 return sub._path
286 return sub._path
288 return reporelpath(sub._repo)
287 return reporelpath(sub._repo)
289
288
290 def _abssource(repo, push=False, abort=True):
289 def _abssource(repo, push=False, abort=True):
291 """return pull/push path of repo - either based on parent repo .hgsub info
290 """return pull/push path of repo - either based on parent repo .hgsub info
292 or on the top repo config. Abort or return None if no source found."""
291 or on the top repo config. Abort or return None if no source found."""
293 if util.safehasattr(repo, '_subparent'):
292 if util.safehasattr(repo, '_subparent'):
294 source = util.url(repo._subsource)
293 source = util.url(repo._subsource)
295 if source.isabs():
294 if source.isabs():
296 return str(source)
295 return str(source)
297 source.path = posixpath.normpath(source.path)
296 source.path = posixpath.normpath(source.path)
298 parent = _abssource(repo._subparent, push, abort=False)
297 parent = _abssource(repo._subparent, push, abort=False)
299 if parent:
298 if parent:
300 parent = util.url(util.pconvert(parent))
299 parent = util.url(util.pconvert(parent))
301 parent.path = posixpath.join(parent.path or '', source.path)
300 parent.path = posixpath.join(parent.path or '', source.path)
302 parent.path = posixpath.normpath(parent.path)
301 parent.path = posixpath.normpath(parent.path)
303 return str(parent)
302 return str(parent)
304 else: # recursion reached top repo
303 else: # recursion reached top repo
305 if util.safehasattr(repo, '_subtoppath'):
304 if util.safehasattr(repo, '_subtoppath'):
306 return repo._subtoppath
305 return repo._subtoppath
307 if push and repo.ui.config('paths', 'default-push'):
306 if push and repo.ui.config('paths', 'default-push'):
308 return repo.ui.config('paths', 'default-push')
307 return repo.ui.config('paths', 'default-push')
309 if repo.ui.config('paths', 'default'):
308 if repo.ui.config('paths', 'default'):
310 return repo.ui.config('paths', 'default')
309 return repo.ui.config('paths', 'default')
311 if repo.sharedpath != repo.path:
310 if repo.sharedpath != repo.path:
312 # chop off the .hg component to get the default path form
311 # chop off the .hg component to get the default path form
313 return os.path.dirname(repo.sharedpath)
312 return os.path.dirname(repo.sharedpath)
314 if abort:
313 if abort:
315 raise util.Abort(_("default path for subrepository not found"))
314 raise util.Abort(_("default path for subrepository not found"))
316
315
317 def _sanitize(ui, path, ignore):
316 def _sanitize(ui, path, ignore):
318 for dirname, dirs, names in os.walk(path):
317 for dirname, dirs, names in os.walk(path):
319 for i, d in enumerate(dirs):
318 for i, d in enumerate(dirs):
320 if d.lower() == ignore:
319 if d.lower() == ignore:
321 del dirs[i]
320 del dirs[i]
322 break
321 break
323 if os.path.basename(dirname).lower() != '.hg':
322 if os.path.basename(dirname).lower() != '.hg':
324 continue
323 continue
325 for f in names:
324 for f in names:
326 if f.lower() == 'hgrc':
325 if f.lower() == 'hgrc':
327 ui.warn(_("warning: removing potentially hostile 'hgrc' "
326 ui.warn(_("warning: removing potentially hostile 'hgrc' "
328 "in '%s'\n") % dirname)
327 "in '%s'\n") % dirname)
329 os.unlink(os.path.join(dirname, f))
328 os.unlink(os.path.join(dirname, f))
330
329
331 def subrepo(ctx, path):
330 def subrepo(ctx, path):
332 """return instance of the right subrepo class for subrepo in path"""
331 """return instance of the right subrepo class for subrepo in path"""
333 # subrepo inherently violates our import layering rules
332 # subrepo inherently violates our import layering rules
334 # because it wants to make repo objects from deep inside the stack
333 # because it wants to make repo objects from deep inside the stack
335 # so we manually delay the circular imports to not break
334 # so we manually delay the circular imports to not break
336 # scripts that don't use our demand-loading
335 # scripts that don't use our demand-loading
337 global hg
336 global hg
338 import hg as h
337 import hg as h
339 hg = h
338 hg = h
340
339
341 pathutil.pathauditor(ctx._repo.root)(path)
340 pathutil.pathauditor(ctx._repo.root)(path)
342 state = ctx.substate[path]
341 state = ctx.substate[path]
343 if state[2] not in types:
342 if state[2] not in types:
344 raise util.Abort(_('unknown subrepo type %s') % state[2])
343 raise util.Abort(_('unknown subrepo type %s') % state[2])
345 return types[state[2]](ctx, path, state[:2])
344 return types[state[2]](ctx, path, state[:2])
346
345
347 def newcommitphase(ui, ctx):
346 def newcommitphase(ui, ctx):
348 commitphase = phases.newcommitphase(ui)
347 commitphase = phases.newcommitphase(ui)
349 substate = getattr(ctx, "substate", None)
348 substate = getattr(ctx, "substate", None)
350 if not substate:
349 if not substate:
351 return commitphase
350 return commitphase
352 check = ui.config('phases', 'checksubrepos', 'follow')
351 check = ui.config('phases', 'checksubrepos', 'follow')
353 if check not in ('ignore', 'follow', 'abort'):
352 if check not in ('ignore', 'follow', 'abort'):
354 raise util.Abort(_('invalid phases.checksubrepos configuration: %s')
353 raise util.Abort(_('invalid phases.checksubrepos configuration: %s')
355 % (check))
354 % (check))
356 if check == 'ignore':
355 if check == 'ignore':
357 return commitphase
356 return commitphase
358 maxphase = phases.public
357 maxphase = phases.public
359 maxsub = None
358 maxsub = None
360 for s in sorted(substate):
359 for s in sorted(substate):
361 sub = ctx.sub(s)
360 sub = ctx.sub(s)
362 subphase = sub.phase(substate[s][1])
361 subphase = sub.phase(substate[s][1])
363 if maxphase < subphase:
362 if maxphase < subphase:
364 maxphase = subphase
363 maxphase = subphase
365 maxsub = s
364 maxsub = s
366 if commitphase < maxphase:
365 if commitphase < maxphase:
367 if check == 'abort':
366 if check == 'abort':
368 raise util.Abort(_("can't commit in %s phase"
367 raise util.Abort(_("can't commit in %s phase"
369 " conflicting %s from subrepository %s") %
368 " conflicting %s from subrepository %s") %
370 (phases.phasenames[commitphase],
369 (phases.phasenames[commitphase],
371 phases.phasenames[maxphase], maxsub))
370 phases.phasenames[maxphase], maxsub))
372 ui.warn(_("warning: changes are committed in"
371 ui.warn(_("warning: changes are committed in"
373 " %s phase from subrepository %s\n") %
372 " %s phase from subrepository %s\n") %
374 (phases.phasenames[maxphase], maxsub))
373 (phases.phasenames[maxphase], maxsub))
375 return maxphase
374 return maxphase
376 return commitphase
375 return commitphase
377
376
378 # subrepo classes need to implement the following abstract class:
377 # subrepo classes need to implement the following abstract class:
379
378
380 class abstractsubrepo(object):
379 class abstractsubrepo(object):
381
380
382 def storeclean(self, path):
381 def storeclean(self, path):
383 """
382 """
384 returns true if the repository has not changed since it was last
383 returns true if the repository has not changed since it was last
385 cloned from or pushed to a given repository.
384 cloned from or pushed to a given repository.
386 """
385 """
387 return False
386 return False
388
387
389 def dirty(self, ignoreupdate=False):
388 def dirty(self, ignoreupdate=False):
390 """returns true if the dirstate of the subrepo is dirty or does not
389 """returns true if the dirstate of the subrepo is dirty or does not
391 match current stored state. If ignoreupdate is true, only check
390 match current stored state. If ignoreupdate is true, only check
392 whether the subrepo has uncommitted changes in its dirstate.
391 whether the subrepo has uncommitted changes in its dirstate.
393 """
392 """
394 raise NotImplementedError
393 raise NotImplementedError
395
394
396 def basestate(self):
395 def basestate(self):
397 """current working directory base state, disregarding .hgsubstate
396 """current working directory base state, disregarding .hgsubstate
398 state and working directory modifications"""
397 state and working directory modifications"""
399 raise NotImplementedError
398 raise NotImplementedError
400
399
401 def checknested(self, path):
400 def checknested(self, path):
402 """check if path is a subrepository within this repository"""
401 """check if path is a subrepository within this repository"""
403 return False
402 return False
404
403
405 def commit(self, text, user, date):
404 def commit(self, text, user, date):
406 """commit the current changes to the subrepo with the given
405 """commit the current changes to the subrepo with the given
407 log message. Use given user and date if possible. Return the
406 log message. Use given user and date if possible. Return the
408 new state of the subrepo.
407 new state of the subrepo.
409 """
408 """
410 raise NotImplementedError
409 raise NotImplementedError
411
410
412 def phase(self, state):
411 def phase(self, state):
413 """returns phase of specified state in the subrepository.
412 """returns phase of specified state in the subrepository.
414 """
413 """
415 return phases.public
414 return phases.public
416
415
417 def remove(self):
416 def remove(self):
418 """remove the subrepo
417 """remove the subrepo
419
418
420 (should verify the dirstate is not dirty first)
419 (should verify the dirstate is not dirty first)
421 """
420 """
422 raise NotImplementedError
421 raise NotImplementedError
423
422
424 def get(self, state, overwrite=False):
423 def get(self, state, overwrite=False):
425 """run whatever commands are needed to put the subrepo into
424 """run whatever commands are needed to put the subrepo into
426 this state
425 this state
427 """
426 """
428 raise NotImplementedError
427 raise NotImplementedError
429
428
430 def merge(self, state):
429 def merge(self, state):
431 """merge currently-saved state with the new state."""
430 """merge currently-saved state with the new state."""
432 raise NotImplementedError
431 raise NotImplementedError
433
432
434 def push(self, opts):
433 def push(self, opts):
435 """perform whatever action is analogous to 'hg push'
434 """perform whatever action is analogous to 'hg push'
436
435
437 This may be a no-op on some systems.
436 This may be a no-op on some systems.
438 """
437 """
439 raise NotImplementedError
438 raise NotImplementedError
440
439
441 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
440 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
442 return []
441 return []
443
442
444 def cat(self, ui, match, prefix, **opts):
443 def cat(self, ui, match, prefix, **opts):
445 return 1
444 return 1
446
445
447 def status(self, rev2, **opts):
446 def status(self, rev2, **opts):
448 return [], [], [], [], [], [], []
447 return [], [], [], [], [], [], []
449
448
450 def diff(self, ui, diffopts, node2, match, prefix, **opts):
449 def diff(self, ui, diffopts, node2, match, prefix, **opts):
451 pass
450 pass
452
451
453 def outgoing(self, ui, dest, opts):
452 def outgoing(self, ui, dest, opts):
454 return 1
453 return 1
455
454
456 def incoming(self, ui, source, opts):
455 def incoming(self, ui, source, opts):
457 return 1
456 return 1
458
457
459 def files(self):
458 def files(self):
460 """return filename iterator"""
459 """return filename iterator"""
461 raise NotImplementedError
460 raise NotImplementedError
462
461
463 def filedata(self, name):
462 def filedata(self, name):
464 """return file data"""
463 """return file data"""
465 raise NotImplementedError
464 raise NotImplementedError
466
465
467 def fileflags(self, name):
466 def fileflags(self, name):
468 """return file flags"""
467 """return file flags"""
469 return ''
468 return ''
470
469
471 def archive(self, ui, archiver, prefix, match=None):
470 def archive(self, ui, archiver, prefix, match=None):
472 if match is not None:
471 if match is not None:
473 files = [f for f in self.files() if match(f)]
472 files = [f for f in self.files() if match(f)]
474 else:
473 else:
475 files = self.files()
474 files = self.files()
476 total = len(files)
475 total = len(files)
477 relpath = subrelpath(self)
476 relpath = subrelpath(self)
478 ui.progress(_('archiving (%s)') % relpath, 0,
477 ui.progress(_('archiving (%s)') % relpath, 0,
479 unit=_('files'), total=total)
478 unit=_('files'), total=total)
480 for i, name in enumerate(files):
479 for i, name in enumerate(files):
481 flags = self.fileflags(name)
480 flags = self.fileflags(name)
482 mode = 'x' in flags and 0755 or 0644
481 mode = 'x' in flags and 0755 or 0644
483 symlink = 'l' in flags
482 symlink = 'l' in flags
484 archiver.addfile(os.path.join(prefix, self._path, name),
483 archiver.addfile(os.path.join(prefix, self._path, name),
485 mode, symlink, self.filedata(name))
484 mode, symlink, self.filedata(name))
486 ui.progress(_('archiving (%s)') % relpath, i + 1,
485 ui.progress(_('archiving (%s)') % relpath, i + 1,
487 unit=_('files'), total=total)
486 unit=_('files'), total=total)
488 ui.progress(_('archiving (%s)') % relpath, None)
487 ui.progress(_('archiving (%s)') % relpath, None)
489 return total
488 return total
490
489
491 def walk(self, match):
490 def walk(self, match):
492 '''
491 '''
493 walk recursively through the directory tree, finding all files
492 walk recursively through the directory tree, finding all files
494 matched by the match function
493 matched by the match function
495 '''
494 '''
496 pass
495 pass
497
496
498 def forget(self, ui, match, prefix):
497 def forget(self, ui, match, prefix):
499 return ([], [])
498 return ([], [])
500
499
501 def revert(self, ui, substate, *pats, **opts):
500 def revert(self, ui, substate, *pats, **opts):
502 ui.warn('%s: reverting %s subrepos is unsupported\n' \
501 ui.warn('%s: reverting %s subrepos is unsupported\n' \
503 % (substate[0], substate[2]))
502 % (substate[0], substate[2]))
504 return []
503 return []
505
504
506 class hgsubrepo(abstractsubrepo):
505 class hgsubrepo(abstractsubrepo):
507 def __init__(self, ctx, path, state):
506 def __init__(self, ctx, path, state):
508 self._path = path
507 self._path = path
509 self._state = state
508 self._state = state
510 r = ctx._repo
509 r = ctx._repo
511 root = r.wjoin(path)
510 root = r.wjoin(path)
512 create = False
511 create = False
513 if not os.path.exists(os.path.join(root, '.hg')):
512 if not os.path.exists(os.path.join(root, '.hg')):
514 create = True
513 create = True
515 util.makedirs(root)
514 util.makedirs(root)
516 self._repo = hg.repository(r.baseui, root, create=create)
515 self._repo = hg.repository(r.baseui, root, create=create)
517 for s, k in [('ui', 'commitsubrepos')]:
516 for s, k in [('ui', 'commitsubrepos')]:
518 v = r.ui.config(s, k)
517 v = r.ui.config(s, k)
519 if v:
518 if v:
520 self._repo.ui.setconfig(s, k, v, 'subrepo')
519 self._repo.ui.setconfig(s, k, v, 'subrepo')
521 self._repo.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
520 self._repo.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
522 self._initrepo(r, state[0], create)
521 self._initrepo(r, state[0], create)
523
522
524 def storeclean(self, path):
523 def storeclean(self, path):
525 clean = True
524 clean = True
526 lock = self._repo.lock()
525 lock = self._repo.lock()
527 itercache = self._calcstorehash(path)
526 itercache = self._calcstorehash(path)
528 try:
527 try:
529 for filehash in self._readstorehashcache(path):
528 for filehash in self._readstorehashcache(path):
530 if filehash != itercache.next():
529 if filehash != itercache.next():
531 clean = False
530 clean = False
532 break
531 break
533 except StopIteration:
532 except StopIteration:
534 # the cached and current pull states have a different size
533 # the cached and current pull states have a different size
535 clean = False
534 clean = False
536 if clean:
535 if clean:
537 try:
536 try:
538 itercache.next()
537 itercache.next()
539 # the cached and current pull states have a different size
538 # the cached and current pull states have a different size
540 clean = False
539 clean = False
541 except StopIteration:
540 except StopIteration:
542 pass
541 pass
543 lock.release()
542 lock.release()
544 return clean
543 return clean
545
544
546 def _calcstorehash(self, remotepath):
545 def _calcstorehash(self, remotepath):
547 '''calculate a unique "store hash"
546 '''calculate a unique "store hash"
548
547
549 This method is used to to detect when there are changes that may
548 This method is used to to detect when there are changes that may
550 require a push to a given remote path.'''
549 require a push to a given remote path.'''
551 # sort the files that will be hashed in increasing (likely) file size
550 # sort the files that will be hashed in increasing (likely) file size
552 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
551 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
553 yield '# %s\n' % _expandedabspath(remotepath)
552 yield '# %s\n' % _expandedabspath(remotepath)
554 for relname in filelist:
553 for relname in filelist:
555 absname = os.path.normpath(self._repo.join(relname))
554 absname = os.path.normpath(self._repo.join(relname))
556 yield '%s = %s\n' % (relname, _calcfilehash(absname))
555 yield '%s = %s\n' % (relname, _calcfilehash(absname))
557
556
558 def _getstorehashcachepath(self, remotepath):
557 def _getstorehashcachepath(self, remotepath):
559 '''get a unique path for the store hash cache'''
558 '''get a unique path for the store hash cache'''
560 return self._repo.join(os.path.join(
559 return self._repo.join(os.path.join(
561 'cache', 'storehash', _getstorehashcachename(remotepath)))
560 'cache', 'storehash', _getstorehashcachename(remotepath)))
562
561
563 def _readstorehashcache(self, remotepath):
562 def _readstorehashcache(self, remotepath):
564 '''read the store hash cache for a given remote repository'''
563 '''read the store hash cache for a given remote repository'''
565 cachefile = self._getstorehashcachepath(remotepath)
564 cachefile = self._getstorehashcachepath(remotepath)
566 if not os.path.exists(cachefile):
565 if not os.path.exists(cachefile):
567 return ''
566 return ''
568 fd = open(cachefile, 'r')
567 fd = open(cachefile, 'r')
569 pullstate = fd.readlines()
568 pullstate = fd.readlines()
570 fd.close()
569 fd.close()
571 return pullstate
570 return pullstate
572
571
573 def _cachestorehash(self, remotepath):
572 def _cachestorehash(self, remotepath):
574 '''cache the current store hash
573 '''cache the current store hash
575
574
576 Each remote repo requires its own store hash cache, because a subrepo
575 Each remote repo requires its own store hash cache, because a subrepo
577 store may be "clean" versus a given remote repo, but not versus another
576 store may be "clean" versus a given remote repo, but not versus another
578 '''
577 '''
579 cachefile = self._getstorehashcachepath(remotepath)
578 cachefile = self._getstorehashcachepath(remotepath)
580 lock = self._repo.lock()
579 lock = self._repo.lock()
581 storehash = list(self._calcstorehash(remotepath))
580 storehash = list(self._calcstorehash(remotepath))
582 cachedir = os.path.dirname(cachefile)
581 cachedir = os.path.dirname(cachefile)
583 if not os.path.exists(cachedir):
582 if not os.path.exists(cachedir):
584 util.makedirs(cachedir, notindexed=True)
583 util.makedirs(cachedir, notindexed=True)
585 fd = open(cachefile, 'w')
584 fd = open(cachefile, 'w')
586 fd.writelines(storehash)
585 fd.writelines(storehash)
587 fd.close()
586 fd.close()
588 lock.release()
587 lock.release()
589
588
590 @annotatesubrepoerror
589 @annotatesubrepoerror
591 def _initrepo(self, parentrepo, source, create):
590 def _initrepo(self, parentrepo, source, create):
592 self._repo._subparent = parentrepo
591 self._repo._subparent = parentrepo
593 self._repo._subsource = source
592 self._repo._subsource = source
594
593
595 if create:
594 if create:
596 fp = self._repo.opener("hgrc", "w", text=True)
595 fp = self._repo.opener("hgrc", "w", text=True)
597 fp.write('[paths]\n')
596 fp.write('[paths]\n')
598
597
599 def addpathconfig(key, value):
598 def addpathconfig(key, value):
600 if value:
599 if value:
601 fp.write('%s = %s\n' % (key, value))
600 fp.write('%s = %s\n' % (key, value))
602 self._repo.ui.setconfig('paths', key, value, 'subrepo')
601 self._repo.ui.setconfig('paths', key, value, 'subrepo')
603
602
604 defpath = _abssource(self._repo, abort=False)
603 defpath = _abssource(self._repo, abort=False)
605 defpushpath = _abssource(self._repo, True, abort=False)
604 defpushpath = _abssource(self._repo, True, abort=False)
606 addpathconfig('default', defpath)
605 addpathconfig('default', defpath)
607 if defpath != defpushpath:
606 if defpath != defpushpath:
608 addpathconfig('default-push', defpushpath)
607 addpathconfig('default-push', defpushpath)
609 fp.close()
608 fp.close()
610
609
611 @annotatesubrepoerror
610 @annotatesubrepoerror
612 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
611 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
613 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
612 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
614 os.path.join(prefix, self._path), explicitonly)
613 os.path.join(prefix, self._path), explicitonly)
615
614
616 @annotatesubrepoerror
615 @annotatesubrepoerror
617 def cat(self, ui, match, prefix, **opts):
616 def cat(self, ui, match, prefix, **opts):
618 rev = self._state[1]
617 rev = self._state[1]
619 ctx = self._repo[rev]
618 ctx = self._repo[rev]
620 return cmdutil.cat(ui, self._repo, ctx, match, prefix, **opts)
619 return cmdutil.cat(ui, self._repo, ctx, match, prefix, **opts)
621
620
622 @annotatesubrepoerror
621 @annotatesubrepoerror
623 def status(self, rev2, **opts):
622 def status(self, rev2, **opts):
624 try:
623 try:
625 rev1 = self._state[1]
624 rev1 = self._state[1]
626 ctx1 = self._repo[rev1]
625 ctx1 = self._repo[rev1]
627 ctx2 = self._repo[rev2]
626 ctx2 = self._repo[rev2]
628 return self._repo.status(ctx1, ctx2, **opts)
627 return self._repo.status(ctx1, ctx2, **opts)
629 except error.RepoLookupError, inst:
628 except error.RepoLookupError, inst:
630 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
629 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
631 % (inst, subrelpath(self)))
630 % (inst, subrelpath(self)))
632 return [], [], [], [], [], [], []
631 return [], [], [], [], [], [], []
633
632
634 @annotatesubrepoerror
633 @annotatesubrepoerror
635 def diff(self, ui, diffopts, node2, match, prefix, **opts):
634 def diff(self, ui, diffopts, node2, match, prefix, **opts):
636 try:
635 try:
637 node1 = node.bin(self._state[1])
636 node1 = node.bin(self._state[1])
638 # We currently expect node2 to come from substate and be
637 # We currently expect node2 to come from substate and be
639 # in hex format
638 # in hex format
640 if node2 is not None:
639 if node2 is not None:
641 node2 = node.bin(node2)
640 node2 = node.bin(node2)
642 cmdutil.diffordiffstat(ui, self._repo, diffopts,
641 cmdutil.diffordiffstat(ui, self._repo, diffopts,
643 node1, node2, match,
642 node1, node2, match,
644 prefix=posixpath.join(prefix, self._path),
643 prefix=posixpath.join(prefix, self._path),
645 listsubrepos=True, **opts)
644 listsubrepos=True, **opts)
646 except error.RepoLookupError, inst:
645 except error.RepoLookupError, inst:
647 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
646 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
648 % (inst, subrelpath(self)))
647 % (inst, subrelpath(self)))
649
648
650 @annotatesubrepoerror
649 @annotatesubrepoerror
651 def archive(self, ui, archiver, prefix, match=None):
650 def archive(self, ui, archiver, prefix, match=None):
652 self._get(self._state + ('hg',))
651 self._get(self._state + ('hg',))
653 total = abstractsubrepo.archive(self, ui, archiver, prefix, match)
652 total = abstractsubrepo.archive(self, ui, archiver, prefix, match)
654 rev = self._state[1]
653 rev = self._state[1]
655 ctx = self._repo[rev]
654 ctx = self._repo[rev]
656 for subpath in ctx.substate:
655 for subpath in ctx.substate:
657 s = subrepo(ctx, subpath)
656 s = subrepo(ctx, subpath)
658 submatch = matchmod.narrowmatcher(subpath, match)
657 submatch = matchmod.narrowmatcher(subpath, match)
659 total += s.archive(
658 total += s.archive(
660 ui, archiver, os.path.join(prefix, self._path), submatch)
659 ui, archiver, os.path.join(prefix, self._path), submatch)
661 return total
660 return total
662
661
663 @annotatesubrepoerror
662 @annotatesubrepoerror
664 def dirty(self, ignoreupdate=False):
663 def dirty(self, ignoreupdate=False):
665 r = self._state[1]
664 r = self._state[1]
666 if r == '' and not ignoreupdate: # no state recorded
665 if r == '' and not ignoreupdate: # no state recorded
667 return True
666 return True
668 w = self._repo[None]
667 w = self._repo[None]
669 if r != w.p1().hex() and not ignoreupdate:
668 if r != w.p1().hex() and not ignoreupdate:
670 # different version checked out
669 # different version checked out
671 return True
670 return True
672 return w.dirty() # working directory changed
671 return w.dirty() # working directory changed
673
672
674 def basestate(self):
673 def basestate(self):
675 return self._repo['.'].hex()
674 return self._repo['.'].hex()
676
675
677 def checknested(self, path):
676 def checknested(self, path):
678 return self._repo._checknested(self._repo.wjoin(path))
677 return self._repo._checknested(self._repo.wjoin(path))
679
678
680 @annotatesubrepoerror
679 @annotatesubrepoerror
681 def commit(self, text, user, date):
680 def commit(self, text, user, date):
682 # don't bother committing in the subrepo if it's only been
681 # don't bother committing in the subrepo if it's only been
683 # updated
682 # updated
684 if not self.dirty(True):
683 if not self.dirty(True):
685 return self._repo['.'].hex()
684 return self._repo['.'].hex()
686 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
685 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
687 n = self._repo.commit(text, user, date)
686 n = self._repo.commit(text, user, date)
688 if not n:
687 if not n:
689 return self._repo['.'].hex() # different version checked out
688 return self._repo['.'].hex() # different version checked out
690 return node.hex(n)
689 return node.hex(n)
691
690
692 @annotatesubrepoerror
691 @annotatesubrepoerror
693 def phase(self, state):
692 def phase(self, state):
694 return self._repo[state].phase()
693 return self._repo[state].phase()
695
694
696 @annotatesubrepoerror
695 @annotatesubrepoerror
697 def remove(self):
696 def remove(self):
698 # we can't fully delete the repository as it may contain
697 # we can't fully delete the repository as it may contain
699 # local-only history
698 # local-only history
700 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
699 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
701 hg.clean(self._repo, node.nullid, False)
700 hg.clean(self._repo, node.nullid, False)
702
701
703 def _get(self, state):
702 def _get(self, state):
704 source, revision, kind = state
703 source, revision, kind = state
705 if revision in self._repo.unfiltered():
704 if revision in self._repo.unfiltered():
706 return True
705 return True
707 self._repo._subsource = source
706 self._repo._subsource = source
708 srcurl = _abssource(self._repo)
707 srcurl = _abssource(self._repo)
709 other = hg.peer(self._repo, {}, srcurl)
708 other = hg.peer(self._repo, {}, srcurl)
710 if len(self._repo) == 0:
709 if len(self._repo) == 0:
711 self._repo.ui.status(_('cloning subrepo %s from %s\n')
710 self._repo.ui.status(_('cloning subrepo %s from %s\n')
712 % (subrelpath(self), srcurl))
711 % (subrelpath(self), srcurl))
713 parentrepo = self._repo._subparent
712 parentrepo = self._repo._subparent
714 shutil.rmtree(self._repo.path)
713 shutil.rmtree(self._repo.path)
715 other, cloned = hg.clone(self._repo._subparent.baseui, {},
714 other, cloned = hg.clone(self._repo._subparent.baseui, {},
716 other, self._repo.root,
715 other, self._repo.root,
717 update=False)
716 update=False)
718 self._repo = cloned.local()
717 self._repo = cloned.local()
719 self._initrepo(parentrepo, source, create=True)
718 self._initrepo(parentrepo, source, create=True)
720 self._cachestorehash(srcurl)
719 self._cachestorehash(srcurl)
721 else:
720 else:
722 self._repo.ui.status(_('pulling subrepo %s from %s\n')
721 self._repo.ui.status(_('pulling subrepo %s from %s\n')
723 % (subrelpath(self), srcurl))
722 % (subrelpath(self), srcurl))
724 cleansub = self.storeclean(srcurl)
723 cleansub = self.storeclean(srcurl)
725 remotebookmarks = other.listkeys('bookmarks')
724 remotebookmarks = other.listkeys('bookmarks')
726 self._repo.pull(other)
725 self._repo.pull(other)
727 bookmarks.updatefromremote(self._repo.ui, self._repo,
726 bookmarks.updatefromremote(self._repo.ui, self._repo,
728 remotebookmarks, srcurl)
727 remotebookmarks, srcurl)
729 if cleansub:
728 if cleansub:
730 # keep the repo clean after pull
729 # keep the repo clean after pull
731 self._cachestorehash(srcurl)
730 self._cachestorehash(srcurl)
732 return False
731 return False
733
732
734 @annotatesubrepoerror
733 @annotatesubrepoerror
735 def get(self, state, overwrite=False):
734 def get(self, state, overwrite=False):
736 inrepo = self._get(state)
735 inrepo = self._get(state)
737 source, revision, kind = state
736 source, revision, kind = state
738 repo = self._repo
737 repo = self._repo
739 repo.ui.debug("getting subrepo %s\n" % self._path)
738 repo.ui.debug("getting subrepo %s\n" % self._path)
740 if inrepo:
739 if inrepo:
741 urepo = repo.unfiltered()
740 urepo = repo.unfiltered()
742 ctx = urepo[revision]
741 ctx = urepo[revision]
743 if ctx.hidden():
742 if ctx.hidden():
744 urepo.ui.warn(
743 urepo.ui.warn(
745 _('revision %s in subrepo %s is hidden\n') \
744 _('revision %s in subrepo %s is hidden\n') \
746 % (revision[0:12], self._path))
745 % (revision[0:12], self._path))
747 repo = urepo
746 repo = urepo
748 hg.updaterepo(repo, revision, overwrite)
747 hg.updaterepo(repo, revision, overwrite)
749
748
750 @annotatesubrepoerror
749 @annotatesubrepoerror
751 def merge(self, state):
750 def merge(self, state):
752 self._get(state)
751 self._get(state)
753 cur = self._repo['.']
752 cur = self._repo['.']
754 dst = self._repo[state[1]]
753 dst = self._repo[state[1]]
755 anc = dst.ancestor(cur)
754 anc = dst.ancestor(cur)
756
755
757 def mergefunc():
756 def mergefunc():
758 if anc == cur and dst.branch() == cur.branch():
757 if anc == cur and dst.branch() == cur.branch():
759 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
758 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
760 hg.update(self._repo, state[1])
759 hg.update(self._repo, state[1])
761 elif anc == dst:
760 elif anc == dst:
762 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
761 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
763 else:
762 else:
764 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
763 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
765 hg.merge(self._repo, state[1], remind=False)
764 hg.merge(self._repo, state[1], remind=False)
766
765
767 wctx = self._repo[None]
766 wctx = self._repo[None]
768 if self.dirty():
767 if self.dirty():
769 if anc != dst:
768 if anc != dst:
770 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
769 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
771 mergefunc()
770 mergefunc()
772 else:
771 else:
773 mergefunc()
772 mergefunc()
774 else:
773 else:
775 mergefunc()
774 mergefunc()
776
775
777 @annotatesubrepoerror
776 @annotatesubrepoerror
778 def push(self, opts):
777 def push(self, opts):
779 force = opts.get('force')
778 force = opts.get('force')
780 newbranch = opts.get('new_branch')
779 newbranch = opts.get('new_branch')
781 ssh = opts.get('ssh')
780 ssh = opts.get('ssh')
782
781
783 # push subrepos depth-first for coherent ordering
782 # push subrepos depth-first for coherent ordering
784 c = self._repo['']
783 c = self._repo['']
785 subs = c.substate # only repos that are committed
784 subs = c.substate # only repos that are committed
786 for s in sorted(subs):
785 for s in sorted(subs):
787 if c.sub(s).push(opts) == 0:
786 if c.sub(s).push(opts) == 0:
788 return False
787 return False
789
788
790 dsturl = _abssource(self._repo, True)
789 dsturl = _abssource(self._repo, True)
791 if not force:
790 if not force:
792 if self.storeclean(dsturl):
791 if self.storeclean(dsturl):
793 self._repo.ui.status(
792 self._repo.ui.status(
794 _('no changes made to subrepo %s since last push to %s\n')
793 _('no changes made to subrepo %s since last push to %s\n')
795 % (subrelpath(self), dsturl))
794 % (subrelpath(self), dsturl))
796 return None
795 return None
797 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
796 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
798 (subrelpath(self), dsturl))
797 (subrelpath(self), dsturl))
799 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
798 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
800 res = self._repo.push(other, force, newbranch=newbranch)
799 res = self._repo.push(other, force, newbranch=newbranch)
801
800
802 # the repo is now clean
801 # the repo is now clean
803 self._cachestorehash(dsturl)
802 self._cachestorehash(dsturl)
804 return res
803 return res
805
804
806 @annotatesubrepoerror
805 @annotatesubrepoerror
807 def outgoing(self, ui, dest, opts):
806 def outgoing(self, ui, dest, opts):
808 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
807 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
809
808
810 @annotatesubrepoerror
809 @annotatesubrepoerror
811 def incoming(self, ui, source, opts):
810 def incoming(self, ui, source, opts):
812 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
811 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
813
812
814 @annotatesubrepoerror
813 @annotatesubrepoerror
815 def files(self):
814 def files(self):
816 rev = self._state[1]
815 rev = self._state[1]
817 ctx = self._repo[rev]
816 ctx = self._repo[rev]
818 return ctx.manifest()
817 return ctx.manifest()
819
818
820 def filedata(self, name):
819 def filedata(self, name):
821 rev = self._state[1]
820 rev = self._state[1]
822 return self._repo[rev][name].data()
821 return self._repo[rev][name].data()
823
822
824 def fileflags(self, name):
823 def fileflags(self, name):
825 rev = self._state[1]
824 rev = self._state[1]
826 ctx = self._repo[rev]
825 ctx = self._repo[rev]
827 return ctx.flags(name)
826 return ctx.flags(name)
828
827
829 def walk(self, match):
828 def walk(self, match):
830 ctx = self._repo[None]
829 ctx = self._repo[None]
831 return ctx.walk(match)
830 return ctx.walk(match)
832
831
833 @annotatesubrepoerror
832 @annotatesubrepoerror
834 def forget(self, ui, match, prefix):
833 def forget(self, ui, match, prefix):
835 return cmdutil.forget(ui, self._repo, match,
834 return cmdutil.forget(ui, self._repo, match,
836 os.path.join(prefix, self._path), True)
835 os.path.join(prefix, self._path), True)
837
836
838 @annotatesubrepoerror
837 @annotatesubrepoerror
839 def revert(self, ui, substate, *pats, **opts):
838 def revert(self, ui, substate, *pats, **opts):
840 # reverting a subrepo is a 2 step process:
839 # reverting a subrepo is a 2 step process:
841 # 1. if the no_backup is not set, revert all modified
840 # 1. if the no_backup is not set, revert all modified
842 # files inside the subrepo
841 # files inside the subrepo
843 # 2. update the subrepo to the revision specified in
842 # 2. update the subrepo to the revision specified in
844 # the corresponding substate dictionary
843 # the corresponding substate dictionary
845 ui.status(_('reverting subrepo %s\n') % substate[0])
844 ui.status(_('reverting subrepo %s\n') % substate[0])
846 if not opts.get('no_backup'):
845 if not opts.get('no_backup'):
847 # Revert all files on the subrepo, creating backups
846 # Revert all files on the subrepo, creating backups
848 # Note that this will not recursively revert subrepos
847 # Note that this will not recursively revert subrepos
849 # We could do it if there was a set:subrepos() predicate
848 # We could do it if there was a set:subrepos() predicate
850 opts = opts.copy()
849 opts = opts.copy()
851 opts['date'] = None
850 opts['date'] = None
852 opts['rev'] = substate[1]
851 opts['rev'] = substate[1]
853
852
854 pats = []
853 pats = []
855 if not opts.get('all'):
854 if not opts.get('all'):
856 pats = ['set:modified()']
855 pats = ['set:modified()']
857 self.filerevert(ui, *pats, **opts)
856 self.filerevert(ui, *pats, **opts)
858
857
859 # Update the repo to the revision specified in the given substate
858 # Update the repo to the revision specified in the given substate
860 self.get(substate, overwrite=True)
859 self.get(substate, overwrite=True)
861
860
862 def filerevert(self, ui, *pats, **opts):
861 def filerevert(self, ui, *pats, **opts):
863 ctx = self._repo[opts['rev']]
862 ctx = self._repo[opts['rev']]
864 parents = self._repo.dirstate.parents()
863 parents = self._repo.dirstate.parents()
865 if opts.get('all'):
864 if opts.get('all'):
866 pats = ['set:modified()']
865 pats = ['set:modified()']
867 else:
866 else:
868 pats = []
867 pats = []
869 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
868 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
870
869
871 class svnsubrepo(abstractsubrepo):
870 class svnsubrepo(abstractsubrepo):
872 def __init__(self, ctx, path, state):
871 def __init__(self, ctx, path, state):
873 self._path = path
872 self._path = path
874 self._state = state
873 self._state = state
875 self._ctx = ctx
874 self._ctx = ctx
876 self._ui = ctx._repo.ui
875 self._ui = ctx._repo.ui
877 self._exe = util.findexe('svn')
876 self._exe = util.findexe('svn')
878 if not self._exe:
877 if not self._exe:
879 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
878 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
880 % self._path)
879 % self._path)
881
880
882 def _svncommand(self, commands, filename='', failok=False):
881 def _svncommand(self, commands, filename='', failok=False):
883 cmd = [self._exe]
882 cmd = [self._exe]
884 extrakw = {}
883 extrakw = {}
885 if not self._ui.interactive():
884 if not self._ui.interactive():
886 # Making stdin be a pipe should prevent svn from behaving
885 # Making stdin be a pipe should prevent svn from behaving
887 # interactively even if we can't pass --non-interactive.
886 # interactively even if we can't pass --non-interactive.
888 extrakw['stdin'] = subprocess.PIPE
887 extrakw['stdin'] = subprocess.PIPE
889 # Starting in svn 1.5 --non-interactive is a global flag
888 # Starting in svn 1.5 --non-interactive is a global flag
890 # instead of being per-command, but we need to support 1.4 so
889 # instead of being per-command, but we need to support 1.4 so
891 # we have to be intelligent about what commands take
890 # we have to be intelligent about what commands take
892 # --non-interactive.
891 # --non-interactive.
893 if commands[0] in ('update', 'checkout', 'commit'):
892 if commands[0] in ('update', 'checkout', 'commit'):
894 cmd.append('--non-interactive')
893 cmd.append('--non-interactive')
895 cmd.extend(commands)
894 cmd.extend(commands)
896 if filename is not None:
895 if filename is not None:
897 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
896 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
898 cmd.append(path)
897 cmd.append(path)
899 env = dict(os.environ)
898 env = dict(os.environ)
900 # Avoid localized output, preserve current locale for everything else.
899 # Avoid localized output, preserve current locale for everything else.
901 lc_all = env.get('LC_ALL')
900 lc_all = env.get('LC_ALL')
902 if lc_all:
901 if lc_all:
903 env['LANG'] = lc_all
902 env['LANG'] = lc_all
904 del env['LC_ALL']
903 del env['LC_ALL']
905 env['LC_MESSAGES'] = 'C'
904 env['LC_MESSAGES'] = 'C'
906 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
905 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
907 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
906 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
908 universal_newlines=True, env=env, **extrakw)
907 universal_newlines=True, env=env, **extrakw)
909 stdout, stderr = p.communicate()
908 stdout, stderr = p.communicate()
910 stderr = stderr.strip()
909 stderr = stderr.strip()
911 if not failok:
910 if not failok:
912 if p.returncode:
911 if p.returncode:
913 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
912 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
914 if stderr:
913 if stderr:
915 self._ui.warn(stderr + '\n')
914 self._ui.warn(stderr + '\n')
916 return stdout, stderr
915 return stdout, stderr
917
916
918 @propertycache
917 @propertycache
919 def _svnversion(self):
918 def _svnversion(self):
920 output, err = self._svncommand(['--version', '--quiet'], filename=None)
919 output, err = self._svncommand(['--version', '--quiet'], filename=None)
921 m = re.search(r'^(\d+)\.(\d+)', output)
920 m = re.search(r'^(\d+)\.(\d+)', output)
922 if not m:
921 if not m:
923 raise util.Abort(_('cannot retrieve svn tool version'))
922 raise util.Abort(_('cannot retrieve svn tool version'))
924 return (int(m.group(1)), int(m.group(2)))
923 return (int(m.group(1)), int(m.group(2)))
925
924
926 def _wcrevs(self):
925 def _wcrevs(self):
927 # Get the working directory revision as well as the last
926 # Get the working directory revision as well as the last
928 # commit revision so we can compare the subrepo state with
927 # commit revision so we can compare the subrepo state with
929 # both. We used to store the working directory one.
928 # both. We used to store the working directory one.
930 output, err = self._svncommand(['info', '--xml'])
929 output, err = self._svncommand(['info', '--xml'])
931 doc = xml.dom.minidom.parseString(output)
930 doc = xml.dom.minidom.parseString(output)
932 entries = doc.getElementsByTagName('entry')
931 entries = doc.getElementsByTagName('entry')
933 lastrev, rev = '0', '0'
932 lastrev, rev = '0', '0'
934 if entries:
933 if entries:
935 rev = str(entries[0].getAttribute('revision')) or '0'
934 rev = str(entries[0].getAttribute('revision')) or '0'
936 commits = entries[0].getElementsByTagName('commit')
935 commits = entries[0].getElementsByTagName('commit')
937 if commits:
936 if commits:
938 lastrev = str(commits[0].getAttribute('revision')) or '0'
937 lastrev = str(commits[0].getAttribute('revision')) or '0'
939 return (lastrev, rev)
938 return (lastrev, rev)
940
939
941 def _wcrev(self):
940 def _wcrev(self):
942 return self._wcrevs()[0]
941 return self._wcrevs()[0]
943
942
944 def _wcchanged(self):
943 def _wcchanged(self):
945 """Return (changes, extchanges, missing) where changes is True
944 """Return (changes, extchanges, missing) where changes is True
946 if the working directory was changed, extchanges is
945 if the working directory was changed, extchanges is
947 True if any of these changes concern an external entry and missing
946 True if any of these changes concern an external entry and missing
948 is True if any change is a missing entry.
947 is True if any change is a missing entry.
949 """
948 """
950 output, err = self._svncommand(['status', '--xml'])
949 output, err = self._svncommand(['status', '--xml'])
951 externals, changes, missing = [], [], []
950 externals, changes, missing = [], [], []
952 doc = xml.dom.minidom.parseString(output)
951 doc = xml.dom.minidom.parseString(output)
953 for e in doc.getElementsByTagName('entry'):
952 for e in doc.getElementsByTagName('entry'):
954 s = e.getElementsByTagName('wc-status')
953 s = e.getElementsByTagName('wc-status')
955 if not s:
954 if not s:
956 continue
955 continue
957 item = s[0].getAttribute('item')
956 item = s[0].getAttribute('item')
958 props = s[0].getAttribute('props')
957 props = s[0].getAttribute('props')
959 path = e.getAttribute('path')
958 path = e.getAttribute('path')
960 if item == 'external':
959 if item == 'external':
961 externals.append(path)
960 externals.append(path)
962 elif item == 'missing':
961 elif item == 'missing':
963 missing.append(path)
962 missing.append(path)
964 if (item not in ('', 'normal', 'unversioned', 'external')
963 if (item not in ('', 'normal', 'unversioned', 'external')
965 or props not in ('', 'none', 'normal')):
964 or props not in ('', 'none', 'normal')):
966 changes.append(path)
965 changes.append(path)
967 for path in changes:
966 for path in changes:
968 for ext in externals:
967 for ext in externals:
969 if path == ext or path.startswith(ext + os.sep):
968 if path == ext or path.startswith(ext + os.sep):
970 return True, True, bool(missing)
969 return True, True, bool(missing)
971 return bool(changes), False, bool(missing)
970 return bool(changes), False, bool(missing)
972
971
973 def dirty(self, ignoreupdate=False):
972 def dirty(self, ignoreupdate=False):
974 if not self._wcchanged()[0]:
973 if not self._wcchanged()[0]:
975 if self._state[1] in self._wcrevs() or ignoreupdate:
974 if self._state[1] in self._wcrevs() or ignoreupdate:
976 return False
975 return False
977 return True
976 return True
978
977
979 def basestate(self):
978 def basestate(self):
980 lastrev, rev = self._wcrevs()
979 lastrev, rev = self._wcrevs()
981 if lastrev != rev:
980 if lastrev != rev:
982 # Last committed rev is not the same than rev. We would
981 # Last committed rev is not the same than rev. We would
983 # like to take lastrev but we do not know if the subrepo
982 # like to take lastrev but we do not know if the subrepo
984 # URL exists at lastrev. Test it and fallback to rev it
983 # URL exists at lastrev. Test it and fallback to rev it
985 # is not there.
984 # is not there.
986 try:
985 try:
987 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
986 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
988 return lastrev
987 return lastrev
989 except error.Abort:
988 except error.Abort:
990 pass
989 pass
991 return rev
990 return rev
992
991
993 @annotatesubrepoerror
992 @annotatesubrepoerror
994 def commit(self, text, user, date):
993 def commit(self, text, user, date):
995 # user and date are out of our hands since svn is centralized
994 # user and date are out of our hands since svn is centralized
996 changed, extchanged, missing = self._wcchanged()
995 changed, extchanged, missing = self._wcchanged()
997 if not changed:
996 if not changed:
998 return self.basestate()
997 return self.basestate()
999 if extchanged:
998 if extchanged:
1000 # Do not try to commit externals
999 # Do not try to commit externals
1001 raise util.Abort(_('cannot commit svn externals'))
1000 raise util.Abort(_('cannot commit svn externals'))
1002 if missing:
1001 if missing:
1003 # svn can commit with missing entries but aborting like hg
1002 # svn can commit with missing entries but aborting like hg
1004 # seems a better approach.
1003 # seems a better approach.
1005 raise util.Abort(_('cannot commit missing svn entries'))
1004 raise util.Abort(_('cannot commit missing svn entries'))
1006 commitinfo, err = self._svncommand(['commit', '-m', text])
1005 commitinfo, err = self._svncommand(['commit', '-m', text])
1007 self._ui.status(commitinfo)
1006 self._ui.status(commitinfo)
1008 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1007 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1009 if not newrev:
1008 if not newrev:
1010 if not commitinfo.strip():
1009 if not commitinfo.strip():
1011 # Sometimes, our definition of "changed" differs from
1010 # Sometimes, our definition of "changed" differs from
1012 # svn one. For instance, svn ignores missing files
1011 # svn one. For instance, svn ignores missing files
1013 # when committing. If there are only missing files, no
1012 # when committing. If there are only missing files, no
1014 # commit is made, no output and no error code.
1013 # commit is made, no output and no error code.
1015 raise util.Abort(_('failed to commit svn changes'))
1014 raise util.Abort(_('failed to commit svn changes'))
1016 raise util.Abort(commitinfo.splitlines()[-1])
1015 raise util.Abort(commitinfo.splitlines()[-1])
1017 newrev = newrev.groups()[0]
1016 newrev = newrev.groups()[0]
1018 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
1017 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
1019 return newrev
1018 return newrev
1020
1019
1021 @annotatesubrepoerror
1020 @annotatesubrepoerror
1022 def remove(self):
1021 def remove(self):
1023 if self.dirty():
1022 if self.dirty():
1024 self._ui.warn(_('not removing repo %s because '
1023 self._ui.warn(_('not removing repo %s because '
1025 'it has changes.\n') % self._path)
1024 'it has changes.\n') % self._path)
1026 return
1025 return
1027 self._ui.note(_('removing subrepo %s\n') % self._path)
1026 self._ui.note(_('removing subrepo %s\n') % self._path)
1028
1027
1029 def onerror(function, path, excinfo):
1028 def onerror(function, path, excinfo):
1030 if function is not os.remove:
1029 if function is not os.remove:
1031 raise
1030 raise
1032 # read-only files cannot be unlinked under Windows
1031 # read-only files cannot be unlinked under Windows
1033 s = os.stat(path)
1032 s = os.stat(path)
1034 if (s.st_mode & stat.S_IWRITE) != 0:
1033 if (s.st_mode & stat.S_IWRITE) != 0:
1035 raise
1034 raise
1036 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
1035 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
1037 os.remove(path)
1036 os.remove(path)
1038
1037
1039 path = self._ctx._repo.wjoin(self._path)
1038 path = self._ctx._repo.wjoin(self._path)
1040 shutil.rmtree(path, onerror=onerror)
1039 shutil.rmtree(path, onerror=onerror)
1041 try:
1040 try:
1042 os.removedirs(os.path.dirname(path))
1041 os.removedirs(os.path.dirname(path))
1043 except OSError:
1042 except OSError:
1044 pass
1043 pass
1045
1044
1046 @annotatesubrepoerror
1045 @annotatesubrepoerror
1047 def get(self, state, overwrite=False):
1046 def get(self, state, overwrite=False):
1048 if overwrite:
1047 if overwrite:
1049 self._svncommand(['revert', '--recursive'])
1048 self._svncommand(['revert', '--recursive'])
1050 args = ['checkout']
1049 args = ['checkout']
1051 if self._svnversion >= (1, 5):
1050 if self._svnversion >= (1, 5):
1052 args.append('--force')
1051 args.append('--force')
1053 # The revision must be specified at the end of the URL to properly
1052 # The revision must be specified at the end of the URL to properly
1054 # update to a directory which has since been deleted and recreated.
1053 # update to a directory which has since been deleted and recreated.
1055 args.append('%s@%s' % (state[0], state[1]))
1054 args.append('%s@%s' % (state[0], state[1]))
1056 status, err = self._svncommand(args, failok=True)
1055 status, err = self._svncommand(args, failok=True)
1057 _sanitize(self._ui, self._ctx._repo.wjoin(self._path), '.svn')
1056 _sanitize(self._ui, self._ctx._repo.wjoin(self._path), '.svn')
1058 if not re.search('Checked out revision [0-9]+.', status):
1057 if not re.search('Checked out revision [0-9]+.', status):
1059 if ('is already a working copy for a different URL' in err
1058 if ('is already a working copy for a different URL' in err
1060 and (self._wcchanged()[:2] == (False, False))):
1059 and (self._wcchanged()[:2] == (False, False))):
1061 # obstructed but clean working copy, so just blow it away.
1060 # obstructed but clean working copy, so just blow it away.
1062 self.remove()
1061 self.remove()
1063 self.get(state, overwrite=False)
1062 self.get(state, overwrite=False)
1064 return
1063 return
1065 raise util.Abort((status or err).splitlines()[-1])
1064 raise util.Abort((status or err).splitlines()[-1])
1066 self._ui.status(status)
1065 self._ui.status(status)
1067
1066
1068 @annotatesubrepoerror
1067 @annotatesubrepoerror
1069 def merge(self, state):
1068 def merge(self, state):
1070 old = self._state[1]
1069 old = self._state[1]
1071 new = state[1]
1070 new = state[1]
1072 wcrev = self._wcrev()
1071 wcrev = self._wcrev()
1073 if new != wcrev:
1072 if new != wcrev:
1074 dirty = old == wcrev or self._wcchanged()[0]
1073 dirty = old == wcrev or self._wcchanged()[0]
1075 if _updateprompt(self._ui, self, dirty, wcrev, new):
1074 if _updateprompt(self._ui, self, dirty, wcrev, new):
1076 self.get(state, False)
1075 self.get(state, False)
1077
1076
1078 def push(self, opts):
1077 def push(self, opts):
1079 # push is a no-op for SVN
1078 # push is a no-op for SVN
1080 return True
1079 return True
1081
1080
1082 @annotatesubrepoerror
1081 @annotatesubrepoerror
1083 def files(self):
1082 def files(self):
1084 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1083 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1085 doc = xml.dom.minidom.parseString(output)
1084 doc = xml.dom.minidom.parseString(output)
1086 paths = []
1085 paths = []
1087 for e in doc.getElementsByTagName('entry'):
1086 for e in doc.getElementsByTagName('entry'):
1088 kind = str(e.getAttribute('kind'))
1087 kind = str(e.getAttribute('kind'))
1089 if kind != 'file':
1088 if kind != 'file':
1090 continue
1089 continue
1091 name = ''.join(c.data for c
1090 name = ''.join(c.data for c
1092 in e.getElementsByTagName('name')[0].childNodes
1091 in e.getElementsByTagName('name')[0].childNodes
1093 if c.nodeType == c.TEXT_NODE)
1092 if c.nodeType == c.TEXT_NODE)
1094 paths.append(name.encode('utf-8'))
1093 paths.append(name.encode('utf-8'))
1095 return paths
1094 return paths
1096
1095
1097 def filedata(self, name):
1096 def filedata(self, name):
1098 return self._svncommand(['cat'], name)[0]
1097 return self._svncommand(['cat'], name)[0]
1099
1098
1100
1099
1101 class gitsubrepo(abstractsubrepo):
1100 class gitsubrepo(abstractsubrepo):
1102 def __init__(self, ctx, path, state):
1101 def __init__(self, ctx, path, state):
1103 self._state = state
1102 self._state = state
1104 self._ctx = ctx
1103 self._ctx = ctx
1105 self._path = path
1104 self._path = path
1106 self._relpath = os.path.join(reporelpath(ctx._repo), path)
1105 self._relpath = os.path.join(reporelpath(ctx._repo), path)
1107 self._abspath = ctx._repo.wjoin(path)
1106 self._abspath = ctx._repo.wjoin(path)
1108 self._subparent = ctx._repo
1107 self._subparent = ctx._repo
1109 self._ui = ctx._repo.ui
1108 self._ui = ctx._repo.ui
1110 self._ensuregit()
1109 self._ensuregit()
1111
1110
1112 def _ensuregit(self):
1111 def _ensuregit(self):
1113 try:
1112 try:
1114 self._gitexecutable = 'git'
1113 self._gitexecutable = 'git'
1115 out, err = self._gitnodir(['--version'])
1114 out, err = self._gitnodir(['--version'])
1116 except OSError, e:
1115 except OSError, e:
1117 if e.errno != 2 or os.name != 'nt':
1116 if e.errno != 2 or os.name != 'nt':
1118 raise
1117 raise
1119 self._gitexecutable = 'git.cmd'
1118 self._gitexecutable = 'git.cmd'
1120 out, err = self._gitnodir(['--version'])
1119 out, err = self._gitnodir(['--version'])
1121 versionstatus = self._checkversion(out)
1120 versionstatus = self._checkversion(out)
1122 if versionstatus == 'unknown':
1121 if versionstatus == 'unknown':
1123 self._ui.warn(_('cannot retrieve git version\n'))
1122 self._ui.warn(_('cannot retrieve git version\n'))
1124 elif versionstatus == 'abort':
1123 elif versionstatus == 'abort':
1125 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
1124 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
1126 elif versionstatus == 'warning':
1125 elif versionstatus == 'warning':
1127 self._ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1126 self._ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1128
1127
1129 @staticmethod
1128 @staticmethod
1130 def _checkversion(out):
1129 def _checkversion(out):
1131 '''ensure git version is new enough
1130 '''ensure git version is new enough
1132
1131
1133 >>> _checkversion = gitsubrepo._checkversion
1132 >>> _checkversion = gitsubrepo._checkversion
1134 >>> _checkversion('git version 1.6.0')
1133 >>> _checkversion('git version 1.6.0')
1135 'ok'
1134 'ok'
1136 >>> _checkversion('git version 1.8.5')
1135 >>> _checkversion('git version 1.8.5')
1137 'ok'
1136 'ok'
1138 >>> _checkversion('git version 1.4.0')
1137 >>> _checkversion('git version 1.4.0')
1139 'abort'
1138 'abort'
1140 >>> _checkversion('git version 1.5.0')
1139 >>> _checkversion('git version 1.5.0')
1141 'warning'
1140 'warning'
1142 >>> _checkversion('git version 1.9-rc0')
1141 >>> _checkversion('git version 1.9-rc0')
1143 'ok'
1142 'ok'
1144 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1143 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1145 'ok'
1144 'ok'
1146 >>> _checkversion('git version 1.9.0.GIT')
1145 >>> _checkversion('git version 1.9.0.GIT')
1147 'ok'
1146 'ok'
1148 >>> _checkversion('git version 12345')
1147 >>> _checkversion('git version 12345')
1149 'unknown'
1148 'unknown'
1150 >>> _checkversion('no')
1149 >>> _checkversion('no')
1151 'unknown'
1150 'unknown'
1152 '''
1151 '''
1153 m = re.search(r'^git version (\d+)\.(\d+)', out)
1152 m = re.search(r'^git version (\d+)\.(\d+)', out)
1154 if not m:
1153 if not m:
1155 return 'unknown'
1154 return 'unknown'
1156 version = (int(m.group(1)), int(m.group(2)))
1155 version = (int(m.group(1)), int(m.group(2)))
1157 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1156 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1158 # despite the docstring comment. For now, error on 1.4.0, warn on
1157 # despite the docstring comment. For now, error on 1.4.0, warn on
1159 # 1.5.0 but attempt to continue.
1158 # 1.5.0 but attempt to continue.
1160 if version < (1, 5):
1159 if version < (1, 5):
1161 return 'abort'
1160 return 'abort'
1162 elif version < (1, 6):
1161 elif version < (1, 6):
1163 return 'warning'
1162 return 'warning'
1164 return 'ok'
1163 return 'ok'
1165
1164
1166 def _gitcommand(self, commands, env=None, stream=False):
1165 def _gitcommand(self, commands, env=None, stream=False):
1167 return self._gitdir(commands, env=env, stream=stream)[0]
1166 return self._gitdir(commands, env=env, stream=stream)[0]
1168
1167
1169 def _gitdir(self, commands, env=None, stream=False):
1168 def _gitdir(self, commands, env=None, stream=False):
1170 return self._gitnodir(commands, env=env, stream=stream,
1169 return self._gitnodir(commands, env=env, stream=stream,
1171 cwd=self._abspath)
1170 cwd=self._abspath)
1172
1171
1173 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1172 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1174 """Calls the git command
1173 """Calls the git command
1175
1174
1176 The methods tries to call the git command. versions prior to 1.6.0
1175 The methods tries to call the git command. versions prior to 1.6.0
1177 are not supported and very probably fail.
1176 are not supported and very probably fail.
1178 """
1177 """
1179 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1178 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1180 # unless ui.quiet is set, print git's stderr,
1179 # unless ui.quiet is set, print git's stderr,
1181 # which is mostly progress and useful info
1180 # which is mostly progress and useful info
1182 errpipe = None
1181 errpipe = None
1183 if self._ui.quiet:
1182 if self._ui.quiet:
1184 errpipe = open(os.devnull, 'w')
1183 errpipe = open(os.devnull, 'w')
1185 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1184 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1186 cwd=cwd, env=env, close_fds=util.closefds,
1185 cwd=cwd, env=env, close_fds=util.closefds,
1187 stdout=subprocess.PIPE, stderr=errpipe)
1186 stdout=subprocess.PIPE, stderr=errpipe)
1188 if stream:
1187 if stream:
1189 return p.stdout, None
1188 return p.stdout, None
1190
1189
1191 retdata = p.stdout.read().strip()
1190 retdata = p.stdout.read().strip()
1192 # wait for the child to exit to avoid race condition.
1191 # wait for the child to exit to avoid race condition.
1193 p.wait()
1192 p.wait()
1194
1193
1195 if p.returncode != 0 and p.returncode != 1:
1194 if p.returncode != 0 and p.returncode != 1:
1196 # there are certain error codes that are ok
1195 # there are certain error codes that are ok
1197 command = commands[0]
1196 command = commands[0]
1198 if command in ('cat-file', 'symbolic-ref'):
1197 if command in ('cat-file', 'symbolic-ref'):
1199 return retdata, p.returncode
1198 return retdata, p.returncode
1200 # for all others, abort
1199 # for all others, abort
1201 raise util.Abort('git %s error %d in %s' %
1200 raise util.Abort('git %s error %d in %s' %
1202 (command, p.returncode, self._relpath))
1201 (command, p.returncode, self._relpath))
1203
1202
1204 return retdata, p.returncode
1203 return retdata, p.returncode
1205
1204
1206 def _gitmissing(self):
1205 def _gitmissing(self):
1207 return not os.path.exists(os.path.join(self._abspath, '.git'))
1206 return not os.path.exists(os.path.join(self._abspath, '.git'))
1208
1207
1209 def _gitstate(self):
1208 def _gitstate(self):
1210 return self._gitcommand(['rev-parse', 'HEAD'])
1209 return self._gitcommand(['rev-parse', 'HEAD'])
1211
1210
1212 def _gitcurrentbranch(self):
1211 def _gitcurrentbranch(self):
1213 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1212 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1214 if err:
1213 if err:
1215 current = None
1214 current = None
1216 return current
1215 return current
1217
1216
1218 def _gitremote(self, remote):
1217 def _gitremote(self, remote):
1219 out = self._gitcommand(['remote', 'show', '-n', remote])
1218 out = self._gitcommand(['remote', 'show', '-n', remote])
1220 line = out.split('\n')[1]
1219 line = out.split('\n')[1]
1221 i = line.index('URL: ') + len('URL: ')
1220 i = line.index('URL: ') + len('URL: ')
1222 return line[i:]
1221 return line[i:]
1223
1222
1224 def _githavelocally(self, revision):
1223 def _githavelocally(self, revision):
1225 out, code = self._gitdir(['cat-file', '-e', revision])
1224 out, code = self._gitdir(['cat-file', '-e', revision])
1226 return code == 0
1225 return code == 0
1227
1226
1228 def _gitisancestor(self, r1, r2):
1227 def _gitisancestor(self, r1, r2):
1229 base = self._gitcommand(['merge-base', r1, r2])
1228 base = self._gitcommand(['merge-base', r1, r2])
1230 return base == r1
1229 return base == r1
1231
1230
1232 def _gitisbare(self):
1231 def _gitisbare(self):
1233 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1232 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1234
1233
1235 def _gitupdatestat(self):
1234 def _gitupdatestat(self):
1236 """This must be run before git diff-index.
1235 """This must be run before git diff-index.
1237 diff-index only looks at changes to file stat;
1236 diff-index only looks at changes to file stat;
1238 this command looks at file contents and updates the stat."""
1237 this command looks at file contents and updates the stat."""
1239 self._gitcommand(['update-index', '-q', '--refresh'])
1238 self._gitcommand(['update-index', '-q', '--refresh'])
1240
1239
1241 def _gitbranchmap(self):
1240 def _gitbranchmap(self):
1242 '''returns 2 things:
1241 '''returns 2 things:
1243 a map from git branch to revision
1242 a map from git branch to revision
1244 a map from revision to branches'''
1243 a map from revision to branches'''
1245 branch2rev = {}
1244 branch2rev = {}
1246 rev2branch = {}
1245 rev2branch = {}
1247
1246
1248 out = self._gitcommand(['for-each-ref', '--format',
1247 out = self._gitcommand(['for-each-ref', '--format',
1249 '%(objectname) %(refname)'])
1248 '%(objectname) %(refname)'])
1250 for line in out.split('\n'):
1249 for line in out.split('\n'):
1251 revision, ref = line.split(' ')
1250 revision, ref = line.split(' ')
1252 if (not ref.startswith('refs/heads/') and
1251 if (not ref.startswith('refs/heads/') and
1253 not ref.startswith('refs/remotes/')):
1252 not ref.startswith('refs/remotes/')):
1254 continue
1253 continue
1255 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1254 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1256 continue # ignore remote/HEAD redirects
1255 continue # ignore remote/HEAD redirects
1257 branch2rev[ref] = revision
1256 branch2rev[ref] = revision
1258 rev2branch.setdefault(revision, []).append(ref)
1257 rev2branch.setdefault(revision, []).append(ref)
1259 return branch2rev, rev2branch
1258 return branch2rev, rev2branch
1260
1259
1261 def _gittracking(self, branches):
1260 def _gittracking(self, branches):
1262 'return map of remote branch to local tracking branch'
1261 'return map of remote branch to local tracking branch'
1263 # assumes no more than one local tracking branch for each remote
1262 # assumes no more than one local tracking branch for each remote
1264 tracking = {}
1263 tracking = {}
1265 for b in branches:
1264 for b in branches:
1266 if b.startswith('refs/remotes/'):
1265 if b.startswith('refs/remotes/'):
1267 continue
1266 continue
1268 bname = b.split('/', 2)[2]
1267 bname = b.split('/', 2)[2]
1269 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1268 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1270 if remote:
1269 if remote:
1271 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1270 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1272 tracking['refs/remotes/%s/%s' %
1271 tracking['refs/remotes/%s/%s' %
1273 (remote, ref.split('/', 2)[2])] = b
1272 (remote, ref.split('/', 2)[2])] = b
1274 return tracking
1273 return tracking
1275
1274
1276 def _abssource(self, source):
1275 def _abssource(self, source):
1277 if '://' not in source:
1276 if '://' not in source:
1278 # recognize the scp syntax as an absolute source
1277 # recognize the scp syntax as an absolute source
1279 colon = source.find(':')
1278 colon = source.find(':')
1280 if colon != -1 and '/' not in source[:colon]:
1279 if colon != -1 and '/' not in source[:colon]:
1281 return source
1280 return source
1282 self._subsource = source
1281 self._subsource = source
1283 return _abssource(self)
1282 return _abssource(self)
1284
1283
1285 def _fetch(self, source, revision):
1284 def _fetch(self, source, revision):
1286 if self._gitmissing():
1285 if self._gitmissing():
1287 source = self._abssource(source)
1286 source = self._abssource(source)
1288 self._ui.status(_('cloning subrepo %s from %s\n') %
1287 self._ui.status(_('cloning subrepo %s from %s\n') %
1289 (self._relpath, source))
1288 (self._relpath, source))
1290 self._gitnodir(['clone', source, self._abspath])
1289 self._gitnodir(['clone', source, self._abspath])
1291 if self._githavelocally(revision):
1290 if self._githavelocally(revision):
1292 return
1291 return
1293 self._ui.status(_('pulling subrepo %s from %s\n') %
1292 self._ui.status(_('pulling subrepo %s from %s\n') %
1294 (self._relpath, self._gitremote('origin')))
1293 (self._relpath, self._gitremote('origin')))
1295 # try only origin: the originally cloned repo
1294 # try only origin: the originally cloned repo
1296 self._gitcommand(['fetch'])
1295 self._gitcommand(['fetch'])
1297 if not self._githavelocally(revision):
1296 if not self._githavelocally(revision):
1298 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1297 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1299 (revision, self._relpath))
1298 (revision, self._relpath))
1300
1299
1301 @annotatesubrepoerror
1300 @annotatesubrepoerror
1302 def dirty(self, ignoreupdate=False):
1301 def dirty(self, ignoreupdate=False):
1303 if self._gitmissing():
1302 if self._gitmissing():
1304 return self._state[1] != ''
1303 return self._state[1] != ''
1305 if self._gitisbare():
1304 if self._gitisbare():
1306 return True
1305 return True
1307 if not ignoreupdate and self._state[1] != self._gitstate():
1306 if not ignoreupdate and self._state[1] != self._gitstate():
1308 # different version checked out
1307 # different version checked out
1309 return True
1308 return True
1310 # check for staged changes or modified files; ignore untracked files
1309 # check for staged changes or modified files; ignore untracked files
1311 self._gitupdatestat()
1310 self._gitupdatestat()
1312 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1311 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1313 return code == 1
1312 return code == 1
1314
1313
1315 def basestate(self):
1314 def basestate(self):
1316 return self._gitstate()
1315 return self._gitstate()
1317
1316
1318 @annotatesubrepoerror
1317 @annotatesubrepoerror
1319 def get(self, state, overwrite=False):
1318 def get(self, state, overwrite=False):
1320 source, revision, kind = state
1319 source, revision, kind = state
1321 if not revision:
1320 if not revision:
1322 self.remove()
1321 self.remove()
1323 return
1322 return
1324 self._fetch(source, revision)
1323 self._fetch(source, revision)
1325 # if the repo was set to be bare, unbare it
1324 # if the repo was set to be bare, unbare it
1326 if self._gitisbare():
1325 if self._gitisbare():
1327 self._gitcommand(['config', 'core.bare', 'false'])
1326 self._gitcommand(['config', 'core.bare', 'false'])
1328 if self._gitstate() == revision:
1327 if self._gitstate() == revision:
1329 self._gitcommand(['reset', '--hard', 'HEAD'])
1328 self._gitcommand(['reset', '--hard', 'HEAD'])
1330 return
1329 return
1331 elif self._gitstate() == revision:
1330 elif self._gitstate() == revision:
1332 if overwrite:
1331 if overwrite:
1333 # first reset the index to unmark new files for commit, because
1332 # first reset the index to unmark new files for commit, because
1334 # reset --hard will otherwise throw away files added for commit,
1333 # reset --hard will otherwise throw away files added for commit,
1335 # not just unmark them.
1334 # not just unmark them.
1336 self._gitcommand(['reset', 'HEAD'])
1335 self._gitcommand(['reset', 'HEAD'])
1337 self._gitcommand(['reset', '--hard', 'HEAD'])
1336 self._gitcommand(['reset', '--hard', 'HEAD'])
1338 return
1337 return
1339 branch2rev, rev2branch = self._gitbranchmap()
1338 branch2rev, rev2branch = self._gitbranchmap()
1340
1339
1341 def checkout(args):
1340 def checkout(args):
1342 cmd = ['checkout']
1341 cmd = ['checkout']
1343 if overwrite:
1342 if overwrite:
1344 # first reset the index to unmark new files for commit, because
1343 # first reset the index to unmark new files for commit, because
1345 # the -f option will otherwise throw away files added for
1344 # the -f option will otherwise throw away files added for
1346 # commit, not just unmark them.
1345 # commit, not just unmark them.
1347 self._gitcommand(['reset', 'HEAD'])
1346 self._gitcommand(['reset', 'HEAD'])
1348 cmd.append('-f')
1347 cmd.append('-f')
1349 self._gitcommand(cmd + args)
1348 self._gitcommand(cmd + args)
1350 _sanitize(self._ui, self._abspath, '.git')
1349 _sanitize(self._ui, self._abspath, '.git')
1351
1350
1352 def rawcheckout():
1351 def rawcheckout():
1353 # no branch to checkout, check it out with no branch
1352 # no branch to checkout, check it out with no branch
1354 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1353 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1355 self._relpath)
1354 self._relpath)
1356 self._ui.warn(_('check out a git branch if you intend '
1355 self._ui.warn(_('check out a git branch if you intend '
1357 'to make changes\n'))
1356 'to make changes\n'))
1358 checkout(['-q', revision])
1357 checkout(['-q', revision])
1359
1358
1360 if revision not in rev2branch:
1359 if revision not in rev2branch:
1361 rawcheckout()
1360 rawcheckout()
1362 return
1361 return
1363 branches = rev2branch[revision]
1362 branches = rev2branch[revision]
1364 firstlocalbranch = None
1363 firstlocalbranch = None
1365 for b in branches:
1364 for b in branches:
1366 if b == 'refs/heads/master':
1365 if b == 'refs/heads/master':
1367 # master trumps all other branches
1366 # master trumps all other branches
1368 checkout(['refs/heads/master'])
1367 checkout(['refs/heads/master'])
1369 return
1368 return
1370 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1369 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1371 firstlocalbranch = b
1370 firstlocalbranch = b
1372 if firstlocalbranch:
1371 if firstlocalbranch:
1373 checkout([firstlocalbranch])
1372 checkout([firstlocalbranch])
1374 return
1373 return
1375
1374
1376 tracking = self._gittracking(branch2rev.keys())
1375 tracking = self._gittracking(branch2rev.keys())
1377 # choose a remote branch already tracked if possible
1376 # choose a remote branch already tracked if possible
1378 remote = branches[0]
1377 remote = branches[0]
1379 if remote not in tracking:
1378 if remote not in tracking:
1380 for b in branches:
1379 for b in branches:
1381 if b in tracking:
1380 if b in tracking:
1382 remote = b
1381 remote = b
1383 break
1382 break
1384
1383
1385 if remote not in tracking:
1384 if remote not in tracking:
1386 # create a new local tracking branch
1385 # create a new local tracking branch
1387 local = remote.split('/', 3)[3]
1386 local = remote.split('/', 3)[3]
1388 checkout(['-b', local, remote])
1387 checkout(['-b', local, remote])
1389 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1388 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1390 # When updating to a tracked remote branch,
1389 # When updating to a tracked remote branch,
1391 # if the local tracking branch is downstream of it,
1390 # if the local tracking branch is downstream of it,
1392 # a normal `git pull` would have performed a "fast-forward merge"
1391 # a normal `git pull` would have performed a "fast-forward merge"
1393 # which is equivalent to updating the local branch to the remote.
1392 # which is equivalent to updating the local branch to the remote.
1394 # Since we are only looking at branching at update, we need to
1393 # Since we are only looking at branching at update, we need to
1395 # detect this situation and perform this action lazily.
1394 # detect this situation and perform this action lazily.
1396 if tracking[remote] != self._gitcurrentbranch():
1395 if tracking[remote] != self._gitcurrentbranch():
1397 checkout([tracking[remote]])
1396 checkout([tracking[remote]])
1398 self._gitcommand(['merge', '--ff', remote])
1397 self._gitcommand(['merge', '--ff', remote])
1399 _sanitize(self._ui, self._abspath, '.git')
1398 _sanitize(self._ui, self._abspath, '.git')
1400 else:
1399 else:
1401 # a real merge would be required, just checkout the revision
1400 # a real merge would be required, just checkout the revision
1402 rawcheckout()
1401 rawcheckout()
1403
1402
1404 @annotatesubrepoerror
1403 @annotatesubrepoerror
1405 def commit(self, text, user, date):
1404 def commit(self, text, user, date):
1406 if self._gitmissing():
1405 if self._gitmissing():
1407 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1406 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1408 cmd = ['commit', '-a', '-m', text]
1407 cmd = ['commit', '-a', '-m', text]
1409 env = os.environ.copy()
1408 env = os.environ.copy()
1410 if user:
1409 if user:
1411 cmd += ['--author', user]
1410 cmd += ['--author', user]
1412 if date:
1411 if date:
1413 # git's date parser silently ignores when seconds < 1e9
1412 # git's date parser silently ignores when seconds < 1e9
1414 # convert to ISO8601
1413 # convert to ISO8601
1415 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1414 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1416 '%Y-%m-%dT%H:%M:%S %1%2')
1415 '%Y-%m-%dT%H:%M:%S %1%2')
1417 self._gitcommand(cmd, env=env)
1416 self._gitcommand(cmd, env=env)
1418 # make sure commit works otherwise HEAD might not exist under certain
1417 # make sure commit works otherwise HEAD might not exist under certain
1419 # circumstances
1418 # circumstances
1420 return self._gitstate()
1419 return self._gitstate()
1421
1420
1422 @annotatesubrepoerror
1421 @annotatesubrepoerror
1423 def merge(self, state):
1422 def merge(self, state):
1424 source, revision, kind = state
1423 source, revision, kind = state
1425 self._fetch(source, revision)
1424 self._fetch(source, revision)
1426 base = self._gitcommand(['merge-base', revision, self._state[1]])
1425 base = self._gitcommand(['merge-base', revision, self._state[1]])
1427 self._gitupdatestat()
1426 self._gitupdatestat()
1428 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1427 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1429
1428
1430 def mergefunc():
1429 def mergefunc():
1431 if base == revision:
1430 if base == revision:
1432 self.get(state) # fast forward merge
1431 self.get(state) # fast forward merge
1433 elif base != self._state[1]:
1432 elif base != self._state[1]:
1434 self._gitcommand(['merge', '--no-commit', revision])
1433 self._gitcommand(['merge', '--no-commit', revision])
1435 _sanitize(self._ui, self._abspath, '.git')
1434 _sanitize(self._ui, self._abspath, '.git')
1436
1435
1437 if self.dirty():
1436 if self.dirty():
1438 if self._gitstate() != revision:
1437 if self._gitstate() != revision:
1439 dirty = self._gitstate() == self._state[1] or code != 0
1438 dirty = self._gitstate() == self._state[1] or code != 0
1440 if _updateprompt(self._ui, self, dirty,
1439 if _updateprompt(self._ui, self, dirty,
1441 self._state[1][:7], revision[:7]):
1440 self._state[1][:7], revision[:7]):
1442 mergefunc()
1441 mergefunc()
1443 else:
1442 else:
1444 mergefunc()
1443 mergefunc()
1445
1444
1446 @annotatesubrepoerror
1445 @annotatesubrepoerror
1447 def push(self, opts):
1446 def push(self, opts):
1448 force = opts.get('force')
1447 force = opts.get('force')
1449
1448
1450 if not self._state[1]:
1449 if not self._state[1]:
1451 return True
1450 return True
1452 if self._gitmissing():
1451 if self._gitmissing():
1453 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1452 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1454 # if a branch in origin contains the revision, nothing to do
1453 # if a branch in origin contains the revision, nothing to do
1455 branch2rev, rev2branch = self._gitbranchmap()
1454 branch2rev, rev2branch = self._gitbranchmap()
1456 if self._state[1] in rev2branch:
1455 if self._state[1] in rev2branch:
1457 for b in rev2branch[self._state[1]]:
1456 for b in rev2branch[self._state[1]]:
1458 if b.startswith('refs/remotes/origin/'):
1457 if b.startswith('refs/remotes/origin/'):
1459 return True
1458 return True
1460 for b, revision in branch2rev.iteritems():
1459 for b, revision in branch2rev.iteritems():
1461 if b.startswith('refs/remotes/origin/'):
1460 if b.startswith('refs/remotes/origin/'):
1462 if self._gitisancestor(self._state[1], revision):
1461 if self._gitisancestor(self._state[1], revision):
1463 return True
1462 return True
1464 # otherwise, try to push the currently checked out branch
1463 # otherwise, try to push the currently checked out branch
1465 cmd = ['push']
1464 cmd = ['push']
1466 if force:
1465 if force:
1467 cmd.append('--force')
1466 cmd.append('--force')
1468
1467
1469 current = self._gitcurrentbranch()
1468 current = self._gitcurrentbranch()
1470 if current:
1469 if current:
1471 # determine if the current branch is even useful
1470 # determine if the current branch is even useful
1472 if not self._gitisancestor(self._state[1], current):
1471 if not self._gitisancestor(self._state[1], current):
1473 self._ui.warn(_('unrelated git branch checked out '
1472 self._ui.warn(_('unrelated git branch checked out '
1474 'in subrepo %s\n') % self._relpath)
1473 'in subrepo %s\n') % self._relpath)
1475 return False
1474 return False
1476 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1475 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1477 (current.split('/', 2)[2], self._relpath))
1476 (current.split('/', 2)[2], self._relpath))
1478 ret = self._gitdir(cmd + ['origin', current])
1477 ret = self._gitdir(cmd + ['origin', current])
1479 return ret[1] == 0
1478 return ret[1] == 0
1480 else:
1479 else:
1481 self._ui.warn(_('no branch checked out in subrepo %s\n'
1480 self._ui.warn(_('no branch checked out in subrepo %s\n'
1482 'cannot push revision %s\n') %
1481 'cannot push revision %s\n') %
1483 (self._relpath, self._state[1]))
1482 (self._relpath, self._state[1]))
1484 return False
1483 return False
1485
1484
1486 @annotatesubrepoerror
1485 @annotatesubrepoerror
1487 def remove(self):
1486 def remove(self):
1488 if self._gitmissing():
1487 if self._gitmissing():
1489 return
1488 return
1490 if self.dirty():
1489 if self.dirty():
1491 self._ui.warn(_('not removing repo %s because '
1490 self._ui.warn(_('not removing repo %s because '
1492 'it has changes.\n') % self._relpath)
1491 'it has changes.\n') % self._relpath)
1493 return
1492 return
1494 # we can't fully delete the repository as it may contain
1493 # we can't fully delete the repository as it may contain
1495 # local-only history
1494 # local-only history
1496 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1495 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1497 self._gitcommand(['config', 'core.bare', 'true'])
1496 self._gitcommand(['config', 'core.bare', 'true'])
1498 for f in os.listdir(self._abspath):
1497 for f in os.listdir(self._abspath):
1499 if f == '.git':
1498 if f == '.git':
1500 continue
1499 continue
1501 path = os.path.join(self._abspath, f)
1500 path = os.path.join(self._abspath, f)
1502 if os.path.isdir(path) and not os.path.islink(path):
1501 if os.path.isdir(path) and not os.path.islink(path):
1503 shutil.rmtree(path)
1502 shutil.rmtree(path)
1504 else:
1503 else:
1505 os.remove(path)
1504 os.remove(path)
1506
1505
1507 def archive(self, ui, archiver, prefix, match=None):
1506 def archive(self, ui, archiver, prefix, match=None):
1508 total = 0
1507 total = 0
1509 source, revision = self._state
1508 source, revision = self._state
1510 if not revision:
1509 if not revision:
1511 return total
1510 return total
1512 self._fetch(source, revision)
1511 self._fetch(source, revision)
1513
1512
1514 # Parse git's native archive command.
1513 # Parse git's native archive command.
1515 # This should be much faster than manually traversing the trees
1514 # This should be much faster than manually traversing the trees
1516 # and objects with many subprocess calls.
1515 # and objects with many subprocess calls.
1517 tarstream = self._gitcommand(['archive', revision], stream=True)
1516 tarstream = self._gitcommand(['archive', revision], stream=True)
1518 tar = tarfile.open(fileobj=tarstream, mode='r|')
1517 tar = tarfile.open(fileobj=tarstream, mode='r|')
1519 relpath = subrelpath(self)
1518 relpath = subrelpath(self)
1520 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1519 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1521 for i, info in enumerate(tar):
1520 for i, info in enumerate(tar):
1522 if info.isdir():
1521 if info.isdir():
1523 continue
1522 continue
1524 if match and not match(info.name):
1523 if match and not match(info.name):
1525 continue
1524 continue
1526 if info.issym():
1525 if info.issym():
1527 data = info.linkname
1526 data = info.linkname
1528 else:
1527 else:
1529 data = tar.extractfile(info).read()
1528 data = tar.extractfile(info).read()
1530 archiver.addfile(os.path.join(prefix, self._path, info.name),
1529 archiver.addfile(os.path.join(prefix, self._path, info.name),
1531 info.mode, info.issym(), data)
1530 info.mode, info.issym(), data)
1532 total += 1
1531 total += 1
1533 ui.progress(_('archiving (%s)') % relpath, i + 1,
1532 ui.progress(_('archiving (%s)') % relpath, i + 1,
1534 unit=_('files'))
1533 unit=_('files'))
1535 ui.progress(_('archiving (%s)') % relpath, None)
1534 ui.progress(_('archiving (%s)') % relpath, None)
1536 return total
1535 return total
1537
1536
1538
1537
1539 @annotatesubrepoerror
1538 @annotatesubrepoerror
1540 def status(self, rev2, **opts):
1539 def status(self, rev2, **opts):
1541 rev1 = self._state[1]
1540 rev1 = self._state[1]
1542 if self._gitmissing() or not rev1:
1541 if self._gitmissing() or not rev1:
1543 # if the repo is missing, return no results
1542 # if the repo is missing, return no results
1544 return [], [], [], [], [], [], []
1543 return [], [], [], [], [], [], []
1545 modified, added, removed = [], [], []
1544 modified, added, removed = [], [], []
1546 self._gitupdatestat()
1545 self._gitupdatestat()
1547 if rev2:
1546 if rev2:
1548 command = ['diff-tree', rev1, rev2]
1547 command = ['diff-tree', rev1, rev2]
1549 else:
1548 else:
1550 command = ['diff-index', rev1]
1549 command = ['diff-index', rev1]
1551 out = self._gitcommand(command)
1550 out = self._gitcommand(command)
1552 for line in out.split('\n'):
1551 for line in out.split('\n'):
1553 tab = line.find('\t')
1552 tab = line.find('\t')
1554 if tab == -1:
1553 if tab == -1:
1555 continue
1554 continue
1556 status, f = line[tab - 1], line[tab + 1:]
1555 status, f = line[tab - 1], line[tab + 1:]
1557 if status == 'M':
1556 if status == 'M':
1558 modified.append(f)
1557 modified.append(f)
1559 elif status == 'A':
1558 elif status == 'A':
1560 added.append(f)
1559 added.append(f)
1561 elif status == 'D':
1560 elif status == 'D':
1562 removed.append(f)
1561 removed.append(f)
1563
1562
1564 deleted = unknown = ignored = clean = []
1563 deleted = unknown = ignored = clean = []
1565 return modified, added, removed, deleted, unknown, ignored, clean
1564 return modified, added, removed, deleted, unknown, ignored, clean
1566
1565
1567 types = {
1566 types = {
1568 'hg': hgsubrepo,
1567 'hg': hgsubrepo,
1569 'svn': svnsubrepo,
1568 'svn': svnsubrepo,
1570 'git': gitsubrepo,
1569 'git': gitsubrepo,
1571 }
1570 }
@@ -1,32 +1,33 b''
1 # this is hack to make sure no escape characters are inserted into the output
1 # this is hack to make sure no escape characters are inserted into the output
2 import os, sys
2 import os, sys
3 if 'TERM' in os.environ:
3 if 'TERM' in os.environ:
4 del os.environ['TERM']
4 del os.environ['TERM']
5 import doctest
5 import doctest
6
6
7 def testmod(name, optionflags=0, testtarget=None):
7 def testmod(name, optionflags=0, testtarget=None):
8 __import__(name)
8 __import__(name)
9 mod = sys.modules[name]
9 mod = sys.modules[name]
10 if testtarget is not None:
10 if testtarget is not None:
11 mod = getattr(mod, testtarget)
11 mod = getattr(mod, testtarget)
12 doctest.testmod(mod, optionflags=optionflags)
12 doctest.testmod(mod, optionflags=optionflags)
13
13
14 testmod('mercurial.changelog')
14 testmod('mercurial.changelog')
15 testmod('mercurial.dagparser', optionflags=doctest.NORMALIZE_WHITESPACE)
15 testmod('mercurial.dagparser', optionflags=doctest.NORMALIZE_WHITESPACE)
16 testmod('mercurial.dispatch')
16 testmod('mercurial.dispatch')
17 testmod('mercurial.encoding')
17 testmod('mercurial.encoding')
18 testmod('mercurial.hg')
18 testmod('mercurial.hg')
19 testmod('mercurial.hgweb.hgwebdir_mod')
19 testmod('mercurial.hgweb.hgwebdir_mod')
20 testmod('mercurial.match')
20 testmod('mercurial.match')
21 testmod('mercurial.minirst')
21 testmod('mercurial.minirst')
22 testmod('mercurial.pathutil')
22 testmod('mercurial.revset')
23 testmod('mercurial.revset')
23 testmod('mercurial.store')
24 testmod('mercurial.store')
24 testmod('mercurial.subrepo')
25 testmod('mercurial.subrepo')
25 testmod('mercurial.templatefilters')
26 testmod('mercurial.templatefilters')
26 testmod('mercurial.ui')
27 testmod('mercurial.ui')
27 testmod('mercurial.url')
28 testmod('mercurial.url')
28 testmod('mercurial.util')
29 testmod('mercurial.util')
29 testmod('mercurial.util', testtarget='platform')
30 testmod('mercurial.util', testtarget='platform')
30 testmod('hgext.convert.cvsps')
31 testmod('hgext.convert.cvsps')
31 testmod('hgext.convert.filemap')
32 testmod('hgext.convert.filemap')
32 testmod('hgext.convert.subversion')
33 testmod('hgext.convert.subversion')
General Comments 0
You need to be logged in to leave comments. Login now