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