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