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