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