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