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