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