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