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