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