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