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