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