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