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