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