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