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