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