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