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