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