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