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