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