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