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