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