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