##// END OF EJS Templates
py3: handle keyword arguments correctly in subrepo.py...
Pulkit Goyal -
r35372:c999d246 default
parent child Browse files
Show More
@@ -1,2128 +1,2128 b''
1 # subrepo.py - sub-repository handling for Mercurial
1 # subrepo.py - sub-repository handling for Mercurial
2 #
2 #
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import copy
10 import copy
11 import errno
11 import errno
12 import hashlib
12 import hashlib
13 import os
13 import os
14 import posixpath
14 import posixpath
15 import re
15 import re
16 import stat
16 import stat
17 import subprocess
17 import subprocess
18 import sys
18 import sys
19 import tarfile
19 import tarfile
20 import xml.dom.minidom
20 import xml.dom.minidom
21
21
22
22
23 from .i18n import _
23 from .i18n import _
24 from . import (
24 from . import (
25 cmdutil,
25 cmdutil,
26 config,
26 config,
27 encoding,
27 encoding,
28 error,
28 error,
29 exchange,
29 exchange,
30 filemerge,
30 filemerge,
31 match as matchmod,
31 match as matchmod,
32 node,
32 node,
33 pathutil,
33 pathutil,
34 phases,
34 phases,
35 pycompat,
35 pycompat,
36 scmutil,
36 scmutil,
37 util,
37 util,
38 vfs as vfsmod,
38 vfs as vfsmod,
39 )
39 )
40
40
41 hg = None
41 hg = None
42 propertycache = util.propertycache
42 propertycache = util.propertycache
43
43
44 nullstate = ('', '', 'empty')
44 nullstate = ('', '', 'empty')
45
45
46 def _expandedabspath(path):
46 def _expandedabspath(path):
47 '''
47 '''
48 get a path or url and if it is a path expand it and return an absolute path
48 get a path or url and if it is a path expand it and return an absolute path
49 '''
49 '''
50 expandedpath = util.urllocalpath(util.expandpath(path))
50 expandedpath = util.urllocalpath(util.expandpath(path))
51 u = util.url(expandedpath)
51 u = util.url(expandedpath)
52 if not u.scheme:
52 if not u.scheme:
53 path = util.normpath(os.path.abspath(u.path))
53 path = util.normpath(os.path.abspath(u.path))
54 return path
54 return path
55
55
56 def _getstorehashcachename(remotepath):
56 def _getstorehashcachename(remotepath):
57 '''get a unique filename for the store hash cache of a remote repository'''
57 '''get a unique filename for the store hash cache of a remote repository'''
58 return hashlib.sha1(_expandedabspath(remotepath)).hexdigest()[0:12]
58 return hashlib.sha1(_expandedabspath(remotepath)).hexdigest()[0:12]
59
59
60 class SubrepoAbort(error.Abort):
60 class SubrepoAbort(error.Abort):
61 """Exception class used to avoid handling a subrepo error more than once"""
61 """Exception class used to avoid handling a subrepo error more than once"""
62 def __init__(self, *args, **kw):
62 def __init__(self, *args, **kw):
63 self.subrepo = kw.pop('subrepo', None)
63 self.subrepo = kw.pop(r'subrepo', None)
64 self.cause = kw.pop('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 str(source)
392 return str(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 str(parent)
399 return str(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 = hashlib.sha1(vfs.tryread(relname)).hexdigest()
792 filehash = hashlib.sha1(vfs.tryread(relname)).hexdigest()
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='w', notindexed=True)
814 vfs.writelines(cachefile, storehash, mode='w', 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 fp = self._repo.vfs("hgrc", "w", text=True)
844 fp = self._repo.vfs("hgrc", "w", text=True)
845 try:
845 try:
846 fp.write(''.join(lines))
846 fp.write(''.join(lines))
847 finally:
847 finally:
848 fp.close()
848 fp.close()
849
849
850 @annotatesubrepoerror
850 @annotatesubrepoerror
851 def add(self, ui, match, prefix, explicitonly, **opts):
851 def add(self, ui, match, prefix, explicitonly, **opts):
852 return cmdutil.add(ui, self._repo, match,
852 return cmdutil.add(ui, self._repo, match,
853 self.wvfs.reljoin(prefix, self._path),
853 self.wvfs.reljoin(prefix, self._path),
854 explicitonly, **opts)
854 explicitonly, **opts)
855
855
856 @annotatesubrepoerror
856 @annotatesubrepoerror
857 def addremove(self, m, prefix, opts, dry_run, similarity):
857 def addremove(self, m, prefix, opts, dry_run, similarity):
858 # In the same way as sub directories are processed, once in a subrepo,
858 # In the same way as sub directories are processed, once in a subrepo,
859 # always entry any of its subrepos. Don't corrupt the options that will
859 # always entry any of its subrepos. Don't corrupt the options that will
860 # be used to process sibling subrepos however.
860 # be used to process sibling subrepos however.
861 opts = copy.copy(opts)
861 opts = copy.copy(opts)
862 opts['subrepos'] = True
862 opts['subrepos'] = True
863 return scmutil.addremove(self._repo, m,
863 return scmutil.addremove(self._repo, m,
864 self.wvfs.reljoin(prefix, self._path), opts,
864 self.wvfs.reljoin(prefix, self._path), opts,
865 dry_run, similarity)
865 dry_run, similarity)
866
866
867 @annotatesubrepoerror
867 @annotatesubrepoerror
868 def cat(self, match, fm, fntemplate, prefix, **opts):
868 def cat(self, match, fm, fntemplate, prefix, **opts):
869 rev = self._state[1]
869 rev = self._state[1]
870 ctx = self._repo[rev]
870 ctx = self._repo[rev]
871 return cmdutil.cat(self.ui, self._repo, ctx, match, fm, fntemplate,
871 return cmdutil.cat(self.ui, self._repo, ctx, match, fm, fntemplate,
872 prefix, **opts)
872 prefix, **opts)
873
873
874 @annotatesubrepoerror
874 @annotatesubrepoerror
875 def status(self, rev2, **opts):
875 def status(self, rev2, **opts):
876 try:
876 try:
877 rev1 = self._state[1]
877 rev1 = self._state[1]
878 ctx1 = self._repo[rev1]
878 ctx1 = self._repo[rev1]
879 ctx2 = self._repo[rev2]
879 ctx2 = self._repo[rev2]
880 return self._repo.status(ctx1, ctx2, **opts)
880 return self._repo.status(ctx1, ctx2, **opts)
881 except error.RepoLookupError as inst:
881 except error.RepoLookupError as inst:
882 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
882 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
883 % (inst, subrelpath(self)))
883 % (inst, subrelpath(self)))
884 return scmutil.status([], [], [], [], [], [], [])
884 return scmutil.status([], [], [], [], [], [], [])
885
885
886 @annotatesubrepoerror
886 @annotatesubrepoerror
887 def diff(self, ui, diffopts, node2, match, prefix, **opts):
887 def diff(self, ui, diffopts, node2, match, prefix, **opts):
888 try:
888 try:
889 node1 = node.bin(self._state[1])
889 node1 = node.bin(self._state[1])
890 # We currently expect node2 to come from substate and be
890 # We currently expect node2 to come from substate and be
891 # in hex format
891 # in hex format
892 if node2 is not None:
892 if node2 is not None:
893 node2 = node.bin(node2)
893 node2 = node.bin(node2)
894 cmdutil.diffordiffstat(ui, self._repo, diffopts,
894 cmdutil.diffordiffstat(ui, self._repo, diffopts,
895 node1, node2, match,
895 node1, node2, match,
896 prefix=posixpath.join(prefix, self._path),
896 prefix=posixpath.join(prefix, self._path),
897 listsubrepos=True, **opts)
897 listsubrepos=True, **opts)
898 except error.RepoLookupError as inst:
898 except error.RepoLookupError as inst:
899 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
899 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
900 % (inst, subrelpath(self)))
900 % (inst, subrelpath(self)))
901
901
902 @annotatesubrepoerror
902 @annotatesubrepoerror
903 def archive(self, archiver, prefix, match=None, decode=True):
903 def archive(self, archiver, prefix, match=None, decode=True):
904 self._get(self._state + ('hg',))
904 self._get(self._state + ('hg',))
905 total = abstractsubrepo.archive(self, archiver, prefix, match)
905 total = abstractsubrepo.archive(self, archiver, prefix, match)
906 rev = self._state[1]
906 rev = self._state[1]
907 ctx = self._repo[rev]
907 ctx = self._repo[rev]
908 for subpath in ctx.substate:
908 for subpath in ctx.substate:
909 s = subrepo(ctx, subpath, True)
909 s = subrepo(ctx, subpath, True)
910 submatch = matchmod.subdirmatcher(subpath, match)
910 submatch = matchmod.subdirmatcher(subpath, match)
911 total += s.archive(archiver, prefix + self._path + '/', submatch,
911 total += s.archive(archiver, prefix + self._path + '/', submatch,
912 decode)
912 decode)
913 return total
913 return total
914
914
915 @annotatesubrepoerror
915 @annotatesubrepoerror
916 def dirty(self, ignoreupdate=False, missing=False):
916 def dirty(self, ignoreupdate=False, missing=False):
917 r = self._state[1]
917 r = self._state[1]
918 if r == '' and not ignoreupdate: # no state recorded
918 if r == '' and not ignoreupdate: # no state recorded
919 return True
919 return True
920 w = self._repo[None]
920 w = self._repo[None]
921 if r != w.p1().hex() and not ignoreupdate:
921 if r != w.p1().hex() and not ignoreupdate:
922 # different version checked out
922 # different version checked out
923 return True
923 return True
924 return w.dirty(missing=missing) # working directory changed
924 return w.dirty(missing=missing) # working directory changed
925
925
926 def basestate(self):
926 def basestate(self):
927 return self._repo['.'].hex()
927 return self._repo['.'].hex()
928
928
929 def checknested(self, path):
929 def checknested(self, path):
930 return self._repo._checknested(self._repo.wjoin(path))
930 return self._repo._checknested(self._repo.wjoin(path))
931
931
932 @annotatesubrepoerror
932 @annotatesubrepoerror
933 def commit(self, text, user, date):
933 def commit(self, text, user, date):
934 # don't bother committing in the subrepo if it's only been
934 # don't bother committing in the subrepo if it's only been
935 # updated
935 # updated
936 if not self.dirty(True):
936 if not self.dirty(True):
937 return self._repo['.'].hex()
937 return self._repo['.'].hex()
938 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
938 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
939 n = self._repo.commit(text, user, date)
939 n = self._repo.commit(text, user, date)
940 if not n:
940 if not n:
941 return self._repo['.'].hex() # different version checked out
941 return self._repo['.'].hex() # different version checked out
942 return node.hex(n)
942 return node.hex(n)
943
943
944 @annotatesubrepoerror
944 @annotatesubrepoerror
945 def phase(self, state):
945 def phase(self, state):
946 return self._repo[state].phase()
946 return self._repo[state].phase()
947
947
948 @annotatesubrepoerror
948 @annotatesubrepoerror
949 def remove(self):
949 def remove(self):
950 # we can't fully delete the repository as it may contain
950 # we can't fully delete the repository as it may contain
951 # local-only history
951 # local-only history
952 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
952 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
953 hg.clean(self._repo, node.nullid, False)
953 hg.clean(self._repo, node.nullid, False)
954
954
955 def _get(self, state):
955 def _get(self, state):
956 source, revision, kind = state
956 source, revision, kind = state
957 parentrepo = self._repo._subparent
957 parentrepo = self._repo._subparent
958
958
959 if revision in self._repo.unfiltered():
959 if revision in self._repo.unfiltered():
960 # Allow shared subrepos tracked at null to setup the sharedpath
960 # Allow shared subrepos tracked at null to setup the sharedpath
961 if len(self._repo) != 0 or not parentrepo.shared():
961 if len(self._repo) != 0 or not parentrepo.shared():
962 return True
962 return True
963 self._repo._subsource = source
963 self._repo._subsource = source
964 srcurl = _abssource(self._repo)
964 srcurl = _abssource(self._repo)
965 other = hg.peer(self._repo, {}, srcurl)
965 other = hg.peer(self._repo, {}, srcurl)
966 if len(self._repo) == 0:
966 if len(self._repo) == 0:
967 # use self._repo.vfs instead of self.wvfs to remove .hg only
967 # use self._repo.vfs instead of self.wvfs to remove .hg only
968 self._repo.vfs.rmtree()
968 self._repo.vfs.rmtree()
969 if parentrepo.shared():
969 if parentrepo.shared():
970 self.ui.status(_('sharing subrepo %s from %s\n')
970 self.ui.status(_('sharing subrepo %s from %s\n')
971 % (subrelpath(self), srcurl))
971 % (subrelpath(self), srcurl))
972 shared = hg.share(self._repo._subparent.baseui,
972 shared = hg.share(self._repo._subparent.baseui,
973 other, self._repo.root,
973 other, self._repo.root,
974 update=False, bookmarks=False)
974 update=False, bookmarks=False)
975 self._repo = shared.local()
975 self._repo = shared.local()
976 else:
976 else:
977 self.ui.status(_('cloning subrepo %s from %s\n')
977 self.ui.status(_('cloning subrepo %s from %s\n')
978 % (subrelpath(self), srcurl))
978 % (subrelpath(self), srcurl))
979 other, cloned = hg.clone(self._repo._subparent.baseui, {},
979 other, cloned = hg.clone(self._repo._subparent.baseui, {},
980 other, self._repo.root,
980 other, self._repo.root,
981 update=False)
981 update=False)
982 self._repo = cloned.local()
982 self._repo = cloned.local()
983 self._initrepo(parentrepo, source, create=True)
983 self._initrepo(parentrepo, source, create=True)
984 self._cachestorehash(srcurl)
984 self._cachestorehash(srcurl)
985 else:
985 else:
986 self.ui.status(_('pulling subrepo %s from %s\n')
986 self.ui.status(_('pulling subrepo %s from %s\n')
987 % (subrelpath(self), srcurl))
987 % (subrelpath(self), srcurl))
988 cleansub = self.storeclean(srcurl)
988 cleansub = self.storeclean(srcurl)
989 exchange.pull(self._repo, other)
989 exchange.pull(self._repo, other)
990 if cleansub:
990 if cleansub:
991 # keep the repo clean after pull
991 # keep the repo clean after pull
992 self._cachestorehash(srcurl)
992 self._cachestorehash(srcurl)
993 return False
993 return False
994
994
995 @annotatesubrepoerror
995 @annotatesubrepoerror
996 def get(self, state, overwrite=False):
996 def get(self, state, overwrite=False):
997 inrepo = self._get(state)
997 inrepo = self._get(state)
998 source, revision, kind = state
998 source, revision, kind = state
999 repo = self._repo
999 repo = self._repo
1000 repo.ui.debug("getting subrepo %s\n" % self._path)
1000 repo.ui.debug("getting subrepo %s\n" % self._path)
1001 if inrepo:
1001 if inrepo:
1002 urepo = repo.unfiltered()
1002 urepo = repo.unfiltered()
1003 ctx = urepo[revision]
1003 ctx = urepo[revision]
1004 if ctx.hidden():
1004 if ctx.hidden():
1005 urepo.ui.warn(
1005 urepo.ui.warn(
1006 _('revision %s in subrepository "%s" is hidden\n') \
1006 _('revision %s in subrepository "%s" is hidden\n') \
1007 % (revision[0:12], self._path))
1007 % (revision[0:12], self._path))
1008 repo = urepo
1008 repo = urepo
1009 hg.updaterepo(repo, revision, overwrite)
1009 hg.updaterepo(repo, revision, overwrite)
1010
1010
1011 @annotatesubrepoerror
1011 @annotatesubrepoerror
1012 def merge(self, state):
1012 def merge(self, state):
1013 self._get(state)
1013 self._get(state)
1014 cur = self._repo['.']
1014 cur = self._repo['.']
1015 dst = self._repo[state[1]]
1015 dst = self._repo[state[1]]
1016 anc = dst.ancestor(cur)
1016 anc = dst.ancestor(cur)
1017
1017
1018 def mergefunc():
1018 def mergefunc():
1019 if anc == cur and dst.branch() == cur.branch():
1019 if anc == cur and dst.branch() == cur.branch():
1020 self.ui.debug('updating subrepository "%s"\n'
1020 self.ui.debug('updating subrepository "%s"\n'
1021 % subrelpath(self))
1021 % subrelpath(self))
1022 hg.update(self._repo, state[1])
1022 hg.update(self._repo, state[1])
1023 elif anc == dst:
1023 elif anc == dst:
1024 self.ui.debug('skipping subrepository "%s"\n'
1024 self.ui.debug('skipping subrepository "%s"\n'
1025 % subrelpath(self))
1025 % subrelpath(self))
1026 else:
1026 else:
1027 self.ui.debug('merging subrepository "%s"\n' % subrelpath(self))
1027 self.ui.debug('merging subrepository "%s"\n' % subrelpath(self))
1028 hg.merge(self._repo, state[1], remind=False)
1028 hg.merge(self._repo, state[1], remind=False)
1029
1029
1030 wctx = self._repo[None]
1030 wctx = self._repo[None]
1031 if self.dirty():
1031 if self.dirty():
1032 if anc != dst:
1032 if anc != dst:
1033 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
1033 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
1034 mergefunc()
1034 mergefunc()
1035 else:
1035 else:
1036 mergefunc()
1036 mergefunc()
1037 else:
1037 else:
1038 mergefunc()
1038 mergefunc()
1039
1039
1040 @annotatesubrepoerror
1040 @annotatesubrepoerror
1041 def push(self, opts):
1041 def push(self, opts):
1042 force = opts.get('force')
1042 force = opts.get('force')
1043 newbranch = opts.get('new_branch')
1043 newbranch = opts.get('new_branch')
1044 ssh = opts.get('ssh')
1044 ssh = opts.get('ssh')
1045
1045
1046 # push subrepos depth-first for coherent ordering
1046 # push subrepos depth-first for coherent ordering
1047 c = self._repo['']
1047 c = self._repo['']
1048 subs = c.substate # only repos that are committed
1048 subs = c.substate # only repos that are committed
1049 for s in sorted(subs):
1049 for s in sorted(subs):
1050 if c.sub(s).push(opts) == 0:
1050 if c.sub(s).push(opts) == 0:
1051 return False
1051 return False
1052
1052
1053 dsturl = _abssource(self._repo, True)
1053 dsturl = _abssource(self._repo, True)
1054 if not force:
1054 if not force:
1055 if self.storeclean(dsturl):
1055 if self.storeclean(dsturl):
1056 self.ui.status(
1056 self.ui.status(
1057 _('no changes made to subrepo %s since last push to %s\n')
1057 _('no changes made to subrepo %s since last push to %s\n')
1058 % (subrelpath(self), dsturl))
1058 % (subrelpath(self), dsturl))
1059 return None
1059 return None
1060 self.ui.status(_('pushing subrepo %s to %s\n') %
1060 self.ui.status(_('pushing subrepo %s to %s\n') %
1061 (subrelpath(self), dsturl))
1061 (subrelpath(self), dsturl))
1062 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
1062 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
1063 res = exchange.push(self._repo, other, force, newbranch=newbranch)
1063 res = exchange.push(self._repo, other, force, newbranch=newbranch)
1064
1064
1065 # the repo is now clean
1065 # the repo is now clean
1066 self._cachestorehash(dsturl)
1066 self._cachestorehash(dsturl)
1067 return res.cgresult
1067 return res.cgresult
1068
1068
1069 @annotatesubrepoerror
1069 @annotatesubrepoerror
1070 def outgoing(self, ui, dest, opts):
1070 def outgoing(self, ui, dest, opts):
1071 if 'rev' in opts or 'branch' in opts:
1071 if 'rev' in opts or 'branch' in opts:
1072 opts = copy.copy(opts)
1072 opts = copy.copy(opts)
1073 opts.pop('rev', None)
1073 opts.pop('rev', None)
1074 opts.pop('branch', None)
1074 opts.pop('branch', None)
1075 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
1075 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
1076
1076
1077 @annotatesubrepoerror
1077 @annotatesubrepoerror
1078 def incoming(self, ui, source, opts):
1078 def incoming(self, ui, source, opts):
1079 if 'rev' in opts or 'branch' in opts:
1079 if 'rev' in opts or 'branch' in opts:
1080 opts = copy.copy(opts)
1080 opts = copy.copy(opts)
1081 opts.pop('rev', None)
1081 opts.pop('rev', None)
1082 opts.pop('branch', None)
1082 opts.pop('branch', None)
1083 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
1083 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
1084
1084
1085 @annotatesubrepoerror
1085 @annotatesubrepoerror
1086 def files(self):
1086 def files(self):
1087 rev = self._state[1]
1087 rev = self._state[1]
1088 ctx = self._repo[rev]
1088 ctx = self._repo[rev]
1089 return ctx.manifest().keys()
1089 return ctx.manifest().keys()
1090
1090
1091 def filedata(self, name, decode):
1091 def filedata(self, name, decode):
1092 rev = self._state[1]
1092 rev = self._state[1]
1093 data = self._repo[rev][name].data()
1093 data = self._repo[rev][name].data()
1094 if decode:
1094 if decode:
1095 data = self._repo.wwritedata(name, data)
1095 data = self._repo.wwritedata(name, data)
1096 return data
1096 return data
1097
1097
1098 def fileflags(self, name):
1098 def fileflags(self, name):
1099 rev = self._state[1]
1099 rev = self._state[1]
1100 ctx = self._repo[rev]
1100 ctx = self._repo[rev]
1101 return ctx.flags(name)
1101 return ctx.flags(name)
1102
1102
1103 @annotatesubrepoerror
1103 @annotatesubrepoerror
1104 def printfiles(self, ui, m, fm, fmt, subrepos):
1104 def printfiles(self, ui, m, fm, fmt, subrepos):
1105 # If the parent context is a workingctx, use the workingctx here for
1105 # If the parent context is a workingctx, use the workingctx here for
1106 # consistency.
1106 # consistency.
1107 if self._ctx.rev() is None:
1107 if self._ctx.rev() is None:
1108 ctx = self._repo[None]
1108 ctx = self._repo[None]
1109 else:
1109 else:
1110 rev = self._state[1]
1110 rev = self._state[1]
1111 ctx = self._repo[rev]
1111 ctx = self._repo[rev]
1112 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
1112 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
1113
1113
1114 @annotatesubrepoerror
1114 @annotatesubrepoerror
1115 def getfileset(self, expr):
1115 def getfileset(self, expr):
1116 if self._ctx.rev() is None:
1116 if self._ctx.rev() is None:
1117 ctx = self._repo[None]
1117 ctx = self._repo[None]
1118 else:
1118 else:
1119 rev = self._state[1]
1119 rev = self._state[1]
1120 ctx = self._repo[rev]
1120 ctx = self._repo[rev]
1121
1121
1122 files = ctx.getfileset(expr)
1122 files = ctx.getfileset(expr)
1123
1123
1124 for subpath in ctx.substate:
1124 for subpath in ctx.substate:
1125 sub = ctx.sub(subpath)
1125 sub = ctx.sub(subpath)
1126
1126
1127 try:
1127 try:
1128 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
1128 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
1129 except error.LookupError:
1129 except error.LookupError:
1130 self.ui.status(_("skipping missing subrepository: %s\n")
1130 self.ui.status(_("skipping missing subrepository: %s\n")
1131 % self.wvfs.reljoin(reporelpath(self), subpath))
1131 % self.wvfs.reljoin(reporelpath(self), subpath))
1132 return files
1132 return files
1133
1133
1134 def walk(self, match):
1134 def walk(self, match):
1135 ctx = self._repo[None]
1135 ctx = self._repo[None]
1136 return ctx.walk(match)
1136 return ctx.walk(match)
1137
1137
1138 @annotatesubrepoerror
1138 @annotatesubrepoerror
1139 def forget(self, match, prefix):
1139 def forget(self, match, prefix):
1140 return cmdutil.forget(self.ui, self._repo, match,
1140 return cmdutil.forget(self.ui, self._repo, match,
1141 self.wvfs.reljoin(prefix, self._path), True)
1141 self.wvfs.reljoin(prefix, self._path), True)
1142
1142
1143 @annotatesubrepoerror
1143 @annotatesubrepoerror
1144 def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
1144 def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
1145 return cmdutil.remove(self.ui, self._repo, matcher,
1145 return cmdutil.remove(self.ui, self._repo, matcher,
1146 self.wvfs.reljoin(prefix, self._path),
1146 self.wvfs.reljoin(prefix, self._path),
1147 after, force, subrepos)
1147 after, force, subrepos)
1148
1148
1149 @annotatesubrepoerror
1149 @annotatesubrepoerror
1150 def revert(self, substate, *pats, **opts):
1150 def revert(self, substate, *pats, **opts):
1151 # reverting a subrepo is a 2 step process:
1151 # reverting a subrepo is a 2 step process:
1152 # 1. if the no_backup is not set, revert all modified
1152 # 1. if the no_backup is not set, revert all modified
1153 # files inside the subrepo
1153 # files inside the subrepo
1154 # 2. update the subrepo to the revision specified in
1154 # 2. update the subrepo to the revision specified in
1155 # the corresponding substate dictionary
1155 # the corresponding substate dictionary
1156 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1156 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1157 if not opts.get(r'no_backup'):
1157 if not opts.get(r'no_backup'):
1158 # Revert all files on the subrepo, creating backups
1158 # Revert all files on the subrepo, creating backups
1159 # Note that this will not recursively revert subrepos
1159 # Note that this will not recursively revert subrepos
1160 # We could do it if there was a set:subrepos() predicate
1160 # We could do it if there was a set:subrepos() predicate
1161 opts = opts.copy()
1161 opts = opts.copy()
1162 opts[r'date'] = None
1162 opts[r'date'] = None
1163 opts[r'rev'] = substate[1]
1163 opts[r'rev'] = substate[1]
1164
1164
1165 self.filerevert(*pats, **opts)
1165 self.filerevert(*pats, **opts)
1166
1166
1167 # Update the repo to the revision specified in the given substate
1167 # Update the repo to the revision specified in the given substate
1168 if not opts.get(r'dry_run'):
1168 if not opts.get(r'dry_run'):
1169 self.get(substate, overwrite=True)
1169 self.get(substate, overwrite=True)
1170
1170
1171 def filerevert(self, *pats, **opts):
1171 def filerevert(self, *pats, **opts):
1172 ctx = self._repo[opts[r'rev']]
1172 ctx = self._repo[opts[r'rev']]
1173 parents = self._repo.dirstate.parents()
1173 parents = self._repo.dirstate.parents()
1174 if opts.get(r'all'):
1174 if opts.get(r'all'):
1175 pats = ['set:modified()']
1175 pats = ['set:modified()']
1176 else:
1176 else:
1177 pats = []
1177 pats = []
1178 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
1178 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
1179
1179
1180 def shortid(self, revid):
1180 def shortid(self, revid):
1181 return revid[:12]
1181 return revid[:12]
1182
1182
1183 @annotatesubrepoerror
1183 @annotatesubrepoerror
1184 def unshare(self):
1184 def unshare(self):
1185 # subrepo inherently violates our import layering rules
1185 # subrepo inherently violates our import layering rules
1186 # because it wants to make repo objects from deep inside the stack
1186 # because it wants to make repo objects from deep inside the stack
1187 # so we manually delay the circular imports to not break
1187 # so we manually delay the circular imports to not break
1188 # scripts that don't use our demand-loading
1188 # scripts that don't use our demand-loading
1189 global hg
1189 global hg
1190 from . import hg as h
1190 from . import hg as h
1191 hg = h
1191 hg = h
1192
1192
1193 # Nothing prevents a user from sharing in a repo, and then making that a
1193 # Nothing prevents a user from sharing in a repo, and then making that a
1194 # subrepo. Alternately, the previous unshare attempt may have failed
1194 # subrepo. Alternately, the previous unshare attempt may have failed
1195 # part way through. So recurse whether or not this layer is shared.
1195 # part way through. So recurse whether or not this layer is shared.
1196 if self._repo.shared():
1196 if self._repo.shared():
1197 self.ui.status(_("unsharing subrepo '%s'\n") % self._relpath)
1197 self.ui.status(_("unsharing subrepo '%s'\n") % self._relpath)
1198
1198
1199 hg.unshare(self.ui, self._repo)
1199 hg.unshare(self.ui, self._repo)
1200
1200
1201 def verify(self):
1201 def verify(self):
1202 try:
1202 try:
1203 rev = self._state[1]
1203 rev = self._state[1]
1204 ctx = self._repo.unfiltered()[rev]
1204 ctx = self._repo.unfiltered()[rev]
1205 if ctx.hidden():
1205 if ctx.hidden():
1206 # Since hidden revisions aren't pushed/pulled, it seems worth an
1206 # Since hidden revisions aren't pushed/pulled, it seems worth an
1207 # explicit warning.
1207 # explicit warning.
1208 ui = self._repo.ui
1208 ui = self._repo.ui
1209 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
1209 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
1210 (self._relpath, node.short(self._ctx.node())))
1210 (self._relpath, node.short(self._ctx.node())))
1211 return 0
1211 return 0
1212 except error.RepoLookupError:
1212 except error.RepoLookupError:
1213 # A missing subrepo revision may be a case of needing to pull it, so
1213 # A missing subrepo revision may be a case of needing to pull it, so
1214 # don't treat this as an error.
1214 # don't treat this as an error.
1215 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
1215 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
1216 (self._relpath, node.short(self._ctx.node())))
1216 (self._relpath, node.short(self._ctx.node())))
1217 return 0
1217 return 0
1218
1218
1219 @propertycache
1219 @propertycache
1220 def wvfs(self):
1220 def wvfs(self):
1221 """return own wvfs for efficiency and consistency
1221 """return own wvfs for efficiency and consistency
1222 """
1222 """
1223 return self._repo.wvfs
1223 return self._repo.wvfs
1224
1224
1225 @propertycache
1225 @propertycache
1226 def _relpath(self):
1226 def _relpath(self):
1227 """return path to this subrepository as seen from outermost repository
1227 """return path to this subrepository as seen from outermost repository
1228 """
1228 """
1229 # Keep consistent dir separators by avoiding vfs.join(self._path)
1229 # Keep consistent dir separators by avoiding vfs.join(self._path)
1230 return reporelpath(self._repo)
1230 return reporelpath(self._repo)
1231
1231
1232 class svnsubrepo(abstractsubrepo):
1232 class svnsubrepo(abstractsubrepo):
1233 def __init__(self, ctx, path, state, allowcreate):
1233 def __init__(self, ctx, path, state, allowcreate):
1234 super(svnsubrepo, self).__init__(ctx, path)
1234 super(svnsubrepo, self).__init__(ctx, path)
1235 self._state = state
1235 self._state = state
1236 self._exe = util.findexe('svn')
1236 self._exe = util.findexe('svn')
1237 if not self._exe:
1237 if not self._exe:
1238 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
1238 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
1239 % self._path)
1239 % self._path)
1240
1240
1241 def _svncommand(self, commands, filename='', failok=False):
1241 def _svncommand(self, commands, filename='', failok=False):
1242 cmd = [self._exe]
1242 cmd = [self._exe]
1243 extrakw = {}
1243 extrakw = {}
1244 if not self.ui.interactive():
1244 if not self.ui.interactive():
1245 # Making stdin be a pipe should prevent svn from behaving
1245 # Making stdin be a pipe should prevent svn from behaving
1246 # interactively even if we can't pass --non-interactive.
1246 # interactively even if we can't pass --non-interactive.
1247 extrakw['stdin'] = subprocess.PIPE
1247 extrakw[r'stdin'] = subprocess.PIPE
1248 # Starting in svn 1.5 --non-interactive is a global flag
1248 # Starting in svn 1.5 --non-interactive is a global flag
1249 # instead of being per-command, but we need to support 1.4 so
1249 # instead of being per-command, but we need to support 1.4 so
1250 # we have to be intelligent about what commands take
1250 # we have to be intelligent about what commands take
1251 # --non-interactive.
1251 # --non-interactive.
1252 if commands[0] in ('update', 'checkout', 'commit'):
1252 if commands[0] in ('update', 'checkout', 'commit'):
1253 cmd.append('--non-interactive')
1253 cmd.append('--non-interactive')
1254 cmd.extend(commands)
1254 cmd.extend(commands)
1255 if filename is not None:
1255 if filename is not None:
1256 path = self.wvfs.reljoin(self._ctx.repo().origroot,
1256 path = self.wvfs.reljoin(self._ctx.repo().origroot,
1257 self._path, filename)
1257 self._path, filename)
1258 cmd.append(path)
1258 cmd.append(path)
1259 env = dict(encoding.environ)
1259 env = dict(encoding.environ)
1260 # Avoid localized output, preserve current locale for everything else.
1260 # Avoid localized output, preserve current locale for everything else.
1261 lc_all = env.get('LC_ALL')
1261 lc_all = env.get('LC_ALL')
1262 if lc_all:
1262 if lc_all:
1263 env['LANG'] = lc_all
1263 env['LANG'] = lc_all
1264 del env['LC_ALL']
1264 del env['LC_ALL']
1265 env['LC_MESSAGES'] = 'C'
1265 env['LC_MESSAGES'] = 'C'
1266 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
1266 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
1267 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1267 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1268 universal_newlines=True, env=env, **extrakw)
1268 universal_newlines=True, env=env, **extrakw)
1269 stdout, stderr = p.communicate()
1269 stdout, stderr = p.communicate()
1270 stderr = stderr.strip()
1270 stderr = stderr.strip()
1271 if not failok:
1271 if not failok:
1272 if p.returncode:
1272 if p.returncode:
1273 raise error.Abort(stderr or 'exited with code %d'
1273 raise error.Abort(stderr or 'exited with code %d'
1274 % p.returncode)
1274 % p.returncode)
1275 if stderr:
1275 if stderr:
1276 self.ui.warn(stderr + '\n')
1276 self.ui.warn(stderr + '\n')
1277 return stdout, stderr
1277 return stdout, stderr
1278
1278
1279 @propertycache
1279 @propertycache
1280 def _svnversion(self):
1280 def _svnversion(self):
1281 output, err = self._svncommand(['--version', '--quiet'], filename=None)
1281 output, err = self._svncommand(['--version', '--quiet'], filename=None)
1282 m = re.search(br'^(\d+)\.(\d+)', output)
1282 m = re.search(br'^(\d+)\.(\d+)', output)
1283 if not m:
1283 if not m:
1284 raise error.Abort(_('cannot retrieve svn tool version'))
1284 raise error.Abort(_('cannot retrieve svn tool version'))
1285 return (int(m.group(1)), int(m.group(2)))
1285 return (int(m.group(1)), int(m.group(2)))
1286
1286
1287 def _wcrevs(self):
1287 def _wcrevs(self):
1288 # Get the working directory revision as well as the last
1288 # Get the working directory revision as well as the last
1289 # commit revision so we can compare the subrepo state with
1289 # commit revision so we can compare the subrepo state with
1290 # both. We used to store the working directory one.
1290 # both. We used to store the working directory one.
1291 output, err = self._svncommand(['info', '--xml'])
1291 output, err = self._svncommand(['info', '--xml'])
1292 doc = xml.dom.minidom.parseString(output)
1292 doc = xml.dom.minidom.parseString(output)
1293 entries = doc.getElementsByTagName('entry')
1293 entries = doc.getElementsByTagName('entry')
1294 lastrev, rev = '0', '0'
1294 lastrev, rev = '0', '0'
1295 if entries:
1295 if entries:
1296 rev = str(entries[0].getAttribute('revision')) or '0'
1296 rev = str(entries[0].getAttribute('revision')) or '0'
1297 commits = entries[0].getElementsByTagName('commit')
1297 commits = entries[0].getElementsByTagName('commit')
1298 if commits:
1298 if commits:
1299 lastrev = str(commits[0].getAttribute('revision')) or '0'
1299 lastrev = str(commits[0].getAttribute('revision')) or '0'
1300 return (lastrev, rev)
1300 return (lastrev, rev)
1301
1301
1302 def _wcrev(self):
1302 def _wcrev(self):
1303 return self._wcrevs()[0]
1303 return self._wcrevs()[0]
1304
1304
1305 def _wcchanged(self):
1305 def _wcchanged(self):
1306 """Return (changes, extchanges, missing) where changes is True
1306 """Return (changes, extchanges, missing) where changes is True
1307 if the working directory was changed, extchanges is
1307 if the working directory was changed, extchanges is
1308 True if any of these changes concern an external entry and missing
1308 True if any of these changes concern an external entry and missing
1309 is True if any change is a missing entry.
1309 is True if any change is a missing entry.
1310 """
1310 """
1311 output, err = self._svncommand(['status', '--xml'])
1311 output, err = self._svncommand(['status', '--xml'])
1312 externals, changes, missing = [], [], []
1312 externals, changes, missing = [], [], []
1313 doc = xml.dom.minidom.parseString(output)
1313 doc = xml.dom.minidom.parseString(output)
1314 for e in doc.getElementsByTagName('entry'):
1314 for e in doc.getElementsByTagName('entry'):
1315 s = e.getElementsByTagName('wc-status')
1315 s = e.getElementsByTagName('wc-status')
1316 if not s:
1316 if not s:
1317 continue
1317 continue
1318 item = s[0].getAttribute('item')
1318 item = s[0].getAttribute('item')
1319 props = s[0].getAttribute('props')
1319 props = s[0].getAttribute('props')
1320 path = e.getAttribute('path')
1320 path = e.getAttribute('path')
1321 if item == 'external':
1321 if item == 'external':
1322 externals.append(path)
1322 externals.append(path)
1323 elif item == 'missing':
1323 elif item == 'missing':
1324 missing.append(path)
1324 missing.append(path)
1325 if (item not in ('', 'normal', 'unversioned', 'external')
1325 if (item not in ('', 'normal', 'unversioned', 'external')
1326 or props not in ('', 'none', 'normal')):
1326 or props not in ('', 'none', 'normal')):
1327 changes.append(path)
1327 changes.append(path)
1328 for path in changes:
1328 for path in changes:
1329 for ext in externals:
1329 for ext in externals:
1330 if path == ext or path.startswith(ext + pycompat.ossep):
1330 if path == ext or path.startswith(ext + pycompat.ossep):
1331 return True, True, bool(missing)
1331 return True, True, bool(missing)
1332 return bool(changes), False, bool(missing)
1332 return bool(changes), False, bool(missing)
1333
1333
1334 def dirty(self, ignoreupdate=False, missing=False):
1334 def dirty(self, ignoreupdate=False, missing=False):
1335 wcchanged = self._wcchanged()
1335 wcchanged = self._wcchanged()
1336 changed = wcchanged[0] or (missing and wcchanged[2])
1336 changed = wcchanged[0] or (missing and wcchanged[2])
1337 if not changed:
1337 if not changed:
1338 if self._state[1] in self._wcrevs() or ignoreupdate:
1338 if self._state[1] in self._wcrevs() or ignoreupdate:
1339 return False
1339 return False
1340 return True
1340 return True
1341
1341
1342 def basestate(self):
1342 def basestate(self):
1343 lastrev, rev = self._wcrevs()
1343 lastrev, rev = self._wcrevs()
1344 if lastrev != rev:
1344 if lastrev != rev:
1345 # Last committed rev is not the same than rev. We would
1345 # Last committed rev is not the same than rev. We would
1346 # like to take lastrev but we do not know if the subrepo
1346 # like to take lastrev but we do not know if the subrepo
1347 # URL exists at lastrev. Test it and fallback to rev it
1347 # URL exists at lastrev. Test it and fallback to rev it
1348 # is not there.
1348 # is not there.
1349 try:
1349 try:
1350 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1350 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1351 return lastrev
1351 return lastrev
1352 except error.Abort:
1352 except error.Abort:
1353 pass
1353 pass
1354 return rev
1354 return rev
1355
1355
1356 @annotatesubrepoerror
1356 @annotatesubrepoerror
1357 def commit(self, text, user, date):
1357 def commit(self, text, user, date):
1358 # user and date are out of our hands since svn is centralized
1358 # user and date are out of our hands since svn is centralized
1359 changed, extchanged, missing = self._wcchanged()
1359 changed, extchanged, missing = self._wcchanged()
1360 if not changed:
1360 if not changed:
1361 return self.basestate()
1361 return self.basestate()
1362 if extchanged:
1362 if extchanged:
1363 # Do not try to commit externals
1363 # Do not try to commit externals
1364 raise error.Abort(_('cannot commit svn externals'))
1364 raise error.Abort(_('cannot commit svn externals'))
1365 if missing:
1365 if missing:
1366 # svn can commit with missing entries but aborting like hg
1366 # svn can commit with missing entries but aborting like hg
1367 # seems a better approach.
1367 # seems a better approach.
1368 raise error.Abort(_('cannot commit missing svn entries'))
1368 raise error.Abort(_('cannot commit missing svn entries'))
1369 commitinfo, err = self._svncommand(['commit', '-m', text])
1369 commitinfo, err = self._svncommand(['commit', '-m', text])
1370 self.ui.status(commitinfo)
1370 self.ui.status(commitinfo)
1371 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1371 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1372 if not newrev:
1372 if not newrev:
1373 if not commitinfo.strip():
1373 if not commitinfo.strip():
1374 # Sometimes, our definition of "changed" differs from
1374 # Sometimes, our definition of "changed" differs from
1375 # svn one. For instance, svn ignores missing files
1375 # svn one. For instance, svn ignores missing files
1376 # when committing. If there are only missing files, no
1376 # when committing. If there are only missing files, no
1377 # commit is made, no output and no error code.
1377 # commit is made, no output and no error code.
1378 raise error.Abort(_('failed to commit svn changes'))
1378 raise error.Abort(_('failed to commit svn changes'))
1379 raise error.Abort(commitinfo.splitlines()[-1])
1379 raise error.Abort(commitinfo.splitlines()[-1])
1380 newrev = newrev.groups()[0]
1380 newrev = newrev.groups()[0]
1381 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1381 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1382 return newrev
1382 return newrev
1383
1383
1384 @annotatesubrepoerror
1384 @annotatesubrepoerror
1385 def remove(self):
1385 def remove(self):
1386 if self.dirty():
1386 if self.dirty():
1387 self.ui.warn(_('not removing repo %s because '
1387 self.ui.warn(_('not removing repo %s because '
1388 'it has changes.\n') % self._path)
1388 'it has changes.\n') % self._path)
1389 return
1389 return
1390 self.ui.note(_('removing subrepo %s\n') % self._path)
1390 self.ui.note(_('removing subrepo %s\n') % self._path)
1391
1391
1392 self.wvfs.rmtree(forcibly=True)
1392 self.wvfs.rmtree(forcibly=True)
1393 try:
1393 try:
1394 pwvfs = self._ctx.repo().wvfs
1394 pwvfs = self._ctx.repo().wvfs
1395 pwvfs.removedirs(pwvfs.dirname(self._path))
1395 pwvfs.removedirs(pwvfs.dirname(self._path))
1396 except OSError:
1396 except OSError:
1397 pass
1397 pass
1398
1398
1399 @annotatesubrepoerror
1399 @annotatesubrepoerror
1400 def get(self, state, overwrite=False):
1400 def get(self, state, overwrite=False):
1401 if overwrite:
1401 if overwrite:
1402 self._svncommand(['revert', '--recursive'])
1402 self._svncommand(['revert', '--recursive'])
1403 args = ['checkout']
1403 args = ['checkout']
1404 if self._svnversion >= (1, 5):
1404 if self._svnversion >= (1, 5):
1405 args.append('--force')
1405 args.append('--force')
1406 # The revision must be specified at the end of the URL to properly
1406 # The revision must be specified at the end of the URL to properly
1407 # update to a directory which has since been deleted and recreated.
1407 # update to a directory which has since been deleted and recreated.
1408 args.append('%s@%s' % (state[0], state[1]))
1408 args.append('%s@%s' % (state[0], state[1]))
1409
1409
1410 # SEC: check that the ssh url is safe
1410 # SEC: check that the ssh url is safe
1411 util.checksafessh(state[0])
1411 util.checksafessh(state[0])
1412
1412
1413 status, err = self._svncommand(args, failok=True)
1413 status, err = self._svncommand(args, failok=True)
1414 _sanitize(self.ui, self.wvfs, '.svn')
1414 _sanitize(self.ui, self.wvfs, '.svn')
1415 if not re.search('Checked out revision [0-9]+.', status):
1415 if not re.search('Checked out revision [0-9]+.', status):
1416 if ('is already a working copy for a different URL' in err
1416 if ('is already a working copy for a different URL' in err
1417 and (self._wcchanged()[:2] == (False, False))):
1417 and (self._wcchanged()[:2] == (False, False))):
1418 # obstructed but clean working copy, so just blow it away.
1418 # obstructed but clean working copy, so just blow it away.
1419 self.remove()
1419 self.remove()
1420 self.get(state, overwrite=False)
1420 self.get(state, overwrite=False)
1421 return
1421 return
1422 raise error.Abort((status or err).splitlines()[-1])
1422 raise error.Abort((status or err).splitlines()[-1])
1423 self.ui.status(status)
1423 self.ui.status(status)
1424
1424
1425 @annotatesubrepoerror
1425 @annotatesubrepoerror
1426 def merge(self, state):
1426 def merge(self, state):
1427 old = self._state[1]
1427 old = self._state[1]
1428 new = state[1]
1428 new = state[1]
1429 wcrev = self._wcrev()
1429 wcrev = self._wcrev()
1430 if new != wcrev:
1430 if new != wcrev:
1431 dirty = old == wcrev or self._wcchanged()[0]
1431 dirty = old == wcrev or self._wcchanged()[0]
1432 if _updateprompt(self.ui, self, dirty, wcrev, new):
1432 if _updateprompt(self.ui, self, dirty, wcrev, new):
1433 self.get(state, False)
1433 self.get(state, False)
1434
1434
1435 def push(self, opts):
1435 def push(self, opts):
1436 # push is a no-op for SVN
1436 # push is a no-op for SVN
1437 return True
1437 return True
1438
1438
1439 @annotatesubrepoerror
1439 @annotatesubrepoerror
1440 def files(self):
1440 def files(self):
1441 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1441 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1442 doc = xml.dom.minidom.parseString(output)
1442 doc = xml.dom.minidom.parseString(output)
1443 paths = []
1443 paths = []
1444 for e in doc.getElementsByTagName('entry'):
1444 for e in doc.getElementsByTagName('entry'):
1445 kind = str(e.getAttribute('kind'))
1445 kind = str(e.getAttribute('kind'))
1446 if kind != 'file':
1446 if kind != 'file':
1447 continue
1447 continue
1448 name = ''.join(c.data for c
1448 name = ''.join(c.data for c
1449 in e.getElementsByTagName('name')[0].childNodes
1449 in e.getElementsByTagName('name')[0].childNodes
1450 if c.nodeType == c.TEXT_NODE)
1450 if c.nodeType == c.TEXT_NODE)
1451 paths.append(name.encode('utf-8'))
1451 paths.append(name.encode('utf-8'))
1452 return paths
1452 return paths
1453
1453
1454 def filedata(self, name, decode):
1454 def filedata(self, name, decode):
1455 return self._svncommand(['cat'], name)[0]
1455 return self._svncommand(['cat'], name)[0]
1456
1456
1457
1457
1458 class gitsubrepo(abstractsubrepo):
1458 class gitsubrepo(abstractsubrepo):
1459 def __init__(self, ctx, path, state, allowcreate):
1459 def __init__(self, ctx, path, state, allowcreate):
1460 super(gitsubrepo, self).__init__(ctx, path)
1460 super(gitsubrepo, self).__init__(ctx, path)
1461 self._state = state
1461 self._state = state
1462 self._abspath = ctx.repo().wjoin(path)
1462 self._abspath = ctx.repo().wjoin(path)
1463 self._subparent = ctx.repo()
1463 self._subparent = ctx.repo()
1464 self._ensuregit()
1464 self._ensuregit()
1465
1465
1466 def _ensuregit(self):
1466 def _ensuregit(self):
1467 try:
1467 try:
1468 self._gitexecutable = 'git'
1468 self._gitexecutable = 'git'
1469 out, err = self._gitnodir(['--version'])
1469 out, err = self._gitnodir(['--version'])
1470 except OSError as e:
1470 except OSError as e:
1471 genericerror = _("error executing git for subrepo '%s': %s")
1471 genericerror = _("error executing git for subrepo '%s': %s")
1472 notfoundhint = _("check git is installed and in your PATH")
1472 notfoundhint = _("check git is installed and in your PATH")
1473 if e.errno != errno.ENOENT:
1473 if e.errno != errno.ENOENT:
1474 raise error.Abort(genericerror % (
1474 raise error.Abort(genericerror % (
1475 self._path, encoding.strtolocal(e.strerror)))
1475 self._path, encoding.strtolocal(e.strerror)))
1476 elif pycompat.iswindows:
1476 elif pycompat.iswindows:
1477 try:
1477 try:
1478 self._gitexecutable = 'git.cmd'
1478 self._gitexecutable = 'git.cmd'
1479 out, err = self._gitnodir(['--version'])
1479 out, err = self._gitnodir(['--version'])
1480 except OSError as e2:
1480 except OSError as e2:
1481 if e2.errno == errno.ENOENT:
1481 if e2.errno == errno.ENOENT:
1482 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1482 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1483 " for subrepo '%s'") % self._path,
1483 " for subrepo '%s'") % self._path,
1484 hint=notfoundhint)
1484 hint=notfoundhint)
1485 else:
1485 else:
1486 raise error.Abort(genericerror % (self._path,
1486 raise error.Abort(genericerror % (self._path,
1487 encoding.strtolocal(e2.strerror)))
1487 encoding.strtolocal(e2.strerror)))
1488 else:
1488 else:
1489 raise error.Abort(_("couldn't find git for subrepo '%s'")
1489 raise error.Abort(_("couldn't find git for subrepo '%s'")
1490 % self._path, hint=notfoundhint)
1490 % self._path, hint=notfoundhint)
1491 versionstatus = self._checkversion(out)
1491 versionstatus = self._checkversion(out)
1492 if versionstatus == 'unknown':
1492 if versionstatus == 'unknown':
1493 self.ui.warn(_('cannot retrieve git version\n'))
1493 self.ui.warn(_('cannot retrieve git version\n'))
1494 elif versionstatus == 'abort':
1494 elif versionstatus == 'abort':
1495 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1495 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1496 elif versionstatus == 'warning':
1496 elif versionstatus == 'warning':
1497 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1497 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1498
1498
1499 @staticmethod
1499 @staticmethod
1500 def _gitversion(out):
1500 def _gitversion(out):
1501 m = re.search(br'^git version (\d+)\.(\d+)\.(\d+)', out)
1501 m = re.search(br'^git version (\d+)\.(\d+)\.(\d+)', out)
1502 if m:
1502 if m:
1503 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1503 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1504
1504
1505 m = re.search(br'^git version (\d+)\.(\d+)', out)
1505 m = re.search(br'^git version (\d+)\.(\d+)', out)
1506 if m:
1506 if m:
1507 return (int(m.group(1)), int(m.group(2)), 0)
1507 return (int(m.group(1)), int(m.group(2)), 0)
1508
1508
1509 return -1
1509 return -1
1510
1510
1511 @staticmethod
1511 @staticmethod
1512 def _checkversion(out):
1512 def _checkversion(out):
1513 '''ensure git version is new enough
1513 '''ensure git version is new enough
1514
1514
1515 >>> _checkversion = gitsubrepo._checkversion
1515 >>> _checkversion = gitsubrepo._checkversion
1516 >>> _checkversion(b'git version 1.6.0')
1516 >>> _checkversion(b'git version 1.6.0')
1517 'ok'
1517 'ok'
1518 >>> _checkversion(b'git version 1.8.5')
1518 >>> _checkversion(b'git version 1.8.5')
1519 'ok'
1519 'ok'
1520 >>> _checkversion(b'git version 1.4.0')
1520 >>> _checkversion(b'git version 1.4.0')
1521 'abort'
1521 'abort'
1522 >>> _checkversion(b'git version 1.5.0')
1522 >>> _checkversion(b'git version 1.5.0')
1523 'warning'
1523 'warning'
1524 >>> _checkversion(b'git version 1.9-rc0')
1524 >>> _checkversion(b'git version 1.9-rc0')
1525 'ok'
1525 'ok'
1526 >>> _checkversion(b'git version 1.9.0.265.g81cdec2')
1526 >>> _checkversion(b'git version 1.9.0.265.g81cdec2')
1527 'ok'
1527 'ok'
1528 >>> _checkversion(b'git version 1.9.0.GIT')
1528 >>> _checkversion(b'git version 1.9.0.GIT')
1529 'ok'
1529 'ok'
1530 >>> _checkversion(b'git version 12345')
1530 >>> _checkversion(b'git version 12345')
1531 'unknown'
1531 'unknown'
1532 >>> _checkversion(b'no')
1532 >>> _checkversion(b'no')
1533 'unknown'
1533 'unknown'
1534 '''
1534 '''
1535 version = gitsubrepo._gitversion(out)
1535 version = gitsubrepo._gitversion(out)
1536 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1536 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1537 # despite the docstring comment. For now, error on 1.4.0, warn on
1537 # despite the docstring comment. For now, error on 1.4.0, warn on
1538 # 1.5.0 but attempt to continue.
1538 # 1.5.0 but attempt to continue.
1539 if version == -1:
1539 if version == -1:
1540 return 'unknown'
1540 return 'unknown'
1541 if version < (1, 5, 0):
1541 if version < (1, 5, 0):
1542 return 'abort'
1542 return 'abort'
1543 elif version < (1, 6, 0):
1543 elif version < (1, 6, 0):
1544 return 'warning'
1544 return 'warning'
1545 return 'ok'
1545 return 'ok'
1546
1546
1547 def _gitcommand(self, commands, env=None, stream=False):
1547 def _gitcommand(self, commands, env=None, stream=False):
1548 return self._gitdir(commands, env=env, stream=stream)[0]
1548 return self._gitdir(commands, env=env, stream=stream)[0]
1549
1549
1550 def _gitdir(self, commands, env=None, stream=False):
1550 def _gitdir(self, commands, env=None, stream=False):
1551 return self._gitnodir(commands, env=env, stream=stream,
1551 return self._gitnodir(commands, env=env, stream=stream,
1552 cwd=self._abspath)
1552 cwd=self._abspath)
1553
1553
1554 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1554 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1555 """Calls the git command
1555 """Calls the git command
1556
1556
1557 The methods tries to call the git command. versions prior to 1.6.0
1557 The methods tries to call the git command. versions prior to 1.6.0
1558 are not supported and very probably fail.
1558 are not supported and very probably fail.
1559 """
1559 """
1560 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1560 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1561 if env is None:
1561 if env is None:
1562 env = encoding.environ.copy()
1562 env = encoding.environ.copy()
1563 # disable localization for Git output (issue5176)
1563 # disable localization for Git output (issue5176)
1564 env['LC_ALL'] = 'C'
1564 env['LC_ALL'] = 'C'
1565 # fix for Git CVE-2015-7545
1565 # fix for Git CVE-2015-7545
1566 if 'GIT_ALLOW_PROTOCOL' not in env:
1566 if 'GIT_ALLOW_PROTOCOL' not in env:
1567 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1567 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1568 # unless ui.quiet is set, print git's stderr,
1568 # unless ui.quiet is set, print git's stderr,
1569 # which is mostly progress and useful info
1569 # which is mostly progress and useful info
1570 errpipe = None
1570 errpipe = None
1571 if self.ui.quiet:
1571 if self.ui.quiet:
1572 errpipe = open(os.devnull, 'w')
1572 errpipe = open(os.devnull, 'w')
1573 if self.ui._colormode and len(commands) and commands[0] == "diff":
1573 if self.ui._colormode and len(commands) and commands[0] == "diff":
1574 # insert the argument in the front,
1574 # insert the argument in the front,
1575 # the end of git diff arguments is used for paths
1575 # the end of git diff arguments is used for paths
1576 commands.insert(1, '--color')
1576 commands.insert(1, '--color')
1577 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1577 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1578 cwd=cwd, env=env, close_fds=util.closefds,
1578 cwd=cwd, env=env, close_fds=util.closefds,
1579 stdout=subprocess.PIPE, stderr=errpipe)
1579 stdout=subprocess.PIPE, stderr=errpipe)
1580 if stream:
1580 if stream:
1581 return p.stdout, None
1581 return p.stdout, None
1582
1582
1583 retdata = p.stdout.read().strip()
1583 retdata = p.stdout.read().strip()
1584 # wait for the child to exit to avoid race condition.
1584 # wait for the child to exit to avoid race condition.
1585 p.wait()
1585 p.wait()
1586
1586
1587 if p.returncode != 0 and p.returncode != 1:
1587 if p.returncode != 0 and p.returncode != 1:
1588 # there are certain error codes that are ok
1588 # there are certain error codes that are ok
1589 command = commands[0]
1589 command = commands[0]
1590 if command in ('cat-file', 'symbolic-ref'):
1590 if command in ('cat-file', 'symbolic-ref'):
1591 return retdata, p.returncode
1591 return retdata, p.returncode
1592 # for all others, abort
1592 # for all others, abort
1593 raise error.Abort(_('git %s error %d in %s') %
1593 raise error.Abort(_('git %s error %d in %s') %
1594 (command, p.returncode, self._relpath))
1594 (command, p.returncode, self._relpath))
1595
1595
1596 return retdata, p.returncode
1596 return retdata, p.returncode
1597
1597
1598 def _gitmissing(self):
1598 def _gitmissing(self):
1599 return not self.wvfs.exists('.git')
1599 return not self.wvfs.exists('.git')
1600
1600
1601 def _gitstate(self):
1601 def _gitstate(self):
1602 return self._gitcommand(['rev-parse', 'HEAD'])
1602 return self._gitcommand(['rev-parse', 'HEAD'])
1603
1603
1604 def _gitcurrentbranch(self):
1604 def _gitcurrentbranch(self):
1605 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1605 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1606 if err:
1606 if err:
1607 current = None
1607 current = None
1608 return current
1608 return current
1609
1609
1610 def _gitremote(self, remote):
1610 def _gitremote(self, remote):
1611 out = self._gitcommand(['remote', 'show', '-n', remote])
1611 out = self._gitcommand(['remote', 'show', '-n', remote])
1612 line = out.split('\n')[1]
1612 line = out.split('\n')[1]
1613 i = line.index('URL: ') + len('URL: ')
1613 i = line.index('URL: ') + len('URL: ')
1614 return line[i:]
1614 return line[i:]
1615
1615
1616 def _githavelocally(self, revision):
1616 def _githavelocally(self, revision):
1617 out, code = self._gitdir(['cat-file', '-e', revision])
1617 out, code = self._gitdir(['cat-file', '-e', revision])
1618 return code == 0
1618 return code == 0
1619
1619
1620 def _gitisancestor(self, r1, r2):
1620 def _gitisancestor(self, r1, r2):
1621 base = self._gitcommand(['merge-base', r1, r2])
1621 base = self._gitcommand(['merge-base', r1, r2])
1622 return base == r1
1622 return base == r1
1623
1623
1624 def _gitisbare(self):
1624 def _gitisbare(self):
1625 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1625 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1626
1626
1627 def _gitupdatestat(self):
1627 def _gitupdatestat(self):
1628 """This must be run before git diff-index.
1628 """This must be run before git diff-index.
1629 diff-index only looks at changes to file stat;
1629 diff-index only looks at changes to file stat;
1630 this command looks at file contents and updates the stat."""
1630 this command looks at file contents and updates the stat."""
1631 self._gitcommand(['update-index', '-q', '--refresh'])
1631 self._gitcommand(['update-index', '-q', '--refresh'])
1632
1632
1633 def _gitbranchmap(self):
1633 def _gitbranchmap(self):
1634 '''returns 2 things:
1634 '''returns 2 things:
1635 a map from git branch to revision
1635 a map from git branch to revision
1636 a map from revision to branches'''
1636 a map from revision to branches'''
1637 branch2rev = {}
1637 branch2rev = {}
1638 rev2branch = {}
1638 rev2branch = {}
1639
1639
1640 out = self._gitcommand(['for-each-ref', '--format',
1640 out = self._gitcommand(['for-each-ref', '--format',
1641 '%(objectname) %(refname)'])
1641 '%(objectname) %(refname)'])
1642 for line in out.split('\n'):
1642 for line in out.split('\n'):
1643 revision, ref = line.split(' ')
1643 revision, ref = line.split(' ')
1644 if (not ref.startswith('refs/heads/') and
1644 if (not ref.startswith('refs/heads/') and
1645 not ref.startswith('refs/remotes/')):
1645 not ref.startswith('refs/remotes/')):
1646 continue
1646 continue
1647 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1647 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1648 continue # ignore remote/HEAD redirects
1648 continue # ignore remote/HEAD redirects
1649 branch2rev[ref] = revision
1649 branch2rev[ref] = revision
1650 rev2branch.setdefault(revision, []).append(ref)
1650 rev2branch.setdefault(revision, []).append(ref)
1651 return branch2rev, rev2branch
1651 return branch2rev, rev2branch
1652
1652
1653 def _gittracking(self, branches):
1653 def _gittracking(self, branches):
1654 'return map of remote branch to local tracking branch'
1654 'return map of remote branch to local tracking branch'
1655 # assumes no more than one local tracking branch for each remote
1655 # assumes no more than one local tracking branch for each remote
1656 tracking = {}
1656 tracking = {}
1657 for b in branches:
1657 for b in branches:
1658 if b.startswith('refs/remotes/'):
1658 if b.startswith('refs/remotes/'):
1659 continue
1659 continue
1660 bname = b.split('/', 2)[2]
1660 bname = b.split('/', 2)[2]
1661 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1661 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1662 if remote:
1662 if remote:
1663 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1663 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1664 tracking['refs/remotes/%s/%s' %
1664 tracking['refs/remotes/%s/%s' %
1665 (remote, ref.split('/', 2)[2])] = b
1665 (remote, ref.split('/', 2)[2])] = b
1666 return tracking
1666 return tracking
1667
1667
1668 def _abssource(self, source):
1668 def _abssource(self, source):
1669 if '://' not in source:
1669 if '://' not in source:
1670 # recognize the scp syntax as an absolute source
1670 # recognize the scp syntax as an absolute source
1671 colon = source.find(':')
1671 colon = source.find(':')
1672 if colon != -1 and '/' not in source[:colon]:
1672 if colon != -1 and '/' not in source[:colon]:
1673 return source
1673 return source
1674 self._subsource = source
1674 self._subsource = source
1675 return _abssource(self)
1675 return _abssource(self)
1676
1676
1677 def _fetch(self, source, revision):
1677 def _fetch(self, source, revision):
1678 if self._gitmissing():
1678 if self._gitmissing():
1679 # SEC: check for safe ssh url
1679 # SEC: check for safe ssh url
1680 util.checksafessh(source)
1680 util.checksafessh(source)
1681
1681
1682 source = self._abssource(source)
1682 source = self._abssource(source)
1683 self.ui.status(_('cloning subrepo %s from %s\n') %
1683 self.ui.status(_('cloning subrepo %s from %s\n') %
1684 (self._relpath, source))
1684 (self._relpath, source))
1685 self._gitnodir(['clone', source, self._abspath])
1685 self._gitnodir(['clone', source, self._abspath])
1686 if self._githavelocally(revision):
1686 if self._githavelocally(revision):
1687 return
1687 return
1688 self.ui.status(_('pulling subrepo %s from %s\n') %
1688 self.ui.status(_('pulling subrepo %s from %s\n') %
1689 (self._relpath, self._gitremote('origin')))
1689 (self._relpath, self._gitremote('origin')))
1690 # try only origin: the originally cloned repo
1690 # try only origin: the originally cloned repo
1691 self._gitcommand(['fetch'])
1691 self._gitcommand(['fetch'])
1692 if not self._githavelocally(revision):
1692 if not self._githavelocally(revision):
1693 raise error.Abort(_('revision %s does not exist in subrepository '
1693 raise error.Abort(_('revision %s does not exist in subrepository '
1694 '"%s"\n') % (revision, self._relpath))
1694 '"%s"\n') % (revision, self._relpath))
1695
1695
1696 @annotatesubrepoerror
1696 @annotatesubrepoerror
1697 def dirty(self, ignoreupdate=False, missing=False):
1697 def dirty(self, ignoreupdate=False, missing=False):
1698 if self._gitmissing():
1698 if self._gitmissing():
1699 return self._state[1] != ''
1699 return self._state[1] != ''
1700 if self._gitisbare():
1700 if self._gitisbare():
1701 return True
1701 return True
1702 if not ignoreupdate and self._state[1] != self._gitstate():
1702 if not ignoreupdate and self._state[1] != self._gitstate():
1703 # different version checked out
1703 # different version checked out
1704 return True
1704 return True
1705 # check for staged changes or modified files; ignore untracked files
1705 # check for staged changes or modified files; ignore untracked files
1706 self._gitupdatestat()
1706 self._gitupdatestat()
1707 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1707 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1708 return code == 1
1708 return code == 1
1709
1709
1710 def basestate(self):
1710 def basestate(self):
1711 return self._gitstate()
1711 return self._gitstate()
1712
1712
1713 @annotatesubrepoerror
1713 @annotatesubrepoerror
1714 def get(self, state, overwrite=False):
1714 def get(self, state, overwrite=False):
1715 source, revision, kind = state
1715 source, revision, kind = state
1716 if not revision:
1716 if not revision:
1717 self.remove()
1717 self.remove()
1718 return
1718 return
1719 self._fetch(source, revision)
1719 self._fetch(source, revision)
1720 # if the repo was set to be bare, unbare it
1720 # if the repo was set to be bare, unbare it
1721 if self._gitisbare():
1721 if self._gitisbare():
1722 self._gitcommand(['config', 'core.bare', 'false'])
1722 self._gitcommand(['config', 'core.bare', 'false'])
1723 if self._gitstate() == revision:
1723 if self._gitstate() == revision:
1724 self._gitcommand(['reset', '--hard', 'HEAD'])
1724 self._gitcommand(['reset', '--hard', 'HEAD'])
1725 return
1725 return
1726 elif self._gitstate() == revision:
1726 elif self._gitstate() == revision:
1727 if overwrite:
1727 if overwrite:
1728 # first reset the index to unmark new files for commit, because
1728 # first reset the index to unmark new files for commit, because
1729 # reset --hard will otherwise throw away files added for commit,
1729 # reset --hard will otherwise throw away files added for commit,
1730 # not just unmark them.
1730 # not just unmark them.
1731 self._gitcommand(['reset', 'HEAD'])
1731 self._gitcommand(['reset', 'HEAD'])
1732 self._gitcommand(['reset', '--hard', 'HEAD'])
1732 self._gitcommand(['reset', '--hard', 'HEAD'])
1733 return
1733 return
1734 branch2rev, rev2branch = self._gitbranchmap()
1734 branch2rev, rev2branch = self._gitbranchmap()
1735
1735
1736 def checkout(args):
1736 def checkout(args):
1737 cmd = ['checkout']
1737 cmd = ['checkout']
1738 if overwrite:
1738 if overwrite:
1739 # first reset the index to unmark new files for commit, because
1739 # first reset the index to unmark new files for commit, because
1740 # the -f option will otherwise throw away files added for
1740 # the -f option will otherwise throw away files added for
1741 # commit, not just unmark them.
1741 # commit, not just unmark them.
1742 self._gitcommand(['reset', 'HEAD'])
1742 self._gitcommand(['reset', 'HEAD'])
1743 cmd.append('-f')
1743 cmd.append('-f')
1744 self._gitcommand(cmd + args)
1744 self._gitcommand(cmd + args)
1745 _sanitize(self.ui, self.wvfs, '.git')
1745 _sanitize(self.ui, self.wvfs, '.git')
1746
1746
1747 def rawcheckout():
1747 def rawcheckout():
1748 # no branch to checkout, check it out with no branch
1748 # no branch to checkout, check it out with no branch
1749 self.ui.warn(_('checking out detached HEAD in '
1749 self.ui.warn(_('checking out detached HEAD in '
1750 'subrepository "%s"\n') % self._relpath)
1750 'subrepository "%s"\n') % self._relpath)
1751 self.ui.warn(_('check out a git branch if you intend '
1751 self.ui.warn(_('check out a git branch if you intend '
1752 'to make changes\n'))
1752 'to make changes\n'))
1753 checkout(['-q', revision])
1753 checkout(['-q', revision])
1754
1754
1755 if revision not in rev2branch:
1755 if revision not in rev2branch:
1756 rawcheckout()
1756 rawcheckout()
1757 return
1757 return
1758 branches = rev2branch[revision]
1758 branches = rev2branch[revision]
1759 firstlocalbranch = None
1759 firstlocalbranch = None
1760 for b in branches:
1760 for b in branches:
1761 if b == 'refs/heads/master':
1761 if b == 'refs/heads/master':
1762 # master trumps all other branches
1762 # master trumps all other branches
1763 checkout(['refs/heads/master'])
1763 checkout(['refs/heads/master'])
1764 return
1764 return
1765 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1765 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1766 firstlocalbranch = b
1766 firstlocalbranch = b
1767 if firstlocalbranch:
1767 if firstlocalbranch:
1768 checkout([firstlocalbranch])
1768 checkout([firstlocalbranch])
1769 return
1769 return
1770
1770
1771 tracking = self._gittracking(branch2rev.keys())
1771 tracking = self._gittracking(branch2rev.keys())
1772 # choose a remote branch already tracked if possible
1772 # choose a remote branch already tracked if possible
1773 remote = branches[0]
1773 remote = branches[0]
1774 if remote not in tracking:
1774 if remote not in tracking:
1775 for b in branches:
1775 for b in branches:
1776 if b in tracking:
1776 if b in tracking:
1777 remote = b
1777 remote = b
1778 break
1778 break
1779
1779
1780 if remote not in tracking:
1780 if remote not in tracking:
1781 # create a new local tracking branch
1781 # create a new local tracking branch
1782 local = remote.split('/', 3)[3]
1782 local = remote.split('/', 3)[3]
1783 checkout(['-b', local, remote])
1783 checkout(['-b', local, remote])
1784 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1784 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1785 # When updating to a tracked remote branch,
1785 # When updating to a tracked remote branch,
1786 # if the local tracking branch is downstream of it,
1786 # if the local tracking branch is downstream of it,
1787 # a normal `git pull` would have performed a "fast-forward merge"
1787 # a normal `git pull` would have performed a "fast-forward merge"
1788 # which is equivalent to updating the local branch to the remote.
1788 # which is equivalent to updating the local branch to the remote.
1789 # Since we are only looking at branching at update, we need to
1789 # Since we are only looking at branching at update, we need to
1790 # detect this situation and perform this action lazily.
1790 # detect this situation and perform this action lazily.
1791 if tracking[remote] != self._gitcurrentbranch():
1791 if tracking[remote] != self._gitcurrentbranch():
1792 checkout([tracking[remote]])
1792 checkout([tracking[remote]])
1793 self._gitcommand(['merge', '--ff', remote])
1793 self._gitcommand(['merge', '--ff', remote])
1794 _sanitize(self.ui, self.wvfs, '.git')
1794 _sanitize(self.ui, self.wvfs, '.git')
1795 else:
1795 else:
1796 # a real merge would be required, just checkout the revision
1796 # a real merge would be required, just checkout the revision
1797 rawcheckout()
1797 rawcheckout()
1798
1798
1799 @annotatesubrepoerror
1799 @annotatesubrepoerror
1800 def commit(self, text, user, date):
1800 def commit(self, text, user, date):
1801 if self._gitmissing():
1801 if self._gitmissing():
1802 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1802 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1803 cmd = ['commit', '-a', '-m', text]
1803 cmd = ['commit', '-a', '-m', text]
1804 env = encoding.environ.copy()
1804 env = encoding.environ.copy()
1805 if user:
1805 if user:
1806 cmd += ['--author', user]
1806 cmd += ['--author', user]
1807 if date:
1807 if date:
1808 # git's date parser silently ignores when seconds < 1e9
1808 # git's date parser silently ignores when seconds < 1e9
1809 # convert to ISO8601
1809 # convert to ISO8601
1810 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1810 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1811 '%Y-%m-%dT%H:%M:%S %1%2')
1811 '%Y-%m-%dT%H:%M:%S %1%2')
1812 self._gitcommand(cmd, env=env)
1812 self._gitcommand(cmd, env=env)
1813 # make sure commit works otherwise HEAD might not exist under certain
1813 # make sure commit works otherwise HEAD might not exist under certain
1814 # circumstances
1814 # circumstances
1815 return self._gitstate()
1815 return self._gitstate()
1816
1816
1817 @annotatesubrepoerror
1817 @annotatesubrepoerror
1818 def merge(self, state):
1818 def merge(self, state):
1819 source, revision, kind = state
1819 source, revision, kind = state
1820 self._fetch(source, revision)
1820 self._fetch(source, revision)
1821 base = self._gitcommand(['merge-base', revision, self._state[1]])
1821 base = self._gitcommand(['merge-base', revision, self._state[1]])
1822 self._gitupdatestat()
1822 self._gitupdatestat()
1823 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1823 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1824
1824
1825 def mergefunc():
1825 def mergefunc():
1826 if base == revision:
1826 if base == revision:
1827 self.get(state) # fast forward merge
1827 self.get(state) # fast forward merge
1828 elif base != self._state[1]:
1828 elif base != self._state[1]:
1829 self._gitcommand(['merge', '--no-commit', revision])
1829 self._gitcommand(['merge', '--no-commit', revision])
1830 _sanitize(self.ui, self.wvfs, '.git')
1830 _sanitize(self.ui, self.wvfs, '.git')
1831
1831
1832 if self.dirty():
1832 if self.dirty():
1833 if self._gitstate() != revision:
1833 if self._gitstate() != revision:
1834 dirty = self._gitstate() == self._state[1] or code != 0
1834 dirty = self._gitstate() == self._state[1] or code != 0
1835 if _updateprompt(self.ui, self, dirty,
1835 if _updateprompt(self.ui, self, dirty,
1836 self._state[1][:7], revision[:7]):
1836 self._state[1][:7], revision[:7]):
1837 mergefunc()
1837 mergefunc()
1838 else:
1838 else:
1839 mergefunc()
1839 mergefunc()
1840
1840
1841 @annotatesubrepoerror
1841 @annotatesubrepoerror
1842 def push(self, opts):
1842 def push(self, opts):
1843 force = opts.get('force')
1843 force = opts.get('force')
1844
1844
1845 if not self._state[1]:
1845 if not self._state[1]:
1846 return True
1846 return True
1847 if self._gitmissing():
1847 if self._gitmissing():
1848 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1848 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1849 # if a branch in origin contains the revision, nothing to do
1849 # if a branch in origin contains the revision, nothing to do
1850 branch2rev, rev2branch = self._gitbranchmap()
1850 branch2rev, rev2branch = self._gitbranchmap()
1851 if self._state[1] in rev2branch:
1851 if self._state[1] in rev2branch:
1852 for b in rev2branch[self._state[1]]:
1852 for b in rev2branch[self._state[1]]:
1853 if b.startswith('refs/remotes/origin/'):
1853 if b.startswith('refs/remotes/origin/'):
1854 return True
1854 return True
1855 for b, revision in branch2rev.iteritems():
1855 for b, revision in branch2rev.iteritems():
1856 if b.startswith('refs/remotes/origin/'):
1856 if b.startswith('refs/remotes/origin/'):
1857 if self._gitisancestor(self._state[1], revision):
1857 if self._gitisancestor(self._state[1], revision):
1858 return True
1858 return True
1859 # otherwise, try to push the currently checked out branch
1859 # otherwise, try to push the currently checked out branch
1860 cmd = ['push']
1860 cmd = ['push']
1861 if force:
1861 if force:
1862 cmd.append('--force')
1862 cmd.append('--force')
1863
1863
1864 current = self._gitcurrentbranch()
1864 current = self._gitcurrentbranch()
1865 if current:
1865 if current:
1866 # determine if the current branch is even useful
1866 # determine if the current branch is even useful
1867 if not self._gitisancestor(self._state[1], current):
1867 if not self._gitisancestor(self._state[1], current):
1868 self.ui.warn(_('unrelated git branch checked out '
1868 self.ui.warn(_('unrelated git branch checked out '
1869 'in subrepository "%s"\n') % self._relpath)
1869 'in subrepository "%s"\n') % self._relpath)
1870 return False
1870 return False
1871 self.ui.status(_('pushing branch %s of subrepository "%s"\n') %
1871 self.ui.status(_('pushing branch %s of subrepository "%s"\n') %
1872 (current.split('/', 2)[2], self._relpath))
1872 (current.split('/', 2)[2], self._relpath))
1873 ret = self._gitdir(cmd + ['origin', current])
1873 ret = self._gitdir(cmd + ['origin', current])
1874 return ret[1] == 0
1874 return ret[1] == 0
1875 else:
1875 else:
1876 self.ui.warn(_('no branch checked out in subrepository "%s"\n'
1876 self.ui.warn(_('no branch checked out in subrepository "%s"\n'
1877 'cannot push revision %s\n') %
1877 'cannot push revision %s\n') %
1878 (self._relpath, self._state[1]))
1878 (self._relpath, self._state[1]))
1879 return False
1879 return False
1880
1880
1881 @annotatesubrepoerror
1881 @annotatesubrepoerror
1882 def add(self, ui, match, prefix, explicitonly, **opts):
1882 def add(self, ui, match, prefix, explicitonly, **opts):
1883 if self._gitmissing():
1883 if self._gitmissing():
1884 return []
1884 return []
1885
1885
1886 (modified, added, removed,
1886 (modified, added, removed,
1887 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1887 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1888 clean=True)
1888 clean=True)
1889
1889
1890 tracked = set()
1890 tracked = set()
1891 # dirstates 'amn' warn, 'r' is added again
1891 # dirstates 'amn' warn, 'r' is added again
1892 for l in (modified, added, deleted, clean):
1892 for l in (modified, added, deleted, clean):
1893 tracked.update(l)
1893 tracked.update(l)
1894
1894
1895 # Unknown files not of interest will be rejected by the matcher
1895 # Unknown files not of interest will be rejected by the matcher
1896 files = unknown
1896 files = unknown
1897 files.extend(match.files())
1897 files.extend(match.files())
1898
1898
1899 rejected = []
1899 rejected = []
1900
1900
1901 files = [f for f in sorted(set(files)) if match(f)]
1901 files = [f for f in sorted(set(files)) if match(f)]
1902 for f in files:
1902 for f in files:
1903 exact = match.exact(f)
1903 exact = match.exact(f)
1904 command = ["add"]
1904 command = ["add"]
1905 if exact:
1905 if exact:
1906 command.append("-f") #should be added, even if ignored
1906 command.append("-f") #should be added, even if ignored
1907 if ui.verbose or not exact:
1907 if ui.verbose or not exact:
1908 ui.status(_('adding %s\n') % match.rel(f))
1908 ui.status(_('adding %s\n') % match.rel(f))
1909
1909
1910 if f in tracked: # hg prints 'adding' even if already tracked
1910 if f in tracked: # hg prints 'adding' even if already tracked
1911 if exact:
1911 if exact:
1912 rejected.append(f)
1912 rejected.append(f)
1913 continue
1913 continue
1914 if not opts.get(r'dry_run'):
1914 if not opts.get(r'dry_run'):
1915 self._gitcommand(command + [f])
1915 self._gitcommand(command + [f])
1916
1916
1917 for f in rejected:
1917 for f in rejected:
1918 ui.warn(_("%s already tracked!\n") % match.abs(f))
1918 ui.warn(_("%s already tracked!\n") % match.abs(f))
1919
1919
1920 return rejected
1920 return rejected
1921
1921
1922 @annotatesubrepoerror
1922 @annotatesubrepoerror
1923 def remove(self):
1923 def remove(self):
1924 if self._gitmissing():
1924 if self._gitmissing():
1925 return
1925 return
1926 if self.dirty():
1926 if self.dirty():
1927 self.ui.warn(_('not removing repo %s because '
1927 self.ui.warn(_('not removing repo %s because '
1928 'it has changes.\n') % self._relpath)
1928 'it has changes.\n') % self._relpath)
1929 return
1929 return
1930 # we can't fully delete the repository as it may contain
1930 # we can't fully delete the repository as it may contain
1931 # local-only history
1931 # local-only history
1932 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1932 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1933 self._gitcommand(['config', 'core.bare', 'true'])
1933 self._gitcommand(['config', 'core.bare', 'true'])
1934 for f, kind in self.wvfs.readdir():
1934 for f, kind in self.wvfs.readdir():
1935 if f == '.git':
1935 if f == '.git':
1936 continue
1936 continue
1937 if kind == stat.S_IFDIR:
1937 if kind == stat.S_IFDIR:
1938 self.wvfs.rmtree(f)
1938 self.wvfs.rmtree(f)
1939 else:
1939 else:
1940 self.wvfs.unlink(f)
1940 self.wvfs.unlink(f)
1941
1941
1942 def archive(self, archiver, prefix, match=None, decode=True):
1942 def archive(self, archiver, prefix, match=None, decode=True):
1943 total = 0
1943 total = 0
1944 source, revision = self._state
1944 source, revision = self._state
1945 if not revision:
1945 if not revision:
1946 return total
1946 return total
1947 self._fetch(source, revision)
1947 self._fetch(source, revision)
1948
1948
1949 # Parse git's native archive command.
1949 # Parse git's native archive command.
1950 # This should be much faster than manually traversing the trees
1950 # This should be much faster than manually traversing the trees
1951 # and objects with many subprocess calls.
1951 # and objects with many subprocess calls.
1952 tarstream = self._gitcommand(['archive', revision], stream=True)
1952 tarstream = self._gitcommand(['archive', revision], stream=True)
1953 tar = tarfile.open(fileobj=tarstream, mode='r|')
1953 tar = tarfile.open(fileobj=tarstream, mode='r|')
1954 relpath = subrelpath(self)
1954 relpath = subrelpath(self)
1955 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1955 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1956 for i, info in enumerate(tar):
1956 for i, info in enumerate(tar):
1957 if info.isdir():
1957 if info.isdir():
1958 continue
1958 continue
1959 if match and not match(info.name):
1959 if match and not match(info.name):
1960 continue
1960 continue
1961 if info.issym():
1961 if info.issym():
1962 data = info.linkname
1962 data = info.linkname
1963 else:
1963 else:
1964 data = tar.extractfile(info).read()
1964 data = tar.extractfile(info).read()
1965 archiver.addfile(prefix + self._path + '/' + info.name,
1965 archiver.addfile(prefix + self._path + '/' + info.name,
1966 info.mode, info.issym(), data)
1966 info.mode, info.issym(), data)
1967 total += 1
1967 total += 1
1968 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1968 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1969 unit=_('files'))
1969 unit=_('files'))
1970 self.ui.progress(_('archiving (%s)') % relpath, None)
1970 self.ui.progress(_('archiving (%s)') % relpath, None)
1971 return total
1971 return total
1972
1972
1973
1973
1974 @annotatesubrepoerror
1974 @annotatesubrepoerror
1975 def cat(self, match, fm, fntemplate, prefix, **opts):
1975 def cat(self, match, fm, fntemplate, prefix, **opts):
1976 rev = self._state[1]
1976 rev = self._state[1]
1977 if match.anypats():
1977 if match.anypats():
1978 return 1 #No support for include/exclude yet
1978 return 1 #No support for include/exclude yet
1979
1979
1980 if not match.files():
1980 if not match.files():
1981 return 1
1981 return 1
1982
1982
1983 # TODO: add support for non-plain formatter (see cmdutil.cat())
1983 # TODO: add support for non-plain formatter (see cmdutil.cat())
1984 for f in match.files():
1984 for f in match.files():
1985 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1985 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1986 fp = cmdutil.makefileobj(self._subparent, fntemplate,
1986 fp = cmdutil.makefileobj(self._subparent, fntemplate,
1987 self._ctx.node(),
1987 self._ctx.node(),
1988 pathname=self.wvfs.reljoin(prefix, f))
1988 pathname=self.wvfs.reljoin(prefix, f))
1989 fp.write(output)
1989 fp.write(output)
1990 fp.close()
1990 fp.close()
1991 return 0
1991 return 0
1992
1992
1993
1993
1994 @annotatesubrepoerror
1994 @annotatesubrepoerror
1995 def status(self, rev2, **opts):
1995 def status(self, rev2, **opts):
1996 rev1 = self._state[1]
1996 rev1 = self._state[1]
1997 if self._gitmissing() or not rev1:
1997 if self._gitmissing() or not rev1:
1998 # if the repo is missing, return no results
1998 # if the repo is missing, return no results
1999 return scmutil.status([], [], [], [], [], [], [])
1999 return scmutil.status([], [], [], [], [], [], [])
2000 modified, added, removed = [], [], []
2000 modified, added, removed = [], [], []
2001 self._gitupdatestat()
2001 self._gitupdatestat()
2002 if rev2:
2002 if rev2:
2003 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
2003 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
2004 else:
2004 else:
2005 command = ['diff-index', '--no-renames', rev1]
2005 command = ['diff-index', '--no-renames', rev1]
2006 out = self._gitcommand(command)
2006 out = self._gitcommand(command)
2007 for line in out.split('\n'):
2007 for line in out.split('\n'):
2008 tab = line.find('\t')
2008 tab = line.find('\t')
2009 if tab == -1:
2009 if tab == -1:
2010 continue
2010 continue
2011 status, f = line[tab - 1], line[tab + 1:]
2011 status, f = line[tab - 1], line[tab + 1:]
2012 if status == 'M':
2012 if status == 'M':
2013 modified.append(f)
2013 modified.append(f)
2014 elif status == 'A':
2014 elif status == 'A':
2015 added.append(f)
2015 added.append(f)
2016 elif status == 'D':
2016 elif status == 'D':
2017 removed.append(f)
2017 removed.append(f)
2018
2018
2019 deleted, unknown, ignored, clean = [], [], [], []
2019 deleted, unknown, ignored, clean = [], [], [], []
2020
2020
2021 command = ['status', '--porcelain', '-z']
2021 command = ['status', '--porcelain', '-z']
2022 if opts.get(r'unknown'):
2022 if opts.get(r'unknown'):
2023 command += ['--untracked-files=all']
2023 command += ['--untracked-files=all']
2024 if opts.get(r'ignored'):
2024 if opts.get(r'ignored'):
2025 command += ['--ignored']
2025 command += ['--ignored']
2026 out = self._gitcommand(command)
2026 out = self._gitcommand(command)
2027
2027
2028 changedfiles = set()
2028 changedfiles = set()
2029 changedfiles.update(modified)
2029 changedfiles.update(modified)
2030 changedfiles.update(added)
2030 changedfiles.update(added)
2031 changedfiles.update(removed)
2031 changedfiles.update(removed)
2032 for line in out.split('\0'):
2032 for line in out.split('\0'):
2033 if not line:
2033 if not line:
2034 continue
2034 continue
2035 st = line[0:2]
2035 st = line[0:2]
2036 #moves and copies show 2 files on one line
2036 #moves and copies show 2 files on one line
2037 if line.find('\0') >= 0:
2037 if line.find('\0') >= 0:
2038 filename1, filename2 = line[3:].split('\0')
2038 filename1, filename2 = line[3:].split('\0')
2039 else:
2039 else:
2040 filename1 = line[3:]
2040 filename1 = line[3:]
2041 filename2 = None
2041 filename2 = None
2042
2042
2043 changedfiles.add(filename1)
2043 changedfiles.add(filename1)
2044 if filename2:
2044 if filename2:
2045 changedfiles.add(filename2)
2045 changedfiles.add(filename2)
2046
2046
2047 if st == '??':
2047 if st == '??':
2048 unknown.append(filename1)
2048 unknown.append(filename1)
2049 elif st == '!!':
2049 elif st == '!!':
2050 ignored.append(filename1)
2050 ignored.append(filename1)
2051
2051
2052 if opts.get(r'clean'):
2052 if opts.get(r'clean'):
2053 out = self._gitcommand(['ls-files'])
2053 out = self._gitcommand(['ls-files'])
2054 for f in out.split('\n'):
2054 for f in out.split('\n'):
2055 if not f in changedfiles:
2055 if not f in changedfiles:
2056 clean.append(f)
2056 clean.append(f)
2057
2057
2058 return scmutil.status(modified, added, removed, deleted,
2058 return scmutil.status(modified, added, removed, deleted,
2059 unknown, ignored, clean)
2059 unknown, ignored, clean)
2060
2060
2061 @annotatesubrepoerror
2061 @annotatesubrepoerror
2062 def diff(self, ui, diffopts, node2, match, prefix, **opts):
2062 def diff(self, ui, diffopts, node2, match, prefix, **opts):
2063 node1 = self._state[1]
2063 node1 = self._state[1]
2064 cmd = ['diff', '--no-renames']
2064 cmd = ['diff', '--no-renames']
2065 if opts[r'stat']:
2065 if opts[r'stat']:
2066 cmd.append('--stat')
2066 cmd.append('--stat')
2067 else:
2067 else:
2068 # for Git, this also implies '-p'
2068 # for Git, this also implies '-p'
2069 cmd.append('-U%d' % diffopts.context)
2069 cmd.append('-U%d' % diffopts.context)
2070
2070
2071 gitprefix = self.wvfs.reljoin(prefix, self._path)
2071 gitprefix = self.wvfs.reljoin(prefix, self._path)
2072
2072
2073 if diffopts.noprefix:
2073 if diffopts.noprefix:
2074 cmd.extend(['--src-prefix=%s/' % gitprefix,
2074 cmd.extend(['--src-prefix=%s/' % gitprefix,
2075 '--dst-prefix=%s/' % gitprefix])
2075 '--dst-prefix=%s/' % gitprefix])
2076 else:
2076 else:
2077 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
2077 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
2078 '--dst-prefix=b/%s/' % gitprefix])
2078 '--dst-prefix=b/%s/' % gitprefix])
2079
2079
2080 if diffopts.ignorews:
2080 if diffopts.ignorews:
2081 cmd.append('--ignore-all-space')
2081 cmd.append('--ignore-all-space')
2082 if diffopts.ignorewsamount:
2082 if diffopts.ignorewsamount:
2083 cmd.append('--ignore-space-change')
2083 cmd.append('--ignore-space-change')
2084 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
2084 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
2085 and diffopts.ignoreblanklines:
2085 and diffopts.ignoreblanklines:
2086 cmd.append('--ignore-blank-lines')
2086 cmd.append('--ignore-blank-lines')
2087
2087
2088 cmd.append(node1)
2088 cmd.append(node1)
2089 if node2:
2089 if node2:
2090 cmd.append(node2)
2090 cmd.append(node2)
2091
2091
2092 output = ""
2092 output = ""
2093 if match.always():
2093 if match.always():
2094 output += self._gitcommand(cmd) + '\n'
2094 output += self._gitcommand(cmd) + '\n'
2095 else:
2095 else:
2096 st = self.status(node2)[:3]
2096 st = self.status(node2)[:3]
2097 files = [f for sublist in st for f in sublist]
2097 files = [f for sublist in st for f in sublist]
2098 for f in files:
2098 for f in files:
2099 if match(f):
2099 if match(f):
2100 output += self._gitcommand(cmd + ['--', f]) + '\n'
2100 output += self._gitcommand(cmd + ['--', f]) + '\n'
2101
2101
2102 if output.strip():
2102 if output.strip():
2103 ui.write(output)
2103 ui.write(output)
2104
2104
2105 @annotatesubrepoerror
2105 @annotatesubrepoerror
2106 def revert(self, substate, *pats, **opts):
2106 def revert(self, substate, *pats, **opts):
2107 self.ui.status(_('reverting subrepo %s\n') % substate[0])
2107 self.ui.status(_('reverting subrepo %s\n') % substate[0])
2108 if not opts.get(r'no_backup'):
2108 if not opts.get(r'no_backup'):
2109 status = self.status(None)
2109 status = self.status(None)
2110 names = status.modified
2110 names = status.modified
2111 for name in names:
2111 for name in names:
2112 bakname = scmutil.origpath(self.ui, self._subparent, name)
2112 bakname = scmutil.origpath(self.ui, self._subparent, name)
2113 self.ui.note(_('saving current version of %s as %s\n') %
2113 self.ui.note(_('saving current version of %s as %s\n') %
2114 (name, bakname))
2114 (name, bakname))
2115 self.wvfs.rename(name, bakname)
2115 self.wvfs.rename(name, bakname)
2116
2116
2117 if not opts.get(r'dry_run'):
2117 if not opts.get(r'dry_run'):
2118 self.get(substate, overwrite=True)
2118 self.get(substate, overwrite=True)
2119 return []
2119 return []
2120
2120
2121 def shortid(self, revid):
2121 def shortid(self, revid):
2122 return revid[:7]
2122 return revid[:7]
2123
2123
2124 types = {
2124 types = {
2125 'hg': hgsubrepo,
2125 'hg': hgsubrepo,
2126 'svn': svnsubrepo,
2126 'svn': svnsubrepo,
2127 'git': gitsubrepo,
2127 'git': gitsubrepo,
2128 }
2128 }
General Comments 0
You need to be logged in to leave comments. Login now