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