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