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