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