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