##// END OF EJS Templates
pytype: drop the last inline type comment...
marmoute -
r52182:7bd7fcc7 default
parent child Browse files
Show More
@@ -1,256 +1,261 b''
1 # config.py - configuration parsing for Mercurial
1 # config.py - configuration parsing for Mercurial
2 #
2 #
3 # Copyright 2009 Olivia Mackall <olivia@selenic.com> and others
3 # Copyright 2009 Olivia Mackall <olivia@selenic.com> and others
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 errno
9 import errno
10 import os
10 import os
11
11
12 from typing import (
13 List,
14 Tuple,
15 )
16
12 from .i18n import _
17 from .i18n import _
13 from . import (
18 from . import (
14 encoding,
19 encoding,
15 error,
20 error,
16 util,
21 util,
17 )
22 )
18
23
19
24
20 class config:
25 class config:
21 def __init__(self, data=None):
26 def __init__(self, data=None):
22 self._current_source_level = 0
27 self._current_source_level = 0
23 self._data = {}
28 self._data = {}
24 self._unset = []
29 self._unset = []
25 if data:
30 if data:
26 for k in data._data:
31 for k in data._data:
27 self._data[k] = data[k].copy()
32 self._data[k] = data[k].copy()
28 self._current_source_level = data._current_source_level + 1
33 self._current_source_level = data._current_source_level + 1
29
34
30 def new_source(self):
35 def new_source(self):
31 """increment the source counter
36 """increment the source counter
32
37
33 This is used to define source priority when reading"""
38 This is used to define source priority when reading"""
34 self._current_source_level += 1
39 self._current_source_level += 1
35
40
36 def copy(self):
41 def copy(self):
37 return config(self)
42 return config(self)
38
43
39 def __contains__(self, section):
44 def __contains__(self, section):
40 return section in self._data
45 return section in self._data
41
46
42 def hasitem(self, section, item):
47 def hasitem(self, section, item):
43 return item in self._data.get(section, {})
48 return item in self._data.get(section, {})
44
49
45 def __getitem__(self, section):
50 def __getitem__(self, section):
46 return self._data.get(section, {})
51 return self._data.get(section, {})
47
52
48 def __iter__(self):
53 def __iter__(self):
49 for d in self.sections():
54 for d in self.sections():
50 yield d
55 yield d
51
56
52 def update(self, src):
57 def update(self, src):
53 current_level = self._current_source_level
58 current_level = self._current_source_level
54 current_level += 1
59 current_level += 1
55 max_level = self._current_source_level
60 max_level = self._current_source_level
56 for s, n in src._unset:
61 for s, n in src._unset:
57 ds = self._data.get(s, None)
62 ds = self._data.get(s, None)
58 if ds is not None and n in ds:
63 if ds is not None and n in ds:
59 self._data[s] = ds.preparewrite()
64 self._data[s] = ds.preparewrite()
60 del self._data[s][n]
65 del self._data[s][n]
61 for s in src:
66 for s in src:
62 ds = self._data.get(s, None)
67 ds = self._data.get(s, None)
63 if ds:
68 if ds:
64 self._data[s] = ds.preparewrite()
69 self._data[s] = ds.preparewrite()
65 else:
70 else:
66 self._data[s] = util.cowsortdict()
71 self._data[s] = util.cowsortdict()
67 for k, v in src._data[s].items():
72 for k, v in src._data[s].items():
68 value, source, level = v
73 value, source, level = v
69 level += current_level
74 level += current_level
70 max_level = max(level, current_level)
75 max_level = max(level, current_level)
71 self._data[s][k] = (value, source, level)
76 self._data[s][k] = (value, source, level)
72 self._current_source_level = max_level
77 self._current_source_level = max_level
73
78
74 def _get(self, section, item):
79 def _get(self, section, item):
75 return self._data.get(section, {}).get(item)
80 return self._data.get(section, {}).get(item)
76
81
77 def get(self, section, item, default=None):
82 def get(self, section, item, default=None):
78 result = self._get(section, item)
83 result = self._get(section, item)
79 if result is None:
84 if result is None:
80 return default
85 return default
81 return result[0]
86 return result[0]
82
87
83 def backup(self, section, key):
88 def backup(self, section, key):
84 """return a tuple allowing restore to reinstall a previous value
89 """return a tuple allowing restore to reinstall a previous value
85
90
86 The main reason we need it is because it handles the "no data" case.
91 The main reason we need it is because it handles the "no data" case.
87 """
92 """
88 try:
93 try:
89 item = self._data[section][key]
94 item = self._data[section][key]
90 except KeyError:
95 except KeyError:
91 return (section, key)
96 return (section, key)
92 else:
97 else:
93 return (section, key) + item
98 return (section, key) + item
94
99
95 def source(self, section, item):
100 def source(self, section, item):
96 result = self._get(section, item)
101 result = self._get(section, item)
97 if result is None:
102 if result is None:
98 return b""
103 return b""
99 return result[1]
104 return result[1]
100
105
101 def level(self, section, item):
106 def level(self, section, item):
102 result = self._get(section, item)
107 result = self._get(section, item)
103 if result is None:
108 if result is None:
104 return None
109 return None
105 return result[2]
110 return result[2]
106
111
107 def sections(self):
112 def sections(self):
108 return sorted(self._data.keys())
113 return sorted(self._data.keys())
109
114
110 def items(self, section):
115 def items(self, section: bytes) -> List[Tuple[bytes, bytes]]:
111 items = self._data.get(section, {}).items()
116 items = self._data.get(section, {}).items()
112 return [(k, v[0]) for (k, v) in items]
117 return [(k, v[0]) for (k, v) in items]
113
118
114 def set(self, section, item, value, source=b""):
119 def set(self, section, item, value, source=b""):
115 assert not isinstance(
120 assert not isinstance(
116 section, str
121 section, str
117 ), b'config section may not be unicode strings on Python 3'
122 ), b'config section may not be unicode strings on Python 3'
118 assert not isinstance(
123 assert not isinstance(
119 item, str
124 item, str
120 ), b'config item may not be unicode strings on Python 3'
125 ), b'config item may not be unicode strings on Python 3'
121 assert not isinstance(
126 assert not isinstance(
122 value, str
127 value, str
123 ), b'config values may not be unicode strings on Python 3'
128 ), b'config values may not be unicode strings on Python 3'
124 if section not in self:
129 if section not in self:
125 self._data[section] = util.cowsortdict()
130 self._data[section] = util.cowsortdict()
126 else:
131 else:
127 self._data[section] = self._data[section].preparewrite()
132 self._data[section] = self._data[section].preparewrite()
128 self._data[section][item] = (value, source, self._current_source_level)
133 self._data[section][item] = (value, source, self._current_source_level)
129
134
130 def alter(self, section, key, new_value):
135 def alter(self, section, key, new_value):
131 """alter a value without altering its source or level
136 """alter a value without altering its source or level
132
137
133 This method is meant to be used by `ui.fixconfig` only."""
138 This method is meant to be used by `ui.fixconfig` only."""
134 item = self._data[section][key]
139 item = self._data[section][key]
135 size = len(item)
140 size = len(item)
136 new_item = (new_value,) + item[1:]
141 new_item = (new_value,) + item[1:]
137 assert len(new_item) == size
142 assert len(new_item) == size
138 self._data[section][key] = new_item
143 self._data[section][key] = new_item
139
144
140 def restore(self, data):
145 def restore(self, data):
141 """restore data returned by self.backup"""
146 """restore data returned by self.backup"""
142 if len(data) != 2:
147 if len(data) != 2:
143 # restore old data
148 # restore old data
144 section, key = data[:2]
149 section, key = data[:2]
145 item = data[2:]
150 item = data[2:]
146 self._data[section] = self._data[section].preparewrite()
151 self._data[section] = self._data[section].preparewrite()
147 self._data[section][key] = item
152 self._data[section][key] = item
148 else:
153 else:
149 # no data before, remove everything
154 # no data before, remove everything
150 section, item = data
155 section, item = data
151 if section in self._data:
156 if section in self._data:
152 self._data[section].pop(item, None)
157 self._data[section].pop(item, None)
153
158
154 def parse(self, src, data, sections=None, remap=None, include=None):
159 def parse(self, src, data, sections=None, remap=None, include=None):
155 sectionre = util.re.compile(br'\[([^\[]+)\]')
160 sectionre = util.re.compile(br'\[([^\[]+)\]')
156 itemre = util.re.compile(br'([^=\s][^=]*?)\s*=\s*(.*\S|)')
161 itemre = util.re.compile(br'([^=\s][^=]*?)\s*=\s*(.*\S|)')
157 contre = util.re.compile(br'\s+(\S|\S.*\S)\s*$')
162 contre = util.re.compile(br'\s+(\S|\S.*\S)\s*$')
158 emptyre = util.re.compile(br'(;|#|\s*$)')
163 emptyre = util.re.compile(br'(;|#|\s*$)')
159 commentre = util.re.compile(br'(;|#)')
164 commentre = util.re.compile(br'(;|#)')
160 unsetre = util.re.compile(br'%unset\s+(\S+)')
165 unsetre = util.re.compile(br'%unset\s+(\S+)')
161 includere = util.re.compile(br'%include\s+(\S|\S.*\S)\s*$')
166 includere = util.re.compile(br'%include\s+(\S|\S.*\S)\s*$')
162 section = b""
167 section = b""
163 item = None
168 item = None
164 line = 0
169 line = 0
165 cont = False
170 cont = False
166
171
167 if remap:
172 if remap:
168 section = remap.get(section, section)
173 section = remap.get(section, section)
169
174
170 for l in data.splitlines(True):
175 for l in data.splitlines(True):
171 line += 1
176 line += 1
172 if line == 1 and l.startswith(b'\xef\xbb\xbf'):
177 if line == 1 and l.startswith(b'\xef\xbb\xbf'):
173 # Someone set us up the BOM
178 # Someone set us up the BOM
174 l = l[3:]
179 l = l[3:]
175 if cont:
180 if cont:
176 if commentre.match(l):
181 if commentre.match(l):
177 continue
182 continue
178 m = contre.match(l)
183 m = contre.match(l)
179 if m:
184 if m:
180 if sections and section not in sections:
185 if sections and section not in sections:
181 continue
186 continue
182 v = self.get(section, item) + b"\n" + m.group(1)
187 v = self.get(section, item) + b"\n" + m.group(1)
183 self.set(section, item, v, b"%s:%d" % (src, line))
188 self.set(section, item, v, b"%s:%d" % (src, line))
184 continue
189 continue
185 item = None
190 item = None
186 cont = False
191 cont = False
187 m = includere.match(l)
192 m = includere.match(l)
188
193
189 if m and include:
194 if m and include:
190 expanded = util.expandpath(m.group(1))
195 expanded = util.expandpath(m.group(1))
191 try:
196 try:
192 include(expanded, remap=remap, sections=sections)
197 include(expanded, remap=remap, sections=sections)
193 except IOError as inst:
198 except IOError as inst:
194 if inst.errno != errno.ENOENT:
199 if inst.errno != errno.ENOENT:
195 raise error.ConfigError(
200 raise error.ConfigError(
196 _(b"cannot include %s (%s)")
201 _(b"cannot include %s (%s)")
197 % (expanded, encoding.strtolocal(inst.strerror)),
202 % (expanded, encoding.strtolocal(inst.strerror)),
198 b"%s:%d" % (src, line),
203 b"%s:%d" % (src, line),
199 )
204 )
200 continue
205 continue
201 if emptyre.match(l):
206 if emptyre.match(l):
202 continue
207 continue
203 m = sectionre.match(l)
208 m = sectionre.match(l)
204 if m:
209 if m:
205 section = m.group(1)
210 section = m.group(1)
206 if remap:
211 if remap:
207 section = remap.get(section, section)
212 section = remap.get(section, section)
208 if section not in self:
213 if section not in self:
209 self._data[section] = util.cowsortdict()
214 self._data[section] = util.cowsortdict()
210 continue
215 continue
211 m = itemre.match(l)
216 m = itemre.match(l)
212 if m:
217 if m:
213 item = m.group(1)
218 item = m.group(1)
214 cont = True
219 cont = True
215 if sections and section not in sections:
220 if sections and section not in sections:
216 continue
221 continue
217 self.set(section, item, m.group(2), b"%s:%d" % (src, line))
222 self.set(section, item, m.group(2), b"%s:%d" % (src, line))
218 continue
223 continue
219 m = unsetre.match(l)
224 m = unsetre.match(l)
220 if m:
225 if m:
221 name = m.group(1)
226 name = m.group(1)
222 if sections and section not in sections:
227 if sections and section not in sections:
223 continue
228 continue
224 if self.get(section, name) is not None:
229 if self.get(section, name) is not None:
225 self._data[section] = self._data[section].preparewrite()
230 self._data[section] = self._data[section].preparewrite()
226 del self._data[section][name]
231 del self._data[section][name]
227 self._unset.append((section, name))
232 self._unset.append((section, name))
228 continue
233 continue
229
234
230 message = l.rstrip()
235 message = l.rstrip()
231 if l.startswith(b' '):
236 if l.startswith(b' '):
232 message = b"unexpected leading whitespace: %s" % message
237 message = b"unexpected leading whitespace: %s" % message
233 raise error.ConfigError(message, (b"%s:%d" % (src, line)))
238 raise error.ConfigError(message, (b"%s:%d" % (src, line)))
234
239
235 def read(self, path, fp=None, sections=None, remap=None):
240 def read(self, path, fp=None, sections=None, remap=None):
236 self.new_source()
241 self.new_source()
237 if not fp:
242 if not fp:
238 fp = util.posixfile(path, b'rb')
243 fp = util.posixfile(path, b'rb')
239 assert (
244 assert (
240 getattr(fp, 'mode', 'rb') == 'rb'
245 getattr(fp, 'mode', 'rb') == 'rb'
241 ), b'config files must be opened in binary mode, got fp=%r mode=%r' % (
246 ), b'config files must be opened in binary mode, got fp=%r mode=%r' % (
242 fp,
247 fp,
243 fp.mode,
248 fp.mode,
244 )
249 )
245
250
246 dir = os.path.dirname(path)
251 dir = os.path.dirname(path)
247
252
248 def include(rel, remap, sections):
253 def include(rel, remap, sections):
249 abs = os.path.normpath(os.path.join(dir, rel))
254 abs = os.path.normpath(os.path.join(dir, rel))
250 self.read(abs, remap=remap, sections=sections)
255 self.read(abs, remap=remap, sections=sections)
251 # anything after the include has a higher level
256 # anything after the include has a higher level
252 self.new_source()
257 self.new_source()
253
258
254 self.parse(
259 self.parse(
255 path, fp.read(), sections=sections, remap=remap, include=include
260 path, fp.read(), sections=sections, remap=remap, include=include
256 )
261 )
@@ -1,533 +1,533 b''
1 # subrepoutil.py - sub-repository operations and substate handling
1 # subrepoutil.py - sub-repository operations and substate handling
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 os
9 import os
10 import posixpath
10 import posixpath
11 import re
11 import re
12 import typing
12 import typing
13
13
14 from typing import (
14 from typing import (
15 Any,
15 Any,
16 Dict,
16 Dict,
17 List,
17 List,
18 Optional,
18 Optional,
19 Set,
19 Set,
20 Tuple,
20 Tuple,
21 )
21 )
22
22
23 from .i18n import _
23 from .i18n import _
24 from . import (
24 from . import (
25 config,
25 config,
26 error,
26 error,
27 filemerge,
27 filemerge,
28 pathutil,
28 pathutil,
29 phases,
29 phases,
30 util,
30 util,
31 )
31 )
32 from .utils import (
32 from .utils import (
33 stringutil,
33 stringutil,
34 urlutil,
34 urlutil,
35 )
35 )
36
36
37 # keeps pyflakes happy
37 # keeps pyflakes happy
38 assert [
38 assert [
39 Any,
39 Any,
40 Dict,
40 Dict,
41 List,
41 List,
42 Optional,
42 Optional,
43 Set,
43 Set,
44 Tuple,
44 Tuple,
45 ]
45 ]
46
46
47 nullstate = (b'', b'', b'empty')
47 nullstate = (b'', b'', b'empty')
48
48
49 if typing.TYPE_CHECKING:
49 if typing.TYPE_CHECKING:
50 from . import (
50 from . import (
51 context,
51 context,
52 localrepo,
52 localrepo,
53 match as matchmod,
53 match as matchmod,
54 scmutil,
54 scmutil,
55 subrepo,
55 subrepo,
56 ui as uimod,
56 ui as uimod,
57 )
57 )
58
58
59 # keeps pyflakes happy
59 # keeps pyflakes happy
60 assert [
60 assert [
61 context,
61 context,
62 localrepo,
62 localrepo,
63 matchmod,
63 matchmod,
64 scmutil,
64 scmutil,
65 subrepo,
65 subrepo,
66 uimod,
66 uimod,
67 ]
67 ]
68
68
69 Substate = Dict[bytes, Tuple[bytes, bytes, bytes]]
69 Substate = Dict[bytes, Tuple[bytes, bytes, bytes]]
70
70
71
71
72 def state(ctx: "context.changectx", ui: "uimod.ui") -> Substate:
72 def state(ctx: "context.changectx", ui: "uimod.ui") -> Substate:
73 """return a state dict, mapping subrepo paths configured in .hgsub
73 """return a state dict, mapping subrepo paths configured in .hgsub
74 to tuple: (source from .hgsub, revision from .hgsubstate, kind
74 to tuple: (source from .hgsub, revision from .hgsubstate, kind
75 (key in types dict))
75 (key in types dict))
76 """
76 """
77 p = config.config()
77 p: config.config = config.config()
78 repo = ctx.repo()
78 repo = ctx.repo()
79
79
80 def read(f, sections=None, remap=None):
80 def read(f, sections=None, remap=None):
81 if f in ctx:
81 if f in ctx:
82 try:
82 try:
83 data = ctx[f].data()
83 data = ctx[f].data()
84 except FileNotFoundError:
84 except FileNotFoundError:
85 # handle missing subrepo spec files as removed
85 # handle missing subrepo spec files as removed
86 ui.warn(
86 ui.warn(
87 _(b"warning: subrepo spec file \'%s\' not found\n")
87 _(b"warning: subrepo spec file \'%s\' not found\n")
88 % repo.pathto(f)
88 % repo.pathto(f)
89 )
89 )
90 return
90 return
91 p.parse(f, data, sections, remap, read)
91 p.parse(f, data, sections, remap, read)
92 else:
92 else:
93 raise error.Abort(
93 raise error.Abort(
94 _(b"subrepo spec file \'%s\' not found") % repo.pathto(f)
94 _(b"subrepo spec file \'%s\' not found") % repo.pathto(f)
95 )
95 )
96
96
97 if b'.hgsub' in ctx:
97 if b'.hgsub' in ctx:
98 read(b'.hgsub')
98 read(b'.hgsub')
99
99
100 for path, src in ui.configitems(b'subpaths'):
100 for path, src in ui.configitems(b'subpaths'):
101 p.set(b'subpaths', path, src, ui.configsource(b'subpaths', path))
101 p.set(b'subpaths', path, src, ui.configsource(b'subpaths', path))
102
102
103 rev = {}
103 rev = {}
104 if b'.hgsubstate' in ctx:
104 if b'.hgsubstate' in ctx:
105 try:
105 try:
106 for i, l in enumerate(ctx[b'.hgsubstate'].data().splitlines()):
106 for i, l in enumerate(ctx[b'.hgsubstate'].data().splitlines()):
107 l = l.lstrip()
107 l = l.lstrip()
108 if not l:
108 if not l:
109 continue
109 continue
110 try:
110 try:
111 revision, path = l.split(b" ", 1)
111 revision, path = l.split(b" ", 1)
112 except ValueError:
112 except ValueError:
113 raise error.Abort(
113 raise error.Abort(
114 _(
114 _(
115 b"invalid subrepository revision "
115 b"invalid subrepository revision "
116 b"specifier in \'%s\' line %d"
116 b"specifier in \'%s\' line %d"
117 )
117 )
118 % (repo.pathto(b'.hgsubstate'), (i + 1))
118 % (repo.pathto(b'.hgsubstate'), (i + 1))
119 )
119 )
120 rev[path] = revision
120 rev[path] = revision
121 except FileNotFoundError:
121 except FileNotFoundError:
122 pass
122 pass
123
123
124 def remap(src: bytes) -> bytes:
124 def remap(src: bytes) -> bytes:
125 for pattern, repl in p.items(b'subpaths'):
125 for pattern, repl in p.items(b'subpaths'):
126 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
126 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
127 # does a string decode.
127 # does a string decode.
128 repl = stringutil.escapestr(repl)
128 repl = stringutil.escapestr(repl)
129 # However, we still want to allow back references to go
129 # However, we still want to allow back references to go
130 # through unharmed, so we turn r'\\1' into r'\1'. Again,
130 # through unharmed, so we turn r'\\1' into r'\1'. Again,
131 # extra escapes are needed because re.sub string decodes.
131 # extra escapes are needed because re.sub string decodes.
132 repl = re.sub(br'\\\\([0-9]+)', br'\\\1', repl)
132 repl = re.sub(br'\\\\([0-9]+)', br'\\\1', repl)
133 try:
133 try:
134 src = re.sub(pattern, repl, src, 1)
134 src = re.sub(pattern, repl, src, 1)
135 except re.error as e:
135 except re.error as e:
136 raise error.Abort(
136 raise error.Abort(
137 _(b"bad subrepository pattern in %s: %s")
137 _(b"bad subrepository pattern in %s: %s")
138 % (
138 % (
139 p.source(b'subpaths', pattern),
139 p.source(b'subpaths', pattern),
140 stringutil.forcebytestr(e),
140 stringutil.forcebytestr(e),
141 )
141 )
142 )
142 )
143 return src
143 return src
144
144
145 state = {}
145 state = {}
146 for path, src in p.items(b''): # type: bytes
146 for path, src in p.items(b''):
147 kind = b'hg'
147 kind = b'hg'
148 if src.startswith(b'['):
148 if src.startswith(b'['):
149 if b']' not in src:
149 if b']' not in src:
150 raise error.Abort(_(b'missing ] in subrepository source'))
150 raise error.Abort(_(b'missing ] in subrepository source'))
151 kind, src = src.split(b']', 1)
151 kind, src = src.split(b']', 1)
152 kind = kind[1:]
152 kind = kind[1:]
153 src = src.lstrip() # strip any extra whitespace after ']'
153 src = src.lstrip() # strip any extra whitespace after ']'
154
154
155 if not urlutil.url(src).isabs():
155 if not urlutil.url(src).isabs():
156 parent = _abssource(repo, abort=False)
156 parent = _abssource(repo, abort=False)
157 if parent:
157 if parent:
158 parent = urlutil.url(parent)
158 parent = urlutil.url(parent)
159 parent.path = posixpath.join(parent.path or b'', src)
159 parent.path = posixpath.join(parent.path or b'', src)
160 parent.path = posixpath.normpath(parent.path)
160 parent.path = posixpath.normpath(parent.path)
161 joined = bytes(parent)
161 joined = bytes(parent)
162 # Remap the full joined path and use it if it changes,
162 # Remap the full joined path and use it if it changes,
163 # else remap the original source.
163 # else remap the original source.
164 remapped = remap(joined)
164 remapped = remap(joined)
165 if remapped == joined:
165 if remapped == joined:
166 src = remap(src)
166 src = remap(src)
167 else:
167 else:
168 src = remapped
168 src = remapped
169
169
170 src = remap(src)
170 src = remap(src)
171 state[util.pconvert(path)] = (src.strip(), rev.get(path, b''), kind)
171 state[util.pconvert(path)] = (src.strip(), rev.get(path, b''), kind)
172
172
173 return state
173 return state
174
174
175
175
176 def writestate(repo: "localrepo.localrepository", state: Substate) -> None:
176 def writestate(repo: "localrepo.localrepository", state: Substate) -> None:
177 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
177 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
178 lines = [
178 lines = [
179 b'%s %s\n' % (state[s][1], s)
179 b'%s %s\n' % (state[s][1], s)
180 for s in sorted(state)
180 for s in sorted(state)
181 if state[s][1] != nullstate[1]
181 if state[s][1] != nullstate[1]
182 ]
182 ]
183 repo.wwrite(b'.hgsubstate', b''.join(lines), b'')
183 repo.wwrite(b'.hgsubstate', b''.join(lines), b'')
184
184
185
185
186 def submerge(
186 def submerge(
187 repo: "localrepo.localrepository",
187 repo: "localrepo.localrepository",
188 wctx: "context.workingctx",
188 wctx: "context.workingctx",
189 mctx: "context.changectx",
189 mctx: "context.changectx",
190 actx: "context.changectx",
190 actx: "context.changectx",
191 overwrite: bool,
191 overwrite: bool,
192 labels: Optional[Any] = None,
192 labels: Optional[Any] = None,
193 ) -> Substate:
193 ) -> Substate:
194 # TODO: type the `labels` arg
194 # TODO: type the `labels` arg
195 """delegated from merge.applyupdates: merging of .hgsubstate file
195 """delegated from merge.applyupdates: merging of .hgsubstate file
196 in working context, merging context and ancestor context"""
196 in working context, merging context and ancestor context"""
197 if mctx == actx: # backwards?
197 if mctx == actx: # backwards?
198 actx = wctx.p1()
198 actx = wctx.p1()
199 s1 = wctx.substate
199 s1 = wctx.substate
200 s2 = mctx.substate
200 s2 = mctx.substate
201 sa = actx.substate
201 sa = actx.substate
202 sm = {}
202 sm = {}
203
203
204 repo.ui.debug(b"subrepo merge %s %s %s\n" % (wctx, mctx, actx))
204 repo.ui.debug(b"subrepo merge %s %s %s\n" % (wctx, mctx, actx))
205
205
206 def debug(s, msg, r=b""):
206 def debug(s, msg, r=b""):
207 if r:
207 if r:
208 r = b"%s:%s:%s" % r
208 r = b"%s:%s:%s" % r
209 repo.ui.debug(b" subrepo %s: %s %s\n" % (s, msg, r))
209 repo.ui.debug(b" subrepo %s: %s %s\n" % (s, msg, r))
210
210
211 promptssrc = filemerge.partextras(labels)
211 promptssrc = filemerge.partextras(labels)
212 for s, l in sorted(s1.items()):
212 for s, l in sorted(s1.items()):
213 a = sa.get(s, nullstate)
213 a = sa.get(s, nullstate)
214 ld = l # local state with possible dirty flag for compares
214 ld = l # local state with possible dirty flag for compares
215 if wctx.sub(s).dirty():
215 if wctx.sub(s).dirty():
216 ld = (l[0], l[1] + b"+")
216 ld = (l[0], l[1] + b"+")
217 if wctx == actx: # overwrite
217 if wctx == actx: # overwrite
218 a = ld
218 a = ld
219
219
220 prompts = promptssrc.copy()
220 prompts = promptssrc.copy()
221 prompts[b's'] = s
221 prompts[b's'] = s
222 if s in s2:
222 if s in s2:
223 r = s2[s]
223 r = s2[s]
224 if ld == r or r == a: # no change or local is newer
224 if ld == r or r == a: # no change or local is newer
225 sm[s] = l
225 sm[s] = l
226 continue
226 continue
227 elif ld == a: # other side changed
227 elif ld == a: # other side changed
228 debug(s, b"other changed, get", r)
228 debug(s, b"other changed, get", r)
229 wctx.sub(s).get(r, overwrite)
229 wctx.sub(s).get(r, overwrite)
230 sm[s] = r
230 sm[s] = r
231 elif ld[0] != r[0]: # sources differ
231 elif ld[0] != r[0]: # sources differ
232 prompts[b'lo'] = l[0]
232 prompts[b'lo'] = l[0]
233 prompts[b'ro'] = r[0]
233 prompts[b'ro'] = r[0]
234 if repo.ui.promptchoice(
234 if repo.ui.promptchoice(
235 _(
235 _(
236 b' subrepository sources for %(s)s differ\n'
236 b' subrepository sources for %(s)s differ\n'
237 b'you can use (l)ocal%(l)s source (%(lo)s)'
237 b'you can use (l)ocal%(l)s source (%(lo)s)'
238 b' or (r)emote%(o)s source (%(ro)s).\n'
238 b' or (r)emote%(o)s source (%(ro)s).\n'
239 b'what do you want to do?'
239 b'what do you want to do?'
240 b'$$ &Local $$ &Remote'
240 b'$$ &Local $$ &Remote'
241 )
241 )
242 % prompts,
242 % prompts,
243 0,
243 0,
244 ):
244 ):
245 debug(s, b"prompt changed, get", r)
245 debug(s, b"prompt changed, get", r)
246 wctx.sub(s).get(r, overwrite)
246 wctx.sub(s).get(r, overwrite)
247 sm[s] = r
247 sm[s] = r
248 elif ld[1] == a[1]: # local side is unchanged
248 elif ld[1] == a[1]: # local side is unchanged
249 debug(s, b"other side changed, get", r)
249 debug(s, b"other side changed, get", r)
250 wctx.sub(s).get(r, overwrite)
250 wctx.sub(s).get(r, overwrite)
251 sm[s] = r
251 sm[s] = r
252 else:
252 else:
253 debug(s, b"both sides changed")
253 debug(s, b"both sides changed")
254 srepo = wctx.sub(s)
254 srepo = wctx.sub(s)
255 prompts[b'sl'] = srepo.shortid(l[1])
255 prompts[b'sl'] = srepo.shortid(l[1])
256 prompts[b'sr'] = srepo.shortid(r[1])
256 prompts[b'sr'] = srepo.shortid(r[1])
257 option = repo.ui.promptchoice(
257 option = repo.ui.promptchoice(
258 _(
258 _(
259 b' subrepository %(s)s diverged (local revision: %(sl)s, '
259 b' subrepository %(s)s diverged (local revision: %(sl)s, '
260 b'remote revision: %(sr)s)\n'
260 b'remote revision: %(sr)s)\n'
261 b'you can (m)erge, keep (l)ocal%(l)s or keep '
261 b'you can (m)erge, keep (l)ocal%(l)s or keep '
262 b'(r)emote%(o)s.\n'
262 b'(r)emote%(o)s.\n'
263 b'what do you want to do?'
263 b'what do you want to do?'
264 b'$$ &Merge $$ &Local $$ &Remote'
264 b'$$ &Merge $$ &Local $$ &Remote'
265 )
265 )
266 % prompts,
266 % prompts,
267 0,
267 0,
268 )
268 )
269 if option == 0:
269 if option == 0:
270 wctx.sub(s).merge(r)
270 wctx.sub(s).merge(r)
271 sm[s] = l
271 sm[s] = l
272 debug(s, b"merge with", r)
272 debug(s, b"merge with", r)
273 elif option == 1:
273 elif option == 1:
274 sm[s] = l
274 sm[s] = l
275 debug(s, b"keep local subrepo revision", l)
275 debug(s, b"keep local subrepo revision", l)
276 else:
276 else:
277 wctx.sub(s).get(r, overwrite)
277 wctx.sub(s).get(r, overwrite)
278 sm[s] = r
278 sm[s] = r
279 debug(s, b"get remote subrepo revision", r)
279 debug(s, b"get remote subrepo revision", r)
280 elif ld == a: # remote removed, local unchanged
280 elif ld == a: # remote removed, local unchanged
281 debug(s, b"remote removed, remove")
281 debug(s, b"remote removed, remove")
282 wctx.sub(s).remove()
282 wctx.sub(s).remove()
283 elif a == nullstate: # not present in remote or ancestor
283 elif a == nullstate: # not present in remote or ancestor
284 debug(s, b"local added, keep")
284 debug(s, b"local added, keep")
285 sm[s] = l
285 sm[s] = l
286 continue
286 continue
287 else:
287 else:
288 if repo.ui.promptchoice(
288 if repo.ui.promptchoice(
289 _(
289 _(
290 b' local%(l)s changed subrepository %(s)s'
290 b' local%(l)s changed subrepository %(s)s'
291 b' which remote%(o)s removed\n'
291 b' which remote%(o)s removed\n'
292 b'use (c)hanged version or (d)elete?'
292 b'use (c)hanged version or (d)elete?'
293 b'$$ &Changed $$ &Delete'
293 b'$$ &Changed $$ &Delete'
294 )
294 )
295 % prompts,
295 % prompts,
296 0,
296 0,
297 ):
297 ):
298 debug(s, b"prompt remove")
298 debug(s, b"prompt remove")
299 wctx.sub(s).remove()
299 wctx.sub(s).remove()
300
300
301 for s, r in sorted(s2.items()):
301 for s, r in sorted(s2.items()):
302 if s in s1:
302 if s in s1:
303 continue
303 continue
304 elif s not in sa:
304 elif s not in sa:
305 debug(s, b"remote added, get", r)
305 debug(s, b"remote added, get", r)
306 mctx.sub(s).get(r)
306 mctx.sub(s).get(r)
307 sm[s] = r
307 sm[s] = r
308 elif r != sa[s]:
308 elif r != sa[s]:
309 prompts = promptssrc.copy()
309 prompts = promptssrc.copy()
310 prompts[b's'] = s
310 prompts[b's'] = s
311 if (
311 if (
312 repo.ui.promptchoice(
312 repo.ui.promptchoice(
313 _(
313 _(
314 b' remote%(o)s changed subrepository %(s)s'
314 b' remote%(o)s changed subrepository %(s)s'
315 b' which local%(l)s removed\n'
315 b' which local%(l)s removed\n'
316 b'use (c)hanged version or (d)elete?'
316 b'use (c)hanged version or (d)elete?'
317 b'$$ &Changed $$ &Delete'
317 b'$$ &Changed $$ &Delete'
318 )
318 )
319 % prompts,
319 % prompts,
320 0,
320 0,
321 )
321 )
322 == 0
322 == 0
323 ):
323 ):
324 debug(s, b"prompt recreate", r)
324 debug(s, b"prompt recreate", r)
325 mctx.sub(s).get(r)
325 mctx.sub(s).get(r)
326 sm[s] = r
326 sm[s] = r
327
327
328 # record merged .hgsubstate
328 # record merged .hgsubstate
329 writestate(repo, sm)
329 writestate(repo, sm)
330 return sm
330 return sm
331
331
332
332
333 def precommit(
333 def precommit(
334 ui: "uimod.ui",
334 ui: "uimod.ui",
335 wctx: "context.workingcommitctx",
335 wctx: "context.workingcommitctx",
336 status: "scmutil.status",
336 status: "scmutil.status",
337 match: "matchmod.basematcher",
337 match: "matchmod.basematcher",
338 force: bool = False,
338 force: bool = False,
339 ) -> Tuple[List[bytes], Set[bytes], Substate]:
339 ) -> Tuple[List[bytes], Set[bytes], Substate]:
340 """Calculate .hgsubstate changes that should be applied before committing
340 """Calculate .hgsubstate changes that should be applied before committing
341
341
342 Returns (subs, commitsubs, newstate) where
342 Returns (subs, commitsubs, newstate) where
343 - subs: changed subrepos (including dirty ones)
343 - subs: changed subrepos (including dirty ones)
344 - commitsubs: dirty subrepos which the caller needs to commit recursively
344 - commitsubs: dirty subrepos which the caller needs to commit recursively
345 - newstate: new state dict which the caller must write to .hgsubstate
345 - newstate: new state dict which the caller must write to .hgsubstate
346
346
347 This also updates the given status argument.
347 This also updates the given status argument.
348 """
348 """
349 subs = []
349 subs = []
350 commitsubs = set()
350 commitsubs = set()
351 newstate = wctx.substate.copy()
351 newstate = wctx.substate.copy()
352
352
353 # only manage subrepos and .hgsubstate if .hgsub is present
353 # only manage subrepos and .hgsubstate if .hgsub is present
354 if b'.hgsub' in wctx:
354 if b'.hgsub' in wctx:
355 # we'll decide whether to track this ourselves, thanks
355 # we'll decide whether to track this ourselves, thanks
356 for c in status.modified, status.added, status.removed:
356 for c in status.modified, status.added, status.removed:
357 if b'.hgsubstate' in c:
357 if b'.hgsubstate' in c:
358 c.remove(b'.hgsubstate')
358 c.remove(b'.hgsubstate')
359
359
360 # compare current state to last committed state
360 # compare current state to last committed state
361 # build new substate based on last committed state
361 # build new substate based on last committed state
362 oldstate = wctx.p1().substate
362 oldstate = wctx.p1().substate
363 for s in sorted(newstate.keys()):
363 for s in sorted(newstate.keys()):
364 if not match(s):
364 if not match(s):
365 # ignore working copy, use old state if present
365 # ignore working copy, use old state if present
366 if s in oldstate:
366 if s in oldstate:
367 newstate[s] = oldstate[s]
367 newstate[s] = oldstate[s]
368 continue
368 continue
369 if not force:
369 if not force:
370 raise error.Abort(
370 raise error.Abort(
371 _(b"commit with new subrepo %s excluded") % s
371 _(b"commit with new subrepo %s excluded") % s
372 )
372 )
373 dirtyreason = wctx.sub(s).dirtyreason(True)
373 dirtyreason = wctx.sub(s).dirtyreason(True)
374 if dirtyreason:
374 if dirtyreason:
375 if not ui.configbool(b'ui', b'commitsubrepos'):
375 if not ui.configbool(b'ui', b'commitsubrepos'):
376 raise error.Abort(
376 raise error.Abort(
377 dirtyreason,
377 dirtyreason,
378 hint=_(b"use --subrepos for recursive commit"),
378 hint=_(b"use --subrepos for recursive commit"),
379 )
379 )
380 subs.append(s)
380 subs.append(s)
381 commitsubs.add(s)
381 commitsubs.add(s)
382 else:
382 else:
383 bs = wctx.sub(s).basestate()
383 bs = wctx.sub(s).basestate()
384 newstate[s] = (newstate[s][0], bs, newstate[s][2])
384 newstate[s] = (newstate[s][0], bs, newstate[s][2])
385 if oldstate.get(s, (None, None, None))[1] != bs:
385 if oldstate.get(s, (None, None, None))[1] != bs:
386 subs.append(s)
386 subs.append(s)
387
387
388 # check for removed subrepos
388 # check for removed subrepos
389 for p in wctx.parents():
389 for p in wctx.parents():
390 r = [s for s in p.substate if s not in newstate]
390 r = [s for s in p.substate if s not in newstate]
391 subs += [s for s in r if match(s)]
391 subs += [s for s in r if match(s)]
392 if subs:
392 if subs:
393 if not match(b'.hgsub') and b'.hgsub' in (
393 if not match(b'.hgsub') and b'.hgsub' in (
394 wctx.modified() + wctx.added()
394 wctx.modified() + wctx.added()
395 ):
395 ):
396 raise error.Abort(_(b"can't commit subrepos without .hgsub"))
396 raise error.Abort(_(b"can't commit subrepos without .hgsub"))
397 status.modified.insert(0, b'.hgsubstate')
397 status.modified.insert(0, b'.hgsubstate')
398
398
399 elif b'.hgsub' in status.removed:
399 elif b'.hgsub' in status.removed:
400 # clean up .hgsubstate when .hgsub is removed
400 # clean up .hgsubstate when .hgsub is removed
401 if b'.hgsubstate' in wctx and b'.hgsubstate' not in (
401 if b'.hgsubstate' in wctx and b'.hgsubstate' not in (
402 status.modified + status.added + status.removed
402 status.modified + status.added + status.removed
403 ):
403 ):
404 status.removed.insert(0, b'.hgsubstate')
404 status.removed.insert(0, b'.hgsubstate')
405
405
406 return subs, commitsubs, newstate
406 return subs, commitsubs, newstate
407
407
408
408
409 def repo_rel_or_abs_source(repo):
409 def repo_rel_or_abs_source(repo):
410 """return the source of this repo
410 """return the source of this repo
411
411
412 Either absolute or relative the outermost repo"""
412 Either absolute or relative the outermost repo"""
413 parent = repo
413 parent = repo
414 chunks = []
414 chunks = []
415 while hasattr(parent, '_subparent'):
415 while hasattr(parent, '_subparent'):
416 source = urlutil.url(parent._subsource)
416 source = urlutil.url(parent._subsource)
417 chunks.append(bytes(source))
417 chunks.append(bytes(source))
418 if source.isabs():
418 if source.isabs():
419 break
419 break
420 parent = parent._subparent
420 parent = parent._subparent
421
421
422 chunks.reverse()
422 chunks.reverse()
423 path = posixpath.join(*chunks)
423 path = posixpath.join(*chunks)
424 return posixpath.normpath(path)
424 return posixpath.normpath(path)
425
425
426
426
427 def reporelpath(repo: "localrepo.localrepository") -> bytes:
427 def reporelpath(repo: "localrepo.localrepository") -> bytes:
428 """return path to this (sub)repo as seen from outermost repo"""
428 """return path to this (sub)repo as seen from outermost repo"""
429 parent = repo
429 parent = repo
430 while hasattr(parent, '_subparent'):
430 while hasattr(parent, '_subparent'):
431 parent = parent._subparent
431 parent = parent._subparent
432 return repo.root[len(pathutil.normasprefix(parent.root)) :]
432 return repo.root[len(pathutil.normasprefix(parent.root)) :]
433
433
434
434
435 def subrelpath(sub: "subrepo.abstractsubrepo") -> bytes:
435 def subrelpath(sub: "subrepo.abstractsubrepo") -> bytes:
436 """return path to this subrepo as seen from outermost repo"""
436 """return path to this subrepo as seen from outermost repo"""
437 return sub._relpath
437 return sub._relpath
438
438
439
439
440 def _abssource(
440 def _abssource(
441 repo: "localrepo.localrepository",
441 repo: "localrepo.localrepository",
442 push: bool = False,
442 push: bool = False,
443 abort: bool = True,
443 abort: bool = True,
444 ) -> Optional[bytes]:
444 ) -> Optional[bytes]:
445 """return pull/push path of repo - either based on parent repo .hgsub info
445 """return pull/push path of repo - either based on parent repo .hgsub info
446 or on the top repo config. Abort or return None if no source found."""
446 or on the top repo config. Abort or return None if no source found."""
447 if hasattr(repo, '_subparent'):
447 if hasattr(repo, '_subparent'):
448 source = urlutil.url(repo._subsource)
448 source = urlutil.url(repo._subsource)
449 if source.isabs():
449 if source.isabs():
450 return bytes(source)
450 return bytes(source)
451 source.path = posixpath.normpath(source.path)
451 source.path = posixpath.normpath(source.path)
452 parent = _abssource(repo._subparent, push, abort=False)
452 parent = _abssource(repo._subparent, push, abort=False)
453 if parent:
453 if parent:
454 parent = urlutil.url(util.pconvert(parent))
454 parent = urlutil.url(util.pconvert(parent))
455 parent.path = posixpath.join(parent.path or b'', source.path)
455 parent.path = posixpath.join(parent.path or b'', source.path)
456 parent.path = posixpath.normpath(parent.path)
456 parent.path = posixpath.normpath(parent.path)
457 return bytes(parent)
457 return bytes(parent)
458 else: # recursion reached top repo
458 else: # recursion reached top repo
459 path = None
459 path = None
460 if hasattr(repo, '_subtoppath'):
460 if hasattr(repo, '_subtoppath'):
461 path = repo._subtoppath
461 path = repo._subtoppath
462 elif push and repo.ui.config(b'paths', b'default-push'):
462 elif push and repo.ui.config(b'paths', b'default-push'):
463 path = repo.ui.config(b'paths', b'default-push')
463 path = repo.ui.config(b'paths', b'default-push')
464 elif repo.ui.config(b'paths', b'default'):
464 elif repo.ui.config(b'paths', b'default'):
465 path = repo.ui.config(b'paths', b'default')
465 path = repo.ui.config(b'paths', b'default')
466 elif repo.shared():
466 elif repo.shared():
467 # chop off the .hg component to get the default path form. This has
467 # chop off the .hg component to get the default path form. This has
468 # already run through vfsmod.vfs(..., realpath=True), so it doesn't
468 # already run through vfsmod.vfs(..., realpath=True), so it doesn't
469 # have problems with 'C:'
469 # have problems with 'C:'
470 return os.path.dirname(repo.sharedpath)
470 return os.path.dirname(repo.sharedpath)
471 if path:
471 if path:
472 # issue5770: 'C:\' and 'C:' are not equivalent paths. The former is
472 # issue5770: 'C:\' and 'C:' are not equivalent paths. The former is
473 # as expected: an absolute path to the root of the C: drive. The
473 # as expected: an absolute path to the root of the C: drive. The
474 # latter is a relative path, and works like so:
474 # latter is a relative path, and works like so:
475 #
475 #
476 # C:\>cd C:\some\path
476 # C:\>cd C:\some\path
477 # C:\>D:
477 # C:\>D:
478 # D:\>python -c "import os; print os.path.abspath('C:')"
478 # D:\>python -c "import os; print os.path.abspath('C:')"
479 # C:\some\path
479 # C:\some\path
480 #
480 #
481 # D:\>python -c "import os; print os.path.abspath('C:relative')"
481 # D:\>python -c "import os; print os.path.abspath('C:relative')"
482 # C:\some\path\relative
482 # C:\some\path\relative
483 if urlutil.hasdriveletter(path):
483 if urlutil.hasdriveletter(path):
484 if len(path) == 2 or path[2:3] not in br'\/':
484 if len(path) == 2 or path[2:3] not in br'\/':
485 path = util.abspath(path)
485 path = util.abspath(path)
486 return path
486 return path
487
487
488 if abort:
488 if abort:
489 raise error.Abort(_(b"default path for subrepository not found"))
489 raise error.Abort(_(b"default path for subrepository not found"))
490
490
491
491
492 def newcommitphase(ui: "uimod.ui", ctx: "context.changectx") -> int:
492 def newcommitphase(ui: "uimod.ui", ctx: "context.changectx") -> int:
493 commitphase = phases.newcommitphase(ui)
493 commitphase = phases.newcommitphase(ui)
494 substate = getattr(ctx, "substate", None)
494 substate = getattr(ctx, "substate", None)
495 if not substate:
495 if not substate:
496 return commitphase
496 return commitphase
497 check = ui.config(b'phases', b'checksubrepos')
497 check = ui.config(b'phases', b'checksubrepos')
498 if check not in (b'ignore', b'follow', b'abort'):
498 if check not in (b'ignore', b'follow', b'abort'):
499 raise error.Abort(
499 raise error.Abort(
500 _(b'invalid phases.checksubrepos configuration: %s') % check
500 _(b'invalid phases.checksubrepos configuration: %s') % check
501 )
501 )
502 if check == b'ignore':
502 if check == b'ignore':
503 return commitphase
503 return commitphase
504 maxphase = phases.public
504 maxphase = phases.public
505 maxsub = None
505 maxsub = None
506 for s in sorted(substate):
506 for s in sorted(substate):
507 sub = ctx.sub(s)
507 sub = ctx.sub(s)
508 subphase = sub.phase(substate[s][1])
508 subphase = sub.phase(substate[s][1])
509 if maxphase < subphase:
509 if maxphase < subphase:
510 maxphase = subphase
510 maxphase = subphase
511 maxsub = s
511 maxsub = s
512 if commitphase < maxphase:
512 if commitphase < maxphase:
513 if check == b'abort':
513 if check == b'abort':
514 raise error.Abort(
514 raise error.Abort(
515 _(
515 _(
516 b"can't commit in %s phase"
516 b"can't commit in %s phase"
517 b" conflicting %s from subrepository %s"
517 b" conflicting %s from subrepository %s"
518 )
518 )
519 % (
519 % (
520 phases.phasenames[commitphase],
520 phases.phasenames[commitphase],
521 phases.phasenames[maxphase],
521 phases.phasenames[maxphase],
522 maxsub,
522 maxsub,
523 )
523 )
524 )
524 )
525 ui.warn(
525 ui.warn(
526 _(
526 _(
527 b"warning: changes are committed in"
527 b"warning: changes are committed in"
528 b" %s phase from subrepository %s\n"
528 b" %s phase from subrepository %s\n"
529 )
529 )
530 % (phases.phasenames[maxphase], maxsub)
530 % (phases.phasenames[maxphase], maxsub)
531 )
531 )
532 return maxphase
532 return maxphase
533 return commitphase
533 return commitphase
General Comments 0
You need to be logged in to leave comments. Login now