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