##// END OF EJS Templates
subrepo: replace direct file APIs around "writelines" by "vfs.writelines"...
FUJIWARA Katsunori -
r23372:6cfa7a73 default
parent child Browse files
Show More
@@ -1,1594 +1,1588
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):
564 def _getstorehashcachepath(self, remotepath):
565 '''get a unique path for the store hash cache'''
565 '''get a unique path for the store hash cache'''
566 return self._repo.join(os.path.join(
566 return self._repo.join(os.path.join(
567 'cache', 'storehash', _getstorehashcachename(remotepath)))
567 'cache', 'storehash', _getstorehashcachename(remotepath)))
568
568
569 @propertycache
569 @propertycache
570 def _cachestorehashvfs(self):
570 def _cachestorehashvfs(self):
571 return scmutil.vfs(self._repo.join('cache/storehash'))
571 return scmutil.vfs(self._repo.join('cache/storehash'))
572
572
573 def _readstorehashcache(self, remotepath):
573 def _readstorehashcache(self, remotepath):
574 '''read the store hash cache for a given remote repository'''
574 '''read the store hash cache for a given remote repository'''
575 cachefile = _getstorehashcachename(remotepath)
575 cachefile = _getstorehashcachename(remotepath)
576 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
576 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
577
577
578 def _cachestorehash(self, remotepath):
578 def _cachestorehash(self, remotepath):
579 '''cache the current store hash
579 '''cache the current store hash
580
580
581 Each remote repo requires its own store hash cache, because a subrepo
581 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
582 store may be "clean" versus a given remote repo, but not versus another
583 '''
583 '''
584 cachefile = self._getstorehashcachepath(remotepath)
584 cachefile = _getstorehashcachename(remotepath)
585 lock = self._repo.lock()
585 lock = self._repo.lock()
586 try:
586 try:
587 storehash = list(self._calcstorehash(remotepath))
587 storehash = list(self._calcstorehash(remotepath))
588 cachedir = os.path.dirname(cachefile)
588 vfs = self._cachestorehashvfs
589 if not os.path.exists(cachedir):
589 vfs.writelines(cachefile, storehash, mode='w', notindexed=True)
590 util.makedirs(cachedir, notindexed=True)
591 fd = open(cachefile, 'w')
592 try:
593 fd.writelines(storehash)
594 finally:
595 fd.close()
596 finally:
590 finally:
597 lock.release()
591 lock.release()
598
592
599 @annotatesubrepoerror
593 @annotatesubrepoerror
600 def _initrepo(self, parentrepo, source, create):
594 def _initrepo(self, parentrepo, source, create):
601 self._repo._subparent = parentrepo
595 self._repo._subparent = parentrepo
602 self._repo._subsource = source
596 self._repo._subsource = source
603
597
604 if create:
598 if create:
605 lines = ['[paths]\n']
599 lines = ['[paths]\n']
606
600
607 def addpathconfig(key, value):
601 def addpathconfig(key, value):
608 if value:
602 if value:
609 lines.append('%s = %s\n' % (key, value))
603 lines.append('%s = %s\n' % (key, value))
610 self._repo.ui.setconfig('paths', key, value, 'subrepo')
604 self._repo.ui.setconfig('paths', key, value, 'subrepo')
611
605
612 defpath = _abssource(self._repo, abort=False)
606 defpath = _abssource(self._repo, abort=False)
613 defpushpath = _abssource(self._repo, True, abort=False)
607 defpushpath = _abssource(self._repo, True, abort=False)
614 addpathconfig('default', defpath)
608 addpathconfig('default', defpath)
615 if defpath != defpushpath:
609 if defpath != defpushpath:
616 addpathconfig('default-push', defpushpath)
610 addpathconfig('default-push', defpushpath)
617
611
618 fp = self._repo.opener("hgrc", "w", text=True)
612 fp = self._repo.opener("hgrc", "w", text=True)
619 try:
613 try:
620 fp.write(''.join(lines))
614 fp.write(''.join(lines))
621 finally:
615 finally:
622 fp.close()
616 fp.close()
623
617
624 @annotatesubrepoerror
618 @annotatesubrepoerror
625 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
619 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
626 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
620 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
627 os.path.join(prefix, self._path), explicitonly)
621 os.path.join(prefix, self._path), explicitonly)
628
622
629 @annotatesubrepoerror
623 @annotatesubrepoerror
630 def cat(self, ui, match, prefix, **opts):
624 def cat(self, ui, match, prefix, **opts):
631 rev = self._state[1]
625 rev = self._state[1]
632 ctx = self._repo[rev]
626 ctx = self._repo[rev]
633 return cmdutil.cat(ui, self._repo, ctx, match, prefix, **opts)
627 return cmdutil.cat(ui, self._repo, ctx, match, prefix, **opts)
634
628
635 @annotatesubrepoerror
629 @annotatesubrepoerror
636 def status(self, rev2, **opts):
630 def status(self, rev2, **opts):
637 try:
631 try:
638 rev1 = self._state[1]
632 rev1 = self._state[1]
639 ctx1 = self._repo[rev1]
633 ctx1 = self._repo[rev1]
640 ctx2 = self._repo[rev2]
634 ctx2 = self._repo[rev2]
641 return self._repo.status(ctx1, ctx2, **opts)
635 return self._repo.status(ctx1, ctx2, **opts)
642 except error.RepoLookupError, inst:
636 except error.RepoLookupError, inst:
643 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
637 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
644 % (inst, subrelpath(self)))
638 % (inst, subrelpath(self)))
645 return scmutil.status([], [], [], [], [], [], [])
639 return scmutil.status([], [], [], [], [], [], [])
646
640
647 @annotatesubrepoerror
641 @annotatesubrepoerror
648 def diff(self, ui, diffopts, node2, match, prefix, **opts):
642 def diff(self, ui, diffopts, node2, match, prefix, **opts):
649 try:
643 try:
650 node1 = node.bin(self._state[1])
644 node1 = node.bin(self._state[1])
651 # We currently expect node2 to come from substate and be
645 # We currently expect node2 to come from substate and be
652 # in hex format
646 # in hex format
653 if node2 is not None:
647 if node2 is not None:
654 node2 = node.bin(node2)
648 node2 = node.bin(node2)
655 cmdutil.diffordiffstat(ui, self._repo, diffopts,
649 cmdutil.diffordiffstat(ui, self._repo, diffopts,
656 node1, node2, match,
650 node1, node2, match,
657 prefix=posixpath.join(prefix, self._path),
651 prefix=posixpath.join(prefix, self._path),
658 listsubrepos=True, **opts)
652 listsubrepos=True, **opts)
659 except error.RepoLookupError, inst:
653 except error.RepoLookupError, inst:
660 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
654 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
661 % (inst, subrelpath(self)))
655 % (inst, subrelpath(self)))
662
656
663 @annotatesubrepoerror
657 @annotatesubrepoerror
664 def archive(self, ui, archiver, prefix, match=None):
658 def archive(self, ui, archiver, prefix, match=None):
665 self._get(self._state + ('hg',))
659 self._get(self._state + ('hg',))
666 total = abstractsubrepo.archive(self, ui, archiver, prefix, match)
660 total = abstractsubrepo.archive(self, ui, archiver, prefix, match)
667 rev = self._state[1]
661 rev = self._state[1]
668 ctx = self._repo[rev]
662 ctx = self._repo[rev]
669 for subpath in ctx.substate:
663 for subpath in ctx.substate:
670 s = subrepo(ctx, subpath)
664 s = subrepo(ctx, subpath)
671 submatch = matchmod.narrowmatcher(subpath, match)
665 submatch = matchmod.narrowmatcher(subpath, match)
672 total += s.archive(
666 total += s.archive(
673 ui, archiver, os.path.join(prefix, self._path), submatch)
667 ui, archiver, os.path.join(prefix, self._path), submatch)
674 return total
668 return total
675
669
676 @annotatesubrepoerror
670 @annotatesubrepoerror
677 def dirty(self, ignoreupdate=False):
671 def dirty(self, ignoreupdate=False):
678 r = self._state[1]
672 r = self._state[1]
679 if r == '' and not ignoreupdate: # no state recorded
673 if r == '' and not ignoreupdate: # no state recorded
680 return True
674 return True
681 w = self._repo[None]
675 w = self._repo[None]
682 if r != w.p1().hex() and not ignoreupdate:
676 if r != w.p1().hex() and not ignoreupdate:
683 # different version checked out
677 # different version checked out
684 return True
678 return True
685 return w.dirty() # working directory changed
679 return w.dirty() # working directory changed
686
680
687 def basestate(self):
681 def basestate(self):
688 return self._repo['.'].hex()
682 return self._repo['.'].hex()
689
683
690 def checknested(self, path):
684 def checknested(self, path):
691 return self._repo._checknested(self._repo.wjoin(path))
685 return self._repo._checknested(self._repo.wjoin(path))
692
686
693 @annotatesubrepoerror
687 @annotatesubrepoerror
694 def commit(self, text, user, date):
688 def commit(self, text, user, date):
695 # don't bother committing in the subrepo if it's only been
689 # don't bother committing in the subrepo if it's only been
696 # updated
690 # updated
697 if not self.dirty(True):
691 if not self.dirty(True):
698 return self._repo['.'].hex()
692 return self._repo['.'].hex()
699 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
693 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
700 n = self._repo.commit(text, user, date)
694 n = self._repo.commit(text, user, date)
701 if not n:
695 if not n:
702 return self._repo['.'].hex() # different version checked out
696 return self._repo['.'].hex() # different version checked out
703 return node.hex(n)
697 return node.hex(n)
704
698
705 @annotatesubrepoerror
699 @annotatesubrepoerror
706 def phase(self, state):
700 def phase(self, state):
707 return self._repo[state].phase()
701 return self._repo[state].phase()
708
702
709 @annotatesubrepoerror
703 @annotatesubrepoerror
710 def remove(self):
704 def remove(self):
711 # we can't fully delete the repository as it may contain
705 # we can't fully delete the repository as it may contain
712 # local-only history
706 # local-only history
713 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
707 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
714 hg.clean(self._repo, node.nullid, False)
708 hg.clean(self._repo, node.nullid, False)
715
709
716 def _get(self, state):
710 def _get(self, state):
717 source, revision, kind = state
711 source, revision, kind = state
718 if revision in self._repo.unfiltered():
712 if revision in self._repo.unfiltered():
719 return True
713 return True
720 self._repo._subsource = source
714 self._repo._subsource = source
721 srcurl = _abssource(self._repo)
715 srcurl = _abssource(self._repo)
722 other = hg.peer(self._repo, {}, srcurl)
716 other = hg.peer(self._repo, {}, srcurl)
723 if len(self._repo) == 0:
717 if len(self._repo) == 0:
724 self._repo.ui.status(_('cloning subrepo %s from %s\n')
718 self._repo.ui.status(_('cloning subrepo %s from %s\n')
725 % (subrelpath(self), srcurl))
719 % (subrelpath(self), srcurl))
726 parentrepo = self._repo._subparent
720 parentrepo = self._repo._subparent
727 shutil.rmtree(self._repo.path)
721 shutil.rmtree(self._repo.path)
728 other, cloned = hg.clone(self._repo._subparent.baseui, {},
722 other, cloned = hg.clone(self._repo._subparent.baseui, {},
729 other, self._repo.root,
723 other, self._repo.root,
730 update=False)
724 update=False)
731 self._repo = cloned.local()
725 self._repo = cloned.local()
732 self._initrepo(parentrepo, source, create=True)
726 self._initrepo(parentrepo, source, create=True)
733 self._cachestorehash(srcurl)
727 self._cachestorehash(srcurl)
734 else:
728 else:
735 self._repo.ui.status(_('pulling subrepo %s from %s\n')
729 self._repo.ui.status(_('pulling subrepo %s from %s\n')
736 % (subrelpath(self), srcurl))
730 % (subrelpath(self), srcurl))
737 cleansub = self.storeclean(srcurl)
731 cleansub = self.storeclean(srcurl)
738 exchange.pull(self._repo, other)
732 exchange.pull(self._repo, other)
739 if cleansub:
733 if cleansub:
740 # keep the repo clean after pull
734 # keep the repo clean after pull
741 self._cachestorehash(srcurl)
735 self._cachestorehash(srcurl)
742 return False
736 return False
743
737
744 @annotatesubrepoerror
738 @annotatesubrepoerror
745 def get(self, state, overwrite=False):
739 def get(self, state, overwrite=False):
746 inrepo = self._get(state)
740 inrepo = self._get(state)
747 source, revision, kind = state
741 source, revision, kind = state
748 repo = self._repo
742 repo = self._repo
749 repo.ui.debug("getting subrepo %s\n" % self._path)
743 repo.ui.debug("getting subrepo %s\n" % self._path)
750 if inrepo:
744 if inrepo:
751 urepo = repo.unfiltered()
745 urepo = repo.unfiltered()
752 ctx = urepo[revision]
746 ctx = urepo[revision]
753 if ctx.hidden():
747 if ctx.hidden():
754 urepo.ui.warn(
748 urepo.ui.warn(
755 _('revision %s in subrepo %s is hidden\n') \
749 _('revision %s in subrepo %s is hidden\n') \
756 % (revision[0:12], self._path))
750 % (revision[0:12], self._path))
757 repo = urepo
751 repo = urepo
758 hg.updaterepo(repo, revision, overwrite)
752 hg.updaterepo(repo, revision, overwrite)
759
753
760 @annotatesubrepoerror
754 @annotatesubrepoerror
761 def merge(self, state):
755 def merge(self, state):
762 self._get(state)
756 self._get(state)
763 cur = self._repo['.']
757 cur = self._repo['.']
764 dst = self._repo[state[1]]
758 dst = self._repo[state[1]]
765 anc = dst.ancestor(cur)
759 anc = dst.ancestor(cur)
766
760
767 def mergefunc():
761 def mergefunc():
768 if anc == cur and dst.branch() == cur.branch():
762 if anc == cur and dst.branch() == cur.branch():
769 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
763 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
770 hg.update(self._repo, state[1])
764 hg.update(self._repo, state[1])
771 elif anc == dst:
765 elif anc == dst:
772 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
766 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
773 else:
767 else:
774 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
768 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
775 hg.merge(self._repo, state[1], remind=False)
769 hg.merge(self._repo, state[1], remind=False)
776
770
777 wctx = self._repo[None]
771 wctx = self._repo[None]
778 if self.dirty():
772 if self.dirty():
779 if anc != dst:
773 if anc != dst:
780 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
774 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
781 mergefunc()
775 mergefunc()
782 else:
776 else:
783 mergefunc()
777 mergefunc()
784 else:
778 else:
785 mergefunc()
779 mergefunc()
786
780
787 @annotatesubrepoerror
781 @annotatesubrepoerror
788 def push(self, opts):
782 def push(self, opts):
789 force = opts.get('force')
783 force = opts.get('force')
790 newbranch = opts.get('new_branch')
784 newbranch = opts.get('new_branch')
791 ssh = opts.get('ssh')
785 ssh = opts.get('ssh')
792
786
793 # push subrepos depth-first for coherent ordering
787 # push subrepos depth-first for coherent ordering
794 c = self._repo['']
788 c = self._repo['']
795 subs = c.substate # only repos that are committed
789 subs = c.substate # only repos that are committed
796 for s in sorted(subs):
790 for s in sorted(subs):
797 if c.sub(s).push(opts) == 0:
791 if c.sub(s).push(opts) == 0:
798 return False
792 return False
799
793
800 dsturl = _abssource(self._repo, True)
794 dsturl = _abssource(self._repo, True)
801 if not force:
795 if not force:
802 if self.storeclean(dsturl):
796 if self.storeclean(dsturl):
803 self._repo.ui.status(
797 self._repo.ui.status(
804 _('no changes made to subrepo %s since last push to %s\n')
798 _('no changes made to subrepo %s since last push to %s\n')
805 % (subrelpath(self), dsturl))
799 % (subrelpath(self), dsturl))
806 return None
800 return None
807 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
801 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
808 (subrelpath(self), dsturl))
802 (subrelpath(self), dsturl))
809 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
803 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
810 res = exchange.push(self._repo, other, force, newbranch=newbranch)
804 res = exchange.push(self._repo, other, force, newbranch=newbranch)
811
805
812 # the repo is now clean
806 # the repo is now clean
813 self._cachestorehash(dsturl)
807 self._cachestorehash(dsturl)
814 return res.cgresult
808 return res.cgresult
815
809
816 @annotatesubrepoerror
810 @annotatesubrepoerror
817 def outgoing(self, ui, dest, opts):
811 def outgoing(self, ui, dest, opts):
818 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
812 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
819
813
820 @annotatesubrepoerror
814 @annotatesubrepoerror
821 def incoming(self, ui, source, opts):
815 def incoming(self, ui, source, opts):
822 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
816 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
823
817
824 @annotatesubrepoerror
818 @annotatesubrepoerror
825 def files(self):
819 def files(self):
826 rev = self._state[1]
820 rev = self._state[1]
827 ctx = self._repo[rev]
821 ctx = self._repo[rev]
828 return ctx.manifest()
822 return ctx.manifest()
829
823
830 def filedata(self, name):
824 def filedata(self, name):
831 rev = self._state[1]
825 rev = self._state[1]
832 return self._repo[rev][name].data()
826 return self._repo[rev][name].data()
833
827
834 def fileflags(self, name):
828 def fileflags(self, name):
835 rev = self._state[1]
829 rev = self._state[1]
836 ctx = self._repo[rev]
830 ctx = self._repo[rev]
837 return ctx.flags(name)
831 return ctx.flags(name)
838
832
839 def walk(self, match):
833 def walk(self, match):
840 ctx = self._repo[None]
834 ctx = self._repo[None]
841 return ctx.walk(match)
835 return ctx.walk(match)
842
836
843 @annotatesubrepoerror
837 @annotatesubrepoerror
844 def forget(self, ui, match, prefix):
838 def forget(self, ui, match, prefix):
845 return cmdutil.forget(ui, self._repo, match,
839 return cmdutil.forget(ui, self._repo, match,
846 os.path.join(prefix, self._path), True)
840 os.path.join(prefix, self._path), True)
847
841
848 @annotatesubrepoerror
842 @annotatesubrepoerror
849 def removefiles(self, ui, matcher, prefix, after, force, subrepos):
843 def removefiles(self, ui, matcher, prefix, after, force, subrepos):
850 return cmdutil.remove(ui, self._repo, matcher,
844 return cmdutil.remove(ui, self._repo, matcher,
851 os.path.join(prefix, self._path), after, force,
845 os.path.join(prefix, self._path), after, force,
852 subrepos)
846 subrepos)
853
847
854 @annotatesubrepoerror
848 @annotatesubrepoerror
855 def revert(self, ui, substate, *pats, **opts):
849 def revert(self, ui, substate, *pats, **opts):
856 # reverting a subrepo is a 2 step process:
850 # reverting a subrepo is a 2 step process:
857 # 1. if the no_backup is not set, revert all modified
851 # 1. if the no_backup is not set, revert all modified
858 # files inside the subrepo
852 # files inside the subrepo
859 # 2. update the subrepo to the revision specified in
853 # 2. update the subrepo to the revision specified in
860 # the corresponding substate dictionary
854 # the corresponding substate dictionary
861 ui.status(_('reverting subrepo %s\n') % substate[0])
855 ui.status(_('reverting subrepo %s\n') % substate[0])
862 if not opts.get('no_backup'):
856 if not opts.get('no_backup'):
863 # Revert all files on the subrepo, creating backups
857 # Revert all files on the subrepo, creating backups
864 # Note that this will not recursively revert subrepos
858 # Note that this will not recursively revert subrepos
865 # We could do it if there was a set:subrepos() predicate
859 # We could do it if there was a set:subrepos() predicate
866 opts = opts.copy()
860 opts = opts.copy()
867 opts['date'] = None
861 opts['date'] = None
868 opts['rev'] = substate[1]
862 opts['rev'] = substate[1]
869
863
870 pats = []
864 pats = []
871 if not opts.get('all'):
865 if not opts.get('all'):
872 pats = ['set:modified()']
866 pats = ['set:modified()']
873 self.filerevert(ui, *pats, **opts)
867 self.filerevert(ui, *pats, **opts)
874
868
875 # Update the repo to the revision specified in the given substate
869 # Update the repo to the revision specified in the given substate
876 self.get(substate, overwrite=True)
870 self.get(substate, overwrite=True)
877
871
878 def filerevert(self, ui, *pats, **opts):
872 def filerevert(self, ui, *pats, **opts):
879 ctx = self._repo[opts['rev']]
873 ctx = self._repo[opts['rev']]
880 parents = self._repo.dirstate.parents()
874 parents = self._repo.dirstate.parents()
881 if opts.get('all'):
875 if opts.get('all'):
882 pats = ['set:modified()']
876 pats = ['set:modified()']
883 else:
877 else:
884 pats = []
878 pats = []
885 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
879 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
886
880
887 def shortid(self, revid):
881 def shortid(self, revid):
888 return revid[:12]
882 return revid[:12]
889
883
890 class svnsubrepo(abstractsubrepo):
884 class svnsubrepo(abstractsubrepo):
891 def __init__(self, ctx, path, state):
885 def __init__(self, ctx, path, state):
892 self._path = path
886 self._path = path
893 self._state = state
887 self._state = state
894 self._ctx = ctx
888 self._ctx = ctx
895 self._ui = ctx._repo.ui
889 self._ui = ctx._repo.ui
896 self._exe = util.findexe('svn')
890 self._exe = util.findexe('svn')
897 if not self._exe:
891 if not self._exe:
898 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
892 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
899 % self._path)
893 % self._path)
900
894
901 def _svncommand(self, commands, filename='', failok=False):
895 def _svncommand(self, commands, filename='', failok=False):
902 cmd = [self._exe]
896 cmd = [self._exe]
903 extrakw = {}
897 extrakw = {}
904 if not self._ui.interactive():
898 if not self._ui.interactive():
905 # Making stdin be a pipe should prevent svn from behaving
899 # Making stdin be a pipe should prevent svn from behaving
906 # interactively even if we can't pass --non-interactive.
900 # interactively even if we can't pass --non-interactive.
907 extrakw['stdin'] = subprocess.PIPE
901 extrakw['stdin'] = subprocess.PIPE
908 # Starting in svn 1.5 --non-interactive is a global flag
902 # Starting in svn 1.5 --non-interactive is a global flag
909 # instead of being per-command, but we need to support 1.4 so
903 # instead of being per-command, but we need to support 1.4 so
910 # we have to be intelligent about what commands take
904 # we have to be intelligent about what commands take
911 # --non-interactive.
905 # --non-interactive.
912 if commands[0] in ('update', 'checkout', 'commit'):
906 if commands[0] in ('update', 'checkout', 'commit'):
913 cmd.append('--non-interactive')
907 cmd.append('--non-interactive')
914 cmd.extend(commands)
908 cmd.extend(commands)
915 if filename is not None:
909 if filename is not None:
916 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
910 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
917 cmd.append(path)
911 cmd.append(path)
918 env = dict(os.environ)
912 env = dict(os.environ)
919 # Avoid localized output, preserve current locale for everything else.
913 # Avoid localized output, preserve current locale for everything else.
920 lc_all = env.get('LC_ALL')
914 lc_all = env.get('LC_ALL')
921 if lc_all:
915 if lc_all:
922 env['LANG'] = lc_all
916 env['LANG'] = lc_all
923 del env['LC_ALL']
917 del env['LC_ALL']
924 env['LC_MESSAGES'] = 'C'
918 env['LC_MESSAGES'] = 'C'
925 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
919 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
926 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
920 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
927 universal_newlines=True, env=env, **extrakw)
921 universal_newlines=True, env=env, **extrakw)
928 stdout, stderr = p.communicate()
922 stdout, stderr = p.communicate()
929 stderr = stderr.strip()
923 stderr = stderr.strip()
930 if not failok:
924 if not failok:
931 if p.returncode:
925 if p.returncode:
932 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
926 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
933 if stderr:
927 if stderr:
934 self._ui.warn(stderr + '\n')
928 self._ui.warn(stderr + '\n')
935 return stdout, stderr
929 return stdout, stderr
936
930
937 @propertycache
931 @propertycache
938 def _svnversion(self):
932 def _svnversion(self):
939 output, err = self._svncommand(['--version', '--quiet'], filename=None)
933 output, err = self._svncommand(['--version', '--quiet'], filename=None)
940 m = re.search(r'^(\d+)\.(\d+)', output)
934 m = re.search(r'^(\d+)\.(\d+)', output)
941 if not m:
935 if not m:
942 raise util.Abort(_('cannot retrieve svn tool version'))
936 raise util.Abort(_('cannot retrieve svn tool version'))
943 return (int(m.group(1)), int(m.group(2)))
937 return (int(m.group(1)), int(m.group(2)))
944
938
945 def _wcrevs(self):
939 def _wcrevs(self):
946 # Get the working directory revision as well as the last
940 # Get the working directory revision as well as the last
947 # commit revision so we can compare the subrepo state with
941 # commit revision so we can compare the subrepo state with
948 # both. We used to store the working directory one.
942 # both. We used to store the working directory one.
949 output, err = self._svncommand(['info', '--xml'])
943 output, err = self._svncommand(['info', '--xml'])
950 doc = xml.dom.minidom.parseString(output)
944 doc = xml.dom.minidom.parseString(output)
951 entries = doc.getElementsByTagName('entry')
945 entries = doc.getElementsByTagName('entry')
952 lastrev, rev = '0', '0'
946 lastrev, rev = '0', '0'
953 if entries:
947 if entries:
954 rev = str(entries[0].getAttribute('revision')) or '0'
948 rev = str(entries[0].getAttribute('revision')) or '0'
955 commits = entries[0].getElementsByTagName('commit')
949 commits = entries[0].getElementsByTagName('commit')
956 if commits:
950 if commits:
957 lastrev = str(commits[0].getAttribute('revision')) or '0'
951 lastrev = str(commits[0].getAttribute('revision')) or '0'
958 return (lastrev, rev)
952 return (lastrev, rev)
959
953
960 def _wcrev(self):
954 def _wcrev(self):
961 return self._wcrevs()[0]
955 return self._wcrevs()[0]
962
956
963 def _wcchanged(self):
957 def _wcchanged(self):
964 """Return (changes, extchanges, missing) where changes is True
958 """Return (changes, extchanges, missing) where changes is True
965 if the working directory was changed, extchanges is
959 if the working directory was changed, extchanges is
966 True if any of these changes concern an external entry and missing
960 True if any of these changes concern an external entry and missing
967 is True if any change is a missing entry.
961 is True if any change is a missing entry.
968 """
962 """
969 output, err = self._svncommand(['status', '--xml'])
963 output, err = self._svncommand(['status', '--xml'])
970 externals, changes, missing = [], [], []
964 externals, changes, missing = [], [], []
971 doc = xml.dom.minidom.parseString(output)
965 doc = xml.dom.minidom.parseString(output)
972 for e in doc.getElementsByTagName('entry'):
966 for e in doc.getElementsByTagName('entry'):
973 s = e.getElementsByTagName('wc-status')
967 s = e.getElementsByTagName('wc-status')
974 if not s:
968 if not s:
975 continue
969 continue
976 item = s[0].getAttribute('item')
970 item = s[0].getAttribute('item')
977 props = s[0].getAttribute('props')
971 props = s[0].getAttribute('props')
978 path = e.getAttribute('path')
972 path = e.getAttribute('path')
979 if item == 'external':
973 if item == 'external':
980 externals.append(path)
974 externals.append(path)
981 elif item == 'missing':
975 elif item == 'missing':
982 missing.append(path)
976 missing.append(path)
983 if (item not in ('', 'normal', 'unversioned', 'external')
977 if (item not in ('', 'normal', 'unversioned', 'external')
984 or props not in ('', 'none', 'normal')):
978 or props not in ('', 'none', 'normal')):
985 changes.append(path)
979 changes.append(path)
986 for path in changes:
980 for path in changes:
987 for ext in externals:
981 for ext in externals:
988 if path == ext or path.startswith(ext + os.sep):
982 if path == ext or path.startswith(ext + os.sep):
989 return True, True, bool(missing)
983 return True, True, bool(missing)
990 return bool(changes), False, bool(missing)
984 return bool(changes), False, bool(missing)
991
985
992 def dirty(self, ignoreupdate=False):
986 def dirty(self, ignoreupdate=False):
993 if not self._wcchanged()[0]:
987 if not self._wcchanged()[0]:
994 if self._state[1] in self._wcrevs() or ignoreupdate:
988 if self._state[1] in self._wcrevs() or ignoreupdate:
995 return False
989 return False
996 return True
990 return True
997
991
998 def basestate(self):
992 def basestate(self):
999 lastrev, rev = self._wcrevs()
993 lastrev, rev = self._wcrevs()
1000 if lastrev != rev:
994 if lastrev != rev:
1001 # Last committed rev is not the same than rev. We would
995 # Last committed rev is not the same than rev. We would
1002 # like to take lastrev but we do not know if the subrepo
996 # like to take lastrev but we do not know if the subrepo
1003 # URL exists at lastrev. Test it and fallback to rev it
997 # URL exists at lastrev. Test it and fallback to rev it
1004 # is not there.
998 # is not there.
1005 try:
999 try:
1006 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1000 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1007 return lastrev
1001 return lastrev
1008 except error.Abort:
1002 except error.Abort:
1009 pass
1003 pass
1010 return rev
1004 return rev
1011
1005
1012 @annotatesubrepoerror
1006 @annotatesubrepoerror
1013 def commit(self, text, user, date):
1007 def commit(self, text, user, date):
1014 # user and date are out of our hands since svn is centralized
1008 # user and date are out of our hands since svn is centralized
1015 changed, extchanged, missing = self._wcchanged()
1009 changed, extchanged, missing = self._wcchanged()
1016 if not changed:
1010 if not changed:
1017 return self.basestate()
1011 return self.basestate()
1018 if extchanged:
1012 if extchanged:
1019 # Do not try to commit externals
1013 # Do not try to commit externals
1020 raise util.Abort(_('cannot commit svn externals'))
1014 raise util.Abort(_('cannot commit svn externals'))
1021 if missing:
1015 if missing:
1022 # svn can commit with missing entries but aborting like hg
1016 # svn can commit with missing entries but aborting like hg
1023 # seems a better approach.
1017 # seems a better approach.
1024 raise util.Abort(_('cannot commit missing svn entries'))
1018 raise util.Abort(_('cannot commit missing svn entries'))
1025 commitinfo, err = self._svncommand(['commit', '-m', text])
1019 commitinfo, err = self._svncommand(['commit', '-m', text])
1026 self._ui.status(commitinfo)
1020 self._ui.status(commitinfo)
1027 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1021 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1028 if not newrev:
1022 if not newrev:
1029 if not commitinfo.strip():
1023 if not commitinfo.strip():
1030 # Sometimes, our definition of "changed" differs from
1024 # Sometimes, our definition of "changed" differs from
1031 # svn one. For instance, svn ignores missing files
1025 # svn one. For instance, svn ignores missing files
1032 # when committing. If there are only missing files, no
1026 # when committing. If there are only missing files, no
1033 # commit is made, no output and no error code.
1027 # commit is made, no output and no error code.
1034 raise util.Abort(_('failed to commit svn changes'))
1028 raise util.Abort(_('failed to commit svn changes'))
1035 raise util.Abort(commitinfo.splitlines()[-1])
1029 raise util.Abort(commitinfo.splitlines()[-1])
1036 newrev = newrev.groups()[0]
1030 newrev = newrev.groups()[0]
1037 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
1031 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
1038 return newrev
1032 return newrev
1039
1033
1040 @annotatesubrepoerror
1034 @annotatesubrepoerror
1041 def remove(self):
1035 def remove(self):
1042 if self.dirty():
1036 if self.dirty():
1043 self._ui.warn(_('not removing repo %s because '
1037 self._ui.warn(_('not removing repo %s because '
1044 'it has changes.\n') % self._path)
1038 'it has changes.\n') % self._path)
1045 return
1039 return
1046 self._ui.note(_('removing subrepo %s\n') % self._path)
1040 self._ui.note(_('removing subrepo %s\n') % self._path)
1047
1041
1048 def onerror(function, path, excinfo):
1042 def onerror(function, path, excinfo):
1049 if function is not os.remove:
1043 if function is not os.remove:
1050 raise
1044 raise
1051 # read-only files cannot be unlinked under Windows
1045 # read-only files cannot be unlinked under Windows
1052 s = os.stat(path)
1046 s = os.stat(path)
1053 if (s.st_mode & stat.S_IWRITE) != 0:
1047 if (s.st_mode & stat.S_IWRITE) != 0:
1054 raise
1048 raise
1055 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
1049 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
1056 os.remove(path)
1050 os.remove(path)
1057
1051
1058 path = self._ctx._repo.wjoin(self._path)
1052 path = self._ctx._repo.wjoin(self._path)
1059 shutil.rmtree(path, onerror=onerror)
1053 shutil.rmtree(path, onerror=onerror)
1060 try:
1054 try:
1061 os.removedirs(os.path.dirname(path))
1055 os.removedirs(os.path.dirname(path))
1062 except OSError:
1056 except OSError:
1063 pass
1057 pass
1064
1058
1065 @annotatesubrepoerror
1059 @annotatesubrepoerror
1066 def get(self, state, overwrite=False):
1060 def get(self, state, overwrite=False):
1067 if overwrite:
1061 if overwrite:
1068 self._svncommand(['revert', '--recursive'])
1062 self._svncommand(['revert', '--recursive'])
1069 args = ['checkout']
1063 args = ['checkout']
1070 if self._svnversion >= (1, 5):
1064 if self._svnversion >= (1, 5):
1071 args.append('--force')
1065 args.append('--force')
1072 # The revision must be specified at the end of the URL to properly
1066 # The revision must be specified at the end of the URL to properly
1073 # update to a directory which has since been deleted and recreated.
1067 # update to a directory which has since been deleted and recreated.
1074 args.append('%s@%s' % (state[0], state[1]))
1068 args.append('%s@%s' % (state[0], state[1]))
1075 status, err = self._svncommand(args, failok=True)
1069 status, err = self._svncommand(args, failok=True)
1076 _sanitize(self._ui, self._ctx._repo.wjoin(self._path), '.svn')
1070 _sanitize(self._ui, self._ctx._repo.wjoin(self._path), '.svn')
1077 if not re.search('Checked out revision [0-9]+.', status):
1071 if not re.search('Checked out revision [0-9]+.', status):
1078 if ('is already a working copy for a different URL' in err
1072 if ('is already a working copy for a different URL' in err
1079 and (self._wcchanged()[:2] == (False, False))):
1073 and (self._wcchanged()[:2] == (False, False))):
1080 # obstructed but clean working copy, so just blow it away.
1074 # obstructed but clean working copy, so just blow it away.
1081 self.remove()
1075 self.remove()
1082 self.get(state, overwrite=False)
1076 self.get(state, overwrite=False)
1083 return
1077 return
1084 raise util.Abort((status or err).splitlines()[-1])
1078 raise util.Abort((status or err).splitlines()[-1])
1085 self._ui.status(status)
1079 self._ui.status(status)
1086
1080
1087 @annotatesubrepoerror
1081 @annotatesubrepoerror
1088 def merge(self, state):
1082 def merge(self, state):
1089 old = self._state[1]
1083 old = self._state[1]
1090 new = state[1]
1084 new = state[1]
1091 wcrev = self._wcrev()
1085 wcrev = self._wcrev()
1092 if new != wcrev:
1086 if new != wcrev:
1093 dirty = old == wcrev or self._wcchanged()[0]
1087 dirty = old == wcrev or self._wcchanged()[0]
1094 if _updateprompt(self._ui, self, dirty, wcrev, new):
1088 if _updateprompt(self._ui, self, dirty, wcrev, new):
1095 self.get(state, False)
1089 self.get(state, False)
1096
1090
1097 def push(self, opts):
1091 def push(self, opts):
1098 # push is a no-op for SVN
1092 # push is a no-op for SVN
1099 return True
1093 return True
1100
1094
1101 @annotatesubrepoerror
1095 @annotatesubrepoerror
1102 def files(self):
1096 def files(self):
1103 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1097 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1104 doc = xml.dom.minidom.parseString(output)
1098 doc = xml.dom.minidom.parseString(output)
1105 paths = []
1099 paths = []
1106 for e in doc.getElementsByTagName('entry'):
1100 for e in doc.getElementsByTagName('entry'):
1107 kind = str(e.getAttribute('kind'))
1101 kind = str(e.getAttribute('kind'))
1108 if kind != 'file':
1102 if kind != 'file':
1109 continue
1103 continue
1110 name = ''.join(c.data for c
1104 name = ''.join(c.data for c
1111 in e.getElementsByTagName('name')[0].childNodes
1105 in e.getElementsByTagName('name')[0].childNodes
1112 if c.nodeType == c.TEXT_NODE)
1106 if c.nodeType == c.TEXT_NODE)
1113 paths.append(name.encode('utf-8'))
1107 paths.append(name.encode('utf-8'))
1114 return paths
1108 return paths
1115
1109
1116 def filedata(self, name):
1110 def filedata(self, name):
1117 return self._svncommand(['cat'], name)[0]
1111 return self._svncommand(['cat'], name)[0]
1118
1112
1119
1113
1120 class gitsubrepo(abstractsubrepo):
1114 class gitsubrepo(abstractsubrepo):
1121 def __init__(self, ctx, path, state):
1115 def __init__(self, ctx, path, state):
1122 self._state = state
1116 self._state = state
1123 self._ctx = ctx
1117 self._ctx = ctx
1124 self._path = path
1118 self._path = path
1125 self._relpath = os.path.join(reporelpath(ctx._repo), path)
1119 self._relpath = os.path.join(reporelpath(ctx._repo), path)
1126 self._abspath = ctx._repo.wjoin(path)
1120 self._abspath = ctx._repo.wjoin(path)
1127 self._subparent = ctx._repo
1121 self._subparent = ctx._repo
1128 self._ui = ctx._repo.ui
1122 self._ui = ctx._repo.ui
1129 self._ensuregit()
1123 self._ensuregit()
1130
1124
1131 def _ensuregit(self):
1125 def _ensuregit(self):
1132 try:
1126 try:
1133 self._gitexecutable = 'git'
1127 self._gitexecutable = 'git'
1134 out, err = self._gitnodir(['--version'])
1128 out, err = self._gitnodir(['--version'])
1135 except OSError, e:
1129 except OSError, e:
1136 if e.errno != 2 or os.name != 'nt':
1130 if e.errno != 2 or os.name != 'nt':
1137 raise
1131 raise
1138 self._gitexecutable = 'git.cmd'
1132 self._gitexecutable = 'git.cmd'
1139 out, err = self._gitnodir(['--version'])
1133 out, err = self._gitnodir(['--version'])
1140 versionstatus = self._checkversion(out)
1134 versionstatus = self._checkversion(out)
1141 if versionstatus == 'unknown':
1135 if versionstatus == 'unknown':
1142 self._ui.warn(_('cannot retrieve git version\n'))
1136 self._ui.warn(_('cannot retrieve git version\n'))
1143 elif versionstatus == 'abort':
1137 elif versionstatus == 'abort':
1144 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
1138 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
1145 elif versionstatus == 'warning':
1139 elif versionstatus == 'warning':
1146 self._ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1140 self._ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1147
1141
1148 @staticmethod
1142 @staticmethod
1149 def _checkversion(out):
1143 def _checkversion(out):
1150 '''ensure git version is new enough
1144 '''ensure git version is new enough
1151
1145
1152 >>> _checkversion = gitsubrepo._checkversion
1146 >>> _checkversion = gitsubrepo._checkversion
1153 >>> _checkversion('git version 1.6.0')
1147 >>> _checkversion('git version 1.6.0')
1154 'ok'
1148 'ok'
1155 >>> _checkversion('git version 1.8.5')
1149 >>> _checkversion('git version 1.8.5')
1156 'ok'
1150 'ok'
1157 >>> _checkversion('git version 1.4.0')
1151 >>> _checkversion('git version 1.4.0')
1158 'abort'
1152 'abort'
1159 >>> _checkversion('git version 1.5.0')
1153 >>> _checkversion('git version 1.5.0')
1160 'warning'
1154 'warning'
1161 >>> _checkversion('git version 1.9-rc0')
1155 >>> _checkversion('git version 1.9-rc0')
1162 'ok'
1156 'ok'
1163 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1157 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1164 'ok'
1158 'ok'
1165 >>> _checkversion('git version 1.9.0.GIT')
1159 >>> _checkversion('git version 1.9.0.GIT')
1166 'ok'
1160 'ok'
1167 >>> _checkversion('git version 12345')
1161 >>> _checkversion('git version 12345')
1168 'unknown'
1162 'unknown'
1169 >>> _checkversion('no')
1163 >>> _checkversion('no')
1170 'unknown'
1164 'unknown'
1171 '''
1165 '''
1172 m = re.search(r'^git version (\d+)\.(\d+)', out)
1166 m = re.search(r'^git version (\d+)\.(\d+)', out)
1173 if not m:
1167 if not m:
1174 return 'unknown'
1168 return 'unknown'
1175 version = (int(m.group(1)), int(m.group(2)))
1169 version = (int(m.group(1)), int(m.group(2)))
1176 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1170 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1177 # despite the docstring comment. For now, error on 1.4.0, warn on
1171 # despite the docstring comment. For now, error on 1.4.0, warn on
1178 # 1.5.0 but attempt to continue.
1172 # 1.5.0 but attempt to continue.
1179 if version < (1, 5):
1173 if version < (1, 5):
1180 return 'abort'
1174 return 'abort'
1181 elif version < (1, 6):
1175 elif version < (1, 6):
1182 return 'warning'
1176 return 'warning'
1183 return 'ok'
1177 return 'ok'
1184
1178
1185 def _gitcommand(self, commands, env=None, stream=False):
1179 def _gitcommand(self, commands, env=None, stream=False):
1186 return self._gitdir(commands, env=env, stream=stream)[0]
1180 return self._gitdir(commands, env=env, stream=stream)[0]
1187
1181
1188 def _gitdir(self, commands, env=None, stream=False):
1182 def _gitdir(self, commands, env=None, stream=False):
1189 return self._gitnodir(commands, env=env, stream=stream,
1183 return self._gitnodir(commands, env=env, stream=stream,
1190 cwd=self._abspath)
1184 cwd=self._abspath)
1191
1185
1192 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1186 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1193 """Calls the git command
1187 """Calls the git command
1194
1188
1195 The methods tries to call the git command. versions prior to 1.6.0
1189 The methods tries to call the git command. versions prior to 1.6.0
1196 are not supported and very probably fail.
1190 are not supported and very probably fail.
1197 """
1191 """
1198 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1192 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1199 # unless ui.quiet is set, print git's stderr,
1193 # unless ui.quiet is set, print git's stderr,
1200 # which is mostly progress and useful info
1194 # which is mostly progress and useful info
1201 errpipe = None
1195 errpipe = None
1202 if self._ui.quiet:
1196 if self._ui.quiet:
1203 errpipe = open(os.devnull, 'w')
1197 errpipe = open(os.devnull, 'w')
1204 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1198 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1205 cwd=cwd, env=env, close_fds=util.closefds,
1199 cwd=cwd, env=env, close_fds=util.closefds,
1206 stdout=subprocess.PIPE, stderr=errpipe)
1200 stdout=subprocess.PIPE, stderr=errpipe)
1207 if stream:
1201 if stream:
1208 return p.stdout, None
1202 return p.stdout, None
1209
1203
1210 retdata = p.stdout.read().strip()
1204 retdata = p.stdout.read().strip()
1211 # wait for the child to exit to avoid race condition.
1205 # wait for the child to exit to avoid race condition.
1212 p.wait()
1206 p.wait()
1213
1207
1214 if p.returncode != 0 and p.returncode != 1:
1208 if p.returncode != 0 and p.returncode != 1:
1215 # there are certain error codes that are ok
1209 # there are certain error codes that are ok
1216 command = commands[0]
1210 command = commands[0]
1217 if command in ('cat-file', 'symbolic-ref'):
1211 if command in ('cat-file', 'symbolic-ref'):
1218 return retdata, p.returncode
1212 return retdata, p.returncode
1219 # for all others, abort
1213 # for all others, abort
1220 raise util.Abort('git %s error %d in %s' %
1214 raise util.Abort('git %s error %d in %s' %
1221 (command, p.returncode, self._relpath))
1215 (command, p.returncode, self._relpath))
1222
1216
1223 return retdata, p.returncode
1217 return retdata, p.returncode
1224
1218
1225 def _gitmissing(self):
1219 def _gitmissing(self):
1226 return not os.path.exists(os.path.join(self._abspath, '.git'))
1220 return not os.path.exists(os.path.join(self._abspath, '.git'))
1227
1221
1228 def _gitstate(self):
1222 def _gitstate(self):
1229 return self._gitcommand(['rev-parse', 'HEAD'])
1223 return self._gitcommand(['rev-parse', 'HEAD'])
1230
1224
1231 def _gitcurrentbranch(self):
1225 def _gitcurrentbranch(self):
1232 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1226 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1233 if err:
1227 if err:
1234 current = None
1228 current = None
1235 return current
1229 return current
1236
1230
1237 def _gitremote(self, remote):
1231 def _gitremote(self, remote):
1238 out = self._gitcommand(['remote', 'show', '-n', remote])
1232 out = self._gitcommand(['remote', 'show', '-n', remote])
1239 line = out.split('\n')[1]
1233 line = out.split('\n')[1]
1240 i = line.index('URL: ') + len('URL: ')
1234 i = line.index('URL: ') + len('URL: ')
1241 return line[i:]
1235 return line[i:]
1242
1236
1243 def _githavelocally(self, revision):
1237 def _githavelocally(self, revision):
1244 out, code = self._gitdir(['cat-file', '-e', revision])
1238 out, code = self._gitdir(['cat-file', '-e', revision])
1245 return code == 0
1239 return code == 0
1246
1240
1247 def _gitisancestor(self, r1, r2):
1241 def _gitisancestor(self, r1, r2):
1248 base = self._gitcommand(['merge-base', r1, r2])
1242 base = self._gitcommand(['merge-base', r1, r2])
1249 return base == r1
1243 return base == r1
1250
1244
1251 def _gitisbare(self):
1245 def _gitisbare(self):
1252 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1246 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1253
1247
1254 def _gitupdatestat(self):
1248 def _gitupdatestat(self):
1255 """This must be run before git diff-index.
1249 """This must be run before git diff-index.
1256 diff-index only looks at changes to file stat;
1250 diff-index only looks at changes to file stat;
1257 this command looks at file contents and updates the stat."""
1251 this command looks at file contents and updates the stat."""
1258 self._gitcommand(['update-index', '-q', '--refresh'])
1252 self._gitcommand(['update-index', '-q', '--refresh'])
1259
1253
1260 def _gitbranchmap(self):
1254 def _gitbranchmap(self):
1261 '''returns 2 things:
1255 '''returns 2 things:
1262 a map from git branch to revision
1256 a map from git branch to revision
1263 a map from revision to branches'''
1257 a map from revision to branches'''
1264 branch2rev = {}
1258 branch2rev = {}
1265 rev2branch = {}
1259 rev2branch = {}
1266
1260
1267 out = self._gitcommand(['for-each-ref', '--format',
1261 out = self._gitcommand(['for-each-ref', '--format',
1268 '%(objectname) %(refname)'])
1262 '%(objectname) %(refname)'])
1269 for line in out.split('\n'):
1263 for line in out.split('\n'):
1270 revision, ref = line.split(' ')
1264 revision, ref = line.split(' ')
1271 if (not ref.startswith('refs/heads/') and
1265 if (not ref.startswith('refs/heads/') and
1272 not ref.startswith('refs/remotes/')):
1266 not ref.startswith('refs/remotes/')):
1273 continue
1267 continue
1274 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1268 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1275 continue # ignore remote/HEAD redirects
1269 continue # ignore remote/HEAD redirects
1276 branch2rev[ref] = revision
1270 branch2rev[ref] = revision
1277 rev2branch.setdefault(revision, []).append(ref)
1271 rev2branch.setdefault(revision, []).append(ref)
1278 return branch2rev, rev2branch
1272 return branch2rev, rev2branch
1279
1273
1280 def _gittracking(self, branches):
1274 def _gittracking(self, branches):
1281 'return map of remote branch to local tracking branch'
1275 'return map of remote branch to local tracking branch'
1282 # assumes no more than one local tracking branch for each remote
1276 # assumes no more than one local tracking branch for each remote
1283 tracking = {}
1277 tracking = {}
1284 for b in branches:
1278 for b in branches:
1285 if b.startswith('refs/remotes/'):
1279 if b.startswith('refs/remotes/'):
1286 continue
1280 continue
1287 bname = b.split('/', 2)[2]
1281 bname = b.split('/', 2)[2]
1288 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1282 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1289 if remote:
1283 if remote:
1290 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1284 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1291 tracking['refs/remotes/%s/%s' %
1285 tracking['refs/remotes/%s/%s' %
1292 (remote, ref.split('/', 2)[2])] = b
1286 (remote, ref.split('/', 2)[2])] = b
1293 return tracking
1287 return tracking
1294
1288
1295 def _abssource(self, source):
1289 def _abssource(self, source):
1296 if '://' not in source:
1290 if '://' not in source:
1297 # recognize the scp syntax as an absolute source
1291 # recognize the scp syntax as an absolute source
1298 colon = source.find(':')
1292 colon = source.find(':')
1299 if colon != -1 and '/' not in source[:colon]:
1293 if colon != -1 and '/' not in source[:colon]:
1300 return source
1294 return source
1301 self._subsource = source
1295 self._subsource = source
1302 return _abssource(self)
1296 return _abssource(self)
1303
1297
1304 def _fetch(self, source, revision):
1298 def _fetch(self, source, revision):
1305 if self._gitmissing():
1299 if self._gitmissing():
1306 source = self._abssource(source)
1300 source = self._abssource(source)
1307 self._ui.status(_('cloning subrepo %s from %s\n') %
1301 self._ui.status(_('cloning subrepo %s from %s\n') %
1308 (self._relpath, source))
1302 (self._relpath, source))
1309 self._gitnodir(['clone', source, self._abspath])
1303 self._gitnodir(['clone', source, self._abspath])
1310 if self._githavelocally(revision):
1304 if self._githavelocally(revision):
1311 return
1305 return
1312 self._ui.status(_('pulling subrepo %s from %s\n') %
1306 self._ui.status(_('pulling subrepo %s from %s\n') %
1313 (self._relpath, self._gitremote('origin')))
1307 (self._relpath, self._gitremote('origin')))
1314 # try only origin: the originally cloned repo
1308 # try only origin: the originally cloned repo
1315 self._gitcommand(['fetch'])
1309 self._gitcommand(['fetch'])
1316 if not self._githavelocally(revision):
1310 if not self._githavelocally(revision):
1317 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1311 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1318 (revision, self._relpath))
1312 (revision, self._relpath))
1319
1313
1320 @annotatesubrepoerror
1314 @annotatesubrepoerror
1321 def dirty(self, ignoreupdate=False):
1315 def dirty(self, ignoreupdate=False):
1322 if self._gitmissing():
1316 if self._gitmissing():
1323 return self._state[1] != ''
1317 return self._state[1] != ''
1324 if self._gitisbare():
1318 if self._gitisbare():
1325 return True
1319 return True
1326 if not ignoreupdate and self._state[1] != self._gitstate():
1320 if not ignoreupdate and self._state[1] != self._gitstate():
1327 # different version checked out
1321 # different version checked out
1328 return True
1322 return True
1329 # check for staged changes or modified files; ignore untracked files
1323 # check for staged changes or modified files; ignore untracked files
1330 self._gitupdatestat()
1324 self._gitupdatestat()
1331 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1325 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1332 return code == 1
1326 return code == 1
1333
1327
1334 def basestate(self):
1328 def basestate(self):
1335 return self._gitstate()
1329 return self._gitstate()
1336
1330
1337 @annotatesubrepoerror
1331 @annotatesubrepoerror
1338 def get(self, state, overwrite=False):
1332 def get(self, state, overwrite=False):
1339 source, revision, kind = state
1333 source, revision, kind = state
1340 if not revision:
1334 if not revision:
1341 self.remove()
1335 self.remove()
1342 return
1336 return
1343 self._fetch(source, revision)
1337 self._fetch(source, revision)
1344 # if the repo was set to be bare, unbare it
1338 # if the repo was set to be bare, unbare it
1345 if self._gitisbare():
1339 if self._gitisbare():
1346 self._gitcommand(['config', 'core.bare', 'false'])
1340 self._gitcommand(['config', 'core.bare', 'false'])
1347 if self._gitstate() == revision:
1341 if self._gitstate() == revision:
1348 self._gitcommand(['reset', '--hard', 'HEAD'])
1342 self._gitcommand(['reset', '--hard', 'HEAD'])
1349 return
1343 return
1350 elif self._gitstate() == revision:
1344 elif self._gitstate() == revision:
1351 if overwrite:
1345 if overwrite:
1352 # first reset the index to unmark new files for commit, because
1346 # first reset the index to unmark new files for commit, because
1353 # reset --hard will otherwise throw away files added for commit,
1347 # reset --hard will otherwise throw away files added for commit,
1354 # not just unmark them.
1348 # not just unmark them.
1355 self._gitcommand(['reset', 'HEAD'])
1349 self._gitcommand(['reset', 'HEAD'])
1356 self._gitcommand(['reset', '--hard', 'HEAD'])
1350 self._gitcommand(['reset', '--hard', 'HEAD'])
1357 return
1351 return
1358 branch2rev, rev2branch = self._gitbranchmap()
1352 branch2rev, rev2branch = self._gitbranchmap()
1359
1353
1360 def checkout(args):
1354 def checkout(args):
1361 cmd = ['checkout']
1355 cmd = ['checkout']
1362 if overwrite:
1356 if overwrite:
1363 # first reset the index to unmark new files for commit, because
1357 # first reset the index to unmark new files for commit, because
1364 # the -f option will otherwise throw away files added for
1358 # the -f option will otherwise throw away files added for
1365 # commit, not just unmark them.
1359 # commit, not just unmark them.
1366 self._gitcommand(['reset', 'HEAD'])
1360 self._gitcommand(['reset', 'HEAD'])
1367 cmd.append('-f')
1361 cmd.append('-f')
1368 self._gitcommand(cmd + args)
1362 self._gitcommand(cmd + args)
1369 _sanitize(self._ui, self._abspath, '.git')
1363 _sanitize(self._ui, self._abspath, '.git')
1370
1364
1371 def rawcheckout():
1365 def rawcheckout():
1372 # no branch to checkout, check it out with no branch
1366 # no branch to checkout, check it out with no branch
1373 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1367 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1374 self._relpath)
1368 self._relpath)
1375 self._ui.warn(_('check out a git branch if you intend '
1369 self._ui.warn(_('check out a git branch if you intend '
1376 'to make changes\n'))
1370 'to make changes\n'))
1377 checkout(['-q', revision])
1371 checkout(['-q', revision])
1378
1372
1379 if revision not in rev2branch:
1373 if revision not in rev2branch:
1380 rawcheckout()
1374 rawcheckout()
1381 return
1375 return
1382 branches = rev2branch[revision]
1376 branches = rev2branch[revision]
1383 firstlocalbranch = None
1377 firstlocalbranch = None
1384 for b in branches:
1378 for b in branches:
1385 if b == 'refs/heads/master':
1379 if b == 'refs/heads/master':
1386 # master trumps all other branches
1380 # master trumps all other branches
1387 checkout(['refs/heads/master'])
1381 checkout(['refs/heads/master'])
1388 return
1382 return
1389 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1383 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1390 firstlocalbranch = b
1384 firstlocalbranch = b
1391 if firstlocalbranch:
1385 if firstlocalbranch:
1392 checkout([firstlocalbranch])
1386 checkout([firstlocalbranch])
1393 return
1387 return
1394
1388
1395 tracking = self._gittracking(branch2rev.keys())
1389 tracking = self._gittracking(branch2rev.keys())
1396 # choose a remote branch already tracked if possible
1390 # choose a remote branch already tracked if possible
1397 remote = branches[0]
1391 remote = branches[0]
1398 if remote not in tracking:
1392 if remote not in tracking:
1399 for b in branches:
1393 for b in branches:
1400 if b in tracking:
1394 if b in tracking:
1401 remote = b
1395 remote = b
1402 break
1396 break
1403
1397
1404 if remote not in tracking:
1398 if remote not in tracking:
1405 # create a new local tracking branch
1399 # create a new local tracking branch
1406 local = remote.split('/', 3)[3]
1400 local = remote.split('/', 3)[3]
1407 checkout(['-b', local, remote])
1401 checkout(['-b', local, remote])
1408 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1402 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1409 # When updating to a tracked remote branch,
1403 # When updating to a tracked remote branch,
1410 # if the local tracking branch is downstream of it,
1404 # if the local tracking branch is downstream of it,
1411 # a normal `git pull` would have performed a "fast-forward merge"
1405 # a normal `git pull` would have performed a "fast-forward merge"
1412 # which is equivalent to updating the local branch to the remote.
1406 # which is equivalent to updating the local branch to the remote.
1413 # Since we are only looking at branching at update, we need to
1407 # Since we are only looking at branching at update, we need to
1414 # detect this situation and perform this action lazily.
1408 # detect this situation and perform this action lazily.
1415 if tracking[remote] != self._gitcurrentbranch():
1409 if tracking[remote] != self._gitcurrentbranch():
1416 checkout([tracking[remote]])
1410 checkout([tracking[remote]])
1417 self._gitcommand(['merge', '--ff', remote])
1411 self._gitcommand(['merge', '--ff', remote])
1418 _sanitize(self._ui, self._abspath, '.git')
1412 _sanitize(self._ui, self._abspath, '.git')
1419 else:
1413 else:
1420 # a real merge would be required, just checkout the revision
1414 # a real merge would be required, just checkout the revision
1421 rawcheckout()
1415 rawcheckout()
1422
1416
1423 @annotatesubrepoerror
1417 @annotatesubrepoerror
1424 def commit(self, text, user, date):
1418 def commit(self, text, user, date):
1425 if self._gitmissing():
1419 if self._gitmissing():
1426 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1420 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1427 cmd = ['commit', '-a', '-m', text]
1421 cmd = ['commit', '-a', '-m', text]
1428 env = os.environ.copy()
1422 env = os.environ.copy()
1429 if user:
1423 if user:
1430 cmd += ['--author', user]
1424 cmd += ['--author', user]
1431 if date:
1425 if date:
1432 # git's date parser silently ignores when seconds < 1e9
1426 # git's date parser silently ignores when seconds < 1e9
1433 # convert to ISO8601
1427 # convert to ISO8601
1434 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1428 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1435 '%Y-%m-%dT%H:%M:%S %1%2')
1429 '%Y-%m-%dT%H:%M:%S %1%2')
1436 self._gitcommand(cmd, env=env)
1430 self._gitcommand(cmd, env=env)
1437 # make sure commit works otherwise HEAD might not exist under certain
1431 # make sure commit works otherwise HEAD might not exist under certain
1438 # circumstances
1432 # circumstances
1439 return self._gitstate()
1433 return self._gitstate()
1440
1434
1441 @annotatesubrepoerror
1435 @annotatesubrepoerror
1442 def merge(self, state):
1436 def merge(self, state):
1443 source, revision, kind = state
1437 source, revision, kind = state
1444 self._fetch(source, revision)
1438 self._fetch(source, revision)
1445 base = self._gitcommand(['merge-base', revision, self._state[1]])
1439 base = self._gitcommand(['merge-base', revision, self._state[1]])
1446 self._gitupdatestat()
1440 self._gitupdatestat()
1447 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1441 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1448
1442
1449 def mergefunc():
1443 def mergefunc():
1450 if base == revision:
1444 if base == revision:
1451 self.get(state) # fast forward merge
1445 self.get(state) # fast forward merge
1452 elif base != self._state[1]:
1446 elif base != self._state[1]:
1453 self._gitcommand(['merge', '--no-commit', revision])
1447 self._gitcommand(['merge', '--no-commit', revision])
1454 _sanitize(self._ui, self._abspath, '.git')
1448 _sanitize(self._ui, self._abspath, '.git')
1455
1449
1456 if self.dirty():
1450 if self.dirty():
1457 if self._gitstate() != revision:
1451 if self._gitstate() != revision:
1458 dirty = self._gitstate() == self._state[1] or code != 0
1452 dirty = self._gitstate() == self._state[1] or code != 0
1459 if _updateprompt(self._ui, self, dirty,
1453 if _updateprompt(self._ui, self, dirty,
1460 self._state[1][:7], revision[:7]):
1454 self._state[1][:7], revision[:7]):
1461 mergefunc()
1455 mergefunc()
1462 else:
1456 else:
1463 mergefunc()
1457 mergefunc()
1464
1458
1465 @annotatesubrepoerror
1459 @annotatesubrepoerror
1466 def push(self, opts):
1460 def push(self, opts):
1467 force = opts.get('force')
1461 force = opts.get('force')
1468
1462
1469 if not self._state[1]:
1463 if not self._state[1]:
1470 return True
1464 return True
1471 if self._gitmissing():
1465 if self._gitmissing():
1472 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1466 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1473 # if a branch in origin contains the revision, nothing to do
1467 # if a branch in origin contains the revision, nothing to do
1474 branch2rev, rev2branch = self._gitbranchmap()
1468 branch2rev, rev2branch = self._gitbranchmap()
1475 if self._state[1] in rev2branch:
1469 if self._state[1] in rev2branch:
1476 for b in rev2branch[self._state[1]]:
1470 for b in rev2branch[self._state[1]]:
1477 if b.startswith('refs/remotes/origin/'):
1471 if b.startswith('refs/remotes/origin/'):
1478 return True
1472 return True
1479 for b, revision in branch2rev.iteritems():
1473 for b, revision in branch2rev.iteritems():
1480 if b.startswith('refs/remotes/origin/'):
1474 if b.startswith('refs/remotes/origin/'):
1481 if self._gitisancestor(self._state[1], revision):
1475 if self._gitisancestor(self._state[1], revision):
1482 return True
1476 return True
1483 # otherwise, try to push the currently checked out branch
1477 # otherwise, try to push the currently checked out branch
1484 cmd = ['push']
1478 cmd = ['push']
1485 if force:
1479 if force:
1486 cmd.append('--force')
1480 cmd.append('--force')
1487
1481
1488 current = self._gitcurrentbranch()
1482 current = self._gitcurrentbranch()
1489 if current:
1483 if current:
1490 # determine if the current branch is even useful
1484 # determine if the current branch is even useful
1491 if not self._gitisancestor(self._state[1], current):
1485 if not self._gitisancestor(self._state[1], current):
1492 self._ui.warn(_('unrelated git branch checked out '
1486 self._ui.warn(_('unrelated git branch checked out '
1493 'in subrepo %s\n') % self._relpath)
1487 'in subrepo %s\n') % self._relpath)
1494 return False
1488 return False
1495 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1489 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1496 (current.split('/', 2)[2], self._relpath))
1490 (current.split('/', 2)[2], self._relpath))
1497 ret = self._gitdir(cmd + ['origin', current])
1491 ret = self._gitdir(cmd + ['origin', current])
1498 return ret[1] == 0
1492 return ret[1] == 0
1499 else:
1493 else:
1500 self._ui.warn(_('no branch checked out in subrepo %s\n'
1494 self._ui.warn(_('no branch checked out in subrepo %s\n'
1501 'cannot push revision %s\n') %
1495 'cannot push revision %s\n') %
1502 (self._relpath, self._state[1]))
1496 (self._relpath, self._state[1]))
1503 return False
1497 return False
1504
1498
1505 @annotatesubrepoerror
1499 @annotatesubrepoerror
1506 def remove(self):
1500 def remove(self):
1507 if self._gitmissing():
1501 if self._gitmissing():
1508 return
1502 return
1509 if self.dirty():
1503 if self.dirty():
1510 self._ui.warn(_('not removing repo %s because '
1504 self._ui.warn(_('not removing repo %s because '
1511 'it has changes.\n') % self._relpath)
1505 'it has changes.\n') % self._relpath)
1512 return
1506 return
1513 # we can't fully delete the repository as it may contain
1507 # we can't fully delete the repository as it may contain
1514 # local-only history
1508 # local-only history
1515 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1509 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1516 self._gitcommand(['config', 'core.bare', 'true'])
1510 self._gitcommand(['config', 'core.bare', 'true'])
1517 for f in os.listdir(self._abspath):
1511 for f in os.listdir(self._abspath):
1518 if f == '.git':
1512 if f == '.git':
1519 continue
1513 continue
1520 path = os.path.join(self._abspath, f)
1514 path = os.path.join(self._abspath, f)
1521 if os.path.isdir(path) and not os.path.islink(path):
1515 if os.path.isdir(path) and not os.path.islink(path):
1522 shutil.rmtree(path)
1516 shutil.rmtree(path)
1523 else:
1517 else:
1524 os.remove(path)
1518 os.remove(path)
1525
1519
1526 def archive(self, ui, archiver, prefix, match=None):
1520 def archive(self, ui, archiver, prefix, match=None):
1527 total = 0
1521 total = 0
1528 source, revision = self._state
1522 source, revision = self._state
1529 if not revision:
1523 if not revision:
1530 return total
1524 return total
1531 self._fetch(source, revision)
1525 self._fetch(source, revision)
1532
1526
1533 # Parse git's native archive command.
1527 # Parse git's native archive command.
1534 # This should be much faster than manually traversing the trees
1528 # This should be much faster than manually traversing the trees
1535 # and objects with many subprocess calls.
1529 # and objects with many subprocess calls.
1536 tarstream = self._gitcommand(['archive', revision], stream=True)
1530 tarstream = self._gitcommand(['archive', revision], stream=True)
1537 tar = tarfile.open(fileobj=tarstream, mode='r|')
1531 tar = tarfile.open(fileobj=tarstream, mode='r|')
1538 relpath = subrelpath(self)
1532 relpath = subrelpath(self)
1539 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1533 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1540 for i, info in enumerate(tar):
1534 for i, info in enumerate(tar):
1541 if info.isdir():
1535 if info.isdir():
1542 continue
1536 continue
1543 if match and not match(info.name):
1537 if match and not match(info.name):
1544 continue
1538 continue
1545 if info.issym():
1539 if info.issym():
1546 data = info.linkname
1540 data = info.linkname
1547 else:
1541 else:
1548 data = tar.extractfile(info).read()
1542 data = tar.extractfile(info).read()
1549 archiver.addfile(os.path.join(prefix, self._path, info.name),
1543 archiver.addfile(os.path.join(prefix, self._path, info.name),
1550 info.mode, info.issym(), data)
1544 info.mode, info.issym(), data)
1551 total += 1
1545 total += 1
1552 ui.progress(_('archiving (%s)') % relpath, i + 1,
1546 ui.progress(_('archiving (%s)') % relpath, i + 1,
1553 unit=_('files'))
1547 unit=_('files'))
1554 ui.progress(_('archiving (%s)') % relpath, None)
1548 ui.progress(_('archiving (%s)') % relpath, None)
1555 return total
1549 return total
1556
1550
1557
1551
1558 @annotatesubrepoerror
1552 @annotatesubrepoerror
1559 def status(self, rev2, **opts):
1553 def status(self, rev2, **opts):
1560 rev1 = self._state[1]
1554 rev1 = self._state[1]
1561 if self._gitmissing() or not rev1:
1555 if self._gitmissing() or not rev1:
1562 # if the repo is missing, return no results
1556 # if the repo is missing, return no results
1563 return [], [], [], [], [], [], []
1557 return [], [], [], [], [], [], []
1564 modified, added, removed = [], [], []
1558 modified, added, removed = [], [], []
1565 self._gitupdatestat()
1559 self._gitupdatestat()
1566 if rev2:
1560 if rev2:
1567 command = ['diff-tree', rev1, rev2]
1561 command = ['diff-tree', rev1, rev2]
1568 else:
1562 else:
1569 command = ['diff-index', rev1]
1563 command = ['diff-index', rev1]
1570 out = self._gitcommand(command)
1564 out = self._gitcommand(command)
1571 for line in out.split('\n'):
1565 for line in out.split('\n'):
1572 tab = line.find('\t')
1566 tab = line.find('\t')
1573 if tab == -1:
1567 if tab == -1:
1574 continue
1568 continue
1575 status, f = line[tab - 1], line[tab + 1:]
1569 status, f = line[tab - 1], line[tab + 1:]
1576 if status == 'M':
1570 if status == 'M':
1577 modified.append(f)
1571 modified.append(f)
1578 elif status == 'A':
1572 elif status == 'A':
1579 added.append(f)
1573 added.append(f)
1580 elif status == 'D':
1574 elif status == 'D':
1581 removed.append(f)
1575 removed.append(f)
1582
1576
1583 deleted, unknown, ignored, clean = [], [], [], []
1577 deleted, unknown, ignored, clean = [], [], [], []
1584 return scmutil.status(modified, added, removed, deleted,
1578 return scmutil.status(modified, added, removed, deleted,
1585 unknown, ignored, clean)
1579 unknown, ignored, clean)
1586
1580
1587 def shortid(self, revid):
1581 def shortid(self, revid):
1588 return revid[:7]
1582 return revid[:7]
1589
1583
1590 types = {
1584 types = {
1591 'hg': hgsubrepo,
1585 'hg': hgsubrepo,
1592 'svn': svnsubrepo,
1586 'svn': svnsubrepo,
1593 'git': gitsubrepo,
1587 'git': gitsubrepo,
1594 }
1588 }
General Comments 0
You need to be logged in to leave comments. Login now