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