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