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