##// END OF EJS Templates
subrepo: better error messages in _ensuregit...
Mason Malone -
r27935:594bdc38 stable
parent child Browse files
Show More
@@ -1,1923 +1,1938 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):
343 def subrepo(ctx, path, allowwdir=False):
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])
359 return types[state[2]](ctx, path, state[:2])
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))
378 return types[state[2]](pctx, path, (state[0], subrev))
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):
578 def removefiles(self, matcher, prefix, after, force, subrepos):
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 return 1
583 return 1
584
584
585 def revert(self, substate, *pats, **opts):
585 def revert(self, substate, *pats, **opts):
586 self.ui.warn('%s: reverting %s subrepos is unsupported\n' \
586 self.ui.warn('%s: reverting %s subrepos is unsupported\n' \
587 % (substate[0], substate[2]))
587 % (substate[0], substate[2]))
588 return []
588 return []
589
589
590 def shortid(self, revid):
590 def shortid(self, revid):
591 return revid
591 return revid
592
592
593 def verify(self):
593 def verify(self):
594 '''verify the integrity of the repository. Return 0 on success or
594 '''verify the integrity of the repository. Return 0 on success or
595 warning, 1 on any error.
595 warning, 1 on any error.
596 '''
596 '''
597 return 0
597 return 0
598
598
599 @propertycache
599 @propertycache
600 def wvfs(self):
600 def wvfs(self):
601 """return vfs to access the working directory of this subrepository
601 """return vfs to access the working directory of this subrepository
602 """
602 """
603 return scmutil.vfs(self._ctx.repo().wvfs.join(self._path))
603 return scmutil.vfs(self._ctx.repo().wvfs.join(self._path))
604
604
605 @propertycache
605 @propertycache
606 def _relpath(self):
606 def _relpath(self):
607 """return path to this subrepository as seen from outermost repository
607 """return path to this subrepository as seen from outermost repository
608 """
608 """
609 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
609 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
610
610
611 class hgsubrepo(abstractsubrepo):
611 class hgsubrepo(abstractsubrepo):
612 def __init__(self, ctx, path, state):
612 def __init__(self, ctx, path, state):
613 super(hgsubrepo, self).__init__(ctx, path)
613 super(hgsubrepo, self).__init__(ctx, path)
614 self._state = state
614 self._state = state
615 r = ctx.repo()
615 r = ctx.repo()
616 root = r.wjoin(path)
616 root = r.wjoin(path)
617 create = not r.wvfs.exists('%s/.hg' % path)
617 create = not r.wvfs.exists('%s/.hg' % path)
618 self._repo = hg.repository(r.baseui, root, create=create)
618 self._repo = hg.repository(r.baseui, root, create=create)
619
619
620 # Propagate the parent's --hidden option
620 # Propagate the parent's --hidden option
621 if r is r.unfiltered():
621 if r is r.unfiltered():
622 self._repo = self._repo.unfiltered()
622 self._repo = self._repo.unfiltered()
623
623
624 self.ui = self._repo.ui
624 self.ui = self._repo.ui
625 for s, k in [('ui', 'commitsubrepos')]:
625 for s, k in [('ui', 'commitsubrepos')]:
626 v = r.ui.config(s, k)
626 v = r.ui.config(s, k)
627 if v:
627 if v:
628 self.ui.setconfig(s, k, v, 'subrepo')
628 self.ui.setconfig(s, k, v, 'subrepo')
629 # internal config: ui._usedassubrepo
629 # internal config: ui._usedassubrepo
630 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
630 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
631 self._initrepo(r, state[0], create)
631 self._initrepo(r, state[0], create)
632
632
633 def storeclean(self, path):
633 def storeclean(self, path):
634 with self._repo.lock():
634 with self._repo.lock():
635 return self._storeclean(path)
635 return self._storeclean(path)
636
636
637 def _storeclean(self, path):
637 def _storeclean(self, path):
638 clean = True
638 clean = True
639 itercache = self._calcstorehash(path)
639 itercache = self._calcstorehash(path)
640 for filehash in self._readstorehashcache(path):
640 for filehash in self._readstorehashcache(path):
641 if filehash != next(itercache, None):
641 if filehash != next(itercache, None):
642 clean = False
642 clean = False
643 break
643 break
644 if clean:
644 if clean:
645 # if not empty:
645 # if not empty:
646 # the cached and current pull states have a different size
646 # the cached and current pull states have a different size
647 clean = next(itercache, None) is None
647 clean = next(itercache, None) is None
648 return clean
648 return clean
649
649
650 def _calcstorehash(self, remotepath):
650 def _calcstorehash(self, remotepath):
651 '''calculate a unique "store hash"
651 '''calculate a unique "store hash"
652
652
653 This method is used to to detect when there are changes that may
653 This method is used to to detect when there are changes that may
654 require a push to a given remote path.'''
654 require a push to a given remote path.'''
655 # sort the files that will be hashed in increasing (likely) file size
655 # sort the files that will be hashed in increasing (likely) file size
656 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
656 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
657 yield '# %s\n' % _expandedabspath(remotepath)
657 yield '# %s\n' % _expandedabspath(remotepath)
658 vfs = self._repo.vfs
658 vfs = self._repo.vfs
659 for relname in filelist:
659 for relname in filelist:
660 filehash = util.sha1(vfs.tryread(relname)).hexdigest()
660 filehash = util.sha1(vfs.tryread(relname)).hexdigest()
661 yield '%s = %s\n' % (relname, filehash)
661 yield '%s = %s\n' % (relname, filehash)
662
662
663 @propertycache
663 @propertycache
664 def _cachestorehashvfs(self):
664 def _cachestorehashvfs(self):
665 return scmutil.vfs(self._repo.join('cache/storehash'))
665 return scmutil.vfs(self._repo.join('cache/storehash'))
666
666
667 def _readstorehashcache(self, remotepath):
667 def _readstorehashcache(self, remotepath):
668 '''read the store hash cache for a given remote repository'''
668 '''read the store hash cache for a given remote repository'''
669 cachefile = _getstorehashcachename(remotepath)
669 cachefile = _getstorehashcachename(remotepath)
670 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
670 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
671
671
672 def _cachestorehash(self, remotepath):
672 def _cachestorehash(self, remotepath):
673 '''cache the current store hash
673 '''cache the current store hash
674
674
675 Each remote repo requires its own store hash cache, because a subrepo
675 Each remote repo requires its own store hash cache, because a subrepo
676 store may be "clean" versus a given remote repo, but not versus another
676 store may be "clean" versus a given remote repo, but not versus another
677 '''
677 '''
678 cachefile = _getstorehashcachename(remotepath)
678 cachefile = _getstorehashcachename(remotepath)
679 with self._repo.lock():
679 with self._repo.lock():
680 storehash = list(self._calcstorehash(remotepath))
680 storehash = list(self._calcstorehash(remotepath))
681 vfs = self._cachestorehashvfs
681 vfs = self._cachestorehashvfs
682 vfs.writelines(cachefile, storehash, mode='w', notindexed=True)
682 vfs.writelines(cachefile, storehash, mode='w', notindexed=True)
683
683
684 def _getctx(self):
684 def _getctx(self):
685 '''fetch the context for this subrepo revision, possibly a workingctx
685 '''fetch the context for this subrepo revision, possibly a workingctx
686 '''
686 '''
687 if self._ctx.rev() is None:
687 if self._ctx.rev() is None:
688 return self._repo[None] # workingctx if parent is workingctx
688 return self._repo[None] # workingctx if parent is workingctx
689 else:
689 else:
690 rev = self._state[1]
690 rev = self._state[1]
691 return self._repo[rev]
691 return self._repo[rev]
692
692
693 @annotatesubrepoerror
693 @annotatesubrepoerror
694 def _initrepo(self, parentrepo, source, create):
694 def _initrepo(self, parentrepo, source, create):
695 self._repo._subparent = parentrepo
695 self._repo._subparent = parentrepo
696 self._repo._subsource = source
696 self._repo._subsource = source
697
697
698 if create:
698 if create:
699 lines = ['[paths]\n']
699 lines = ['[paths]\n']
700
700
701 def addpathconfig(key, value):
701 def addpathconfig(key, value):
702 if value:
702 if value:
703 lines.append('%s = %s\n' % (key, value))
703 lines.append('%s = %s\n' % (key, value))
704 self.ui.setconfig('paths', key, value, 'subrepo')
704 self.ui.setconfig('paths', key, value, 'subrepo')
705
705
706 defpath = _abssource(self._repo, abort=False)
706 defpath = _abssource(self._repo, abort=False)
707 defpushpath = _abssource(self._repo, True, abort=False)
707 defpushpath = _abssource(self._repo, True, abort=False)
708 addpathconfig('default', defpath)
708 addpathconfig('default', defpath)
709 if defpath != defpushpath:
709 if defpath != defpushpath:
710 addpathconfig('default-push', defpushpath)
710 addpathconfig('default-push', defpushpath)
711
711
712 fp = self._repo.vfs("hgrc", "w", text=True)
712 fp = self._repo.vfs("hgrc", "w", text=True)
713 try:
713 try:
714 fp.write(''.join(lines))
714 fp.write(''.join(lines))
715 finally:
715 finally:
716 fp.close()
716 fp.close()
717
717
718 @annotatesubrepoerror
718 @annotatesubrepoerror
719 def add(self, ui, match, prefix, explicitonly, **opts):
719 def add(self, ui, match, prefix, explicitonly, **opts):
720 return cmdutil.add(ui, self._repo, match,
720 return cmdutil.add(ui, self._repo, match,
721 self.wvfs.reljoin(prefix, self._path),
721 self.wvfs.reljoin(prefix, self._path),
722 explicitonly, **opts)
722 explicitonly, **opts)
723
723
724 @annotatesubrepoerror
724 @annotatesubrepoerror
725 def addremove(self, m, prefix, opts, dry_run, similarity):
725 def addremove(self, m, prefix, opts, dry_run, similarity):
726 # In the same way as sub directories are processed, once in a subrepo,
726 # In the same way as sub directories are processed, once in a subrepo,
727 # always entry any of its subrepos. Don't corrupt the options that will
727 # always entry any of its subrepos. Don't corrupt the options that will
728 # be used to process sibling subrepos however.
728 # be used to process sibling subrepos however.
729 opts = copy.copy(opts)
729 opts = copy.copy(opts)
730 opts['subrepos'] = True
730 opts['subrepos'] = True
731 return scmutil.addremove(self._repo, m,
731 return scmutil.addremove(self._repo, m,
732 self.wvfs.reljoin(prefix, self._path), opts,
732 self.wvfs.reljoin(prefix, self._path), opts,
733 dry_run, similarity)
733 dry_run, similarity)
734
734
735 @annotatesubrepoerror
735 @annotatesubrepoerror
736 def cat(self, match, prefix, **opts):
736 def cat(self, match, prefix, **opts):
737 rev = self._state[1]
737 rev = self._state[1]
738 ctx = self._repo[rev]
738 ctx = self._repo[rev]
739 return cmdutil.cat(self.ui, self._repo, ctx, match, prefix, **opts)
739 return cmdutil.cat(self.ui, self._repo, ctx, match, prefix, **opts)
740
740
741 @annotatesubrepoerror
741 @annotatesubrepoerror
742 def status(self, rev2, **opts):
742 def status(self, rev2, **opts):
743 try:
743 try:
744 rev1 = self._state[1]
744 rev1 = self._state[1]
745 ctx1 = self._repo[rev1]
745 ctx1 = self._repo[rev1]
746 ctx2 = self._repo[rev2]
746 ctx2 = self._repo[rev2]
747 return self._repo.status(ctx1, ctx2, **opts)
747 return self._repo.status(ctx1, ctx2, **opts)
748 except error.RepoLookupError as inst:
748 except error.RepoLookupError as inst:
749 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
749 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
750 % (inst, subrelpath(self)))
750 % (inst, subrelpath(self)))
751 return scmutil.status([], [], [], [], [], [], [])
751 return scmutil.status([], [], [], [], [], [], [])
752
752
753 @annotatesubrepoerror
753 @annotatesubrepoerror
754 def diff(self, ui, diffopts, node2, match, prefix, **opts):
754 def diff(self, ui, diffopts, node2, match, prefix, **opts):
755 try:
755 try:
756 node1 = node.bin(self._state[1])
756 node1 = node.bin(self._state[1])
757 # We currently expect node2 to come from substate and be
757 # We currently expect node2 to come from substate and be
758 # in hex format
758 # in hex format
759 if node2 is not None:
759 if node2 is not None:
760 node2 = node.bin(node2)
760 node2 = node.bin(node2)
761 cmdutil.diffordiffstat(ui, self._repo, diffopts,
761 cmdutil.diffordiffstat(ui, self._repo, diffopts,
762 node1, node2, match,
762 node1, node2, match,
763 prefix=posixpath.join(prefix, self._path),
763 prefix=posixpath.join(prefix, self._path),
764 listsubrepos=True, **opts)
764 listsubrepos=True, **opts)
765 except error.RepoLookupError as inst:
765 except error.RepoLookupError as inst:
766 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
766 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
767 % (inst, subrelpath(self)))
767 % (inst, subrelpath(self)))
768
768
769 @annotatesubrepoerror
769 @annotatesubrepoerror
770 def archive(self, archiver, prefix, match=None):
770 def archive(self, archiver, prefix, match=None):
771 self._get(self._state + ('hg',))
771 self._get(self._state + ('hg',))
772 total = abstractsubrepo.archive(self, archiver, prefix, match)
772 total = abstractsubrepo.archive(self, archiver, prefix, match)
773 rev = self._state[1]
773 rev = self._state[1]
774 ctx = self._repo[rev]
774 ctx = self._repo[rev]
775 for subpath in ctx.substate:
775 for subpath in ctx.substate:
776 s = subrepo(ctx, subpath, True)
776 s = subrepo(ctx, subpath, True)
777 submatch = matchmod.narrowmatcher(subpath, match)
777 submatch = matchmod.narrowmatcher(subpath, match)
778 total += s.archive(archiver, prefix + self._path + '/', submatch)
778 total += s.archive(archiver, prefix + self._path + '/', submatch)
779 return total
779 return total
780
780
781 @annotatesubrepoerror
781 @annotatesubrepoerror
782 def dirty(self, ignoreupdate=False):
782 def dirty(self, ignoreupdate=False):
783 r = self._state[1]
783 r = self._state[1]
784 if r == '' and not ignoreupdate: # no state recorded
784 if r == '' and not ignoreupdate: # no state recorded
785 return True
785 return True
786 w = self._repo[None]
786 w = self._repo[None]
787 if r != w.p1().hex() and not ignoreupdate:
787 if r != w.p1().hex() and not ignoreupdate:
788 # different version checked out
788 # different version checked out
789 return True
789 return True
790 return w.dirty() # working directory changed
790 return w.dirty() # working directory changed
791
791
792 def basestate(self):
792 def basestate(self):
793 return self._repo['.'].hex()
793 return self._repo['.'].hex()
794
794
795 def checknested(self, path):
795 def checknested(self, path):
796 return self._repo._checknested(self._repo.wjoin(path))
796 return self._repo._checknested(self._repo.wjoin(path))
797
797
798 @annotatesubrepoerror
798 @annotatesubrepoerror
799 def commit(self, text, user, date):
799 def commit(self, text, user, date):
800 # don't bother committing in the subrepo if it's only been
800 # don't bother committing in the subrepo if it's only been
801 # updated
801 # updated
802 if not self.dirty(True):
802 if not self.dirty(True):
803 return self._repo['.'].hex()
803 return self._repo['.'].hex()
804 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
804 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
805 n = self._repo.commit(text, user, date)
805 n = self._repo.commit(text, user, date)
806 if not n:
806 if not n:
807 return self._repo['.'].hex() # different version checked out
807 return self._repo['.'].hex() # different version checked out
808 return node.hex(n)
808 return node.hex(n)
809
809
810 @annotatesubrepoerror
810 @annotatesubrepoerror
811 def phase(self, state):
811 def phase(self, state):
812 return self._repo[state].phase()
812 return self._repo[state].phase()
813
813
814 @annotatesubrepoerror
814 @annotatesubrepoerror
815 def remove(self):
815 def remove(self):
816 # we can't fully delete the repository as it may contain
816 # we can't fully delete the repository as it may contain
817 # local-only history
817 # local-only history
818 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
818 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
819 hg.clean(self._repo, node.nullid, False)
819 hg.clean(self._repo, node.nullid, False)
820
820
821 def _get(self, state):
821 def _get(self, state):
822 source, revision, kind = state
822 source, revision, kind = state
823 if revision in self._repo.unfiltered():
823 if revision in self._repo.unfiltered():
824 return True
824 return True
825 self._repo._subsource = source
825 self._repo._subsource = source
826 srcurl = _abssource(self._repo)
826 srcurl = _abssource(self._repo)
827 other = hg.peer(self._repo, {}, srcurl)
827 other = hg.peer(self._repo, {}, srcurl)
828 if len(self._repo) == 0:
828 if len(self._repo) == 0:
829 self.ui.status(_('cloning subrepo %s from %s\n')
829 self.ui.status(_('cloning subrepo %s from %s\n')
830 % (subrelpath(self), srcurl))
830 % (subrelpath(self), srcurl))
831 parentrepo = self._repo._subparent
831 parentrepo = self._repo._subparent
832 # use self._repo.vfs instead of self.wvfs to remove .hg only
832 # use self._repo.vfs instead of self.wvfs to remove .hg only
833 self._repo.vfs.rmtree()
833 self._repo.vfs.rmtree()
834 other, cloned = hg.clone(self._repo._subparent.baseui, {},
834 other, cloned = hg.clone(self._repo._subparent.baseui, {},
835 other, self._repo.root,
835 other, self._repo.root,
836 update=False)
836 update=False)
837 self._repo = cloned.local()
837 self._repo = cloned.local()
838 self._initrepo(parentrepo, source, create=True)
838 self._initrepo(parentrepo, source, create=True)
839 self._cachestorehash(srcurl)
839 self._cachestorehash(srcurl)
840 else:
840 else:
841 self.ui.status(_('pulling subrepo %s from %s\n')
841 self.ui.status(_('pulling subrepo %s from %s\n')
842 % (subrelpath(self), srcurl))
842 % (subrelpath(self), srcurl))
843 cleansub = self.storeclean(srcurl)
843 cleansub = self.storeclean(srcurl)
844 exchange.pull(self._repo, other)
844 exchange.pull(self._repo, other)
845 if cleansub:
845 if cleansub:
846 # keep the repo clean after pull
846 # keep the repo clean after pull
847 self._cachestorehash(srcurl)
847 self._cachestorehash(srcurl)
848 return False
848 return False
849
849
850 @annotatesubrepoerror
850 @annotatesubrepoerror
851 def get(self, state, overwrite=False):
851 def get(self, state, overwrite=False):
852 inrepo = self._get(state)
852 inrepo = self._get(state)
853 source, revision, kind = state
853 source, revision, kind = state
854 repo = self._repo
854 repo = self._repo
855 repo.ui.debug("getting subrepo %s\n" % self._path)
855 repo.ui.debug("getting subrepo %s\n" % self._path)
856 if inrepo:
856 if inrepo:
857 urepo = repo.unfiltered()
857 urepo = repo.unfiltered()
858 ctx = urepo[revision]
858 ctx = urepo[revision]
859 if ctx.hidden():
859 if ctx.hidden():
860 urepo.ui.warn(
860 urepo.ui.warn(
861 _('revision %s in subrepo %s is hidden\n') \
861 _('revision %s in subrepo %s is hidden\n') \
862 % (revision[0:12], self._path))
862 % (revision[0:12], self._path))
863 repo = urepo
863 repo = urepo
864 hg.updaterepo(repo, revision, overwrite)
864 hg.updaterepo(repo, revision, overwrite)
865
865
866 @annotatesubrepoerror
866 @annotatesubrepoerror
867 def merge(self, state):
867 def merge(self, state):
868 self._get(state)
868 self._get(state)
869 cur = self._repo['.']
869 cur = self._repo['.']
870 dst = self._repo[state[1]]
870 dst = self._repo[state[1]]
871 anc = dst.ancestor(cur)
871 anc = dst.ancestor(cur)
872
872
873 def mergefunc():
873 def mergefunc():
874 if anc == cur and dst.branch() == cur.branch():
874 if anc == cur and dst.branch() == cur.branch():
875 self.ui.debug("updating subrepo %s\n" % subrelpath(self))
875 self.ui.debug("updating subrepo %s\n" % subrelpath(self))
876 hg.update(self._repo, state[1])
876 hg.update(self._repo, state[1])
877 elif anc == dst:
877 elif anc == dst:
878 self.ui.debug("skipping subrepo %s\n" % subrelpath(self))
878 self.ui.debug("skipping subrepo %s\n" % subrelpath(self))
879 else:
879 else:
880 self.ui.debug("merging subrepo %s\n" % subrelpath(self))
880 self.ui.debug("merging subrepo %s\n" % subrelpath(self))
881 hg.merge(self._repo, state[1], remind=False)
881 hg.merge(self._repo, state[1], remind=False)
882
882
883 wctx = self._repo[None]
883 wctx = self._repo[None]
884 if self.dirty():
884 if self.dirty():
885 if anc != dst:
885 if anc != dst:
886 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
886 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
887 mergefunc()
887 mergefunc()
888 else:
888 else:
889 mergefunc()
889 mergefunc()
890 else:
890 else:
891 mergefunc()
891 mergefunc()
892
892
893 @annotatesubrepoerror
893 @annotatesubrepoerror
894 def push(self, opts):
894 def push(self, opts):
895 force = opts.get('force')
895 force = opts.get('force')
896 newbranch = opts.get('new_branch')
896 newbranch = opts.get('new_branch')
897 ssh = opts.get('ssh')
897 ssh = opts.get('ssh')
898
898
899 # push subrepos depth-first for coherent ordering
899 # push subrepos depth-first for coherent ordering
900 c = self._repo['']
900 c = self._repo['']
901 subs = c.substate # only repos that are committed
901 subs = c.substate # only repos that are committed
902 for s in sorted(subs):
902 for s in sorted(subs):
903 if c.sub(s).push(opts) == 0:
903 if c.sub(s).push(opts) == 0:
904 return False
904 return False
905
905
906 dsturl = _abssource(self._repo, True)
906 dsturl = _abssource(self._repo, True)
907 if not force:
907 if not force:
908 if self.storeclean(dsturl):
908 if self.storeclean(dsturl):
909 self.ui.status(
909 self.ui.status(
910 _('no changes made to subrepo %s since last push to %s\n')
910 _('no changes made to subrepo %s since last push to %s\n')
911 % (subrelpath(self), dsturl))
911 % (subrelpath(self), dsturl))
912 return None
912 return None
913 self.ui.status(_('pushing subrepo %s to %s\n') %
913 self.ui.status(_('pushing subrepo %s to %s\n') %
914 (subrelpath(self), dsturl))
914 (subrelpath(self), dsturl))
915 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
915 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
916 res = exchange.push(self._repo, other, force, newbranch=newbranch)
916 res = exchange.push(self._repo, other, force, newbranch=newbranch)
917
917
918 # the repo is now clean
918 # the repo is now clean
919 self._cachestorehash(dsturl)
919 self._cachestorehash(dsturl)
920 return res.cgresult
920 return res.cgresult
921
921
922 @annotatesubrepoerror
922 @annotatesubrepoerror
923 def outgoing(self, ui, dest, opts):
923 def outgoing(self, ui, dest, opts):
924 if 'rev' in opts or 'branch' in opts:
924 if 'rev' in opts or 'branch' in opts:
925 opts = copy.copy(opts)
925 opts = copy.copy(opts)
926 opts.pop('rev', None)
926 opts.pop('rev', None)
927 opts.pop('branch', None)
927 opts.pop('branch', None)
928 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
928 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
929
929
930 @annotatesubrepoerror
930 @annotatesubrepoerror
931 def incoming(self, ui, source, opts):
931 def incoming(self, ui, source, opts):
932 if 'rev' in opts or 'branch' in opts:
932 if 'rev' in opts or 'branch' in opts:
933 opts = copy.copy(opts)
933 opts = copy.copy(opts)
934 opts.pop('rev', None)
934 opts.pop('rev', None)
935 opts.pop('branch', None)
935 opts.pop('branch', None)
936 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
936 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
937
937
938 @annotatesubrepoerror
938 @annotatesubrepoerror
939 def files(self):
939 def files(self):
940 rev = self._state[1]
940 rev = self._state[1]
941 ctx = self._repo[rev]
941 ctx = self._repo[rev]
942 return ctx.manifest().keys()
942 return ctx.manifest().keys()
943
943
944 def filedata(self, name):
944 def filedata(self, name):
945 rev = self._state[1]
945 rev = self._state[1]
946 return self._repo[rev][name].data()
946 return self._repo[rev][name].data()
947
947
948 def fileflags(self, name):
948 def fileflags(self, name):
949 rev = self._state[1]
949 rev = self._state[1]
950 ctx = self._repo[rev]
950 ctx = self._repo[rev]
951 return ctx.flags(name)
951 return ctx.flags(name)
952
952
953 @annotatesubrepoerror
953 @annotatesubrepoerror
954 def printfiles(self, ui, m, fm, fmt, subrepos):
954 def printfiles(self, ui, m, fm, fmt, subrepos):
955 # If the parent context is a workingctx, use the workingctx here for
955 # If the parent context is a workingctx, use the workingctx here for
956 # consistency.
956 # consistency.
957 if self._ctx.rev() is None:
957 if self._ctx.rev() is None:
958 ctx = self._repo[None]
958 ctx = self._repo[None]
959 else:
959 else:
960 rev = self._state[1]
960 rev = self._state[1]
961 ctx = self._repo[rev]
961 ctx = self._repo[rev]
962 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
962 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
963
963
964 @annotatesubrepoerror
964 @annotatesubrepoerror
965 def getfileset(self, expr):
965 def getfileset(self, expr):
966 if self._ctx.rev() is None:
966 if self._ctx.rev() is None:
967 ctx = self._repo[None]
967 ctx = self._repo[None]
968 else:
968 else:
969 rev = self._state[1]
969 rev = self._state[1]
970 ctx = self._repo[rev]
970 ctx = self._repo[rev]
971
971
972 files = ctx.getfileset(expr)
972 files = ctx.getfileset(expr)
973
973
974 for subpath in ctx.substate:
974 for subpath in ctx.substate:
975 sub = ctx.sub(subpath)
975 sub = ctx.sub(subpath)
976
976
977 try:
977 try:
978 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
978 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
979 except error.LookupError:
979 except error.LookupError:
980 self.ui.status(_("skipping missing subrepository: %s\n")
980 self.ui.status(_("skipping missing subrepository: %s\n")
981 % self.wvfs.reljoin(reporelpath(self), subpath))
981 % self.wvfs.reljoin(reporelpath(self), subpath))
982 return files
982 return files
983
983
984 def walk(self, match):
984 def walk(self, match):
985 ctx = self._repo[None]
985 ctx = self._repo[None]
986 return ctx.walk(match)
986 return ctx.walk(match)
987
987
988 @annotatesubrepoerror
988 @annotatesubrepoerror
989 def forget(self, match, prefix):
989 def forget(self, match, prefix):
990 return cmdutil.forget(self.ui, self._repo, match,
990 return cmdutil.forget(self.ui, self._repo, match,
991 self.wvfs.reljoin(prefix, self._path), True)
991 self.wvfs.reljoin(prefix, self._path), True)
992
992
993 @annotatesubrepoerror
993 @annotatesubrepoerror
994 def removefiles(self, matcher, prefix, after, force, subrepos):
994 def removefiles(self, matcher, prefix, after, force, subrepos):
995 return cmdutil.remove(self.ui, self._repo, matcher,
995 return cmdutil.remove(self.ui, self._repo, matcher,
996 self.wvfs.reljoin(prefix, self._path),
996 self.wvfs.reljoin(prefix, self._path),
997 after, force, subrepos)
997 after, force, subrepos)
998
998
999 @annotatesubrepoerror
999 @annotatesubrepoerror
1000 def revert(self, substate, *pats, **opts):
1000 def revert(self, substate, *pats, **opts):
1001 # reverting a subrepo is a 2 step process:
1001 # reverting a subrepo is a 2 step process:
1002 # 1. if the no_backup is not set, revert all modified
1002 # 1. if the no_backup is not set, revert all modified
1003 # files inside the subrepo
1003 # files inside the subrepo
1004 # 2. update the subrepo to the revision specified in
1004 # 2. update the subrepo to the revision specified in
1005 # the corresponding substate dictionary
1005 # the corresponding substate dictionary
1006 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1006 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1007 if not opts.get('no_backup'):
1007 if not opts.get('no_backup'):
1008 # Revert all files on the subrepo, creating backups
1008 # Revert all files on the subrepo, creating backups
1009 # Note that this will not recursively revert subrepos
1009 # Note that this will not recursively revert subrepos
1010 # We could do it if there was a set:subrepos() predicate
1010 # We could do it if there was a set:subrepos() predicate
1011 opts = opts.copy()
1011 opts = opts.copy()
1012 opts['date'] = None
1012 opts['date'] = None
1013 opts['rev'] = substate[1]
1013 opts['rev'] = substate[1]
1014
1014
1015 self.filerevert(*pats, **opts)
1015 self.filerevert(*pats, **opts)
1016
1016
1017 # Update the repo to the revision specified in the given substate
1017 # Update the repo to the revision specified in the given substate
1018 if not opts.get('dry_run'):
1018 if not opts.get('dry_run'):
1019 self.get(substate, overwrite=True)
1019 self.get(substate, overwrite=True)
1020
1020
1021 def filerevert(self, *pats, **opts):
1021 def filerevert(self, *pats, **opts):
1022 ctx = self._repo[opts['rev']]
1022 ctx = self._repo[opts['rev']]
1023 parents = self._repo.dirstate.parents()
1023 parents = self._repo.dirstate.parents()
1024 if opts.get('all'):
1024 if opts.get('all'):
1025 pats = ['set:modified()']
1025 pats = ['set:modified()']
1026 else:
1026 else:
1027 pats = []
1027 pats = []
1028 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
1028 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
1029
1029
1030 def shortid(self, revid):
1030 def shortid(self, revid):
1031 return revid[:12]
1031 return revid[:12]
1032
1032
1033 def verify(self):
1033 def verify(self):
1034 try:
1034 try:
1035 rev = self._state[1]
1035 rev = self._state[1]
1036 ctx = self._repo.unfiltered()[rev]
1036 ctx = self._repo.unfiltered()[rev]
1037 if ctx.hidden():
1037 if ctx.hidden():
1038 # Since hidden revisions aren't pushed/pulled, it seems worth an
1038 # Since hidden revisions aren't pushed/pulled, it seems worth an
1039 # explicit warning.
1039 # explicit warning.
1040 ui = self._repo.ui
1040 ui = self._repo.ui
1041 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
1041 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
1042 (self._relpath, node.short(self._ctx.node())))
1042 (self._relpath, node.short(self._ctx.node())))
1043 return 0
1043 return 0
1044 except error.RepoLookupError:
1044 except error.RepoLookupError:
1045 # A missing subrepo revision may be a case of needing to pull it, so
1045 # A missing subrepo revision may be a case of needing to pull it, so
1046 # don't treat this as an error.
1046 # don't treat this as an error.
1047 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
1047 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
1048 (self._relpath, node.short(self._ctx.node())))
1048 (self._relpath, node.short(self._ctx.node())))
1049 return 0
1049 return 0
1050
1050
1051 @propertycache
1051 @propertycache
1052 def wvfs(self):
1052 def wvfs(self):
1053 """return own wvfs for efficiency and consistency
1053 """return own wvfs for efficiency and consistency
1054 """
1054 """
1055 return self._repo.wvfs
1055 return self._repo.wvfs
1056
1056
1057 @propertycache
1057 @propertycache
1058 def _relpath(self):
1058 def _relpath(self):
1059 """return path to this subrepository as seen from outermost repository
1059 """return path to this subrepository as seen from outermost repository
1060 """
1060 """
1061 # Keep consistent dir separators by avoiding vfs.join(self._path)
1061 # Keep consistent dir separators by avoiding vfs.join(self._path)
1062 return reporelpath(self._repo)
1062 return reporelpath(self._repo)
1063
1063
1064 class svnsubrepo(abstractsubrepo):
1064 class svnsubrepo(abstractsubrepo):
1065 def __init__(self, ctx, path, state):
1065 def __init__(self, ctx, path, state):
1066 super(svnsubrepo, self).__init__(ctx, path)
1066 super(svnsubrepo, self).__init__(ctx, path)
1067 self._state = state
1067 self._state = state
1068 self._exe = util.findexe('svn')
1068 self._exe = util.findexe('svn')
1069 if not self._exe:
1069 if not self._exe:
1070 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
1070 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
1071 % self._path)
1071 % self._path)
1072
1072
1073 def _svncommand(self, commands, filename='', failok=False):
1073 def _svncommand(self, commands, filename='', failok=False):
1074 cmd = [self._exe]
1074 cmd = [self._exe]
1075 extrakw = {}
1075 extrakw = {}
1076 if not self.ui.interactive():
1076 if not self.ui.interactive():
1077 # Making stdin be a pipe should prevent svn from behaving
1077 # Making stdin be a pipe should prevent svn from behaving
1078 # interactively even if we can't pass --non-interactive.
1078 # interactively even if we can't pass --non-interactive.
1079 extrakw['stdin'] = subprocess.PIPE
1079 extrakw['stdin'] = subprocess.PIPE
1080 # Starting in svn 1.5 --non-interactive is a global flag
1080 # Starting in svn 1.5 --non-interactive is a global flag
1081 # instead of being per-command, but we need to support 1.4 so
1081 # instead of being per-command, but we need to support 1.4 so
1082 # we have to be intelligent about what commands take
1082 # we have to be intelligent about what commands take
1083 # --non-interactive.
1083 # --non-interactive.
1084 if commands[0] in ('update', 'checkout', 'commit'):
1084 if commands[0] in ('update', 'checkout', 'commit'):
1085 cmd.append('--non-interactive')
1085 cmd.append('--non-interactive')
1086 cmd.extend(commands)
1086 cmd.extend(commands)
1087 if filename is not None:
1087 if filename is not None:
1088 path = self.wvfs.reljoin(self._ctx.repo().origroot,
1088 path = self.wvfs.reljoin(self._ctx.repo().origroot,
1089 self._path, filename)
1089 self._path, filename)
1090 cmd.append(path)
1090 cmd.append(path)
1091 env = dict(os.environ)
1091 env = dict(os.environ)
1092 # Avoid localized output, preserve current locale for everything else.
1092 # Avoid localized output, preserve current locale for everything else.
1093 lc_all = env.get('LC_ALL')
1093 lc_all = env.get('LC_ALL')
1094 if lc_all:
1094 if lc_all:
1095 env['LANG'] = lc_all
1095 env['LANG'] = lc_all
1096 del env['LC_ALL']
1096 del env['LC_ALL']
1097 env['LC_MESSAGES'] = 'C'
1097 env['LC_MESSAGES'] = 'C'
1098 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
1098 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
1099 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1099 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1100 universal_newlines=True, env=env, **extrakw)
1100 universal_newlines=True, env=env, **extrakw)
1101 stdout, stderr = p.communicate()
1101 stdout, stderr = p.communicate()
1102 stderr = stderr.strip()
1102 stderr = stderr.strip()
1103 if not failok:
1103 if not failok:
1104 if p.returncode:
1104 if p.returncode:
1105 raise error.Abort(stderr or 'exited with code %d'
1105 raise error.Abort(stderr or 'exited with code %d'
1106 % p.returncode)
1106 % p.returncode)
1107 if stderr:
1107 if stderr:
1108 self.ui.warn(stderr + '\n')
1108 self.ui.warn(stderr + '\n')
1109 return stdout, stderr
1109 return stdout, stderr
1110
1110
1111 @propertycache
1111 @propertycache
1112 def _svnversion(self):
1112 def _svnversion(self):
1113 output, err = self._svncommand(['--version', '--quiet'], filename=None)
1113 output, err = self._svncommand(['--version', '--quiet'], filename=None)
1114 m = re.search(r'^(\d+)\.(\d+)', output)
1114 m = re.search(r'^(\d+)\.(\d+)', output)
1115 if not m:
1115 if not m:
1116 raise error.Abort(_('cannot retrieve svn tool version'))
1116 raise error.Abort(_('cannot retrieve svn tool version'))
1117 return (int(m.group(1)), int(m.group(2)))
1117 return (int(m.group(1)), int(m.group(2)))
1118
1118
1119 def _wcrevs(self):
1119 def _wcrevs(self):
1120 # Get the working directory revision as well as the last
1120 # Get the working directory revision as well as the last
1121 # commit revision so we can compare the subrepo state with
1121 # commit revision so we can compare the subrepo state with
1122 # both. We used to store the working directory one.
1122 # both. We used to store the working directory one.
1123 output, err = self._svncommand(['info', '--xml'])
1123 output, err = self._svncommand(['info', '--xml'])
1124 doc = xml.dom.minidom.parseString(output)
1124 doc = xml.dom.minidom.parseString(output)
1125 entries = doc.getElementsByTagName('entry')
1125 entries = doc.getElementsByTagName('entry')
1126 lastrev, rev = '0', '0'
1126 lastrev, rev = '0', '0'
1127 if entries:
1127 if entries:
1128 rev = str(entries[0].getAttribute('revision')) or '0'
1128 rev = str(entries[0].getAttribute('revision')) or '0'
1129 commits = entries[0].getElementsByTagName('commit')
1129 commits = entries[0].getElementsByTagName('commit')
1130 if commits:
1130 if commits:
1131 lastrev = str(commits[0].getAttribute('revision')) or '0'
1131 lastrev = str(commits[0].getAttribute('revision')) or '0'
1132 return (lastrev, rev)
1132 return (lastrev, rev)
1133
1133
1134 def _wcrev(self):
1134 def _wcrev(self):
1135 return self._wcrevs()[0]
1135 return self._wcrevs()[0]
1136
1136
1137 def _wcchanged(self):
1137 def _wcchanged(self):
1138 """Return (changes, extchanges, missing) where changes is True
1138 """Return (changes, extchanges, missing) where changes is True
1139 if the working directory was changed, extchanges is
1139 if the working directory was changed, extchanges is
1140 True if any of these changes concern an external entry and missing
1140 True if any of these changes concern an external entry and missing
1141 is True if any change is a missing entry.
1141 is True if any change is a missing entry.
1142 """
1142 """
1143 output, err = self._svncommand(['status', '--xml'])
1143 output, err = self._svncommand(['status', '--xml'])
1144 externals, changes, missing = [], [], []
1144 externals, changes, missing = [], [], []
1145 doc = xml.dom.minidom.parseString(output)
1145 doc = xml.dom.minidom.parseString(output)
1146 for e in doc.getElementsByTagName('entry'):
1146 for e in doc.getElementsByTagName('entry'):
1147 s = e.getElementsByTagName('wc-status')
1147 s = e.getElementsByTagName('wc-status')
1148 if not s:
1148 if not s:
1149 continue
1149 continue
1150 item = s[0].getAttribute('item')
1150 item = s[0].getAttribute('item')
1151 props = s[0].getAttribute('props')
1151 props = s[0].getAttribute('props')
1152 path = e.getAttribute('path')
1152 path = e.getAttribute('path')
1153 if item == 'external':
1153 if item == 'external':
1154 externals.append(path)
1154 externals.append(path)
1155 elif item == 'missing':
1155 elif item == 'missing':
1156 missing.append(path)
1156 missing.append(path)
1157 if (item not in ('', 'normal', 'unversioned', 'external')
1157 if (item not in ('', 'normal', 'unversioned', 'external')
1158 or props not in ('', 'none', 'normal')):
1158 or props not in ('', 'none', 'normal')):
1159 changes.append(path)
1159 changes.append(path)
1160 for path in changes:
1160 for path in changes:
1161 for ext in externals:
1161 for ext in externals:
1162 if path == ext or path.startswith(ext + os.sep):
1162 if path == ext or path.startswith(ext + os.sep):
1163 return True, True, bool(missing)
1163 return True, True, bool(missing)
1164 return bool(changes), False, bool(missing)
1164 return bool(changes), False, bool(missing)
1165
1165
1166 def dirty(self, ignoreupdate=False):
1166 def dirty(self, ignoreupdate=False):
1167 if not self._wcchanged()[0]:
1167 if not self._wcchanged()[0]:
1168 if self._state[1] in self._wcrevs() or ignoreupdate:
1168 if self._state[1] in self._wcrevs() or ignoreupdate:
1169 return False
1169 return False
1170 return True
1170 return True
1171
1171
1172 def basestate(self):
1172 def basestate(self):
1173 lastrev, rev = self._wcrevs()
1173 lastrev, rev = self._wcrevs()
1174 if lastrev != rev:
1174 if lastrev != rev:
1175 # Last committed rev is not the same than rev. We would
1175 # Last committed rev is not the same than rev. We would
1176 # like to take lastrev but we do not know if the subrepo
1176 # like to take lastrev but we do not know if the subrepo
1177 # URL exists at lastrev. Test it and fallback to rev it
1177 # URL exists at lastrev. Test it and fallback to rev it
1178 # is not there.
1178 # is not there.
1179 try:
1179 try:
1180 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1180 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1181 return lastrev
1181 return lastrev
1182 except error.Abort:
1182 except error.Abort:
1183 pass
1183 pass
1184 return rev
1184 return rev
1185
1185
1186 @annotatesubrepoerror
1186 @annotatesubrepoerror
1187 def commit(self, text, user, date):
1187 def commit(self, text, user, date):
1188 # user and date are out of our hands since svn is centralized
1188 # user and date are out of our hands since svn is centralized
1189 changed, extchanged, missing = self._wcchanged()
1189 changed, extchanged, missing = self._wcchanged()
1190 if not changed:
1190 if not changed:
1191 return self.basestate()
1191 return self.basestate()
1192 if extchanged:
1192 if extchanged:
1193 # Do not try to commit externals
1193 # Do not try to commit externals
1194 raise error.Abort(_('cannot commit svn externals'))
1194 raise error.Abort(_('cannot commit svn externals'))
1195 if missing:
1195 if missing:
1196 # svn can commit with missing entries but aborting like hg
1196 # svn can commit with missing entries but aborting like hg
1197 # seems a better approach.
1197 # seems a better approach.
1198 raise error.Abort(_('cannot commit missing svn entries'))
1198 raise error.Abort(_('cannot commit missing svn entries'))
1199 commitinfo, err = self._svncommand(['commit', '-m', text])
1199 commitinfo, err = self._svncommand(['commit', '-m', text])
1200 self.ui.status(commitinfo)
1200 self.ui.status(commitinfo)
1201 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1201 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1202 if not newrev:
1202 if not newrev:
1203 if not commitinfo.strip():
1203 if not commitinfo.strip():
1204 # Sometimes, our definition of "changed" differs from
1204 # Sometimes, our definition of "changed" differs from
1205 # svn one. For instance, svn ignores missing files
1205 # svn one. For instance, svn ignores missing files
1206 # when committing. If there are only missing files, no
1206 # when committing. If there are only missing files, no
1207 # commit is made, no output and no error code.
1207 # commit is made, no output and no error code.
1208 raise error.Abort(_('failed to commit svn changes'))
1208 raise error.Abort(_('failed to commit svn changes'))
1209 raise error.Abort(commitinfo.splitlines()[-1])
1209 raise error.Abort(commitinfo.splitlines()[-1])
1210 newrev = newrev.groups()[0]
1210 newrev = newrev.groups()[0]
1211 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1211 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1212 return newrev
1212 return newrev
1213
1213
1214 @annotatesubrepoerror
1214 @annotatesubrepoerror
1215 def remove(self):
1215 def remove(self):
1216 if self.dirty():
1216 if self.dirty():
1217 self.ui.warn(_('not removing repo %s because '
1217 self.ui.warn(_('not removing repo %s because '
1218 'it has changes.\n') % self._path)
1218 'it has changes.\n') % self._path)
1219 return
1219 return
1220 self.ui.note(_('removing subrepo %s\n') % self._path)
1220 self.ui.note(_('removing subrepo %s\n') % self._path)
1221
1221
1222 self.wvfs.rmtree(forcibly=True)
1222 self.wvfs.rmtree(forcibly=True)
1223 try:
1223 try:
1224 pwvfs = self._ctx.repo().wvfs
1224 pwvfs = self._ctx.repo().wvfs
1225 pwvfs.removedirs(pwvfs.dirname(self._path))
1225 pwvfs.removedirs(pwvfs.dirname(self._path))
1226 except OSError:
1226 except OSError:
1227 pass
1227 pass
1228
1228
1229 @annotatesubrepoerror
1229 @annotatesubrepoerror
1230 def get(self, state, overwrite=False):
1230 def get(self, state, overwrite=False):
1231 if overwrite:
1231 if overwrite:
1232 self._svncommand(['revert', '--recursive'])
1232 self._svncommand(['revert', '--recursive'])
1233 args = ['checkout']
1233 args = ['checkout']
1234 if self._svnversion >= (1, 5):
1234 if self._svnversion >= (1, 5):
1235 args.append('--force')
1235 args.append('--force')
1236 # The revision must be specified at the end of the URL to properly
1236 # The revision must be specified at the end of the URL to properly
1237 # update to a directory which has since been deleted and recreated.
1237 # update to a directory which has since been deleted and recreated.
1238 args.append('%s@%s' % (state[0], state[1]))
1238 args.append('%s@%s' % (state[0], state[1]))
1239 status, err = self._svncommand(args, failok=True)
1239 status, err = self._svncommand(args, failok=True)
1240 _sanitize(self.ui, self.wvfs, '.svn')
1240 _sanitize(self.ui, self.wvfs, '.svn')
1241 if not re.search('Checked out revision [0-9]+.', status):
1241 if not re.search('Checked out revision [0-9]+.', status):
1242 if ('is already a working copy for a different URL' in err
1242 if ('is already a working copy for a different URL' in err
1243 and (self._wcchanged()[:2] == (False, False))):
1243 and (self._wcchanged()[:2] == (False, False))):
1244 # obstructed but clean working copy, so just blow it away.
1244 # obstructed but clean working copy, so just blow it away.
1245 self.remove()
1245 self.remove()
1246 self.get(state, overwrite=False)
1246 self.get(state, overwrite=False)
1247 return
1247 return
1248 raise error.Abort((status or err).splitlines()[-1])
1248 raise error.Abort((status or err).splitlines()[-1])
1249 self.ui.status(status)
1249 self.ui.status(status)
1250
1250
1251 @annotatesubrepoerror
1251 @annotatesubrepoerror
1252 def merge(self, state):
1252 def merge(self, state):
1253 old = self._state[1]
1253 old = self._state[1]
1254 new = state[1]
1254 new = state[1]
1255 wcrev = self._wcrev()
1255 wcrev = self._wcrev()
1256 if new != wcrev:
1256 if new != wcrev:
1257 dirty = old == wcrev or self._wcchanged()[0]
1257 dirty = old == wcrev or self._wcchanged()[0]
1258 if _updateprompt(self.ui, self, dirty, wcrev, new):
1258 if _updateprompt(self.ui, self, dirty, wcrev, new):
1259 self.get(state, False)
1259 self.get(state, False)
1260
1260
1261 def push(self, opts):
1261 def push(self, opts):
1262 # push is a no-op for SVN
1262 # push is a no-op for SVN
1263 return True
1263 return True
1264
1264
1265 @annotatesubrepoerror
1265 @annotatesubrepoerror
1266 def files(self):
1266 def files(self):
1267 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1267 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1268 doc = xml.dom.minidom.parseString(output)
1268 doc = xml.dom.minidom.parseString(output)
1269 paths = []
1269 paths = []
1270 for e in doc.getElementsByTagName('entry'):
1270 for e in doc.getElementsByTagName('entry'):
1271 kind = str(e.getAttribute('kind'))
1271 kind = str(e.getAttribute('kind'))
1272 if kind != 'file':
1272 if kind != 'file':
1273 continue
1273 continue
1274 name = ''.join(c.data for c
1274 name = ''.join(c.data for c
1275 in e.getElementsByTagName('name')[0].childNodes
1275 in e.getElementsByTagName('name')[0].childNodes
1276 if c.nodeType == c.TEXT_NODE)
1276 if c.nodeType == c.TEXT_NODE)
1277 paths.append(name.encode('utf-8'))
1277 paths.append(name.encode('utf-8'))
1278 return paths
1278 return paths
1279
1279
1280 def filedata(self, name):
1280 def filedata(self, name):
1281 return self._svncommand(['cat'], name)[0]
1281 return self._svncommand(['cat'], name)[0]
1282
1282
1283
1283
1284 class gitsubrepo(abstractsubrepo):
1284 class gitsubrepo(abstractsubrepo):
1285 def __init__(self, ctx, path, state):
1285 def __init__(self, ctx, path, state):
1286 super(gitsubrepo, self).__init__(ctx, path)
1286 super(gitsubrepo, self).__init__(ctx, path)
1287 self._state = state
1287 self._state = state
1288 self._abspath = ctx.repo().wjoin(path)
1288 self._abspath = ctx.repo().wjoin(path)
1289 self._subparent = ctx.repo()
1289 self._subparent = ctx.repo()
1290 self._ensuregit()
1290 self._ensuregit()
1291
1291
1292 def _ensuregit(self):
1292 def _ensuregit(self):
1293 try:
1293 try:
1294 self._gitexecutable = 'git'
1294 self._gitexecutable = 'git'
1295 out, err = self._gitnodir(['--version'])
1295 out, err = self._gitnodir(['--version'])
1296 except OSError as e:
1296 except OSError as e:
1297 if e.errno != 2 or os.name != 'nt':
1297 genericerror = _("error executing git for subrepo '%s': %s")
1298 raise
1298 notfoundhint = _("check git is installed and in your PATH")
1299 self._gitexecutable = 'git.cmd'
1299 if e.errno != errno.ENOENT:
1300 out, err = self._gitnodir(['--version'])
1300 raise error.Abort(genericerror % (self._path, e.strerror))
1301 elif os.name == 'nt':
1302 try:
1303 self._gitexecutable = 'git.cmd'
1304 out, err = self._gitnodir(['--version'])
1305 except OSError as e2:
1306 if e2.errno == errno.ENOENT:
1307 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1308 " for subrepo '%s'") % self._path,
1309 hint=notfoundhint)
1310 else:
1311 raise error.Abort(genericerror % (self._path,
1312 e2.strerror))
1313 else:
1314 raise error.Abort(_("couldn't find git for subrepo '%s'")
1315 % self._path, hint=notfoundhint)
1301 versionstatus = self._checkversion(out)
1316 versionstatus = self._checkversion(out)
1302 if versionstatus == 'unknown':
1317 if versionstatus == 'unknown':
1303 self.ui.warn(_('cannot retrieve git version\n'))
1318 self.ui.warn(_('cannot retrieve git version\n'))
1304 elif versionstatus == 'abort':
1319 elif versionstatus == 'abort':
1305 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1320 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1306 elif versionstatus == 'warning':
1321 elif versionstatus == 'warning':
1307 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1322 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1308
1323
1309 @staticmethod
1324 @staticmethod
1310 def _gitversion(out):
1325 def _gitversion(out):
1311 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
1326 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
1312 if m:
1327 if m:
1313 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1328 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1314
1329
1315 m = re.search(r'^git version (\d+)\.(\d+)', out)
1330 m = re.search(r'^git version (\d+)\.(\d+)', out)
1316 if m:
1331 if m:
1317 return (int(m.group(1)), int(m.group(2)), 0)
1332 return (int(m.group(1)), int(m.group(2)), 0)
1318
1333
1319 return -1
1334 return -1
1320
1335
1321 @staticmethod
1336 @staticmethod
1322 def _checkversion(out):
1337 def _checkversion(out):
1323 '''ensure git version is new enough
1338 '''ensure git version is new enough
1324
1339
1325 >>> _checkversion = gitsubrepo._checkversion
1340 >>> _checkversion = gitsubrepo._checkversion
1326 >>> _checkversion('git version 1.6.0')
1341 >>> _checkversion('git version 1.6.0')
1327 'ok'
1342 'ok'
1328 >>> _checkversion('git version 1.8.5')
1343 >>> _checkversion('git version 1.8.5')
1329 'ok'
1344 'ok'
1330 >>> _checkversion('git version 1.4.0')
1345 >>> _checkversion('git version 1.4.0')
1331 'abort'
1346 'abort'
1332 >>> _checkversion('git version 1.5.0')
1347 >>> _checkversion('git version 1.5.0')
1333 'warning'
1348 'warning'
1334 >>> _checkversion('git version 1.9-rc0')
1349 >>> _checkversion('git version 1.9-rc0')
1335 'ok'
1350 'ok'
1336 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1351 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1337 'ok'
1352 'ok'
1338 >>> _checkversion('git version 1.9.0.GIT')
1353 >>> _checkversion('git version 1.9.0.GIT')
1339 'ok'
1354 'ok'
1340 >>> _checkversion('git version 12345')
1355 >>> _checkversion('git version 12345')
1341 'unknown'
1356 'unknown'
1342 >>> _checkversion('no')
1357 >>> _checkversion('no')
1343 'unknown'
1358 'unknown'
1344 '''
1359 '''
1345 version = gitsubrepo._gitversion(out)
1360 version = gitsubrepo._gitversion(out)
1346 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1361 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1347 # despite the docstring comment. For now, error on 1.4.0, warn on
1362 # despite the docstring comment. For now, error on 1.4.0, warn on
1348 # 1.5.0 but attempt to continue.
1363 # 1.5.0 but attempt to continue.
1349 if version == -1:
1364 if version == -1:
1350 return 'unknown'
1365 return 'unknown'
1351 if version < (1, 5, 0):
1366 if version < (1, 5, 0):
1352 return 'abort'
1367 return 'abort'
1353 elif version < (1, 6, 0):
1368 elif version < (1, 6, 0):
1354 return 'warning'
1369 return 'warning'
1355 return 'ok'
1370 return 'ok'
1356
1371
1357 def _gitcommand(self, commands, env=None, stream=False):
1372 def _gitcommand(self, commands, env=None, stream=False):
1358 return self._gitdir(commands, env=env, stream=stream)[0]
1373 return self._gitdir(commands, env=env, stream=stream)[0]
1359
1374
1360 def _gitdir(self, commands, env=None, stream=False):
1375 def _gitdir(self, commands, env=None, stream=False):
1361 return self._gitnodir(commands, env=env, stream=stream,
1376 return self._gitnodir(commands, env=env, stream=stream,
1362 cwd=self._abspath)
1377 cwd=self._abspath)
1363
1378
1364 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1379 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1365 """Calls the git command
1380 """Calls the git command
1366
1381
1367 The methods tries to call the git command. versions prior to 1.6.0
1382 The methods tries to call the git command. versions prior to 1.6.0
1368 are not supported and very probably fail.
1383 are not supported and very probably fail.
1369 """
1384 """
1370 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1385 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1371 # unless ui.quiet is set, print git's stderr,
1386 # unless ui.quiet is set, print git's stderr,
1372 # which is mostly progress and useful info
1387 # which is mostly progress and useful info
1373 errpipe = None
1388 errpipe = None
1374 if self.ui.quiet:
1389 if self.ui.quiet:
1375 errpipe = open(os.devnull, 'w')
1390 errpipe = open(os.devnull, 'w')
1376 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1391 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1377 cwd=cwd, env=env, close_fds=util.closefds,
1392 cwd=cwd, env=env, close_fds=util.closefds,
1378 stdout=subprocess.PIPE, stderr=errpipe)
1393 stdout=subprocess.PIPE, stderr=errpipe)
1379 if stream:
1394 if stream:
1380 return p.stdout, None
1395 return p.stdout, None
1381
1396
1382 retdata = p.stdout.read().strip()
1397 retdata = p.stdout.read().strip()
1383 # wait for the child to exit to avoid race condition.
1398 # wait for the child to exit to avoid race condition.
1384 p.wait()
1399 p.wait()
1385
1400
1386 if p.returncode != 0 and p.returncode != 1:
1401 if p.returncode != 0 and p.returncode != 1:
1387 # there are certain error codes that are ok
1402 # there are certain error codes that are ok
1388 command = commands[0]
1403 command = commands[0]
1389 if command in ('cat-file', 'symbolic-ref'):
1404 if command in ('cat-file', 'symbolic-ref'):
1390 return retdata, p.returncode
1405 return retdata, p.returncode
1391 # for all others, abort
1406 # for all others, abort
1392 raise error.Abort('git %s error %d in %s' %
1407 raise error.Abort('git %s error %d in %s' %
1393 (command, p.returncode, self._relpath))
1408 (command, p.returncode, self._relpath))
1394
1409
1395 return retdata, p.returncode
1410 return retdata, p.returncode
1396
1411
1397 def _gitmissing(self):
1412 def _gitmissing(self):
1398 return not self.wvfs.exists('.git')
1413 return not self.wvfs.exists('.git')
1399
1414
1400 def _gitstate(self):
1415 def _gitstate(self):
1401 return self._gitcommand(['rev-parse', 'HEAD'])
1416 return self._gitcommand(['rev-parse', 'HEAD'])
1402
1417
1403 def _gitcurrentbranch(self):
1418 def _gitcurrentbranch(self):
1404 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1419 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1405 if err:
1420 if err:
1406 current = None
1421 current = None
1407 return current
1422 return current
1408
1423
1409 def _gitremote(self, remote):
1424 def _gitremote(self, remote):
1410 out = self._gitcommand(['remote', 'show', '-n', remote])
1425 out = self._gitcommand(['remote', 'show', '-n', remote])
1411 line = out.split('\n')[1]
1426 line = out.split('\n')[1]
1412 i = line.index('URL: ') + len('URL: ')
1427 i = line.index('URL: ') + len('URL: ')
1413 return line[i:]
1428 return line[i:]
1414
1429
1415 def _githavelocally(self, revision):
1430 def _githavelocally(self, revision):
1416 out, code = self._gitdir(['cat-file', '-e', revision])
1431 out, code = self._gitdir(['cat-file', '-e', revision])
1417 return code == 0
1432 return code == 0
1418
1433
1419 def _gitisancestor(self, r1, r2):
1434 def _gitisancestor(self, r1, r2):
1420 base = self._gitcommand(['merge-base', r1, r2])
1435 base = self._gitcommand(['merge-base', r1, r2])
1421 return base == r1
1436 return base == r1
1422
1437
1423 def _gitisbare(self):
1438 def _gitisbare(self):
1424 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1439 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1425
1440
1426 def _gitupdatestat(self):
1441 def _gitupdatestat(self):
1427 """This must be run before git diff-index.
1442 """This must be run before git diff-index.
1428 diff-index only looks at changes to file stat;
1443 diff-index only looks at changes to file stat;
1429 this command looks at file contents and updates the stat."""
1444 this command looks at file contents and updates the stat."""
1430 self._gitcommand(['update-index', '-q', '--refresh'])
1445 self._gitcommand(['update-index', '-q', '--refresh'])
1431
1446
1432 def _gitbranchmap(self):
1447 def _gitbranchmap(self):
1433 '''returns 2 things:
1448 '''returns 2 things:
1434 a map from git branch to revision
1449 a map from git branch to revision
1435 a map from revision to branches'''
1450 a map from revision to branches'''
1436 branch2rev = {}
1451 branch2rev = {}
1437 rev2branch = {}
1452 rev2branch = {}
1438
1453
1439 out = self._gitcommand(['for-each-ref', '--format',
1454 out = self._gitcommand(['for-each-ref', '--format',
1440 '%(objectname) %(refname)'])
1455 '%(objectname) %(refname)'])
1441 for line in out.split('\n'):
1456 for line in out.split('\n'):
1442 revision, ref = line.split(' ')
1457 revision, ref = line.split(' ')
1443 if (not ref.startswith('refs/heads/') and
1458 if (not ref.startswith('refs/heads/') and
1444 not ref.startswith('refs/remotes/')):
1459 not ref.startswith('refs/remotes/')):
1445 continue
1460 continue
1446 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1461 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1447 continue # ignore remote/HEAD redirects
1462 continue # ignore remote/HEAD redirects
1448 branch2rev[ref] = revision
1463 branch2rev[ref] = revision
1449 rev2branch.setdefault(revision, []).append(ref)
1464 rev2branch.setdefault(revision, []).append(ref)
1450 return branch2rev, rev2branch
1465 return branch2rev, rev2branch
1451
1466
1452 def _gittracking(self, branches):
1467 def _gittracking(self, branches):
1453 'return map of remote branch to local tracking branch'
1468 'return map of remote branch to local tracking branch'
1454 # assumes no more than one local tracking branch for each remote
1469 # assumes no more than one local tracking branch for each remote
1455 tracking = {}
1470 tracking = {}
1456 for b in branches:
1471 for b in branches:
1457 if b.startswith('refs/remotes/'):
1472 if b.startswith('refs/remotes/'):
1458 continue
1473 continue
1459 bname = b.split('/', 2)[2]
1474 bname = b.split('/', 2)[2]
1460 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1475 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1461 if remote:
1476 if remote:
1462 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1477 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1463 tracking['refs/remotes/%s/%s' %
1478 tracking['refs/remotes/%s/%s' %
1464 (remote, ref.split('/', 2)[2])] = b
1479 (remote, ref.split('/', 2)[2])] = b
1465 return tracking
1480 return tracking
1466
1481
1467 def _abssource(self, source):
1482 def _abssource(self, source):
1468 if '://' not in source:
1483 if '://' not in source:
1469 # recognize the scp syntax as an absolute source
1484 # recognize the scp syntax as an absolute source
1470 colon = source.find(':')
1485 colon = source.find(':')
1471 if colon != -1 and '/' not in source[:colon]:
1486 if colon != -1 and '/' not in source[:colon]:
1472 return source
1487 return source
1473 self._subsource = source
1488 self._subsource = source
1474 return _abssource(self)
1489 return _abssource(self)
1475
1490
1476 def _fetch(self, source, revision):
1491 def _fetch(self, source, revision):
1477 if self._gitmissing():
1492 if self._gitmissing():
1478 source = self._abssource(source)
1493 source = self._abssource(source)
1479 self.ui.status(_('cloning subrepo %s from %s\n') %
1494 self.ui.status(_('cloning subrepo %s from %s\n') %
1480 (self._relpath, source))
1495 (self._relpath, source))
1481 self._gitnodir(['clone', source, self._abspath])
1496 self._gitnodir(['clone', source, self._abspath])
1482 if self._githavelocally(revision):
1497 if self._githavelocally(revision):
1483 return
1498 return
1484 self.ui.status(_('pulling subrepo %s from %s\n') %
1499 self.ui.status(_('pulling subrepo %s from %s\n') %
1485 (self._relpath, self._gitremote('origin')))
1500 (self._relpath, self._gitremote('origin')))
1486 # try only origin: the originally cloned repo
1501 # try only origin: the originally cloned repo
1487 self._gitcommand(['fetch'])
1502 self._gitcommand(['fetch'])
1488 if not self._githavelocally(revision):
1503 if not self._githavelocally(revision):
1489 raise error.Abort(_("revision %s does not exist in subrepo %s\n") %
1504 raise error.Abort(_("revision %s does not exist in subrepo %s\n") %
1490 (revision, self._relpath))
1505 (revision, self._relpath))
1491
1506
1492 @annotatesubrepoerror
1507 @annotatesubrepoerror
1493 def dirty(self, ignoreupdate=False):
1508 def dirty(self, ignoreupdate=False):
1494 if self._gitmissing():
1509 if self._gitmissing():
1495 return self._state[1] != ''
1510 return self._state[1] != ''
1496 if self._gitisbare():
1511 if self._gitisbare():
1497 return True
1512 return True
1498 if not ignoreupdate and self._state[1] != self._gitstate():
1513 if not ignoreupdate and self._state[1] != self._gitstate():
1499 # different version checked out
1514 # different version checked out
1500 return True
1515 return True
1501 # check for staged changes or modified files; ignore untracked files
1516 # check for staged changes or modified files; ignore untracked files
1502 self._gitupdatestat()
1517 self._gitupdatestat()
1503 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1518 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1504 return code == 1
1519 return code == 1
1505
1520
1506 def basestate(self):
1521 def basestate(self):
1507 return self._gitstate()
1522 return self._gitstate()
1508
1523
1509 @annotatesubrepoerror
1524 @annotatesubrepoerror
1510 def get(self, state, overwrite=False):
1525 def get(self, state, overwrite=False):
1511 source, revision, kind = state
1526 source, revision, kind = state
1512 if not revision:
1527 if not revision:
1513 self.remove()
1528 self.remove()
1514 return
1529 return
1515 self._fetch(source, revision)
1530 self._fetch(source, revision)
1516 # if the repo was set to be bare, unbare it
1531 # if the repo was set to be bare, unbare it
1517 if self._gitisbare():
1532 if self._gitisbare():
1518 self._gitcommand(['config', 'core.bare', 'false'])
1533 self._gitcommand(['config', 'core.bare', 'false'])
1519 if self._gitstate() == revision:
1534 if self._gitstate() == revision:
1520 self._gitcommand(['reset', '--hard', 'HEAD'])
1535 self._gitcommand(['reset', '--hard', 'HEAD'])
1521 return
1536 return
1522 elif self._gitstate() == revision:
1537 elif self._gitstate() == revision:
1523 if overwrite:
1538 if overwrite:
1524 # first reset the index to unmark new files for commit, because
1539 # first reset the index to unmark new files for commit, because
1525 # reset --hard will otherwise throw away files added for commit,
1540 # reset --hard will otherwise throw away files added for commit,
1526 # not just unmark them.
1541 # not just unmark them.
1527 self._gitcommand(['reset', 'HEAD'])
1542 self._gitcommand(['reset', 'HEAD'])
1528 self._gitcommand(['reset', '--hard', 'HEAD'])
1543 self._gitcommand(['reset', '--hard', 'HEAD'])
1529 return
1544 return
1530 branch2rev, rev2branch = self._gitbranchmap()
1545 branch2rev, rev2branch = self._gitbranchmap()
1531
1546
1532 def checkout(args):
1547 def checkout(args):
1533 cmd = ['checkout']
1548 cmd = ['checkout']
1534 if overwrite:
1549 if overwrite:
1535 # first reset the index to unmark new files for commit, because
1550 # first reset the index to unmark new files for commit, because
1536 # the -f option will otherwise throw away files added for
1551 # the -f option will otherwise throw away files added for
1537 # commit, not just unmark them.
1552 # commit, not just unmark them.
1538 self._gitcommand(['reset', 'HEAD'])
1553 self._gitcommand(['reset', 'HEAD'])
1539 cmd.append('-f')
1554 cmd.append('-f')
1540 self._gitcommand(cmd + args)
1555 self._gitcommand(cmd + args)
1541 _sanitize(self.ui, self.wvfs, '.git')
1556 _sanitize(self.ui, self.wvfs, '.git')
1542
1557
1543 def rawcheckout():
1558 def rawcheckout():
1544 # no branch to checkout, check it out with no branch
1559 # no branch to checkout, check it out with no branch
1545 self.ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1560 self.ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1546 self._relpath)
1561 self._relpath)
1547 self.ui.warn(_('check out a git branch if you intend '
1562 self.ui.warn(_('check out a git branch if you intend '
1548 'to make changes\n'))
1563 'to make changes\n'))
1549 checkout(['-q', revision])
1564 checkout(['-q', revision])
1550
1565
1551 if revision not in rev2branch:
1566 if revision not in rev2branch:
1552 rawcheckout()
1567 rawcheckout()
1553 return
1568 return
1554 branches = rev2branch[revision]
1569 branches = rev2branch[revision]
1555 firstlocalbranch = None
1570 firstlocalbranch = None
1556 for b in branches:
1571 for b in branches:
1557 if b == 'refs/heads/master':
1572 if b == 'refs/heads/master':
1558 # master trumps all other branches
1573 # master trumps all other branches
1559 checkout(['refs/heads/master'])
1574 checkout(['refs/heads/master'])
1560 return
1575 return
1561 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1576 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1562 firstlocalbranch = b
1577 firstlocalbranch = b
1563 if firstlocalbranch:
1578 if firstlocalbranch:
1564 checkout([firstlocalbranch])
1579 checkout([firstlocalbranch])
1565 return
1580 return
1566
1581
1567 tracking = self._gittracking(branch2rev.keys())
1582 tracking = self._gittracking(branch2rev.keys())
1568 # choose a remote branch already tracked if possible
1583 # choose a remote branch already tracked if possible
1569 remote = branches[0]
1584 remote = branches[0]
1570 if remote not in tracking:
1585 if remote not in tracking:
1571 for b in branches:
1586 for b in branches:
1572 if b in tracking:
1587 if b in tracking:
1573 remote = b
1588 remote = b
1574 break
1589 break
1575
1590
1576 if remote not in tracking:
1591 if remote not in tracking:
1577 # create a new local tracking branch
1592 # create a new local tracking branch
1578 local = remote.split('/', 3)[3]
1593 local = remote.split('/', 3)[3]
1579 checkout(['-b', local, remote])
1594 checkout(['-b', local, remote])
1580 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1595 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1581 # When updating to a tracked remote branch,
1596 # When updating to a tracked remote branch,
1582 # if the local tracking branch is downstream of it,
1597 # if the local tracking branch is downstream of it,
1583 # a normal `git pull` would have performed a "fast-forward merge"
1598 # a normal `git pull` would have performed a "fast-forward merge"
1584 # which is equivalent to updating the local branch to the remote.
1599 # which is equivalent to updating the local branch to the remote.
1585 # Since we are only looking at branching at update, we need to
1600 # Since we are only looking at branching at update, we need to
1586 # detect this situation and perform this action lazily.
1601 # detect this situation and perform this action lazily.
1587 if tracking[remote] != self._gitcurrentbranch():
1602 if tracking[remote] != self._gitcurrentbranch():
1588 checkout([tracking[remote]])
1603 checkout([tracking[remote]])
1589 self._gitcommand(['merge', '--ff', remote])
1604 self._gitcommand(['merge', '--ff', remote])
1590 _sanitize(self.ui, self.wvfs, '.git')
1605 _sanitize(self.ui, self.wvfs, '.git')
1591 else:
1606 else:
1592 # a real merge would be required, just checkout the revision
1607 # a real merge would be required, just checkout the revision
1593 rawcheckout()
1608 rawcheckout()
1594
1609
1595 @annotatesubrepoerror
1610 @annotatesubrepoerror
1596 def commit(self, text, user, date):
1611 def commit(self, text, user, date):
1597 if self._gitmissing():
1612 if self._gitmissing():
1598 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1613 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1599 cmd = ['commit', '-a', '-m', text]
1614 cmd = ['commit', '-a', '-m', text]
1600 env = os.environ.copy()
1615 env = os.environ.copy()
1601 if user:
1616 if user:
1602 cmd += ['--author', user]
1617 cmd += ['--author', user]
1603 if date:
1618 if date:
1604 # git's date parser silently ignores when seconds < 1e9
1619 # git's date parser silently ignores when seconds < 1e9
1605 # convert to ISO8601
1620 # convert to ISO8601
1606 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1621 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1607 '%Y-%m-%dT%H:%M:%S %1%2')
1622 '%Y-%m-%dT%H:%M:%S %1%2')
1608 self._gitcommand(cmd, env=env)
1623 self._gitcommand(cmd, env=env)
1609 # make sure commit works otherwise HEAD might not exist under certain
1624 # make sure commit works otherwise HEAD might not exist under certain
1610 # circumstances
1625 # circumstances
1611 return self._gitstate()
1626 return self._gitstate()
1612
1627
1613 @annotatesubrepoerror
1628 @annotatesubrepoerror
1614 def merge(self, state):
1629 def merge(self, state):
1615 source, revision, kind = state
1630 source, revision, kind = state
1616 self._fetch(source, revision)
1631 self._fetch(source, revision)
1617 base = self._gitcommand(['merge-base', revision, self._state[1]])
1632 base = self._gitcommand(['merge-base', revision, self._state[1]])
1618 self._gitupdatestat()
1633 self._gitupdatestat()
1619 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1634 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1620
1635
1621 def mergefunc():
1636 def mergefunc():
1622 if base == revision:
1637 if base == revision:
1623 self.get(state) # fast forward merge
1638 self.get(state) # fast forward merge
1624 elif base != self._state[1]:
1639 elif base != self._state[1]:
1625 self._gitcommand(['merge', '--no-commit', revision])
1640 self._gitcommand(['merge', '--no-commit', revision])
1626 _sanitize(self.ui, self.wvfs, '.git')
1641 _sanitize(self.ui, self.wvfs, '.git')
1627
1642
1628 if self.dirty():
1643 if self.dirty():
1629 if self._gitstate() != revision:
1644 if self._gitstate() != revision:
1630 dirty = self._gitstate() == self._state[1] or code != 0
1645 dirty = self._gitstate() == self._state[1] or code != 0
1631 if _updateprompt(self.ui, self, dirty,
1646 if _updateprompt(self.ui, self, dirty,
1632 self._state[1][:7], revision[:7]):
1647 self._state[1][:7], revision[:7]):
1633 mergefunc()
1648 mergefunc()
1634 else:
1649 else:
1635 mergefunc()
1650 mergefunc()
1636
1651
1637 @annotatesubrepoerror
1652 @annotatesubrepoerror
1638 def push(self, opts):
1653 def push(self, opts):
1639 force = opts.get('force')
1654 force = opts.get('force')
1640
1655
1641 if not self._state[1]:
1656 if not self._state[1]:
1642 return True
1657 return True
1643 if self._gitmissing():
1658 if self._gitmissing():
1644 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1659 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1645 # if a branch in origin contains the revision, nothing to do
1660 # if a branch in origin contains the revision, nothing to do
1646 branch2rev, rev2branch = self._gitbranchmap()
1661 branch2rev, rev2branch = self._gitbranchmap()
1647 if self._state[1] in rev2branch:
1662 if self._state[1] in rev2branch:
1648 for b in rev2branch[self._state[1]]:
1663 for b in rev2branch[self._state[1]]:
1649 if b.startswith('refs/remotes/origin/'):
1664 if b.startswith('refs/remotes/origin/'):
1650 return True
1665 return True
1651 for b, revision in branch2rev.iteritems():
1666 for b, revision in branch2rev.iteritems():
1652 if b.startswith('refs/remotes/origin/'):
1667 if b.startswith('refs/remotes/origin/'):
1653 if self._gitisancestor(self._state[1], revision):
1668 if self._gitisancestor(self._state[1], revision):
1654 return True
1669 return True
1655 # otherwise, try to push the currently checked out branch
1670 # otherwise, try to push the currently checked out branch
1656 cmd = ['push']
1671 cmd = ['push']
1657 if force:
1672 if force:
1658 cmd.append('--force')
1673 cmd.append('--force')
1659
1674
1660 current = self._gitcurrentbranch()
1675 current = self._gitcurrentbranch()
1661 if current:
1676 if current:
1662 # determine if the current branch is even useful
1677 # determine if the current branch is even useful
1663 if not self._gitisancestor(self._state[1], current):
1678 if not self._gitisancestor(self._state[1], current):
1664 self.ui.warn(_('unrelated git branch checked out '
1679 self.ui.warn(_('unrelated git branch checked out '
1665 'in subrepo %s\n') % self._relpath)
1680 'in subrepo %s\n') % self._relpath)
1666 return False
1681 return False
1667 self.ui.status(_('pushing branch %s of subrepo %s\n') %
1682 self.ui.status(_('pushing branch %s of subrepo %s\n') %
1668 (current.split('/', 2)[2], self._relpath))
1683 (current.split('/', 2)[2], self._relpath))
1669 ret = self._gitdir(cmd + ['origin', current])
1684 ret = self._gitdir(cmd + ['origin', current])
1670 return ret[1] == 0
1685 return ret[1] == 0
1671 else:
1686 else:
1672 self.ui.warn(_('no branch checked out in subrepo %s\n'
1687 self.ui.warn(_('no branch checked out in subrepo %s\n'
1673 'cannot push revision %s\n') %
1688 'cannot push revision %s\n') %
1674 (self._relpath, self._state[1]))
1689 (self._relpath, self._state[1]))
1675 return False
1690 return False
1676
1691
1677 @annotatesubrepoerror
1692 @annotatesubrepoerror
1678 def add(self, ui, match, prefix, explicitonly, **opts):
1693 def add(self, ui, match, prefix, explicitonly, **opts):
1679 if self._gitmissing():
1694 if self._gitmissing():
1680 return []
1695 return []
1681
1696
1682 (modified, added, removed,
1697 (modified, added, removed,
1683 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1698 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1684 clean=True)
1699 clean=True)
1685
1700
1686 tracked = set()
1701 tracked = set()
1687 # dirstates 'amn' warn, 'r' is added again
1702 # dirstates 'amn' warn, 'r' is added again
1688 for l in (modified, added, deleted, clean):
1703 for l in (modified, added, deleted, clean):
1689 tracked.update(l)
1704 tracked.update(l)
1690
1705
1691 # Unknown files not of interest will be rejected by the matcher
1706 # Unknown files not of interest will be rejected by the matcher
1692 files = unknown
1707 files = unknown
1693 files.extend(match.files())
1708 files.extend(match.files())
1694
1709
1695 rejected = []
1710 rejected = []
1696
1711
1697 files = [f for f in sorted(set(files)) if match(f)]
1712 files = [f for f in sorted(set(files)) if match(f)]
1698 for f in files:
1713 for f in files:
1699 exact = match.exact(f)
1714 exact = match.exact(f)
1700 command = ["add"]
1715 command = ["add"]
1701 if exact:
1716 if exact:
1702 command.append("-f") #should be added, even if ignored
1717 command.append("-f") #should be added, even if ignored
1703 if ui.verbose or not exact:
1718 if ui.verbose or not exact:
1704 ui.status(_('adding %s\n') % match.rel(f))
1719 ui.status(_('adding %s\n') % match.rel(f))
1705
1720
1706 if f in tracked: # hg prints 'adding' even if already tracked
1721 if f in tracked: # hg prints 'adding' even if already tracked
1707 if exact:
1722 if exact:
1708 rejected.append(f)
1723 rejected.append(f)
1709 continue
1724 continue
1710 if not opts.get('dry_run'):
1725 if not opts.get('dry_run'):
1711 self._gitcommand(command + [f])
1726 self._gitcommand(command + [f])
1712
1727
1713 for f in rejected:
1728 for f in rejected:
1714 ui.warn(_("%s already tracked!\n") % match.abs(f))
1729 ui.warn(_("%s already tracked!\n") % match.abs(f))
1715
1730
1716 return rejected
1731 return rejected
1717
1732
1718 @annotatesubrepoerror
1733 @annotatesubrepoerror
1719 def remove(self):
1734 def remove(self):
1720 if self._gitmissing():
1735 if self._gitmissing():
1721 return
1736 return
1722 if self.dirty():
1737 if self.dirty():
1723 self.ui.warn(_('not removing repo %s because '
1738 self.ui.warn(_('not removing repo %s because '
1724 'it has changes.\n') % self._relpath)
1739 'it has changes.\n') % self._relpath)
1725 return
1740 return
1726 # we can't fully delete the repository as it may contain
1741 # we can't fully delete the repository as it may contain
1727 # local-only history
1742 # local-only history
1728 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1743 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1729 self._gitcommand(['config', 'core.bare', 'true'])
1744 self._gitcommand(['config', 'core.bare', 'true'])
1730 for f, kind in self.wvfs.readdir():
1745 for f, kind in self.wvfs.readdir():
1731 if f == '.git':
1746 if f == '.git':
1732 continue
1747 continue
1733 if kind == stat.S_IFDIR:
1748 if kind == stat.S_IFDIR:
1734 self.wvfs.rmtree(f)
1749 self.wvfs.rmtree(f)
1735 else:
1750 else:
1736 self.wvfs.unlink(f)
1751 self.wvfs.unlink(f)
1737
1752
1738 def archive(self, archiver, prefix, match=None):
1753 def archive(self, archiver, prefix, match=None):
1739 total = 0
1754 total = 0
1740 source, revision = self._state
1755 source, revision = self._state
1741 if not revision:
1756 if not revision:
1742 return total
1757 return total
1743 self._fetch(source, revision)
1758 self._fetch(source, revision)
1744
1759
1745 # Parse git's native archive command.
1760 # Parse git's native archive command.
1746 # This should be much faster than manually traversing the trees
1761 # This should be much faster than manually traversing the trees
1747 # and objects with many subprocess calls.
1762 # and objects with many subprocess calls.
1748 tarstream = self._gitcommand(['archive', revision], stream=True)
1763 tarstream = self._gitcommand(['archive', revision], stream=True)
1749 tar = tarfile.open(fileobj=tarstream, mode='r|')
1764 tar = tarfile.open(fileobj=tarstream, mode='r|')
1750 relpath = subrelpath(self)
1765 relpath = subrelpath(self)
1751 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1766 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1752 for i, info in enumerate(tar):
1767 for i, info in enumerate(tar):
1753 if info.isdir():
1768 if info.isdir():
1754 continue
1769 continue
1755 if match and not match(info.name):
1770 if match and not match(info.name):
1756 continue
1771 continue
1757 if info.issym():
1772 if info.issym():
1758 data = info.linkname
1773 data = info.linkname
1759 else:
1774 else:
1760 data = tar.extractfile(info).read()
1775 data = tar.extractfile(info).read()
1761 archiver.addfile(prefix + self._path + '/' + info.name,
1776 archiver.addfile(prefix + self._path + '/' + info.name,
1762 info.mode, info.issym(), data)
1777 info.mode, info.issym(), data)
1763 total += 1
1778 total += 1
1764 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1779 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1765 unit=_('files'))
1780 unit=_('files'))
1766 self.ui.progress(_('archiving (%s)') % relpath, None)
1781 self.ui.progress(_('archiving (%s)') % relpath, None)
1767 return total
1782 return total
1768
1783
1769
1784
1770 @annotatesubrepoerror
1785 @annotatesubrepoerror
1771 def cat(self, match, prefix, **opts):
1786 def cat(self, match, prefix, **opts):
1772 rev = self._state[1]
1787 rev = self._state[1]
1773 if match.anypats():
1788 if match.anypats():
1774 return 1 #No support for include/exclude yet
1789 return 1 #No support for include/exclude yet
1775
1790
1776 if not match.files():
1791 if not match.files():
1777 return 1
1792 return 1
1778
1793
1779 for f in match.files():
1794 for f in match.files():
1780 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1795 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1781 fp = cmdutil.makefileobj(self._subparent, opts.get('output'),
1796 fp = cmdutil.makefileobj(self._subparent, opts.get('output'),
1782 self._ctx.node(),
1797 self._ctx.node(),
1783 pathname=self.wvfs.reljoin(prefix, f))
1798 pathname=self.wvfs.reljoin(prefix, f))
1784 fp.write(output)
1799 fp.write(output)
1785 fp.close()
1800 fp.close()
1786 return 0
1801 return 0
1787
1802
1788
1803
1789 @annotatesubrepoerror
1804 @annotatesubrepoerror
1790 def status(self, rev2, **opts):
1805 def status(self, rev2, **opts):
1791 rev1 = self._state[1]
1806 rev1 = self._state[1]
1792 if self._gitmissing() or not rev1:
1807 if self._gitmissing() or not rev1:
1793 # if the repo is missing, return no results
1808 # if the repo is missing, return no results
1794 return scmutil.status([], [], [], [], [], [], [])
1809 return scmutil.status([], [], [], [], [], [], [])
1795 modified, added, removed = [], [], []
1810 modified, added, removed = [], [], []
1796 self._gitupdatestat()
1811 self._gitupdatestat()
1797 if rev2:
1812 if rev2:
1798 command = ['diff-tree', '-r', rev1, rev2]
1813 command = ['diff-tree', '-r', rev1, rev2]
1799 else:
1814 else:
1800 command = ['diff-index', rev1]
1815 command = ['diff-index', rev1]
1801 out = self._gitcommand(command)
1816 out = self._gitcommand(command)
1802 for line in out.split('\n'):
1817 for line in out.split('\n'):
1803 tab = line.find('\t')
1818 tab = line.find('\t')
1804 if tab == -1:
1819 if tab == -1:
1805 continue
1820 continue
1806 status, f = line[tab - 1], line[tab + 1:]
1821 status, f = line[tab - 1], line[tab + 1:]
1807 if status == 'M':
1822 if status == 'M':
1808 modified.append(f)
1823 modified.append(f)
1809 elif status == 'A':
1824 elif status == 'A':
1810 added.append(f)
1825 added.append(f)
1811 elif status == 'D':
1826 elif status == 'D':
1812 removed.append(f)
1827 removed.append(f)
1813
1828
1814 deleted, unknown, ignored, clean = [], [], [], []
1829 deleted, unknown, ignored, clean = [], [], [], []
1815
1830
1816 command = ['status', '--porcelain', '-z']
1831 command = ['status', '--porcelain', '-z']
1817 if opts.get('unknown'):
1832 if opts.get('unknown'):
1818 command += ['--untracked-files=all']
1833 command += ['--untracked-files=all']
1819 if opts.get('ignored'):
1834 if opts.get('ignored'):
1820 command += ['--ignored']
1835 command += ['--ignored']
1821 out = self._gitcommand(command)
1836 out = self._gitcommand(command)
1822
1837
1823 changedfiles = set()
1838 changedfiles = set()
1824 changedfiles.update(modified)
1839 changedfiles.update(modified)
1825 changedfiles.update(added)
1840 changedfiles.update(added)
1826 changedfiles.update(removed)
1841 changedfiles.update(removed)
1827 for line in out.split('\0'):
1842 for line in out.split('\0'):
1828 if not line:
1843 if not line:
1829 continue
1844 continue
1830 st = line[0:2]
1845 st = line[0:2]
1831 #moves and copies show 2 files on one line
1846 #moves and copies show 2 files on one line
1832 if line.find('\0') >= 0:
1847 if line.find('\0') >= 0:
1833 filename1, filename2 = line[3:].split('\0')
1848 filename1, filename2 = line[3:].split('\0')
1834 else:
1849 else:
1835 filename1 = line[3:]
1850 filename1 = line[3:]
1836 filename2 = None
1851 filename2 = None
1837
1852
1838 changedfiles.add(filename1)
1853 changedfiles.add(filename1)
1839 if filename2:
1854 if filename2:
1840 changedfiles.add(filename2)
1855 changedfiles.add(filename2)
1841
1856
1842 if st == '??':
1857 if st == '??':
1843 unknown.append(filename1)
1858 unknown.append(filename1)
1844 elif st == '!!':
1859 elif st == '!!':
1845 ignored.append(filename1)
1860 ignored.append(filename1)
1846
1861
1847 if opts.get('clean'):
1862 if opts.get('clean'):
1848 out = self._gitcommand(['ls-files'])
1863 out = self._gitcommand(['ls-files'])
1849 for f in out.split('\n'):
1864 for f in out.split('\n'):
1850 if not f in changedfiles:
1865 if not f in changedfiles:
1851 clean.append(f)
1866 clean.append(f)
1852
1867
1853 return scmutil.status(modified, added, removed, deleted,
1868 return scmutil.status(modified, added, removed, deleted,
1854 unknown, ignored, clean)
1869 unknown, ignored, clean)
1855
1870
1856 @annotatesubrepoerror
1871 @annotatesubrepoerror
1857 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1872 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1858 node1 = self._state[1]
1873 node1 = self._state[1]
1859 cmd = ['diff']
1874 cmd = ['diff']
1860 if opts['stat']:
1875 if opts['stat']:
1861 cmd.append('--stat')
1876 cmd.append('--stat')
1862 else:
1877 else:
1863 # for Git, this also implies '-p'
1878 # for Git, this also implies '-p'
1864 cmd.append('-U%d' % diffopts.context)
1879 cmd.append('-U%d' % diffopts.context)
1865
1880
1866 gitprefix = self.wvfs.reljoin(prefix, self._path)
1881 gitprefix = self.wvfs.reljoin(prefix, self._path)
1867
1882
1868 if diffopts.noprefix:
1883 if diffopts.noprefix:
1869 cmd.extend(['--src-prefix=%s/' % gitprefix,
1884 cmd.extend(['--src-prefix=%s/' % gitprefix,
1870 '--dst-prefix=%s/' % gitprefix])
1885 '--dst-prefix=%s/' % gitprefix])
1871 else:
1886 else:
1872 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1887 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1873 '--dst-prefix=b/%s/' % gitprefix])
1888 '--dst-prefix=b/%s/' % gitprefix])
1874
1889
1875 if diffopts.ignorews:
1890 if diffopts.ignorews:
1876 cmd.append('--ignore-all-space')
1891 cmd.append('--ignore-all-space')
1877 if diffopts.ignorewsamount:
1892 if diffopts.ignorewsamount:
1878 cmd.append('--ignore-space-change')
1893 cmd.append('--ignore-space-change')
1879 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1894 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1880 and diffopts.ignoreblanklines:
1895 and diffopts.ignoreblanklines:
1881 cmd.append('--ignore-blank-lines')
1896 cmd.append('--ignore-blank-lines')
1882
1897
1883 cmd.append(node1)
1898 cmd.append(node1)
1884 if node2:
1899 if node2:
1885 cmd.append(node2)
1900 cmd.append(node2)
1886
1901
1887 output = ""
1902 output = ""
1888 if match.always():
1903 if match.always():
1889 output += self._gitcommand(cmd) + '\n'
1904 output += self._gitcommand(cmd) + '\n'
1890 else:
1905 else:
1891 st = self.status(node2)[:3]
1906 st = self.status(node2)[:3]
1892 files = [f for sublist in st for f in sublist]
1907 files = [f for sublist in st for f in sublist]
1893 for f in files:
1908 for f in files:
1894 if match(f):
1909 if match(f):
1895 output += self._gitcommand(cmd + ['--', f]) + '\n'
1910 output += self._gitcommand(cmd + ['--', f]) + '\n'
1896
1911
1897 if output.strip():
1912 if output.strip():
1898 ui.write(output)
1913 ui.write(output)
1899
1914
1900 @annotatesubrepoerror
1915 @annotatesubrepoerror
1901 def revert(self, substate, *pats, **opts):
1916 def revert(self, substate, *pats, **opts):
1902 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1917 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1903 if not opts.get('no_backup'):
1918 if not opts.get('no_backup'):
1904 status = self.status(None)
1919 status = self.status(None)
1905 names = status.modified
1920 names = status.modified
1906 for name in names:
1921 for name in names:
1907 bakname = scmutil.origpath(self.ui, self._subparent, name)
1922 bakname = scmutil.origpath(self.ui, self._subparent, name)
1908 self.ui.note(_('saving current version of %s as %s\n') %
1923 self.ui.note(_('saving current version of %s as %s\n') %
1909 (name, bakname))
1924 (name, bakname))
1910 self.wvfs.rename(name, bakname)
1925 self.wvfs.rename(name, bakname)
1911
1926
1912 if not opts.get('dry_run'):
1927 if not opts.get('dry_run'):
1913 self.get(substate, overwrite=True)
1928 self.get(substate, overwrite=True)
1914 return []
1929 return []
1915
1930
1916 def shortid(self, revid):
1931 def shortid(self, revid):
1917 return revid[:7]
1932 return revid[:7]
1918
1933
1919 types = {
1934 types = {
1920 'hg': hgsubrepo,
1935 'hg': hgsubrepo,
1921 'svn': svnsubrepo,
1936 'svn': svnsubrepo,
1922 'git': gitsubrepo,
1937 'git': gitsubrepo,
1923 }
1938 }
General Comments 0
You need to be logged in to leave comments. Login now