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