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