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