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