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