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