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