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