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