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