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