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