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