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