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