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