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