##// END OF EJS Templates
branching: merge stable into default
Raphaël Gomès -
r50559:f56873a7 merge default
parent child Browse files
Show More
@@ -1,1232 +1,1232 b''
1 # help.py - help data for mercurial
1 # help.py - help data for mercurial
2 #
2 #
3 # Copyright 2006 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2006 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 itertools
9 import itertools
10 import re
10 import re
11 import textwrap
11 import textwrap
12
12
13 from typing import (
13 from typing import (
14 Callable,
14 Callable,
15 Dict,
15 Dict,
16 Iterable,
16 Iterable,
17 List,
17 List,
18 Optional,
18 Optional,
19 Set,
19 Set,
20 Tuple,
20 Tuple,
21 Union,
21 Union,
22 cast,
22 cast,
23 )
23 )
24
24
25 from .i18n import (
25 from .i18n import (
26 _,
26 _,
27 gettext,
27 gettext,
28 )
28 )
29 from .pycompat import getattr
29 from .pycompat import getattr
30 from . import (
30 from . import (
31 cmdutil,
31 cmdutil,
32 encoding,
32 encoding,
33 error,
33 error,
34 extensions,
34 extensions,
35 fancyopts,
35 fancyopts,
36 filemerge,
36 filemerge,
37 fileset,
37 fileset,
38 minirst,
38 minirst,
39 pycompat,
39 pycompat,
40 registrar,
40 registrar,
41 revset,
41 revset,
42 templatefilters,
42 templatefilters,
43 templatefuncs,
43 templatefuncs,
44 templatekw,
44 templatekw,
45 ui as uimod,
45 ui as uimod,
46 util,
46 util,
47 )
47 )
48 from .hgweb import webcommands
48 from .hgweb import webcommands
49 from .utils import (
49 from .utils import (
50 compression,
50 compression,
51 resourceutil,
51 resourceutil,
52 stringutil,
52 stringutil,
53 )
53 )
54
54
55 _DocLoader = Callable[[uimod.ui], bytes]
55 _DocLoader = Callable[[uimod.ui], bytes]
56 # Old extensions may not register with a category
56 # Old extensions may not register with a category
57 _HelpEntry = Union["_HelpEntryNoCategory", "_HelpEntryWithCategory"]
57 _HelpEntry = Union["_HelpEntryNoCategory", "_HelpEntryWithCategory"]
58 _HelpEntryNoCategory = Tuple[List[bytes], bytes, _DocLoader]
58 _HelpEntryNoCategory = Tuple[List[bytes], bytes, _DocLoader]
59 _HelpEntryWithCategory = Tuple[List[bytes], bytes, _DocLoader, bytes]
59 _HelpEntryWithCategory = Tuple[List[bytes], bytes, _DocLoader, bytes]
60 _SelectFn = Callable[[object], bool]
60 _SelectFn = Callable[[object], bool]
61 _SynonymTable = Dict[bytes, List[bytes]]
61 _SynonymTable = Dict[bytes, List[bytes]]
62 _TopicHook = Callable[[uimod.ui, bytes, bytes], bytes]
62 _TopicHook = Callable[[uimod.ui, bytes, bytes], bytes]
63
63
64 _exclkeywords: Set[bytes] = {
64 _exclkeywords: Set[bytes] = {
65 b"(ADVANCED)",
65 b"(ADVANCED)",
66 b"(DEPRECATED)",
66 b"(DEPRECATED)",
67 b"(EXPERIMENTAL)",
67 b"(EXPERIMENTAL)",
68 # i18n: "(ADVANCED)" is a keyword, must be translated consistently
68 # i18n: "(ADVANCED)" is a keyword, must be translated consistently
69 _(b"(ADVANCED)"),
69 _(b"(ADVANCED)"),
70 # i18n: "(DEPRECATED)" is a keyword, must be translated consistently
70 # i18n: "(DEPRECATED)" is a keyword, must be translated consistently
71 _(b"(DEPRECATED)"),
71 _(b"(DEPRECATED)"),
72 # i18n: "(EXPERIMENTAL)" is a keyword, must be translated consistently
72 # i18n: "(EXPERIMENTAL)" is a keyword, must be translated consistently
73 _(b"(EXPERIMENTAL)"),
73 _(b"(EXPERIMENTAL)"),
74 }
74 }
75
75
76 # The order in which command categories will be displayed.
76 # The order in which command categories will be displayed.
77 # Extensions with custom categories should insert them into this list
77 # Extensions with custom categories should insert them into this list
78 # after/before the appropriate item, rather than replacing the list or
78 # after/before the appropriate item, rather than replacing the list or
79 # assuming absolute positions.
79 # assuming absolute positions.
80 CATEGORY_ORDER: List[bytes] = [
80 CATEGORY_ORDER: List[bytes] = [
81 registrar.command.CATEGORY_REPO_CREATION,
81 registrar.command.CATEGORY_REPO_CREATION,
82 registrar.command.CATEGORY_REMOTE_REPO_MANAGEMENT,
82 registrar.command.CATEGORY_REMOTE_REPO_MANAGEMENT,
83 registrar.command.CATEGORY_COMMITTING,
83 registrar.command.CATEGORY_COMMITTING,
84 registrar.command.CATEGORY_CHANGE_MANAGEMENT,
84 registrar.command.CATEGORY_CHANGE_MANAGEMENT,
85 registrar.command.CATEGORY_CHANGE_ORGANIZATION,
85 registrar.command.CATEGORY_CHANGE_ORGANIZATION,
86 registrar.command.CATEGORY_FILE_CONTENTS,
86 registrar.command.CATEGORY_FILE_CONTENTS,
87 registrar.command.CATEGORY_CHANGE_NAVIGATION,
87 registrar.command.CATEGORY_CHANGE_NAVIGATION,
88 registrar.command.CATEGORY_WORKING_DIRECTORY,
88 registrar.command.CATEGORY_WORKING_DIRECTORY,
89 registrar.command.CATEGORY_IMPORT_EXPORT,
89 registrar.command.CATEGORY_IMPORT_EXPORT,
90 registrar.command.CATEGORY_MAINTENANCE,
90 registrar.command.CATEGORY_MAINTENANCE,
91 registrar.command.CATEGORY_HELP,
91 registrar.command.CATEGORY_HELP,
92 registrar.command.CATEGORY_MISC,
92 registrar.command.CATEGORY_MISC,
93 registrar.command.CATEGORY_NONE,
93 registrar.command.CATEGORY_NONE,
94 ]
94 ]
95
95
96 # Human-readable category names. These are translated.
96 # Human-readable category names. These are translated.
97 # Extensions with custom categories should add their names here.
97 # Extensions with custom categories should add their names here.
98 CATEGORY_NAMES: Dict[bytes, bytes] = {
98 CATEGORY_NAMES: Dict[bytes, bytes] = {
99 registrar.command.CATEGORY_REPO_CREATION: b'Repository creation',
99 registrar.command.CATEGORY_REPO_CREATION: b'Repository creation',
100 registrar.command.CATEGORY_REMOTE_REPO_MANAGEMENT: b'Remote repository management',
100 registrar.command.CATEGORY_REMOTE_REPO_MANAGEMENT: b'Remote repository management',
101 registrar.command.CATEGORY_COMMITTING: b'Change creation',
101 registrar.command.CATEGORY_COMMITTING: b'Change creation',
102 registrar.command.CATEGORY_CHANGE_NAVIGATION: b'Change navigation',
102 registrar.command.CATEGORY_CHANGE_NAVIGATION: b'Change navigation',
103 registrar.command.CATEGORY_CHANGE_MANAGEMENT: b'Change manipulation',
103 registrar.command.CATEGORY_CHANGE_MANAGEMENT: b'Change manipulation',
104 registrar.command.CATEGORY_CHANGE_ORGANIZATION: b'Change organization',
104 registrar.command.CATEGORY_CHANGE_ORGANIZATION: b'Change organization',
105 registrar.command.CATEGORY_WORKING_DIRECTORY: b'Working directory management',
105 registrar.command.CATEGORY_WORKING_DIRECTORY: b'Working directory management',
106 registrar.command.CATEGORY_FILE_CONTENTS: b'File content management',
106 registrar.command.CATEGORY_FILE_CONTENTS: b'File content management',
107 registrar.command.CATEGORY_IMPORT_EXPORT: b'Change import/export',
107 registrar.command.CATEGORY_IMPORT_EXPORT: b'Change import/export',
108 registrar.command.CATEGORY_MAINTENANCE: b'Repository maintenance',
108 registrar.command.CATEGORY_MAINTENANCE: b'Repository maintenance',
109 registrar.command.CATEGORY_HELP: b'Help',
109 registrar.command.CATEGORY_HELP: b'Help',
110 registrar.command.CATEGORY_MISC: b'Miscellaneous commands',
110 registrar.command.CATEGORY_MISC: b'Miscellaneous commands',
111 registrar.command.CATEGORY_NONE: b'Uncategorized commands',
111 registrar.command.CATEGORY_NONE: b'Uncategorized commands',
112 }
112 }
113
113
114 # Topic categories.
114 # Topic categories.
115 TOPIC_CATEGORY_IDS = b'ids'
115 TOPIC_CATEGORY_IDS = b'ids'
116 TOPIC_CATEGORY_OUTPUT = b'output'
116 TOPIC_CATEGORY_OUTPUT = b'output'
117 TOPIC_CATEGORY_CONFIG = b'config'
117 TOPIC_CATEGORY_CONFIG = b'config'
118 TOPIC_CATEGORY_CONCEPTS = b'concepts'
118 TOPIC_CATEGORY_CONCEPTS = b'concepts'
119 TOPIC_CATEGORY_MISC = b'misc'
119 TOPIC_CATEGORY_MISC = b'misc'
120 TOPIC_CATEGORY_NONE = b'none'
120 TOPIC_CATEGORY_NONE = b'none'
121
121
122 # The order in which topic categories will be displayed.
122 # The order in which topic categories will be displayed.
123 # Extensions with custom categories should insert them into this list
123 # Extensions with custom categories should insert them into this list
124 # after/before the appropriate item, rather than replacing the list or
124 # after/before the appropriate item, rather than replacing the list or
125 # assuming absolute positions.
125 # assuming absolute positions.
126 TOPIC_CATEGORY_ORDER: List[bytes] = [
126 TOPIC_CATEGORY_ORDER: List[bytes] = [
127 TOPIC_CATEGORY_IDS,
127 TOPIC_CATEGORY_IDS,
128 TOPIC_CATEGORY_OUTPUT,
128 TOPIC_CATEGORY_OUTPUT,
129 TOPIC_CATEGORY_CONFIG,
129 TOPIC_CATEGORY_CONFIG,
130 TOPIC_CATEGORY_CONCEPTS,
130 TOPIC_CATEGORY_CONCEPTS,
131 TOPIC_CATEGORY_MISC,
131 TOPIC_CATEGORY_MISC,
132 TOPIC_CATEGORY_NONE,
132 TOPIC_CATEGORY_NONE,
133 ]
133 ]
134
134
135 # Human-readable topic category names. These are translated.
135 # Human-readable topic category names. These are translated.
136 TOPIC_CATEGORY_NAMES: Dict[bytes, bytes] = {
136 TOPIC_CATEGORY_NAMES: Dict[bytes, bytes] = {
137 TOPIC_CATEGORY_IDS: b'Mercurial identifiers',
137 TOPIC_CATEGORY_IDS: b'Mercurial identifiers',
138 TOPIC_CATEGORY_OUTPUT: b'Mercurial output',
138 TOPIC_CATEGORY_OUTPUT: b'Mercurial output',
139 TOPIC_CATEGORY_CONFIG: b'Mercurial configuration',
139 TOPIC_CATEGORY_CONFIG: b'Mercurial configuration',
140 TOPIC_CATEGORY_CONCEPTS: b'Concepts',
140 TOPIC_CATEGORY_CONCEPTS: b'Concepts',
141 TOPIC_CATEGORY_MISC: b'Miscellaneous',
141 TOPIC_CATEGORY_MISC: b'Miscellaneous',
142 TOPIC_CATEGORY_NONE: b'Uncategorized topics',
142 TOPIC_CATEGORY_NONE: b'Uncategorized topics',
143 }
143 }
144
144
145
145
146 def listexts(
146 def listexts(
147 header: bytes,
147 header: bytes,
148 exts: Dict[bytes, bytes],
148 exts: Dict[bytes, bytes],
149 indent: int = 1,
149 indent: int = 1,
150 showdeprecated: bool = False,
150 showdeprecated: bool = False,
151 ) -> List[bytes]:
151 ) -> List[bytes]:
152 '''return a text listing of the given extensions'''
152 '''return a text listing of the given extensions'''
153 rst = []
153 rst = []
154 if exts:
154 if exts:
155 for name, desc in sorted(exts.items()):
155 for name, desc in sorted(exts.items()):
156 if not showdeprecated and any(w in desc for w in _exclkeywords):
156 if not showdeprecated and any(w in desc for w in _exclkeywords):
157 continue
157 continue
158 rst.append(b'%s:%s: %s\n' % (b' ' * indent, name, desc))
158 rst.append(b'%s:%s: %s\n' % (b' ' * indent, name, desc))
159 if rst:
159 if rst:
160 rst.insert(0, b'\n%s\n\n' % header)
160 rst.insert(0, b'\n%s\n\n' % header)
161 return rst
161 return rst
162
162
163
163
164 def extshelp(ui: uimod.ui) -> bytes:
164 def extshelp(ui: uimod.ui) -> bytes:
165 rst = loaddoc(b'extensions')(ui).splitlines(True)
165 rst = loaddoc(b'extensions')(ui).splitlines(True)
166 rst.extend(
166 rst.extend(
167 listexts(
167 listexts(
168 _(b'enabled extensions:'), extensions.enabled(), showdeprecated=True
168 _(b'enabled extensions:'), extensions.enabled(), showdeprecated=True
169 )
169 )
170 )
170 )
171 rst.extend(
171 rst.extend(
172 listexts(
172 listexts(
173 _(b'disabled extensions:'),
173 _(b'disabled extensions:'),
174 extensions.disabled(),
174 extensions.disabled(),
175 showdeprecated=ui.verbose,
175 showdeprecated=ui.verbose,
176 )
176 )
177 )
177 )
178 doc = b''.join(rst)
178 doc = b''.join(rst)
179 return doc
179 return doc
180
180
181
181
182 def parsedefaultmarker(text: bytes) -> Optional[Tuple[bytes, List[bytes]]]:
182 def parsedefaultmarker(text: bytes) -> Optional[Tuple[bytes, List[bytes]]]:
183 """given a text 'abc (DEFAULT: def.ghi)',
183 """given a text 'abc (DEFAULT: def.ghi)',
184 returns (b'abc', (b'def', b'ghi')). Otherwise return None"""
184 returns (b'abc', (b'def', b'ghi')). Otherwise return None"""
185 if text[-1:] == b')':
185 if text[-1:] == b')':
186 marker = b' (DEFAULT: '
186 marker = b' (DEFAULT: '
187 pos = text.find(marker)
187 pos = text.find(marker)
188 if pos >= 0:
188 if pos >= 0:
189 item = text[pos + len(marker) : -1]
189 item = text[pos + len(marker) : -1]
190 return text[:pos], item.split(b'.', 2)
190 return text[:pos], item.split(b'.', 2)
191
191
192
192
193 def optrst(header: bytes, options, verbose: bool, ui: uimod.ui) -> bytes:
193 def optrst(header: bytes, options, verbose: bool, ui: uimod.ui) -> bytes:
194 data = []
194 data = []
195 multioccur = False
195 multioccur = False
196 for option in options:
196 for option in options:
197 if len(option) == 5:
197 if len(option) == 5:
198 shortopt, longopt, default, desc, optlabel = option
198 shortopt, longopt, default, desc, optlabel = option
199 else:
199 else:
200 shortopt, longopt, default, desc = option
200 shortopt, longopt, default, desc = option
201 optlabel = _(b"VALUE") # default label
201 optlabel = _(b"VALUE") # default label
202
202
203 if not verbose and any(w in desc for w in _exclkeywords):
203 if not verbose and any(w in desc for w in _exclkeywords):
204 continue
204 continue
205 defaultstrsuffix = b''
205 defaultstrsuffix = b''
206 if default is None:
206 if default is None:
207 parseresult = parsedefaultmarker(desc)
207 parseresult = parsedefaultmarker(desc)
208 if parseresult is not None:
208 if parseresult is not None:
209 (desc, (section, name)) = parseresult
209 (desc, (section, name)) = parseresult
210 if ui.configbool(section, name):
210 if ui.configbool(section, name):
211 default = True
211 default = True
212 defaultstrsuffix = _(b' from config')
212 defaultstrsuffix = _(b' from config')
213 so = b''
213 so = b''
214 if shortopt:
214 if shortopt:
215 so = b'-' + shortopt
215 so = b'-' + shortopt
216 lo = b'--' + longopt
216 lo = b'--' + longopt
217 if default is True:
217 if default is True:
218 lo = b'--[no-]' + longopt
218 lo = b'--[no-]' + longopt
219
219
220 if isinstance(default, fancyopts.customopt):
220 if isinstance(default, fancyopts.customopt):
221 default = default.getdefaultvalue()
221 default = default.getdefaultvalue()
222 if default and not callable(default):
222 if default and not callable(default):
223 # default is of unknown type, and in Python 2 we abused
223 # default is of unknown type, and in Python 2 we abused
224 # the %s-shows-repr property to handle integers etc. To
224 # the %s-shows-repr property to handle integers etc. To
225 # match that behavior on Python 3, we do str(default) and
225 # match that behavior on Python 3, we do str(default) and
226 # then convert it to bytes.
226 # then convert it to bytes.
227 defaultstr = pycompat.bytestr(default)
227 defaultstr = pycompat.bytestr(default)
228 if default is True:
228 if default is True:
229 defaultstr = _(b"on")
229 defaultstr = _(b"on")
230 desc += _(b" (default: %s)") % (defaultstr + defaultstrsuffix)
230 desc += _(b" (default: %s)") % (defaultstr + defaultstrsuffix)
231
231
232 if isinstance(default, list):
232 if isinstance(default, list):
233 lo += b" %s [+]" % optlabel
233 lo += b" %s [+]" % optlabel
234 multioccur = True
234 multioccur = True
235 elif (default is not None) and not isinstance(default, bool):
235 elif (default is not None) and not isinstance(default, bool):
236 lo += b" %s" % optlabel
236 lo += b" %s" % optlabel
237
237
238 data.append((so, lo, desc))
238 data.append((so, lo, desc))
239
239
240 if multioccur:
240 if multioccur:
241 header += _(b" ([+] can be repeated)")
241 header += _(b" ([+] can be repeated)")
242
242
243 rst = [b'\n%s:\n\n' % header]
243 rst = [b'\n%s:\n\n' % header]
244 rst.extend(minirst.maketable(data, 1))
244 rst.extend(minirst.maketable(data, 1))
245
245
246 return b''.join(rst)
246 return b''.join(rst)
247
247
248
248
249 def indicateomitted(
249 def indicateomitted(
250 rst: List[bytes], omitted: bytes, notomitted: Optional[bytes] = None
250 rst: List[bytes], omitted: bytes, notomitted: Optional[bytes] = None
251 ) -> None:
251 ) -> None:
252 rst.append(b'\n\n.. container:: omitted\n\n %s\n\n' % omitted)
252 rst.append(b'\n\n.. container:: omitted\n\n %s\n\n' % omitted)
253 if notomitted:
253 if notomitted:
254 rst.append(b'\n\n.. container:: notomitted\n\n %s\n\n' % notomitted)
254 rst.append(b'\n\n.. container:: notomitted\n\n %s\n\n' % notomitted)
255
255
256
256
257 def filtercmd(ui: uimod.ui, cmd: bytes, func, kw: bytes, doc: bytes) -> bool:
257 def filtercmd(ui: uimod.ui, cmd: bytes, func, kw: bytes, doc: bytes) -> bool:
258 if not ui.debugflag and cmd.startswith(b"debug") and kw != b"debug":
258 if not ui.debugflag and cmd.startswith(b"debug") and kw != b"debug":
259 # Debug command, and user is not looking for those.
259 # Debug command, and user is not looking for those.
260 return True
260 return True
261 if not ui.verbose:
261 if not ui.verbose:
262 if not kw and not doc:
262 if not kw and not doc:
263 # Command had no documentation, no point in showing it by default.
263 # Command had no documentation, no point in showing it by default.
264 return True
264 return True
265 if getattr(func, 'alias', False) and not getattr(func, 'owndoc', False):
265 if getattr(func, 'alias', False) and not getattr(func, 'owndoc', False):
266 # Alias didn't have its own documentation.
266 # Alias didn't have its own documentation.
267 return True
267 return True
268 if doc and any(w in doc for w in _exclkeywords):
268 if doc and any(w in doc for w in _exclkeywords):
269 # Documentation has excluded keywords.
269 # Documentation has excluded keywords.
270 return True
270 return True
271 if kw == b"shortlist" and not getattr(func, 'helpbasic', False):
271 if kw == b"shortlist" and not getattr(func, 'helpbasic', False):
272 # We're presenting the short list but the command is not basic.
272 # We're presenting the short list but the command is not basic.
273 return True
273 return True
274 if ui.configbool(b'help', b'hidden-command.%s' % cmd):
274 if ui.configbool(b'help', b'hidden-command.%s' % cmd):
275 # Configuration explicitly hides the command.
275 # Configuration explicitly hides the command.
276 return True
276 return True
277 return False
277 return False
278
278
279
279
280 def filtertopic(ui: uimod.ui, topic: bytes) -> bool:
280 def filtertopic(ui: uimod.ui, topic: bytes) -> bool:
281 return ui.configbool(b'help', b'hidden-topic.%s' % topic, False)
281 return ui.configbool(b'help', b'hidden-topic.%s' % topic, False)
282
282
283
283
284 def topicmatch(
284 def topicmatch(
285 ui: uimod.ui, commands, kw: bytes
285 ui: uimod.ui, commands, kw: bytes
286 ) -> Dict[bytes, List[Tuple[bytes, bytes]]]:
286 ) -> Dict[bytes, List[Tuple[bytes, bytes]]]:
287 """Return help topics matching kw.
287 """Return help topics matching kw.
288
288
289 Returns {'section': [(name, summary), ...], ...} where section is
289 Returns {'section': [(name, summary), ...], ...} where section is
290 one of topics, commands, extensions, or extensioncommands.
290 one of topics, commands, extensions, or extensioncommands.
291 """
291 """
292 kw = encoding.lower(kw)
292 kw = encoding.lower(kw)
293
293
294 def lowercontains(container):
294 def lowercontains(container):
295 return kw in encoding.lower(container) # translated in helptable
295 return kw in encoding.lower(container) # translated in helptable
296
296
297 results = {
297 results = {
298 b'topics': [],
298 b'topics': [],
299 b'commands': [],
299 b'commands': [],
300 b'extensions': [],
300 b'extensions': [],
301 b'extensioncommands': [],
301 b'extensioncommands': [],
302 }
302 }
303 for topic in helptable:
303 for topic in helptable:
304 names, header, doc = topic[0:3]
304 names, header, doc = topic[0:3]
305 # Old extensions may use a str as doc.
305 # Old extensions may use a str as doc.
306 if (
306 if (
307 sum(map(lowercontains, names))
307 sum(map(lowercontains, names))
308 or lowercontains(header)
308 or lowercontains(header)
309 or (callable(doc) and lowercontains(doc(ui)))
309 or (callable(doc) and lowercontains(doc(ui)))
310 ):
310 ):
311 name = names[0]
311 name = names[0]
312 if not filtertopic(ui, name):
312 if not filtertopic(ui, name):
313 results[b'topics'].append((names[0], header))
313 results[b'topics'].append((names[0], header))
314 for cmd, entry in commands.table.items():
314 for cmd, entry in commands.table.items():
315 if len(entry) == 3:
315 if len(entry) == 3:
316 summary = entry[2]
316 summary = entry[2]
317 else:
317 else:
318 summary = b''
318 summary = b''
319 # translate docs *before* searching there
319 # translate docs *before* searching there
320 func = entry[0]
320 func = entry[0]
321 docs = _(pycompat.getdoc(func)) or b''
321 docs = _(pycompat.getdoc(func)) or b''
322 if kw in cmd or lowercontains(summary) or lowercontains(docs):
322 if kw in cmd or lowercontains(summary) or lowercontains(docs):
323 if docs:
323 if docs:
324 summary = stringutil.firstline(docs)
324 summary = stringutil.firstline(docs)
325 cmdname = cmdutil.parsealiases(cmd)[0]
325 cmdname = cmdutil.parsealiases(cmd)[0]
326 if filtercmd(ui, cmdname, func, kw, docs):
326 if filtercmd(ui, cmdname, func, kw, docs):
327 continue
327 continue
328 results[b'commands'].append((cmdname, summary))
328 results[b'commands'].append((cmdname, summary))
329 for name, docs in itertools.chain(
329 for name, docs in itertools.chain(
330 extensions.enabled(False).items(),
330 extensions.enabled(False).items(),
331 extensions.disabled().items(),
331 extensions.disabled().items(),
332 ):
332 ):
333 if not docs:
333 if not docs:
334 continue
334 continue
335 name = name.rpartition(b'.')[-1]
335 name = name.rpartition(b'.')[-1]
336 if lowercontains(name) or lowercontains(docs):
336 if lowercontains(name) or lowercontains(docs):
337 # extension docs are already translated
337 # extension docs are already translated
338 results[b'extensions'].append((name, stringutil.firstline(docs)))
338 results[b'extensions'].append((name, stringutil.firstline(docs)))
339 try:
339 try:
340 mod = extensions.load(ui, name, b'')
340 mod = extensions.load(ui, name, b'')
341 except ImportError:
341 except ImportError:
342 # debug message would be printed in extensions.load()
342 # debug message would be printed in extensions.load()
343 continue
343 continue
344 for cmd, entry in getattr(mod, 'cmdtable', {}).items():
344 for cmd, entry in getattr(mod, 'cmdtable', {}).items():
345 if kw in cmd or (len(entry) > 2 and lowercontains(entry[2])):
345 if kw in cmd or (len(entry) > 2 and lowercontains(entry[2])):
346 cmdname = cmdutil.parsealiases(cmd)[0]
346 cmdname = cmdutil.parsealiases(cmd)[0]
347 func = entry[0]
347 func = entry[0]
348 cmddoc = pycompat.getdoc(func)
348 cmddoc = pycompat.getdoc(func)
349 if cmddoc:
349 if cmddoc:
350 cmddoc = stringutil.firstline(gettext(cmddoc))
350 cmddoc = stringutil.firstline(gettext(cmddoc))
351 else:
351 else:
352 cmddoc = _(b'(no help text available)')
352 cmddoc = _(b'(no help text available)')
353 if filtercmd(ui, cmdname, func, kw, cmddoc):
353 if filtercmd(ui, cmdname, func, kw, cmddoc):
354 continue
354 continue
355 results[b'extensioncommands'].append((cmdname, cmddoc))
355 results[b'extensioncommands'].append((cmdname, cmddoc))
356 return results
356 return results
357
357
358
358
359 def loaddoc(topic: bytes, subdir: Optional[bytes] = None) -> _DocLoader:
359 def loaddoc(topic: bytes, subdir: Optional[bytes] = None) -> _DocLoader:
360 """Return a delayed loader for help/topic.txt."""
360 """Return a delayed loader for help/topic.txt."""
361
361
362 def loader(ui: uimod.ui) -> bytes:
362 def loader(ui: uimod.ui) -> bytes:
363 package = b'mercurial.helptext'
363 package = b'mercurial.helptext'
364 if subdir:
364 if subdir:
365 package += b'.' + subdir
365 package += b'.' + subdir
366 with resourceutil.open_resource(package, topic + b'.txt') as fp:
366 with resourceutil.open_resource(package, topic + b'.txt') as fp:
367 doc = gettext(fp.read())
367 doc = gettext(fp.read())
368 for rewriter in helphooks.get(topic, []):
368 for rewriter in helphooks.get(topic, []):
369 doc = rewriter(ui, topic, doc)
369 doc = rewriter(ui, topic, doc)
370 return doc
370 return doc
371
371
372 return loader
372 return loader
373
373
374
374
375 internalstable: List[_HelpEntryNoCategory] = sorted(
375 internalstable: List[_HelpEntryNoCategory] = sorted(
376 [
376 [
377 (
377 (
378 [b'bid-merge'],
378 [b'bid-merge'],
379 _(b'Bid Merge Algorithm'),
379 _(b'Bid Merge Algorithm'),
380 loaddoc(b'bid-merge', subdir=b'internals'),
380 loaddoc(b'bid-merge', subdir=b'internals'),
381 ),
381 ),
382 ([b'bundle2'], _(b'Bundle2'), loaddoc(b'bundle2', subdir=b'internals')),
382 ([b'bundle2'], _(b'Bundle2'), loaddoc(b'bundle2', subdir=b'internals')),
383 ([b'bundles'], _(b'Bundles'), loaddoc(b'bundles', subdir=b'internals')),
383 ([b'bundles'], _(b'Bundles'), loaddoc(b'bundles', subdir=b'internals')),
384 ([b'cbor'], _(b'CBOR'), loaddoc(b'cbor', subdir=b'internals')),
384 ([b'cbor'], _(b'CBOR'), loaddoc(b'cbor', subdir=b'internals')),
385 ([b'censor'], _(b'Censor'), loaddoc(b'censor', subdir=b'internals')),
385 ([b'censor'], _(b'Censor'), loaddoc(b'censor', subdir=b'internals')),
386 (
386 (
387 [b'changegroups'],
387 [b'changegroups'],
388 _(b'Changegroups'),
388 _(b'Changegroups'),
389 loaddoc(b'changegroups', subdir=b'internals'),
389 loaddoc(b'changegroups', subdir=b'internals'),
390 ),
390 ),
391 (
391 (
392 [b'config'],
392 [b'config'],
393 _(b'Config Registrar'),
393 _(b'Config Registrar'),
394 loaddoc(b'config', subdir=b'internals'),
394 loaddoc(b'config', subdir=b'internals'),
395 ),
395 ),
396 (
396 (
397 [b'dirstate-v2'],
397 [b'dirstate-v2'],
398 _(b'dirstate-v2 file format'),
398 _(b'dirstate-v2 file format'),
399 loaddoc(b'dirstate-v2', subdir=b'internals'),
399 loaddoc(b'dirstate-v2', subdir=b'internals'),
400 ),
400 ),
401 (
401 (
402 [b'extensions', b'extension'],
402 [b'extensions', b'extension'],
403 _(b'Extension API'),
403 _(b'Extension API'),
404 loaddoc(b'extensions', subdir=b'internals'),
404 loaddoc(b'extensions', subdir=b'internals'),
405 ),
405 ),
406 (
406 (
407 [b'mergestate'],
407 [b'mergestate'],
408 _(b'Mergestate'),
408 _(b'Mergestate'),
409 loaddoc(b'mergestate', subdir=b'internals'),
409 loaddoc(b'mergestate', subdir=b'internals'),
410 ),
410 ),
411 (
411 (
412 [b'requirements'],
412 [b'requirements'],
413 _(b'Repository Requirements'),
413 _(b'Repository Requirements'),
414 loaddoc(b'requirements', subdir=b'internals'),
414 loaddoc(b'requirements', subdir=b'internals'),
415 ),
415 ),
416 (
416 (
417 [b'revlogs'],
417 [b'revlogs'],
418 _(b'Revision Logs'),
418 _(b'Revision Logs'),
419 loaddoc(b'revlogs', subdir=b'internals'),
419 loaddoc(b'revlogs', subdir=b'internals'),
420 ),
420 ),
421 (
421 (
422 [b'wireprotocol'],
422 [b'wireprotocol'],
423 _(b'Wire Protocol'),
423 _(b'Wire Protocol'),
424 loaddoc(b'wireprotocol', subdir=b'internals'),
424 loaddoc(b'wireprotocol', subdir=b'internals'),
425 ),
425 ),
426 (
426 (
427 [b'wireprotocolrpc'],
427 [b'wireprotocolrpc'],
428 _(b'Wire Protocol RPC'),
428 _(b'Wire Protocol RPC'),
429 loaddoc(b'wireprotocolrpc', subdir=b'internals'),
429 loaddoc(b'wireprotocolrpc', subdir=b'internals'),
430 ),
430 ),
431 (
431 (
432 [b'wireprotocolv2'],
432 [b'wireprotocolv2'],
433 _(b'Wire Protocol Version 2'),
433 _(b'Wire Protocol Version 2'),
434 loaddoc(b'wireprotocolv2', subdir=b'internals'),
434 loaddoc(b'wireprotocolv2', subdir=b'internals'),
435 ),
435 ),
436 ]
436 ]
437 )
437 )
438
438
439
439
440 def internalshelp(ui: uimod.ui) -> bytes:
440 def internalshelp(ui: uimod.ui) -> bytes:
441 """Generate the index for the "internals" topic."""
441 """Generate the index for the "internals" topic."""
442 lines = [
442 lines = [
443 b'To access a subtopic, use "hg help internals.{subtopic-name}"\n',
443 b'To access a subtopic, use "hg help internals.{subtopic-name}"\n',
444 b'\n',
444 b'\n',
445 ]
445 ]
446 for names, header, doc in internalstable:
446 for names, header, doc in internalstable:
447 lines.append(b' :%s: %s\n' % (names[0], header))
447 lines.append(b' :%s: %s\n' % (names[0], header))
448
448
449 return b''.join(lines)
449 return b''.join(lines)
450
450
451
451
452 helptable: List[_HelpEntryWithCategory] = sorted(
452 helptable: List[_HelpEntryWithCategory] = sorted(
453 [
453 [
454 (
454 (
455 [b'bundlespec'],
455 [b'bundlespec'],
456 _(b"Bundle File Formats"),
456 _(b"Bundle File Formats"),
457 loaddoc(b'bundlespec'),
457 loaddoc(b'bundlespec'),
458 TOPIC_CATEGORY_CONCEPTS,
458 TOPIC_CATEGORY_CONCEPTS,
459 ),
459 ),
460 (
460 (
461 [b'color'],
461 [b'color'],
462 _(b"Colorizing Outputs"),
462 _(b"Colorizing Outputs"),
463 loaddoc(b'color'),
463 loaddoc(b'color'),
464 TOPIC_CATEGORY_OUTPUT,
464 TOPIC_CATEGORY_OUTPUT,
465 ),
465 ),
466 (
466 (
467 [b"config", b"hgrc"],
467 [b"config", b"hgrc"],
468 _(b"Configuration Files"),
468 _(b"Configuration Files"),
469 loaddoc(b'config'),
469 loaddoc(b'config'),
470 TOPIC_CATEGORY_CONFIG,
470 TOPIC_CATEGORY_CONFIG,
471 ),
471 ),
472 (
472 (
473 [b'deprecated'],
473 [b'deprecated'],
474 _(b"Deprecated Features"),
474 _(b"Deprecated Features"),
475 loaddoc(b'deprecated'),
475 loaddoc(b'deprecated'),
476 TOPIC_CATEGORY_MISC,
476 TOPIC_CATEGORY_MISC,
477 ),
477 ),
478 (
478 (
479 [b"dates"],
479 [b"dates"],
480 _(b"Date Formats"),
480 _(b"Date Formats"),
481 loaddoc(b'dates'),
481 loaddoc(b'dates'),
482 TOPIC_CATEGORY_OUTPUT,
482 TOPIC_CATEGORY_OUTPUT,
483 ),
483 ),
484 (
484 (
485 [b"flags"],
485 [b"flags"],
486 _(b"Command-line flags"),
486 _(b"Command-line flags"),
487 loaddoc(b'flags'),
487 loaddoc(b'flags'),
488 TOPIC_CATEGORY_CONFIG,
488 TOPIC_CATEGORY_CONFIG,
489 ),
489 ),
490 (
490 (
491 [b"patterns"],
491 [b"patterns"],
492 _(b"File Name Patterns"),
492 _(b"File Name Patterns"),
493 loaddoc(b'patterns'),
493 loaddoc(b'patterns'),
494 TOPIC_CATEGORY_IDS,
494 TOPIC_CATEGORY_IDS,
495 ),
495 ),
496 (
496 (
497 [b'environment', b'env'],
497 [b'environment', b'env'],
498 _(b'Environment Variables'),
498 _(b'Environment Variables'),
499 loaddoc(b'environment'),
499 loaddoc(b'environment'),
500 TOPIC_CATEGORY_CONFIG,
500 TOPIC_CATEGORY_CONFIG,
501 ),
501 ),
502 (
502 (
503 [
503 [
504 b'revisions',
504 b'revisions',
505 b'revs',
505 b'revs',
506 b'revsets',
506 b'revsets',
507 b'revset',
507 b'revset',
508 b'multirevs',
508 b'multirevs',
509 b'mrevs',
509 b'mrevs',
510 ],
510 ],
511 _(b'Specifying Revisions'),
511 _(b'Specifying Revisions'),
512 loaddoc(b'revisions'),
512 loaddoc(b'revisions'),
513 TOPIC_CATEGORY_IDS,
513 TOPIC_CATEGORY_IDS,
514 ),
514 ),
515 (
515 (
516 [
516 [
517 b'rust',
517 b'rust',
518 b'rustext',
518 b'rustext',
519 b'rhg',
519 b'rhg',
520 ],
520 ],
521 _(b'Rust in Mercurial'),
521 _(b'Rust in Mercurial'),
522 loaddoc(b'rust'),
522 loaddoc(b'rust'),
523 TOPIC_CATEGORY_CONFIG,
523 TOPIC_CATEGORY_CONFIG,
524 ),
524 ),
525 (
525 (
526 [b'filesets', b'fileset'],
526 [b'filesets', b'fileset'],
527 _(b"Specifying File Sets"),
527 _(b"Specifying File Sets"),
528 loaddoc(b'filesets'),
528 loaddoc(b'filesets'),
529 TOPIC_CATEGORY_IDS,
529 TOPIC_CATEGORY_IDS,
530 ),
530 ),
531 (
531 (
532 [b'diffs'],
532 [b'diffs'],
533 _(b'Diff Formats'),
533 _(b'Diff Formats'),
534 loaddoc(b'diffs'),
534 loaddoc(b'diffs'),
535 TOPIC_CATEGORY_OUTPUT,
535 TOPIC_CATEGORY_OUTPUT,
536 ),
536 ),
537 (
537 (
538 [b'merge-tools', b'mergetools', b'mergetool'],
538 [b'merge-tools', b'mergetools', b'mergetool'],
539 _(b'Merge Tools'),
539 _(b'Merge Tools'),
540 loaddoc(b'merge-tools'),
540 loaddoc(b'merge-tools'),
541 TOPIC_CATEGORY_CONFIG,
541 TOPIC_CATEGORY_CONFIG,
542 ),
542 ),
543 (
543 (
544 [b'templating', b'templates', b'template', b'style'],
544 [b'templating', b'templates', b'template', b'style'],
545 _(b'Template Usage'),
545 _(b'Template Usage'),
546 loaddoc(b'templates'),
546 loaddoc(b'templates'),
547 TOPIC_CATEGORY_OUTPUT,
547 TOPIC_CATEGORY_OUTPUT,
548 ),
548 ),
549 ([b'urls'], _(b'URL Paths'), loaddoc(b'urls'), TOPIC_CATEGORY_IDS),
549 ([b'urls'], _(b'URL Paths'), loaddoc(b'urls'), TOPIC_CATEGORY_IDS),
550 (
550 (
551 [b"extensions"],
551 [b"extensions"],
552 _(b"Using Additional Features"),
552 _(b"Using Additional Features"),
553 extshelp,
553 extshelp,
554 TOPIC_CATEGORY_CONFIG,
554 TOPIC_CATEGORY_CONFIG,
555 ),
555 ),
556 (
556 (
557 [b"subrepos", b"subrepo"],
557 [b"subrepos", b"subrepo"],
558 _(b"Subrepositories"),
558 _(b"Subrepositories"),
559 loaddoc(b'subrepos'),
559 loaddoc(b'subrepos'),
560 TOPIC_CATEGORY_CONCEPTS,
560 TOPIC_CATEGORY_CONCEPTS,
561 ),
561 ),
562 (
562 (
563 [b"hgweb"],
563 [b"hgweb"],
564 _(b"Configuring hgweb"),
564 _(b"Configuring hgweb"),
565 loaddoc(b'hgweb'),
565 loaddoc(b'hgweb'),
566 TOPIC_CATEGORY_CONFIG,
566 TOPIC_CATEGORY_CONFIG,
567 ),
567 ),
568 (
568 (
569 [b"glossary"],
569 [b"glossary"],
570 _(b"Glossary"),
570 _(b"Glossary"),
571 loaddoc(b'glossary'),
571 loaddoc(b'glossary'),
572 TOPIC_CATEGORY_CONCEPTS,
572 TOPIC_CATEGORY_CONCEPTS,
573 ),
573 ),
574 (
574 (
575 [b"hgignore", b"ignore"],
575 [b"hgignore", b"ignore"],
576 _(b"Syntax for Mercurial Ignore Files"),
576 _(b"Syntax for Mercurial Ignore Files"),
577 loaddoc(b'hgignore'),
577 loaddoc(b'hgignore'),
578 TOPIC_CATEGORY_IDS,
578 TOPIC_CATEGORY_IDS,
579 ),
579 ),
580 (
580 (
581 [b"phases"],
581 [b"phases"],
582 _(b"Working with Phases"),
582 _(b"Working with Phases"),
583 loaddoc(b'phases'),
583 loaddoc(b'phases'),
584 TOPIC_CATEGORY_CONCEPTS,
584 TOPIC_CATEGORY_CONCEPTS,
585 ),
585 ),
586 (
586 (
587 [b"evolution"],
587 [b"evolution"],
588 _(b"Safely rewriting history (EXPERIMENTAL)"),
588 _(b"Safely rewriting history (EXPERIMENTAL)"),
589 loaddoc(b'evolution'),
589 loaddoc(b'evolution'),
590 TOPIC_CATEGORY_CONCEPTS,
590 TOPIC_CATEGORY_CONCEPTS,
591 ),
591 ),
592 (
592 (
593 [b'scripting'],
593 [b'scripting'],
594 _(b'Using Mercurial from scripts and automation'),
594 _(b'Using Mercurial from scripts and automation'),
595 loaddoc(b'scripting'),
595 loaddoc(b'scripting'),
596 TOPIC_CATEGORY_MISC,
596 TOPIC_CATEGORY_MISC,
597 ),
597 ),
598 (
598 (
599 [b'internals'],
599 [b'internals'],
600 _(b"Technical implementation topics"),
600 _(b"Technical implementation topics"),
601 internalshelp,
601 internalshelp,
602 TOPIC_CATEGORY_MISC,
602 TOPIC_CATEGORY_MISC,
603 ),
603 ),
604 (
604 (
605 [b'pager'],
605 [b'pager'],
606 _(b"Pager Support"),
606 _(b"Pager Support"),
607 loaddoc(b'pager'),
607 loaddoc(b'pager'),
608 TOPIC_CATEGORY_CONFIG,
608 TOPIC_CATEGORY_CONFIG,
609 ),
609 ),
610 ]
610 ]
611 )
611 )
612
612
613 # Maps topics with sub-topics to a list of their sub-topics.
613 # Maps topics with sub-topics to a list of their sub-topics.
614 subtopics: Dict[bytes, List[_HelpEntryNoCategory]] = {
614 subtopics: Dict[bytes, List[_HelpEntryNoCategory]] = {
615 b'internals': internalstable,
615 b'internals': internalstable,
616 }
616 }
617
617
618 # Map topics to lists of callable taking the current topic help and
618 # Map topics to lists of callable taking the current topic help and
619 # returning the updated version
619 # returning the updated version
620 helphooks: Dict[bytes, List[_TopicHook]] = {}
620 helphooks: Dict[bytes, List[_TopicHook]] = {}
621
621
622
622
623 def addtopichook(topic: bytes, rewriter: _TopicHook) -> None:
623 def addtopichook(topic: bytes, rewriter: _TopicHook) -> None:
624 helphooks.setdefault(topic, []).append(rewriter)
624 helphooks.setdefault(topic, []).append(rewriter)
625
625
626
626
627 def makeitemsdoc(
627 def makeitemsdoc(
628 ui: uimod.ui,
628 ui: uimod.ui,
629 topic: bytes,
629 topic: bytes,
630 doc: bytes,
630 doc: bytes,
631 marker: bytes,
631 marker: bytes,
632 items: Dict[bytes, bytes],
632 items: Dict[bytes, bytes],
633 dedent: bool = False,
633 dedent: bool = False,
634 ) -> bytes:
634 ) -> bytes:
635 """Extract docstring from the items key to function mapping, build a
635 """Extract docstring from the items key to function mapping, build a
636 single documentation block and use it to overwrite the marker in doc.
636 single documentation block and use it to overwrite the marker in doc.
637 """
637 """
638 entries = []
638 entries = []
639 for name in sorted(items):
639 for name in sorted(items):
640 text = (pycompat.getdoc(items[name]) or b'').rstrip()
640 text = (pycompat.getdoc(items[name]) or b'').rstrip()
641 if not text or not ui.verbose and any(w in text for w in _exclkeywords):
641 if not text or not ui.verbose and any(w in text for w in _exclkeywords):
642 continue
642 continue
643 text = gettext(text)
643 text = gettext(text)
644 if dedent:
644 if dedent:
645 # Abuse latin1 to use textwrap.dedent() on bytes.
645 # Abuse latin1 to use textwrap.dedent() on bytes.
646 text = textwrap.dedent(text.decode('latin1')).encode('latin1')
646 text = textwrap.dedent(text.decode('latin1')).encode('latin1')
647 lines = text.splitlines()
647 lines = text.splitlines()
648 doclines = [lines[0]]
648 doclines = [lines[0]]
649 for l in lines[1:]:
649 for l in lines[1:]:
650 # Stop once we find some Python doctest
650 # Stop once we find some Python doctest
651 if l.strip().startswith(b'>>>'):
651 if l.strip().startswith(b'>>>'):
652 break
652 break
653 if dedent:
653 if dedent:
654 doclines.append(l.rstrip())
654 doclines.append(l.rstrip())
655 else:
655 else:
656 doclines.append(b' ' + l.strip())
656 doclines.append(b' ' + l.strip())
657 entries.append(b'\n'.join(doclines))
657 entries.append(b'\n'.join(doclines))
658 entries = b'\n\n'.join(entries)
658 entries = b'\n\n'.join(entries)
659 return doc.replace(marker, entries)
659 return doc.replace(marker, entries)
660
660
661
661
662 def addtopicsymbols(
662 def addtopicsymbols(
663 topic: bytes, marker: bytes, symbols, dedent: bool = False
663 topic: bytes, marker: bytes, symbols, dedent: bool = False
664 ) -> None:
664 ) -> None:
665 def add(ui: uimod.ui, topic: bytes, doc: bytes):
665 def add(ui: uimod.ui, topic: bytes, doc: bytes):
666 return makeitemsdoc(ui, topic, doc, marker, symbols, dedent=dedent)
666 return makeitemsdoc(ui, topic, doc, marker, symbols, dedent=dedent)
667
667
668 addtopichook(topic, add)
668 addtopichook(topic, add)
669
669
670
670
671 addtopicsymbols(
671 addtopicsymbols(
672 b'bundlespec',
672 b'bundlespec',
673 b'.. bundlecompressionmarker',
673 b'.. bundlecompressionmarker',
674 compression.bundlecompressiontopics(),
674 compression.bundlecompressiontopics(),
675 )
675 )
676 addtopicsymbols(b'filesets', b'.. predicatesmarker', fileset.symbols)
676 addtopicsymbols(b'filesets', b'.. predicatesmarker', fileset.symbols)
677 addtopicsymbols(
677 addtopicsymbols(
678 b'merge-tools', b'.. internaltoolsmarker', filemerge.internalsdoc
678 b'merge-tools', b'.. internaltoolsmarker', filemerge.internalsdoc
679 )
679 )
680 addtopicsymbols(b'revisions', b'.. predicatesmarker', revset.symbols)
680 addtopicsymbols(b'revisions', b'.. predicatesmarker', revset.symbols)
681 addtopicsymbols(b'templates', b'.. keywordsmarker', templatekw.keywords)
681 addtopicsymbols(b'templates', b'.. keywordsmarker', templatekw.keywords)
682 addtopicsymbols(b'templates', b'.. filtersmarker', templatefilters.filters)
682 addtopicsymbols(b'templates', b'.. filtersmarker', templatefilters.filters)
683 addtopicsymbols(b'templates', b'.. functionsmarker', templatefuncs.funcs)
683 addtopicsymbols(b'templates', b'.. functionsmarker', templatefuncs.funcs)
684 addtopicsymbols(
684 addtopicsymbols(
685 b'hgweb', b'.. webcommandsmarker', webcommands.commands, dedent=True
685 b'hgweb', b'.. webcommandsmarker', webcommands.commands, dedent=True
686 )
686 )
687
687
688
688
689 def inserttweakrc(ui: uimod.ui, topic: bytes, doc: bytes) -> bytes:
689 def inserttweakrc(ui: uimod.ui, topic: bytes, doc: bytes) -> bytes:
690 marker = b'.. tweakdefaultsmarker'
690 marker = b'.. tweakdefaultsmarker'
691 repl = uimod.tweakrc
691 repl = uimod.tweakrc
692
692
693 def sub(m):
693 def sub(m):
694 lines = [m.group(1) + s for s in repl.splitlines()]
694 lines = [m.group(1) + s for s in repl.splitlines()]
695 return b'\n'.join(lines)
695 return b'\n'.join(lines)
696
696
697 return re.sub(br'( *)%s' % re.escape(marker), sub, doc)
697 return re.sub(br'( *)%s' % re.escape(marker), sub, doc)
698
698
699
699
700 def _getcategorizedhelpcmds(
700 def _getcategorizedhelpcmds(
701 ui: uimod.ui, cmdtable, name: bytes, select: Optional[_SelectFn] = None
701 ui: uimod.ui, cmdtable, name: bytes, select: Optional[_SelectFn] = None
702 ) -> Tuple[Dict[bytes, List[bytes]], Dict[bytes, bytes], _SynonymTable]:
702 ) -> Tuple[Dict[bytes, List[bytes]], Dict[bytes, bytes], _SynonymTable]:
703 # Category -> list of commands
703 # Category -> list of commands
704 cats = {}
704 cats = {}
705 # Command -> short description
705 # Command -> short description
706 h = {}
706 h = {}
707 # Command -> string showing synonyms
707 # Command -> string showing synonyms
708 syns = {}
708 syns = {}
709 for c, e in cmdtable.items():
709 for c, e in cmdtable.items():
710 fs = cmdutil.parsealiases(c)
710 fs = cmdutil.parsealiases(c)
711 f = fs[0]
711 f = fs[0]
712 syns[f] = fs
712 syns[f] = fs
713 func = e[0]
713 func = e[0]
714 if select and not select(f):
714 if select and not select(f):
715 continue
715 continue
716 doc = pycompat.getdoc(func)
716 doc = pycompat.getdoc(func)
717 if filtercmd(ui, f, func, name, doc):
717 if filtercmd(ui, f, func, name, doc):
718 continue
718 continue
719 doc = gettext(doc)
719 doc = gettext(doc)
720 if not doc:
720 if not doc:
721 doc = _(b"(no help text available)")
721 doc = _(b"(no help text available)")
722 h[f] = stringutil.firstline(doc).rstrip()
722 h[f] = stringutil.firstline(doc).rstrip()
723
723
724 cat = getattr(func, 'helpcategory', None) or (
724 cat = getattr(func, 'helpcategory', None) or (
725 registrar.command.CATEGORY_NONE
725 registrar.command.CATEGORY_NONE
726 )
726 )
727 cats.setdefault(cat, []).append(f)
727 cats.setdefault(cat, []).append(f)
728 return cats, h, syns
728 return cats, h, syns
729
729
730
730
731 def _getcategorizedhelptopics(
731 def _getcategorizedhelptopics(
732 ui: uimod.ui, topictable: List[_HelpEntry]
732 ui: uimod.ui, topictable: List[_HelpEntry]
733 ) -> Tuple[Dict[bytes, List[Tuple[bytes, bytes]]], Dict[bytes, List[bytes]]]:
733 ) -> Tuple[Dict[bytes, List[Tuple[bytes, bytes]]], Dict[bytes, List[bytes]]]:
734 # Group commands by category.
734 # Group commands by category.
735 topiccats = {}
735 topiccats = {}
736 syns = {}
736 syns = {}
737 for topic in topictable:
737 for topic in topictable:
738 names, header, doc = topic[0:3]
738 names, header, doc = topic[0:3]
739 if len(topic) > 3 and topic[3]:
739 if len(topic) > 3 and topic[3]:
740 category: bytes = cast(bytes, topic[3]) # help pytype
740 category: bytes = cast(bytes, topic[3]) # help pytype
741 else:
741 else:
742 category: bytes = TOPIC_CATEGORY_NONE
742 category: bytes = TOPIC_CATEGORY_NONE
743
743
744 topicname = names[0]
744 topicname = names[0]
745 syns[topicname] = list(names)
745 syns[topicname] = list(names)
746 if not filtertopic(ui, topicname):
746 if not filtertopic(ui, topicname):
747 topiccats.setdefault(category, []).append((topicname, header))
747 topiccats.setdefault(category, []).append((topicname, header))
748 return topiccats, syns
748 return topiccats, syns
749
749
750
750
751 addtopichook(b'config', inserttweakrc)
751 addtopichook(b'config', inserttweakrc)
752
752
753
753
754 def help_(
754 def help_(
755 ui: uimod.ui,
755 ui: uimod.ui,
756 commands,
756 commands,
757 name: bytes,
757 name: bytes,
758 unknowncmd: bool = False,
758 unknowncmd: bool = False,
759 full: bool = True,
759 full: bool = True,
760 subtopic: Optional[bytes] = None,
760 subtopic: Optional[bytes] = None,
761 fullname: Optional[bytes] = None,
761 fullname: Optional[bytes] = None,
762 **opts
762 **opts
763 ) -> bytes:
763 ) -> bytes:
764 """
764 """
765 Generate the help for 'name' as unformatted restructured text. If
765 Generate the help for 'name' as unformatted restructured text. If
766 'name' is None, describe the commands available.
766 'name' is None, describe the commands available.
767 """
767 """
768
768
769 opts = pycompat.byteskwargs(opts)
769 opts = pycompat.byteskwargs(opts)
770
770
771 def helpcmd(name: bytes, subtopic: Optional[bytes]) -> List[bytes]:
771 def helpcmd(name: bytes, subtopic: Optional[bytes]) -> List[bytes]:
772 try:
772 try:
773 aliases, entry = cmdutil.findcmd(
773 aliases, entry = cmdutil.findcmd(
774 name, commands.table, strict=unknowncmd
774 name, commands.table, strict=unknowncmd
775 )
775 )
776 except error.AmbiguousCommand as inst:
776 except error.AmbiguousCommand as inst:
777 # py3 fix: except vars can't be used outside the scope of the
777 # py3 fix: except vars can't be used outside the scope of the
778 # except block, nor can be used inside a lambda. python issue4617
778 # except block, nor can be used inside a lambda. python issue4617
779 prefix = inst.prefix
779 prefix = inst.prefix
780 select = lambda c: cmdutil.parsealiases(c)[0].startswith(prefix)
780 select = lambda c: cmdutil.parsealiases(c)[0].startswith(prefix)
781 rst = helplist(select)
781 rst = helplist(select)
782 return rst
782 return rst
783
783
784 rst = []
784 rst = []
785
785
786 # check if it's an invalid alias and display its error if it is
786 # check if it's an invalid alias and display its error if it is
787 if getattr(entry[0], 'badalias', None):
787 if getattr(entry[0], 'badalias', None):
788 rst.append(entry[0].badalias + b'\n')
788 rst.append(entry[0].badalias + b'\n')
789 if entry[0].unknowncmd:
789 if entry[0].unknowncmd:
790 try:
790 try:
791 rst.extend(helpextcmd(entry[0].cmdname))
791 rst.extend(helpextcmd(entry[0].cmdname))
792 except error.UnknownCommand:
792 except error.UnknownCommand:
793 pass
793 pass
794 return rst
794 return rst
795
795
796 # synopsis
796 # synopsis
797 if len(entry) > 2:
797 if len(entry) > 2:
798 if entry[2].startswith(b'hg'):
798 if entry[2].startswith(b'hg'):
799 rst.append(b"%s\n" % entry[2])
799 rst.append(b"%s\n" % entry[2])
800 else:
800 else:
801 rst.append(b'hg %s %s\n' % (aliases[0], entry[2]))
801 rst.append(b'hg %s %s\n' % (aliases[0], entry[2]))
802 else:
802 else:
803 rst.append(b'hg %s\n' % aliases[0])
803 rst.append(b'hg %s\n' % aliases[0])
804 # aliases
804 # aliases
805 if full and not ui.quiet and len(aliases) > 1:
805 if full and not ui.quiet and len(aliases) > 1:
806 rst.append(_(b"\naliases: %s\n") % b', '.join(aliases[1:]))
806 rst.append(_(b"\naliases: %s\n") % b', '.join(aliases[1:]))
807 rst.append(b'\n')
807 rst.append(b'\n')
808
808
809 # description
809 # description
810 doc = gettext(pycompat.getdoc(entry[0]))
810 doc = gettext(pycompat.getdoc(entry[0]))
811 if not doc:
811 if not doc:
812 doc = _(b"(no help text available)")
812 doc = _(b"(no help text available)")
813 if util.safehasattr(entry[0], b'definition'): # aliased command
813 if util.safehasattr(entry[0], b'definition'): # aliased command
814 source = entry[0].source
814 source = entry[0].source
815 if entry[0].definition.startswith(b'!'): # shell alias
815 if entry[0].definition.startswith(b'!'): # shell alias
816 doc = _(b'shell alias for: %s\n\n%s\n\ndefined by: %s\n') % (
816 doc = _(b'shell alias for: %s\n\n%s\n\ndefined by: %s\n') % (
817 entry[0].definition[1:],
817 entry[0].definition[1:],
818 doc,
818 doc,
819 source,
819 source,
820 )
820 )
821 else:
821 else:
822 doc = _(b'alias for: hg %s\n\n%s\n\ndefined by: %s\n') % (
822 doc = _(b'alias for: hg %s\n\n%s\n\ndefined by: %s\n') % (
823 entry[0].definition,
823 entry[0].definition,
824 doc,
824 doc,
825 source,
825 source,
826 )
826 )
827 doc = doc.splitlines(True)
827 doc = doc.splitlines(True)
828 if ui.quiet or not full:
828 if ui.quiet or not full:
829 rst.append(doc[0])
829 rst.append(doc[0])
830 else:
830 else:
831 rst.extend(doc)
831 rst.extend(doc)
832 rst.append(b'\n')
832 rst.append(b'\n')
833
833
834 # check if this command shadows a non-trivial (multi-line)
834 # check if this command shadows a non-trivial (multi-line)
835 # extension help text
835 # extension help text
836 try:
836 try:
837 mod = extensions.find(name)
837 mod = extensions.find(name)
838 doc = gettext(pycompat.getdoc(mod)) or b''
838 doc = gettext(pycompat.getdoc(mod)) or b''
839 if b'\n' in doc.strip():
839 if b'\n' in doc.strip():
840 msg = _(
840 msg = _(
841 b"(use 'hg help -e %s' to show help for "
841 b"(use 'hg help -e %s' to show help for "
842 b"the %s extension)"
842 b"the %s extension)"
843 ) % (name, name)
843 ) % (name, name)
844 rst.append(b'\n%s\n' % msg)
844 rst.append(b'\n%s\n' % msg)
845 except KeyError:
845 except KeyError:
846 pass
846 pass
847
847
848 # options
848 # options
849 if not ui.quiet and entry[1]:
849 if not ui.quiet and entry[1]:
850 rst.append(optrst(_(b"options"), entry[1], ui.verbose, ui))
850 rst.append(optrst(_(b"options"), entry[1], ui.verbose, ui))
851
851
852 if ui.verbose:
852 if ui.verbose:
853 rst.append(
853 rst.append(
854 optrst(
854 optrst(
855 _(b"global options"), commands.globalopts, ui.verbose, ui
855 _(b"global options"), commands.globalopts, ui.verbose, ui
856 )
856 )
857 )
857 )
858
858
859 if not ui.verbose:
859 if not ui.verbose:
860 if not full:
860 if not full:
861 rst.append(_(b"\n(use 'hg %s -h' to show more help)\n") % name)
861 rst.append(_(b"\n(use 'hg %s -h' to show more help)\n") % name)
862 elif not ui.quiet:
862 elif not ui.quiet:
863 rst.append(
863 rst.append(
864 _(
864 _(
865 b'\n(some details hidden, use --verbose '
865 b'\n(some details hidden, use --verbose '
866 b'to show complete help)'
866 b'to show complete help)'
867 )
867 )
868 )
868 )
869
869
870 return rst
870 return rst
871
871
872 def helplist(select: Optional[_SelectFn] = None, **opts) -> List[bytes]:
872 def helplist(select: Optional[_SelectFn] = None, **opts) -> List[bytes]:
873 cats, h, syns = _getcategorizedhelpcmds(
873 cats, h, syns = _getcategorizedhelpcmds(
874 ui, commands.table, name, select
874 ui, commands.table, name, select
875 )
875 )
876
876
877 rst = []
877 rst = []
878 if not h:
878 if not h:
879 if not ui.quiet:
879 if not ui.quiet:
880 rst.append(_(b'no commands defined\n'))
880 rst.append(_(b'no commands defined\n'))
881 return rst
881 return rst
882
882
883 # Output top header.
883 # Output top header.
884 if not ui.quiet:
884 if not ui.quiet:
885 if name == b"shortlist":
885 if name == b"shortlist":
886 rst.append(_(b'basic commands:\n\n'))
886 rst.append(_(b'basic commands:\n\n'))
887 elif name == b"debug":
887 elif name == b"debug":
888 rst.append(_(b'debug commands (internal and unsupported):\n\n'))
888 rst.append(_(b'debug commands (internal and unsupported):\n\n'))
889 else:
889 else:
890 rst.append(_(b'list of commands:\n'))
890 rst.append(_(b'list of commands:\n'))
891
891
892 def appendcmds(cmds: Iterable[bytes]) -> None:
892 def appendcmds(cmds: Iterable[bytes]) -> None:
893 cmds = sorted(cmds)
893 cmds = sorted(cmds)
894 for c in cmds:
894 for c in cmds:
895 display_cmd = c
895 display_cmd = c
896 if ui.verbose:
896 if ui.verbose:
897 display_cmd = b', '.join(syns[c])
897 display_cmd = b', '.join(syns[c])
898 display_cmd = display_cmd.replace(b':', br'\:')
898 display_cmd = display_cmd.replace(b':', br'\:')
899 rst.append(b' :%s: %s\n' % (display_cmd, h[c]))
899 rst.append(b' :%s: %s\n' % (display_cmd, h[c]))
900
900
901 if name in (b'shortlist', b'debug'):
901 if name in (b'shortlist', b'debug'):
902 # List without categories.
902 # List without categories.
903 appendcmds(h)
903 appendcmds(h)
904 else:
904 else:
905 # Check that all categories have an order.
905 # Check that all categories have an order.
906 missing_order = set(cats.keys()) - set(CATEGORY_ORDER)
906 missing_order = set(cats.keys()) - set(CATEGORY_ORDER)
907 if missing_order:
907 if missing_order:
908 ui.develwarn(
908 ui.develwarn(
909 b'help categories missing from CATEGORY_ORDER: %s'
909 b'help categories missing from CATEGORY_ORDER: %s'
910 % missing_order
910 % stringutil.forcebytestr(missing_order)
911 )
911 )
912
912
913 # List per category.
913 # List per category.
914 for cat in CATEGORY_ORDER:
914 for cat in CATEGORY_ORDER:
915 catfns = cats.get(cat, [])
915 catfns = cats.get(cat, [])
916 if catfns:
916 if catfns:
917 if len(cats) > 1:
917 if len(cats) > 1:
918 catname = gettext(CATEGORY_NAMES[cat])
918 catname = gettext(CATEGORY_NAMES[cat])
919 rst.append(b"\n%s:\n" % catname)
919 rst.append(b"\n%s:\n" % catname)
920 rst.append(b"\n")
920 rst.append(b"\n")
921 appendcmds(catfns)
921 appendcmds(catfns)
922
922
923 ex = opts.get
923 ex = opts.get
924 anyopts = ex('keyword') or not (ex('command') or ex('extension'))
924 anyopts = ex('keyword') or not (ex('command') or ex('extension'))
925 if not name and anyopts:
925 if not name and anyopts:
926 exts = listexts(
926 exts = listexts(
927 _(b'enabled extensions:'),
927 _(b'enabled extensions:'),
928 extensions.enabled(),
928 extensions.enabled(),
929 showdeprecated=ui.verbose,
929 showdeprecated=ui.verbose,
930 )
930 )
931 if exts:
931 if exts:
932 rst.append(b'\n')
932 rst.append(b'\n')
933 rst.extend(exts)
933 rst.extend(exts)
934
934
935 rst.append(_(b"\nadditional help topics:\n"))
935 rst.append(_(b"\nadditional help topics:\n"))
936 topiccats, topicsyns = _getcategorizedhelptopics(ui, helptable)
936 topiccats, topicsyns = _getcategorizedhelptopics(ui, helptable)
937
937
938 # Check that all categories have an order.
938 # Check that all categories have an order.
939 missing_order = set(topiccats.keys()) - set(TOPIC_CATEGORY_ORDER)
939 missing_order = set(topiccats.keys()) - set(TOPIC_CATEGORY_ORDER)
940 if missing_order:
940 if missing_order:
941 ui.develwarn(
941 ui.develwarn(
942 b'help categories missing from TOPIC_CATEGORY_ORDER: %s'
942 b'help categories missing from TOPIC_CATEGORY_ORDER: %s'
943 % missing_order
943 % stringutil.forcebytestr(missing_order)
944 )
944 )
945
945
946 # Output topics per category.
946 # Output topics per category.
947 for cat in TOPIC_CATEGORY_ORDER:
947 for cat in TOPIC_CATEGORY_ORDER:
948 topics = topiccats.get(cat, [])
948 topics = topiccats.get(cat, [])
949 if topics:
949 if topics:
950 if len(topiccats) > 1:
950 if len(topiccats) > 1:
951 catname = gettext(TOPIC_CATEGORY_NAMES[cat])
951 catname = gettext(TOPIC_CATEGORY_NAMES[cat])
952 rst.append(b"\n%s:\n" % catname)
952 rst.append(b"\n%s:\n" % catname)
953 rst.append(b"\n")
953 rst.append(b"\n")
954 for t, desc in topics:
954 for t, desc in topics:
955 rst.append(b" :%s: %s\n" % (t, desc))
955 rst.append(b" :%s: %s\n" % (t, desc))
956
956
957 if ui.quiet:
957 if ui.quiet:
958 pass
958 pass
959 elif ui.verbose:
959 elif ui.verbose:
960 rst.append(
960 rst.append(
961 b'\n%s\n'
961 b'\n%s\n'
962 % optrst(
962 % optrst(
963 _(b"global options"), commands.globalopts, ui.verbose, ui
963 _(b"global options"), commands.globalopts, ui.verbose, ui
964 )
964 )
965 )
965 )
966 if name == b'shortlist':
966 if name == b'shortlist':
967 rst.append(
967 rst.append(
968 _(b"\n(use 'hg help' for the full list of commands)\n")
968 _(b"\n(use 'hg help' for the full list of commands)\n")
969 )
969 )
970 else:
970 else:
971 if name == b'shortlist':
971 if name == b'shortlist':
972 rst.append(
972 rst.append(
973 _(
973 _(
974 b"\n(use 'hg help' for the full list of commands "
974 b"\n(use 'hg help' for the full list of commands "
975 b"or 'hg -v' for details)\n"
975 b"or 'hg -v' for details)\n"
976 )
976 )
977 )
977 )
978 elif name and not full:
978 elif name and not full:
979 rst.append(
979 rst.append(
980 _(b"\n(use 'hg help %s' to show the full help text)\n")
980 _(b"\n(use 'hg help %s' to show the full help text)\n")
981 % name
981 % name
982 )
982 )
983 elif name and syns and name in syns.keys():
983 elif name and syns and name in syns.keys():
984 rst.append(
984 rst.append(
985 _(
985 _(
986 b"\n(use 'hg help -v -e %s' to show built-in "
986 b"\n(use 'hg help -v -e %s' to show built-in "
987 b"aliases and global options)\n"
987 b"aliases and global options)\n"
988 )
988 )
989 % name
989 % name
990 )
990 )
991 else:
991 else:
992 rst.append(
992 rst.append(
993 _(
993 _(
994 b"\n(use 'hg help -v%s' to show built-in aliases "
994 b"\n(use 'hg help -v%s' to show built-in aliases "
995 b"and global options)\n"
995 b"and global options)\n"
996 )
996 )
997 % (name and b" " + name or b"")
997 % (name and b" " + name or b"")
998 )
998 )
999 return rst
999 return rst
1000
1000
1001 def helptopic(name: bytes, subtopic: Optional[bytes] = None) -> List[bytes]:
1001 def helptopic(name: bytes, subtopic: Optional[bytes] = None) -> List[bytes]:
1002 # Look for sub-topic entry first.
1002 # Look for sub-topic entry first.
1003 header, doc = None, None
1003 header, doc = None, None
1004 if subtopic and name in subtopics:
1004 if subtopic and name in subtopics:
1005 for names, header, doc in subtopics[name]:
1005 for names, header, doc in subtopics[name]:
1006 if subtopic in names:
1006 if subtopic in names:
1007 break
1007 break
1008 if not any(subtopic in s[0] for s in subtopics[name]):
1008 if not any(subtopic in s[0] for s in subtopics[name]):
1009 raise error.UnknownCommand(name)
1009 raise error.UnknownCommand(name)
1010
1010
1011 if not header:
1011 if not header:
1012 for topic in helptable:
1012 for topic in helptable:
1013 names, header, doc = topic[0:3]
1013 names, header, doc = topic[0:3]
1014 if name in names:
1014 if name in names:
1015 break
1015 break
1016 else:
1016 else:
1017 raise error.UnknownCommand(name)
1017 raise error.UnknownCommand(name)
1018
1018
1019 rst = [minirst.section(header)]
1019 rst = [minirst.section(header)]
1020
1020
1021 # description
1021 # description
1022 if not doc:
1022 if not doc:
1023 rst.append(b" %s\n" % _(b"(no help text available)"))
1023 rst.append(b" %s\n" % _(b"(no help text available)"))
1024 if callable(doc):
1024 if callable(doc):
1025 rst += [b" %s\n" % l for l in doc(ui).splitlines()]
1025 rst += [b" %s\n" % l for l in doc(ui).splitlines()]
1026
1026
1027 if not ui.verbose:
1027 if not ui.verbose:
1028 omitted = _(
1028 omitted = _(
1029 b'(some details hidden, use --verbose'
1029 b'(some details hidden, use --verbose'
1030 b' to show complete help)'
1030 b' to show complete help)'
1031 )
1031 )
1032 indicateomitted(rst, omitted)
1032 indicateomitted(rst, omitted)
1033
1033
1034 try:
1034 try:
1035 cmdutil.findcmd(name, commands.table)
1035 cmdutil.findcmd(name, commands.table)
1036 rst.append(
1036 rst.append(
1037 _(b"\nuse 'hg help -c %s' to see help for the %s command\n")
1037 _(b"\nuse 'hg help -c %s' to see help for the %s command\n")
1038 % (name, name)
1038 % (name, name)
1039 )
1039 )
1040 except error.UnknownCommand:
1040 except error.UnknownCommand:
1041 pass
1041 pass
1042 return rst
1042 return rst
1043
1043
1044 def helpext(name: bytes, subtopic: Optional[bytes] = None) -> List[bytes]:
1044 def helpext(name: bytes, subtopic: Optional[bytes] = None) -> List[bytes]:
1045 try:
1045 try:
1046 mod = extensions.find(name)
1046 mod = extensions.find(name)
1047 doc = gettext(pycompat.getdoc(mod)) or _(b'no help text available')
1047 doc = gettext(pycompat.getdoc(mod)) or _(b'no help text available')
1048 except KeyError:
1048 except KeyError:
1049 mod = None
1049 mod = None
1050 doc = extensions.disabled_help(name)
1050 doc = extensions.disabled_help(name)
1051 if not doc:
1051 if not doc:
1052 raise error.UnknownCommand(name)
1052 raise error.UnknownCommand(name)
1053
1053
1054 if b'\n' not in doc:
1054 if b'\n' not in doc:
1055 head, tail = doc, b""
1055 head, tail = doc, b""
1056 else:
1056 else:
1057 head, tail = doc.split(b'\n', 1)
1057 head, tail = doc.split(b'\n', 1)
1058 rst = [_(b'%s extension - %s\n\n') % (name.rpartition(b'.')[-1], head)]
1058 rst = [_(b'%s extension - %s\n\n') % (name.rpartition(b'.')[-1], head)]
1059 if tail:
1059 if tail:
1060 rst.extend(tail.splitlines(True))
1060 rst.extend(tail.splitlines(True))
1061 rst.append(b'\n')
1061 rst.append(b'\n')
1062
1062
1063 if not ui.verbose:
1063 if not ui.verbose:
1064 omitted = _(
1064 omitted = _(
1065 b'(some details hidden, use --verbose'
1065 b'(some details hidden, use --verbose'
1066 b' to show complete help)'
1066 b' to show complete help)'
1067 )
1067 )
1068 indicateomitted(rst, omitted)
1068 indicateomitted(rst, omitted)
1069
1069
1070 if mod:
1070 if mod:
1071 try:
1071 try:
1072 ct = mod.cmdtable
1072 ct = mod.cmdtable
1073 except AttributeError:
1073 except AttributeError:
1074 ct = {}
1074 ct = {}
1075 modcmds = {c.partition(b'|')[0] for c in ct}
1075 modcmds = {c.partition(b'|')[0] for c in ct}
1076 rst.extend(helplist(modcmds.__contains__))
1076 rst.extend(helplist(modcmds.__contains__))
1077 else:
1077 else:
1078 rst.append(
1078 rst.append(
1079 _(
1079 _(
1080 b"(use 'hg help extensions' for information on enabling"
1080 b"(use 'hg help extensions' for information on enabling"
1081 b" extensions)\n"
1081 b" extensions)\n"
1082 )
1082 )
1083 )
1083 )
1084 return rst
1084 return rst
1085
1085
1086 def helpextcmd(
1086 def helpextcmd(
1087 name: bytes, subtopic: Optional[bytes] = None
1087 name: bytes, subtopic: Optional[bytes] = None
1088 ) -> List[bytes]:
1088 ) -> List[bytes]:
1089 cmd, ext, doc = extensions.disabledcmd(
1089 cmd, ext, doc = extensions.disabledcmd(
1090 ui, name, ui.configbool(b'ui', b'strict')
1090 ui, name, ui.configbool(b'ui', b'strict')
1091 )
1091 )
1092 doc = stringutil.firstline(doc)
1092 doc = stringutil.firstline(doc)
1093
1093
1094 rst = listexts(
1094 rst = listexts(
1095 _(b"'%s' is provided by the following extension:") % cmd,
1095 _(b"'%s' is provided by the following extension:") % cmd,
1096 {ext: doc},
1096 {ext: doc},
1097 indent=4,
1097 indent=4,
1098 showdeprecated=True,
1098 showdeprecated=True,
1099 )
1099 )
1100 rst.append(b'\n')
1100 rst.append(b'\n')
1101 rst.append(
1101 rst.append(
1102 _(
1102 _(
1103 b"(use 'hg help extensions' for information on enabling "
1103 b"(use 'hg help extensions' for information on enabling "
1104 b"extensions)\n"
1104 b"extensions)\n"
1105 )
1105 )
1106 )
1106 )
1107 return rst
1107 return rst
1108
1108
1109 rst = []
1109 rst = []
1110 kw = opts.get(b'keyword')
1110 kw = opts.get(b'keyword')
1111 if kw or name is None and any(opts[o] for o in opts):
1111 if kw or name is None and any(opts[o] for o in opts):
1112 matches = topicmatch(ui, commands, name or b'')
1112 matches = topicmatch(ui, commands, name or b'')
1113 helpareas = []
1113 helpareas = []
1114 if opts.get(b'extension'):
1114 if opts.get(b'extension'):
1115 helpareas += [(b'extensions', _(b'Extensions'))]
1115 helpareas += [(b'extensions', _(b'Extensions'))]
1116 if opts.get(b'command'):
1116 if opts.get(b'command'):
1117 helpareas += [(b'commands', _(b'Commands'))]
1117 helpareas += [(b'commands', _(b'Commands'))]
1118 if not helpareas:
1118 if not helpareas:
1119 helpareas = [
1119 helpareas = [
1120 (b'topics', _(b'Topics')),
1120 (b'topics', _(b'Topics')),
1121 (b'commands', _(b'Commands')),
1121 (b'commands', _(b'Commands')),
1122 (b'extensions', _(b'Extensions')),
1122 (b'extensions', _(b'Extensions')),
1123 (b'extensioncommands', _(b'Extension Commands')),
1123 (b'extensioncommands', _(b'Extension Commands')),
1124 ]
1124 ]
1125 for t, title in helpareas:
1125 for t, title in helpareas:
1126 if matches[t]:
1126 if matches[t]:
1127 rst.append(b'%s:\n\n' % title)
1127 rst.append(b'%s:\n\n' % title)
1128 rst.extend(minirst.maketable(sorted(matches[t]), 1))
1128 rst.extend(minirst.maketable(sorted(matches[t]), 1))
1129 rst.append(b'\n')
1129 rst.append(b'\n')
1130 if not rst:
1130 if not rst:
1131 msg = _(b'no matches')
1131 msg = _(b'no matches')
1132 hint = _(b"try 'hg help' for a list of topics")
1132 hint = _(b"try 'hg help' for a list of topics")
1133 raise error.InputError(msg, hint=hint)
1133 raise error.InputError(msg, hint=hint)
1134 elif name and name != b'shortlist':
1134 elif name and name != b'shortlist':
1135 queries = []
1135 queries = []
1136 if unknowncmd:
1136 if unknowncmd:
1137 queries += [helpextcmd]
1137 queries += [helpextcmd]
1138 if opts.get(b'extension'):
1138 if opts.get(b'extension'):
1139 queries += [helpext]
1139 queries += [helpext]
1140 if opts.get(b'command'):
1140 if opts.get(b'command'):
1141 queries += [helpcmd]
1141 queries += [helpcmd]
1142 if not queries:
1142 if not queries:
1143 queries = (helptopic, helpcmd, helpext, helpextcmd)
1143 queries = (helptopic, helpcmd, helpext, helpextcmd)
1144 for f in queries:
1144 for f in queries:
1145 try:
1145 try:
1146 rst = f(name, subtopic)
1146 rst = f(name, subtopic)
1147 break
1147 break
1148 except error.UnknownCommand:
1148 except error.UnknownCommand:
1149 pass
1149 pass
1150 else:
1150 else:
1151 if unknowncmd:
1151 if unknowncmd:
1152 raise error.UnknownCommand(name)
1152 raise error.UnknownCommand(name)
1153 else:
1153 else:
1154 if fullname:
1154 if fullname:
1155 formatname = fullname
1155 formatname = fullname
1156 else:
1156 else:
1157 formatname = name
1157 formatname = name
1158 if subtopic:
1158 if subtopic:
1159 hintname = subtopic
1159 hintname = subtopic
1160 else:
1160 else:
1161 hintname = name
1161 hintname = name
1162 msg = _(b'no such help topic: %s') % formatname
1162 msg = _(b'no such help topic: %s') % formatname
1163 hint = _(b"try 'hg help --keyword %s'") % hintname
1163 hint = _(b"try 'hg help --keyword %s'") % hintname
1164 raise error.InputError(msg, hint=hint)
1164 raise error.InputError(msg, hint=hint)
1165 else:
1165 else:
1166 # program name
1166 # program name
1167 if not ui.quiet:
1167 if not ui.quiet:
1168 rst = [_(b"Mercurial Distributed SCM\n"), b'\n']
1168 rst = [_(b"Mercurial Distributed SCM\n"), b'\n']
1169 rst.extend(helplist(None, **pycompat.strkwargs(opts)))
1169 rst.extend(helplist(None, **pycompat.strkwargs(opts)))
1170
1170
1171 return b''.join(rst)
1171 return b''.join(rst)
1172
1172
1173
1173
1174 def formattedhelp(
1174 def formattedhelp(
1175 ui: uimod.ui,
1175 ui: uimod.ui,
1176 commands,
1176 commands,
1177 fullname: Optional[bytes],
1177 fullname: Optional[bytes],
1178 keep: Optional[Iterable[bytes]] = None,
1178 keep: Optional[Iterable[bytes]] = None,
1179 unknowncmd: bool = False,
1179 unknowncmd: bool = False,
1180 full: bool = True,
1180 full: bool = True,
1181 **opts
1181 **opts
1182 ) -> bytes:
1182 ) -> bytes:
1183 """get help for a given topic (as a dotted name) as rendered rst
1183 """get help for a given topic (as a dotted name) as rendered rst
1184
1184
1185 Either returns the rendered help text or raises an exception.
1185 Either returns the rendered help text or raises an exception.
1186 """
1186 """
1187 if keep is None:
1187 if keep is None:
1188 keep = []
1188 keep = []
1189 else:
1189 else:
1190 keep = list(keep) # make a copy so we can mutate this later
1190 keep = list(keep) # make a copy so we can mutate this later
1191
1191
1192 # <fullname> := <name>[.<subtopic][.<section>]
1192 # <fullname> := <name>[.<subtopic][.<section>]
1193 name = subtopic = section = None
1193 name = subtopic = section = None
1194 if fullname is not None:
1194 if fullname is not None:
1195 nameparts = fullname.split(b'.')
1195 nameparts = fullname.split(b'.')
1196 name = nameparts.pop(0)
1196 name = nameparts.pop(0)
1197 if nameparts and name in subtopics:
1197 if nameparts and name in subtopics:
1198 subtopic = nameparts.pop(0)
1198 subtopic = nameparts.pop(0)
1199 if nameparts:
1199 if nameparts:
1200 section = encoding.lower(b'.'.join(nameparts))
1200 section = encoding.lower(b'.'.join(nameparts))
1201
1201
1202 textwidth = ui.configint(b'ui', b'textwidth')
1202 textwidth = ui.configint(b'ui', b'textwidth')
1203 termwidth = ui.termwidth() - 2
1203 termwidth = ui.termwidth() - 2
1204 if textwidth <= 0 or termwidth < textwidth:
1204 if textwidth <= 0 or termwidth < textwidth:
1205 textwidth = termwidth
1205 textwidth = termwidth
1206 text = help_(
1206 text = help_(
1207 ui,
1207 ui,
1208 commands,
1208 commands,
1209 name,
1209 name,
1210 fullname=fullname,
1210 fullname=fullname,
1211 subtopic=subtopic,
1211 subtopic=subtopic,
1212 unknowncmd=unknowncmd,
1212 unknowncmd=unknowncmd,
1213 full=full,
1213 full=full,
1214 **opts
1214 **opts
1215 )
1215 )
1216
1216
1217 blocks, pruned = minirst.parse(text, keep=keep)
1217 blocks, pruned = minirst.parse(text, keep=keep)
1218 if b'verbose' in pruned:
1218 if b'verbose' in pruned:
1219 keep.append(b'omitted')
1219 keep.append(b'omitted')
1220 else:
1220 else:
1221 keep.append(b'notomitted')
1221 keep.append(b'notomitted')
1222 blocks, pruned = minirst.parse(text, keep=keep)
1222 blocks, pruned = minirst.parse(text, keep=keep)
1223 if section:
1223 if section:
1224 blocks = minirst.filtersections(blocks, section)
1224 blocks = minirst.filtersections(blocks, section)
1225
1225
1226 # We could have been given a weird ".foo" section without a name
1226 # We could have been given a weird ".foo" section without a name
1227 # to look for, or we could have simply failed to found "foo.bar"
1227 # to look for, or we could have simply failed to found "foo.bar"
1228 # because bar isn't a section of foo
1228 # because bar isn't a section of foo
1229 if section and not (blocks and name):
1229 if section and not (blocks and name):
1230 raise error.InputError(_(b"help section not found: %s") % fullname)
1230 raise error.InputError(_(b"help section not found: %s") % fullname)
1231
1231
1232 return minirst.formatplain(blocks, textwidth)
1232 return minirst.formatplain(blocks, textwidth)
@@ -1,1664 +1,1664 b''
1 # match.py - filename matching
1 # match.py - filename matching
2 #
2 #
3 # Copyright 2008, 2009 Olivia Mackall <olivia@selenic.com> and others
3 # Copyright 2008, 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 bisect
9 import bisect
10 import copy
10 import copy
11 import itertools
11 import itertools
12 import os
12 import os
13 import re
13 import re
14
14
15 from .i18n import _
15 from .i18n import _
16 from .pycompat import open
16 from .pycompat import open
17 from . import (
17 from . import (
18 encoding,
18 encoding,
19 error,
19 error,
20 pathutil,
20 pathutil,
21 policy,
21 policy,
22 pycompat,
22 pycompat,
23 util,
23 util,
24 )
24 )
25 from .utils import stringutil
25 from .utils import stringutil
26
26
27 rustmod = policy.importrust('dirstate')
27 rustmod = policy.importrust('dirstate')
28
28
29 allpatternkinds = (
29 allpatternkinds = (
30 b're',
30 b're',
31 b'glob',
31 b'glob',
32 b'path',
32 b'path',
33 b'relglob',
33 b'relglob',
34 b'relpath',
34 b'relpath',
35 b'relre',
35 b'relre',
36 b'rootglob',
36 b'rootglob',
37 b'listfile',
37 b'listfile',
38 b'listfile0',
38 b'listfile0',
39 b'set',
39 b'set',
40 b'include',
40 b'include',
41 b'subinclude',
41 b'subinclude',
42 b'rootfilesin',
42 b'rootfilesin',
43 )
43 )
44 cwdrelativepatternkinds = (b'relpath', b'glob')
44 cwdrelativepatternkinds = (b'relpath', b'glob')
45
45
46 propertycache = util.propertycache
46 propertycache = util.propertycache
47
47
48
48
49 def _rematcher(regex):
49 def _rematcher(regex):
50 """compile the regexp with the best available regexp engine and return a
50 """compile the regexp with the best available regexp engine and return a
51 matcher function"""
51 matcher function"""
52 m = util.re.compile(regex)
52 m = util.re.compile(regex)
53 try:
53 try:
54 # slightly faster, provided by facebook's re2 bindings
54 # slightly faster, provided by facebook's re2 bindings
55 return m.test_match
55 return m.test_match
56 except AttributeError:
56 except AttributeError:
57 return m.match
57 return m.match
58
58
59
59
60 def _expandsets(cwd, kindpats, ctx=None, listsubrepos=False, badfn=None):
60 def _expandsets(cwd, kindpats, ctx=None, listsubrepos=False, badfn=None):
61 '''Returns the kindpats list with the 'set' patterns expanded to matchers'''
61 '''Returns the kindpats list with the 'set' patterns expanded to matchers'''
62 matchers = []
62 matchers = []
63 other = []
63 other = []
64
64
65 for kind, pat, source in kindpats:
65 for kind, pat, source in kindpats:
66 if kind == b'set':
66 if kind == b'set':
67 if ctx is None:
67 if ctx is None:
68 raise error.ProgrammingError(
68 raise error.ProgrammingError(
69 b"fileset expression with no context"
69 b"fileset expression with no context"
70 )
70 )
71 matchers.append(ctx.matchfileset(cwd, pat, badfn=badfn))
71 matchers.append(ctx.matchfileset(cwd, pat, badfn=badfn))
72
72
73 if listsubrepos:
73 if listsubrepos:
74 for subpath in ctx.substate:
74 for subpath in ctx.substate:
75 sm = ctx.sub(subpath).matchfileset(cwd, pat, badfn=badfn)
75 sm = ctx.sub(subpath).matchfileset(cwd, pat, badfn=badfn)
76 pm = prefixdirmatcher(subpath, sm, badfn=badfn)
76 pm = prefixdirmatcher(subpath, sm, badfn=badfn)
77 matchers.append(pm)
77 matchers.append(pm)
78
78
79 continue
79 continue
80 other.append((kind, pat, source))
80 other.append((kind, pat, source))
81 return matchers, other
81 return matchers, other
82
82
83
83
84 def _expandsubinclude(kindpats, root):
84 def _expandsubinclude(kindpats, root):
85 """Returns the list of subinclude matcher args and the kindpats without the
85 """Returns the list of subinclude matcher args and the kindpats without the
86 subincludes in it."""
86 subincludes in it."""
87 relmatchers = []
87 relmatchers = []
88 other = []
88 other = []
89
89
90 for kind, pat, source in kindpats:
90 for kind, pat, source in kindpats:
91 if kind == b'subinclude':
91 if kind == b'subinclude':
92 sourceroot = pathutil.dirname(util.normpath(source))
92 sourceroot = pathutil.dirname(util.normpath(source))
93 pat = util.pconvert(pat)
93 pat = util.pconvert(pat)
94 path = pathutil.join(sourceroot, pat)
94 path = pathutil.join(sourceroot, pat)
95
95
96 newroot = pathutil.dirname(path)
96 newroot = pathutil.dirname(path)
97 matcherargs = (newroot, b'', [], [b'include:%s' % path])
97 matcherargs = (newroot, b'', [], [b'include:%s' % path])
98
98
99 prefix = pathutil.canonpath(root, root, newroot)
99 prefix = pathutil.canonpath(root, root, newroot)
100 if prefix:
100 if prefix:
101 prefix += b'/'
101 prefix += b'/'
102 relmatchers.append((prefix, matcherargs))
102 relmatchers.append((prefix, matcherargs))
103 else:
103 else:
104 other.append((kind, pat, source))
104 other.append((kind, pat, source))
105
105
106 return relmatchers, other
106 return relmatchers, other
107
107
108
108
109 def _kindpatsalwaysmatch(kindpats):
109 def _kindpatsalwaysmatch(kindpats):
110 """Checks whether the kindspats match everything, as e.g.
110 """Checks whether the kindspats match everything, as e.g.
111 'relpath:.' does.
111 'relpath:.' does.
112 """
112 """
113 for kind, pat, source in kindpats:
113 for kind, pat, source in kindpats:
114 if pat != b'' or kind not in [b'relpath', b'glob']:
114 if pat != b'' or kind not in [b'relpath', b'glob']:
115 return False
115 return False
116 return True
116 return True
117
117
118
118
119 def _buildkindpatsmatcher(
119 def _buildkindpatsmatcher(
120 matchercls,
120 matchercls,
121 root,
121 root,
122 cwd,
122 cwd,
123 kindpats,
123 kindpats,
124 ctx=None,
124 ctx=None,
125 listsubrepos=False,
125 listsubrepos=False,
126 badfn=None,
126 badfn=None,
127 ):
127 ):
128 matchers = []
128 matchers = []
129 fms, kindpats = _expandsets(
129 fms, kindpats = _expandsets(
130 cwd,
130 cwd,
131 kindpats,
131 kindpats,
132 ctx=ctx,
132 ctx=ctx,
133 listsubrepos=listsubrepos,
133 listsubrepos=listsubrepos,
134 badfn=badfn,
134 badfn=badfn,
135 )
135 )
136 if kindpats:
136 if kindpats:
137 m = matchercls(root, kindpats, badfn=badfn)
137 m = matchercls(root, kindpats, badfn=badfn)
138 matchers.append(m)
138 matchers.append(m)
139 if fms:
139 if fms:
140 matchers.extend(fms)
140 matchers.extend(fms)
141 if not matchers:
141 if not matchers:
142 return nevermatcher(badfn=badfn)
142 return nevermatcher(badfn=badfn)
143 if len(matchers) == 1:
143 if len(matchers) == 1:
144 return matchers[0]
144 return matchers[0]
145 return unionmatcher(matchers)
145 return unionmatcher(matchers)
146
146
147
147
148 def match(
148 def match(
149 root,
149 root,
150 cwd,
150 cwd,
151 patterns=None,
151 patterns=None,
152 include=None,
152 include=None,
153 exclude=None,
153 exclude=None,
154 default=b'glob',
154 default=b'glob',
155 auditor=None,
155 auditor=None,
156 ctx=None,
156 ctx=None,
157 listsubrepos=False,
157 listsubrepos=False,
158 warn=None,
158 warn=None,
159 badfn=None,
159 badfn=None,
160 icasefs=False,
160 icasefs=False,
161 ):
161 ):
162 r"""build an object to match a set of file patterns
162 r"""build an object to match a set of file patterns
163
163
164 arguments:
164 arguments:
165 root - the canonical root of the tree you're matching against
165 root - the canonical root of the tree you're matching against
166 cwd - the current working directory, if relevant
166 cwd - the current working directory, if relevant
167 patterns - patterns to find
167 patterns - patterns to find
168 include - patterns to include (unless they are excluded)
168 include - patterns to include (unless they are excluded)
169 exclude - patterns to exclude (even if they are included)
169 exclude - patterns to exclude (even if they are included)
170 default - if a pattern in patterns has no explicit type, assume this one
170 default - if a pattern in patterns has no explicit type, assume this one
171 auditor - optional path auditor
171 auditor - optional path auditor
172 ctx - optional changecontext
172 ctx - optional changecontext
173 listsubrepos - if True, recurse into subrepositories
173 listsubrepos - if True, recurse into subrepositories
174 warn - optional function used for printing warnings
174 warn - optional function used for printing warnings
175 badfn - optional bad() callback for this matcher instead of the default
175 badfn - optional bad() callback for this matcher instead of the default
176 icasefs - make a matcher for wdir on case insensitive filesystems, which
176 icasefs - make a matcher for wdir on case insensitive filesystems, which
177 normalizes the given patterns to the case in the filesystem
177 normalizes the given patterns to the case in the filesystem
178
178
179 a pattern is one of:
179 a pattern is one of:
180 'glob:<glob>' - a glob relative to cwd
180 'glob:<glob>' - a glob relative to cwd
181 're:<regexp>' - a regular expression
181 're:<regexp>' - a regular expression
182 'path:<path>' - a path relative to repository root, which is matched
182 'path:<path>' - a path relative to repository root, which is matched
183 recursively
183 recursively
184 'rootfilesin:<path>' - a path relative to repository root, which is
184 'rootfilesin:<path>' - a path relative to repository root, which is
185 matched non-recursively (will not match subdirectories)
185 matched non-recursively (will not match subdirectories)
186 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
186 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
187 'relpath:<path>' - a path relative to cwd
187 'relpath:<path>' - a path relative to cwd
188 'relre:<regexp>' - a regexp that needn't match the start of a name
188 'relre:<regexp>' - a regexp that needn't match the start of a name
189 'set:<fileset>' - a fileset expression
189 'set:<fileset>' - a fileset expression
190 'include:<path>' - a file of patterns to read and include
190 'include:<path>' - a file of patterns to read and include
191 'subinclude:<path>' - a file of patterns to match against files under
191 'subinclude:<path>' - a file of patterns to match against files under
192 the same directory
192 the same directory
193 '<something>' - a pattern of the specified default type
193 '<something>' - a pattern of the specified default type
194
194
195 >>> def _match(root, *args, **kwargs):
195 >>> def _match(root, *args, **kwargs):
196 ... return match(util.localpath(root), *args, **kwargs)
196 ... return match(util.localpath(root), *args, **kwargs)
197
197
198 Usually a patternmatcher is returned:
198 Usually a patternmatcher is returned:
199 >>> _match(b'/foo', b'.', [b're:.*\.c$', b'path:foo/a', b'*.py'])
199 >>> _match(b'/foo', b'.', [b're:.*\.c$', b'path:foo/a', b'*.py'])
200 <patternmatcher patterns='.*\\.c$|foo/a(?:/|$)|[^/]*\\.py$'>
200 <patternmatcher patterns='.*\\.c$|foo/a(?:/|$)|[^/]*\\.py$'>
201
201
202 Combining 'patterns' with 'include' (resp. 'exclude') gives an
202 Combining 'patterns' with 'include' (resp. 'exclude') gives an
203 intersectionmatcher (resp. a differencematcher):
203 intersectionmatcher (resp. a differencematcher):
204 >>> type(_match(b'/foo', b'.', [b're:.*\.c$'], include=[b'path:lib']))
204 >>> type(_match(b'/foo', b'.', [b're:.*\.c$'], include=[b'path:lib']))
205 <class 'mercurial.match.intersectionmatcher'>
205 <class 'mercurial.match.intersectionmatcher'>
206 >>> type(_match(b'/foo', b'.', [b're:.*\.c$'], exclude=[b'path:build']))
206 >>> type(_match(b'/foo', b'.', [b're:.*\.c$'], exclude=[b'path:build']))
207 <class 'mercurial.match.differencematcher'>
207 <class 'mercurial.match.differencematcher'>
208
208
209 Notice that, if 'patterns' is empty, an alwaysmatcher is returned:
209 Notice that, if 'patterns' is empty, an alwaysmatcher is returned:
210 >>> _match(b'/foo', b'.', [])
210 >>> _match(b'/foo', b'.', [])
211 <alwaysmatcher>
211 <alwaysmatcher>
212
212
213 The 'default' argument determines which kind of pattern is assumed if a
213 The 'default' argument determines which kind of pattern is assumed if a
214 pattern has no prefix:
214 pattern has no prefix:
215 >>> _match(b'/foo', b'.', [b'.*\.c$'], default=b're')
215 >>> _match(b'/foo', b'.', [b'.*\.c$'], default=b're')
216 <patternmatcher patterns='.*\\.c$'>
216 <patternmatcher patterns='.*\\.c$'>
217 >>> _match(b'/foo', b'.', [b'main.py'], default=b'relpath')
217 >>> _match(b'/foo', b'.', [b'main.py'], default=b'relpath')
218 <patternmatcher patterns='main\\.py(?:/|$)'>
218 <patternmatcher patterns='main\\.py(?:/|$)'>
219 >>> _match(b'/foo', b'.', [b'main.py'], default=b're')
219 >>> _match(b'/foo', b'.', [b'main.py'], default=b're')
220 <patternmatcher patterns='main.py'>
220 <patternmatcher patterns='main.py'>
221
221
222 The primary use of matchers is to check whether a value (usually a file
222 The primary use of matchers is to check whether a value (usually a file
223 name) matches againset one of the patterns given at initialization. There
223 name) matches againset one of the patterns given at initialization. There
224 are two ways of doing this check.
224 are two ways of doing this check.
225
225
226 >>> m = _match(b'/foo', b'', [b're:.*\.c$', b'relpath:a'])
226 >>> m = _match(b'/foo', b'', [b're:.*\.c$', b'relpath:a'])
227
227
228 1. Calling the matcher with a file name returns True if any pattern
228 1. Calling the matcher with a file name returns True if any pattern
229 matches that file name:
229 matches that file name:
230 >>> m(b'a')
230 >>> m(b'a')
231 True
231 True
232 >>> m(b'main.c')
232 >>> m(b'main.c')
233 True
233 True
234 >>> m(b'test.py')
234 >>> m(b'test.py')
235 False
235 False
236
236
237 2. Using the exact() method only returns True if the file name matches one
237 2. Using the exact() method only returns True if the file name matches one
238 of the exact patterns (i.e. not re: or glob: patterns):
238 of the exact patterns (i.e. not re: or glob: patterns):
239 >>> m.exact(b'a')
239 >>> m.exact(b'a')
240 True
240 True
241 >>> m.exact(b'main.c')
241 >>> m.exact(b'main.c')
242 False
242 False
243 """
243 """
244 assert os.path.isabs(root)
244 assert os.path.isabs(root)
245 cwd = os.path.join(root, util.localpath(cwd))
245 cwd = os.path.join(root, util.localpath(cwd))
246 normalize = _donormalize
246 normalize = _donormalize
247 if icasefs:
247 if icasefs:
248 dirstate = ctx.repo().dirstate
248 dirstate = ctx.repo().dirstate
249 dsnormalize = dirstate.normalize
249 dsnormalize = dirstate.normalize
250
250
251 def normalize(patterns, default, root, cwd, auditor, warn):
251 def normalize(patterns, default, root, cwd, auditor, warn):
252 kp = _donormalize(patterns, default, root, cwd, auditor, warn)
252 kp = _donormalize(patterns, default, root, cwd, auditor, warn)
253 kindpats = []
253 kindpats = []
254 for kind, pats, source in kp:
254 for kind, pats, source in kp:
255 if kind not in (b're', b'relre'): # regex can't be normalized
255 if kind not in (b're', b'relre'): # regex can't be normalized
256 p = pats
256 p = pats
257 pats = dsnormalize(pats)
257 pats = dsnormalize(pats)
258
258
259 # Preserve the original to handle a case only rename.
259 # Preserve the original to handle a case only rename.
260 if p != pats and p in dirstate:
260 if p != pats and p in dirstate:
261 kindpats.append((kind, p, source))
261 kindpats.append((kind, p, source))
262
262
263 kindpats.append((kind, pats, source))
263 kindpats.append((kind, pats, source))
264 return kindpats
264 return kindpats
265
265
266 if patterns:
266 if patterns:
267 kindpats = normalize(patterns, default, root, cwd, auditor, warn)
267 kindpats = normalize(patterns, default, root, cwd, auditor, warn)
268 if _kindpatsalwaysmatch(kindpats):
268 if _kindpatsalwaysmatch(kindpats):
269 m = alwaysmatcher(badfn)
269 m = alwaysmatcher(badfn)
270 else:
270 else:
271 m = _buildkindpatsmatcher(
271 m = _buildkindpatsmatcher(
272 patternmatcher,
272 patternmatcher,
273 root,
273 root,
274 cwd,
274 cwd,
275 kindpats,
275 kindpats,
276 ctx=ctx,
276 ctx=ctx,
277 listsubrepos=listsubrepos,
277 listsubrepos=listsubrepos,
278 badfn=badfn,
278 badfn=badfn,
279 )
279 )
280 else:
280 else:
281 # It's a little strange that no patterns means to match everything.
281 # It's a little strange that no patterns means to match everything.
282 # Consider changing this to match nothing (probably using nevermatcher).
282 # Consider changing this to match nothing (probably using nevermatcher).
283 m = alwaysmatcher(badfn)
283 m = alwaysmatcher(badfn)
284
284
285 if include:
285 if include:
286 kindpats = normalize(include, b'glob', root, cwd, auditor, warn)
286 kindpats = normalize(include, b'glob', root, cwd, auditor, warn)
287 im = _buildkindpatsmatcher(
287 im = _buildkindpatsmatcher(
288 includematcher,
288 includematcher,
289 root,
289 root,
290 cwd,
290 cwd,
291 kindpats,
291 kindpats,
292 ctx=ctx,
292 ctx=ctx,
293 listsubrepos=listsubrepos,
293 listsubrepos=listsubrepos,
294 badfn=None,
294 badfn=None,
295 )
295 )
296 m = intersectmatchers(m, im)
296 m = intersectmatchers(m, im)
297 if exclude:
297 if exclude:
298 kindpats = normalize(exclude, b'glob', root, cwd, auditor, warn)
298 kindpats = normalize(exclude, b'glob', root, cwd, auditor, warn)
299 em = _buildkindpatsmatcher(
299 em = _buildkindpatsmatcher(
300 includematcher,
300 includematcher,
301 root,
301 root,
302 cwd,
302 cwd,
303 kindpats,
303 kindpats,
304 ctx=ctx,
304 ctx=ctx,
305 listsubrepos=listsubrepos,
305 listsubrepos=listsubrepos,
306 badfn=None,
306 badfn=None,
307 )
307 )
308 m = differencematcher(m, em)
308 m = differencematcher(m, em)
309 return m
309 return m
310
310
311
311
312 def exact(files, badfn=None):
312 def exact(files, badfn=None):
313 return exactmatcher(files, badfn=badfn)
313 return exactmatcher(files, badfn=badfn)
314
314
315
315
316 def always(badfn=None):
316 def always(badfn=None):
317 return alwaysmatcher(badfn)
317 return alwaysmatcher(badfn)
318
318
319
319
320 def never(badfn=None):
320 def never(badfn=None):
321 return nevermatcher(badfn)
321 return nevermatcher(badfn)
322
322
323
323
324 def badmatch(match, badfn):
324 def badmatch(match, badfn):
325 """Make a copy of the given matcher, replacing its bad method with the given
325 """Make a copy of the given matcher, replacing its bad method with the given
326 one.
326 one.
327 """
327 """
328 m = copy.copy(match)
328 m = copy.copy(match)
329 m.bad = badfn
329 m.bad = badfn
330 return m
330 return m
331
331
332
332
333 def _donormalize(patterns, default, root, cwd, auditor=None, warn=None):
333 def _donormalize(patterns, default, root, cwd, auditor=None, warn=None):
334 """Convert 'kind:pat' from the patterns list to tuples with kind and
334 """Convert 'kind:pat' from the patterns list to tuples with kind and
335 normalized and rooted patterns and with listfiles expanded."""
335 normalized and rooted patterns and with listfiles expanded."""
336 kindpats = []
336 kindpats = []
337 for kind, pat in [_patsplit(p, default) for p in patterns]:
337 for kind, pat in [_patsplit(p, default) for p in patterns]:
338 if kind in cwdrelativepatternkinds:
338 if kind in cwdrelativepatternkinds:
339 pat = pathutil.canonpath(root, cwd, pat, auditor=auditor)
339 pat = pathutil.canonpath(root, cwd, pat, auditor=auditor)
340 elif kind in (b'relglob', b'path', b'rootfilesin', b'rootglob'):
340 elif kind in (b'relglob', b'path', b'rootfilesin', b'rootglob'):
341 pat = util.normpath(pat)
341 pat = util.normpath(pat)
342 elif kind in (b'listfile', b'listfile0'):
342 elif kind in (b'listfile', b'listfile0'):
343 try:
343 try:
344 files = util.readfile(pat)
344 files = util.readfile(pat)
345 if kind == b'listfile0':
345 if kind == b'listfile0':
346 files = files.split(b'\0')
346 files = files.split(b'\0')
347 else:
347 else:
348 files = files.splitlines()
348 files = files.splitlines()
349 files = [f for f in files if f]
349 files = [f for f in files if f]
350 except EnvironmentError:
350 except EnvironmentError:
351 raise error.Abort(_(b"unable to read file list (%s)") % pat)
351 raise error.Abort(_(b"unable to read file list (%s)") % pat)
352 for k, p, source in _donormalize(
352 for k, p, source in _donormalize(
353 files, default, root, cwd, auditor, warn
353 files, default, root, cwd, auditor, warn
354 ):
354 ):
355 kindpats.append((k, p, pat))
355 kindpats.append((k, p, pat))
356 continue
356 continue
357 elif kind == b'include':
357 elif kind == b'include':
358 try:
358 try:
359 fullpath = os.path.join(root, util.localpath(pat))
359 fullpath = os.path.join(root, util.localpath(pat))
360 includepats = readpatternfile(fullpath, warn)
360 includepats = readpatternfile(fullpath, warn)
361 for k, p, source in _donormalize(
361 for k, p, source in _donormalize(
362 includepats, default, root, cwd, auditor, warn
362 includepats, default, root, cwd, auditor, warn
363 ):
363 ):
364 kindpats.append((k, p, source or pat))
364 kindpats.append((k, p, source or pat))
365 except error.Abort as inst:
365 except error.Abort as inst:
366 raise error.Abort(
366 raise error.Abort(
367 b'%s: %s'
367 b'%s: %s'
368 % (
368 % (
369 pat,
369 pat,
370 inst.message,
370 inst.message,
371 ) # pytype: disable=unsupported-operands
371 ) # pytype: disable=unsupported-operands
372 )
372 )
373 except IOError as inst:
373 except IOError as inst:
374 if warn:
374 if warn:
375 warn(
375 warn(
376 _(b"skipping unreadable pattern file '%s': %s\n")
376 _(b"skipping unreadable pattern file '%s': %s\n")
377 % (pat, stringutil.forcebytestr(inst.strerror))
377 % (pat, stringutil.forcebytestr(inst.strerror))
378 )
378 )
379 continue
379 continue
380 # else: re or relre - which cannot be normalized
380 # else: re or relre - which cannot be normalized
381 kindpats.append((kind, pat, b''))
381 kindpats.append((kind, pat, b''))
382 return kindpats
382 return kindpats
383
383
384
384
385 class basematcher:
385 class basematcher:
386 def __init__(self, badfn=None):
386 def __init__(self, badfn=None):
387 if badfn is not None:
387 if badfn is not None:
388 self.bad = badfn
388 self.bad = badfn
389
389
390 def __call__(self, fn):
390 def __call__(self, fn):
391 return self.matchfn(fn)
391 return self.matchfn(fn)
392
392
393 # Callbacks related to how the matcher is used by dirstate.walk.
393 # Callbacks related to how the matcher is used by dirstate.walk.
394 # Subscribers to these events must monkeypatch the matcher object.
394 # Subscribers to these events must monkeypatch the matcher object.
395 def bad(self, f, msg):
395 def bad(self, f, msg):
396 """Callback from dirstate.walk for each explicit file that can't be
396 """Callback from dirstate.walk for each explicit file that can't be
397 found/accessed, with an error message."""
397 found/accessed, with an error message."""
398
398
399 # If an traversedir is set, it will be called when a directory discovered
399 # If an traversedir is set, it will be called when a directory discovered
400 # by recursive traversal is visited.
400 # by recursive traversal is visited.
401 traversedir = None
401 traversedir = None
402
402
403 @propertycache
403 @propertycache
404 def _files(self):
404 def _files(self):
405 return []
405 return []
406
406
407 def files(self):
407 def files(self):
408 """Explicitly listed files or patterns or roots:
408 """Explicitly listed files or patterns or roots:
409 if no patterns or .always(): empty list,
409 if no patterns or .always(): empty list,
410 if exact: list exact files,
410 if exact: list exact files,
411 if not .anypats(): list all files and dirs,
411 if not .anypats(): list all files and dirs,
412 else: optimal roots"""
412 else: optimal roots"""
413 return self._files
413 return self._files
414
414
415 @propertycache
415 @propertycache
416 def _fileset(self):
416 def _fileset(self):
417 return set(self._files)
417 return set(self._files)
418
418
419 def exact(self, f):
419 def exact(self, f):
420 '''Returns True if f is in .files().'''
420 '''Returns True if f is in .files().'''
421 return f in self._fileset
421 return f in self._fileset
422
422
423 def matchfn(self, f):
423 def matchfn(self, f):
424 return False
424 return False
425
425
426 def visitdir(self, dir):
426 def visitdir(self, dir):
427 """Decides whether a directory should be visited based on whether it
427 """Decides whether a directory should be visited based on whether it
428 has potential matches in it or one of its subdirectories. This is
428 has potential matches in it or one of its subdirectories. This is
429 based on the match's primary, included, and excluded patterns.
429 based on the match's primary, included, and excluded patterns.
430
430
431 Returns the string 'all' if the given directory and all subdirectories
431 Returns the string 'all' if the given directory and all subdirectories
432 should be visited. Otherwise returns True or False indicating whether
432 should be visited. Otherwise returns True or False indicating whether
433 the given directory should be visited.
433 the given directory should be visited.
434 """
434 """
435 return True
435 return True
436
436
437 def visitchildrenset(self, dir):
437 def visitchildrenset(self, dir):
438 """Decides whether a directory should be visited based on whether it
438 """Decides whether a directory should be visited based on whether it
439 has potential matches in it or one of its subdirectories, and
439 has potential matches in it or one of its subdirectories, and
440 potentially lists which subdirectories of that directory should be
440 potentially lists which subdirectories of that directory should be
441 visited. This is based on the match's primary, included, and excluded
441 visited. This is based on the match's primary, included, and excluded
442 patterns.
442 patterns.
443
443
444 This function is very similar to 'visitdir', and the following mapping
444 This function is very similar to 'visitdir', and the following mapping
445 can be applied:
445 can be applied:
446
446
447 visitdir | visitchildrenlist
447 visitdir | visitchildrenlist
448 ----------+-------------------
448 ----------+-------------------
449 False | set()
449 False | set()
450 'all' | 'all'
450 'all' | 'all'
451 True | 'this' OR non-empty set of subdirs -or files- to visit
451 True | 'this' OR non-empty set of subdirs -or files- to visit
452
452
453 Example:
453 Example:
454 Assume matchers ['path:foo/bar', 'rootfilesin:qux'], we would return
454 Assume matchers ['path:foo/bar', 'rootfilesin:qux'], we would return
455 the following values (assuming the implementation of visitchildrenset
455 the following values (assuming the implementation of visitchildrenset
456 is capable of recognizing this; some implementations are not).
456 is capable of recognizing this; some implementations are not).
457
457
458 '' -> {'foo', 'qux'}
458 '' -> {'foo', 'qux'}
459 'baz' -> set()
459 'baz' -> set()
460 'foo' -> {'bar'}
460 'foo' -> {'bar'}
461 # Ideally this would be 'all', but since the prefix nature of matchers
461 # Ideally this would be 'all', but since the prefix nature of matchers
462 # is applied to the entire matcher, we have to downgrade this to
462 # is applied to the entire matcher, we have to downgrade this to
463 # 'this' due to the non-prefix 'rootfilesin'-kind matcher being mixed
463 # 'this' due to the non-prefix 'rootfilesin'-kind matcher being mixed
464 # in.
464 # in.
465 'foo/bar' -> 'this'
465 'foo/bar' -> 'this'
466 'qux' -> 'this'
466 'qux' -> 'this'
467
467
468 Important:
468 Important:
469 Most matchers do not know if they're representing files or
469 Most matchers do not know if they're representing files or
470 directories. They see ['path:dir/f'] and don't know whether 'f' is a
470 directories. They see ['path:dir/f'] and don't know whether 'f' is a
471 file or a directory, so visitchildrenset('dir') for most matchers will
471 file or a directory, so visitchildrenset('dir') for most matchers will
472 return {'f'}, but if the matcher knows it's a file (like exactmatcher
472 return {'f'}, but if the matcher knows it's a file (like exactmatcher
473 does), it may return 'this'. Do not rely on the return being a set
473 does), it may return 'this'. Do not rely on the return being a set
474 indicating that there are no files in this dir to investigate (or
474 indicating that there are no files in this dir to investigate (or
475 equivalently that if there are files to investigate in 'dir' that it
475 equivalently that if there are files to investigate in 'dir' that it
476 will always return 'this').
476 will always return 'this').
477 """
477 """
478 return b'this'
478 return b'this'
479
479
480 def always(self):
480 def always(self):
481 """Matcher will match everything and .files() will be empty --
481 """Matcher will match everything and .files() will be empty --
482 optimization might be possible."""
482 optimization might be possible."""
483 return False
483 return False
484
484
485 def isexact(self):
485 def isexact(self):
486 """Matcher will match exactly the list of files in .files() --
486 """Matcher will match exactly the list of files in .files() --
487 optimization might be possible."""
487 optimization might be possible."""
488 return False
488 return False
489
489
490 def prefix(self):
490 def prefix(self):
491 """Matcher will match the paths in .files() recursively --
491 """Matcher will match the paths in .files() recursively --
492 optimization might be possible."""
492 optimization might be possible."""
493 return False
493 return False
494
494
495 def anypats(self):
495 def anypats(self):
496 """None of .always(), .isexact(), and .prefix() is true --
496 """None of .always(), .isexact(), and .prefix() is true --
497 optimizations will be difficult."""
497 optimizations will be difficult."""
498 return not self.always() and not self.isexact() and not self.prefix()
498 return not self.always() and not self.isexact() and not self.prefix()
499
499
500
500
501 class alwaysmatcher(basematcher):
501 class alwaysmatcher(basematcher):
502 '''Matches everything.'''
502 '''Matches everything.'''
503
503
504 def __init__(self, badfn=None):
504 def __init__(self, badfn=None):
505 super(alwaysmatcher, self).__init__(badfn)
505 super(alwaysmatcher, self).__init__(badfn)
506
506
507 def always(self):
507 def always(self):
508 return True
508 return True
509
509
510 def matchfn(self, f):
510 def matchfn(self, f):
511 return True
511 return True
512
512
513 def visitdir(self, dir):
513 def visitdir(self, dir):
514 return b'all'
514 return b'all'
515
515
516 def visitchildrenset(self, dir):
516 def visitchildrenset(self, dir):
517 return b'all'
517 return b'all'
518
518
519 def __repr__(self):
519 def __repr__(self):
520 return r'<alwaysmatcher>'
520 return r'<alwaysmatcher>'
521
521
522
522
523 class nevermatcher(basematcher):
523 class nevermatcher(basematcher):
524 '''Matches nothing.'''
524 '''Matches nothing.'''
525
525
526 def __init__(self, badfn=None):
526 def __init__(self, badfn=None):
527 super(nevermatcher, self).__init__(badfn)
527 super(nevermatcher, self).__init__(badfn)
528
528
529 # It's a little weird to say that the nevermatcher is an exact matcher
529 # It's a little weird to say that the nevermatcher is an exact matcher
530 # or a prefix matcher, but it seems to make sense to let callers take
530 # or a prefix matcher, but it seems to make sense to let callers take
531 # fast paths based on either. There will be no exact matches, nor any
531 # fast paths based on either. There will be no exact matches, nor any
532 # prefixes (files() returns []), so fast paths iterating over them should
532 # prefixes (files() returns []), so fast paths iterating over them should
533 # be efficient (and correct).
533 # be efficient (and correct).
534 def isexact(self):
534 def isexact(self):
535 return True
535 return True
536
536
537 def prefix(self):
537 def prefix(self):
538 return True
538 return True
539
539
540 def visitdir(self, dir):
540 def visitdir(self, dir):
541 return False
541 return False
542
542
543 def visitchildrenset(self, dir):
543 def visitchildrenset(self, dir):
544 return set()
544 return set()
545
545
546 def __repr__(self):
546 def __repr__(self):
547 return r'<nevermatcher>'
547 return r'<nevermatcher>'
548
548
549
549
550 class predicatematcher(basematcher):
550 class predicatematcher(basematcher):
551 """A matcher adapter for a simple boolean function"""
551 """A matcher adapter for a simple boolean function"""
552
552
553 def __init__(self, predfn, predrepr=None, badfn=None):
553 def __init__(self, predfn, predrepr=None, badfn=None):
554 super(predicatematcher, self).__init__(badfn)
554 super(predicatematcher, self).__init__(badfn)
555 self.matchfn = predfn
555 self.matchfn = predfn
556 self._predrepr = predrepr
556 self._predrepr = predrepr
557
557
558 @encoding.strmethod
558 @encoding.strmethod
559 def __repr__(self):
559 def __repr__(self):
560 s = stringutil.buildrepr(self._predrepr) or pycompat.byterepr(
560 s = stringutil.buildrepr(self._predrepr) or pycompat.byterepr(
561 self.matchfn
561 self.matchfn
562 )
562 )
563 return b'<predicatenmatcher pred=%s>' % s
563 return b'<predicatenmatcher pred=%s>' % s
564
564
565
565
566 def path_or_parents_in_set(path, prefix_set):
566 def path_or_parents_in_set(path, prefix_set):
567 """Returns True if `path` (or any parent of `path`) is in `prefix_set`."""
567 """Returns True if `path` (or any parent of `path`) is in `prefix_set`."""
568 l = len(prefix_set)
568 l = len(prefix_set)
569 if l == 0:
569 if l == 0:
570 return False
570 return False
571 if path in prefix_set:
571 if path in prefix_set:
572 return True
572 return True
573 # If there's more than 5 paths in prefix_set, it's *probably* quicker to
573 # If there's more than 5 paths in prefix_set, it's *probably* quicker to
574 # "walk up" the directory hierarchy instead, with the assumption that most
574 # "walk up" the directory hierarchy instead, with the assumption that most
575 # directory hierarchies are relatively shallow and hash lookup is cheap.
575 # directory hierarchies are relatively shallow and hash lookup is cheap.
576 if l > 5:
576 if l > 5:
577 return any(
577 return any(
578 parentdir in prefix_set for parentdir in pathutil.finddirs(path)
578 parentdir in prefix_set for parentdir in pathutil.finddirs(path)
579 )
579 )
580
580
581 # FIXME: Ideally we'd never get to this point if this is the case - we'd
581 # FIXME: Ideally we'd never get to this point if this is the case - we'd
582 # recognize ourselves as an 'always' matcher and skip this.
582 # recognize ourselves as an 'always' matcher and skip this.
583 if b'' in prefix_set:
583 if b'' in prefix_set:
584 return True
584 return True
585
585
586 sl = ord(b'/')
586 sl = ord(b'/')
587
587
588 # We already checked that path isn't in prefix_set exactly, so
588 # We already checked that path isn't in prefix_set exactly, so
589 # `path[len(pf)] should never raise IndexError.
589 # `path[len(pf)] should never raise IndexError.
590 return any(path.startswith(pf) and path[len(pf)] == sl for pf in prefix_set)
590 return any(path.startswith(pf) and path[len(pf)] == sl for pf in prefix_set)
591
591
592
592
593 class patternmatcher(basematcher):
593 class patternmatcher(basematcher):
594 r"""Matches a set of (kind, pat, source) against a 'root' directory.
594 r"""Matches a set of (kind, pat, source) against a 'root' directory.
595
595
596 >>> kindpats = [
596 >>> kindpats = [
597 ... (b're', br'.*\.c$', b''),
597 ... (b're', br'.*\.c$', b''),
598 ... (b'path', b'foo/a', b''),
598 ... (b'path', b'foo/a', b''),
599 ... (b'relpath', b'b', b''),
599 ... (b'relpath', b'b', b''),
600 ... (b'glob', b'*.h', b''),
600 ... (b'glob', b'*.h', b''),
601 ... ]
601 ... ]
602 >>> m = patternmatcher(b'foo', kindpats)
602 >>> m = patternmatcher(b'foo', kindpats)
603 >>> m(b'main.c') # matches re:.*\.c$
603 >>> m(b'main.c') # matches re:.*\.c$
604 True
604 True
605 >>> m(b'b.txt')
605 >>> m(b'b.txt')
606 False
606 False
607 >>> m(b'foo/a') # matches path:foo/a
607 >>> m(b'foo/a') # matches path:foo/a
608 True
608 True
609 >>> m(b'a') # does not match path:b, since 'root' is 'foo'
609 >>> m(b'a') # does not match path:b, since 'root' is 'foo'
610 False
610 False
611 >>> m(b'b') # matches relpath:b, since 'root' is 'foo'
611 >>> m(b'b') # matches relpath:b, since 'root' is 'foo'
612 True
612 True
613 >>> m(b'lib.h') # matches glob:*.h
613 >>> m(b'lib.h') # matches glob:*.h
614 True
614 True
615
615
616 >>> m.files()
616 >>> m.files()
617 ['', 'foo/a', 'b', '']
617 ['', 'foo/a', 'b', '']
618 >>> m.exact(b'foo/a')
618 >>> m.exact(b'foo/a')
619 True
619 True
620 >>> m.exact(b'b')
620 >>> m.exact(b'b')
621 True
621 True
622 >>> m.exact(b'lib.h') # exact matches are for (rel)path kinds
622 >>> m.exact(b'lib.h') # exact matches are for (rel)path kinds
623 False
623 False
624 """
624 """
625
625
626 def __init__(self, root, kindpats, badfn=None):
626 def __init__(self, root, kindpats, badfn=None):
627 super(patternmatcher, self).__init__(badfn)
627 super(patternmatcher, self).__init__(badfn)
628
628
629 self._files = _explicitfiles(kindpats)
629 self._files = _explicitfiles(kindpats)
630 self._prefix = _prefix(kindpats)
630 self._prefix = _prefix(kindpats)
631 self._pats, self.matchfn = _buildmatch(kindpats, b'$', root)
631 self._pats, self.matchfn = _buildmatch(kindpats, b'$', root)
632
632
633 @propertycache
633 @propertycache
634 def _dirs(self):
634 def _dirs(self):
635 return set(pathutil.dirs(self._fileset))
635 return set(pathutil.dirs(self._fileset))
636
636
637 def visitdir(self, dir):
637 def visitdir(self, dir):
638 if self._prefix and dir in self._fileset:
638 if self._prefix and dir in self._fileset:
639 return b'all'
639 return b'all'
640 return dir in self._dirs or path_or_parents_in_set(dir, self._fileset)
640 return dir in self._dirs or path_or_parents_in_set(dir, self._fileset)
641
641
642 def visitchildrenset(self, dir):
642 def visitchildrenset(self, dir):
643 ret = self.visitdir(dir)
643 ret = self.visitdir(dir)
644 if ret is True:
644 if ret is True:
645 return b'this'
645 return b'this'
646 elif not ret:
646 elif not ret:
647 return set()
647 return set()
648 assert ret == b'all'
648 assert ret == b'all'
649 return b'all'
649 return b'all'
650
650
651 def prefix(self):
651 def prefix(self):
652 return self._prefix
652 return self._prefix
653
653
654 @encoding.strmethod
654 @encoding.strmethod
655 def __repr__(self):
655 def __repr__(self):
656 return b'<patternmatcher patterns=%r>' % pycompat.bytestr(self._pats)
656 return b'<patternmatcher patterns=%r>' % pycompat.bytestr(self._pats)
657
657
658
658
659 # This is basically a reimplementation of pathutil.dirs that stores the
659 # This is basically a reimplementation of pathutil.dirs that stores the
660 # children instead of just a count of them, plus a small optional optimization
660 # children instead of just a count of them, plus a small optional optimization
661 # to avoid some directories we don't need.
661 # to avoid some directories we don't need.
662 class _dirchildren:
662 class _dirchildren:
663 def __init__(self, paths, onlyinclude=None):
663 def __init__(self, paths, onlyinclude=None):
664 self._dirs = {}
664 self._dirs = {}
665 self._onlyinclude = onlyinclude or []
665 self._onlyinclude = onlyinclude or []
666 addpath = self.addpath
666 addpath = self.addpath
667 for f in paths:
667 for f in paths:
668 addpath(f)
668 addpath(f)
669
669
670 def addpath(self, path):
670 def addpath(self, path):
671 if path == b'':
671 if path == b'':
672 return
672 return
673 dirs = self._dirs
673 dirs = self._dirs
674 findsplitdirs = _dirchildren._findsplitdirs
674 findsplitdirs = _dirchildren._findsplitdirs
675 for d, b in findsplitdirs(path):
675 for d, b in findsplitdirs(path):
676 if d not in self._onlyinclude:
676 if d not in self._onlyinclude:
677 continue
677 continue
678 dirs.setdefault(d, set()).add(b)
678 dirs.setdefault(d, set()).add(b)
679
679
680 @staticmethod
680 @staticmethod
681 def _findsplitdirs(path):
681 def _findsplitdirs(path):
682 # yields (dirname, basename) tuples, walking back to the root. This is
682 # yields (dirname, basename) tuples, walking back to the root. This is
683 # very similar to pathutil.finddirs, except:
683 # very similar to pathutil.finddirs, except:
684 # - produces a (dirname, basename) tuple, not just 'dirname'
684 # - produces a (dirname, basename) tuple, not just 'dirname'
685 # Unlike manifest._splittopdir, this does not suffix `dirname` with a
685 # Unlike manifest._splittopdir, this does not suffix `dirname` with a
686 # slash.
686 # slash.
687 oldpos = len(path)
687 oldpos = len(path)
688 pos = path.rfind(b'/')
688 pos = path.rfind(b'/')
689 while pos != -1:
689 while pos != -1:
690 yield path[:pos], path[pos + 1 : oldpos]
690 yield path[:pos], path[pos + 1 : oldpos]
691 oldpos = pos
691 oldpos = pos
692 pos = path.rfind(b'/', 0, pos)
692 pos = path.rfind(b'/', 0, pos)
693 yield b'', path[:oldpos]
693 yield b'', path[:oldpos]
694
694
695 def get(self, path):
695 def get(self, path):
696 return self._dirs.get(path, set())
696 return self._dirs.get(path, set())
697
697
698
698
699 class includematcher(basematcher):
699 class includematcher(basematcher):
700 def __init__(self, root, kindpats, badfn=None):
700 def __init__(self, root, kindpats, badfn=None):
701 super(includematcher, self).__init__(badfn)
701 super(includematcher, self).__init__(badfn)
702 if rustmod is not None:
702 if rustmod is not None:
703 # We need to pass the patterns to Rust because they can contain
703 # We need to pass the patterns to Rust because they can contain
704 # patterns from the user interface
704 # patterns from the user interface
705 self._kindpats = kindpats
705 self._kindpats = kindpats
706 self._pats, self.matchfn = _buildmatch(kindpats, b'(?:/|$)', root)
706 self._pats, self.matchfn = _buildmatch(kindpats, b'(?:/|$)', root)
707 self._prefix = _prefix(kindpats)
707 self._prefix = _prefix(kindpats)
708 roots, dirs, parents = _rootsdirsandparents(kindpats)
708 roots, dirs, parents = _rootsdirsandparents(kindpats)
709 # roots are directories which are recursively included.
709 # roots are directories which are recursively included.
710 self._roots = set(roots)
710 self._roots = set(roots)
711 # dirs are directories which are non-recursively included.
711 # dirs are directories which are non-recursively included.
712 self._dirs = set(dirs)
712 self._dirs = set(dirs)
713 # parents are directories which are non-recursively included because
713 # parents are directories which are non-recursively included because
714 # they are needed to get to items in _dirs or _roots.
714 # they are needed to get to items in _dirs or _roots.
715 self._parents = parents
715 self._parents = parents
716
716
717 def visitdir(self, dir):
717 def visitdir(self, dir):
718 if self._prefix and dir in self._roots:
718 if self._prefix and dir in self._roots:
719 return b'all'
719 return b'all'
720 return (
720 return (
721 dir in self._dirs
721 dir in self._dirs
722 or dir in self._parents
722 or dir in self._parents
723 or path_or_parents_in_set(dir, self._roots)
723 or path_or_parents_in_set(dir, self._roots)
724 )
724 )
725
725
726 @propertycache
726 @propertycache
727 def _allparentschildren(self):
727 def _allparentschildren(self):
728 # It may seem odd that we add dirs, roots, and parents, and then
728 # It may seem odd that we add dirs, roots, and parents, and then
729 # restrict to only parents. This is to catch the case of:
729 # restrict to only parents. This is to catch the case of:
730 # dirs = ['foo/bar']
730 # dirs = ['foo/bar']
731 # parents = ['foo']
731 # parents = ['foo']
732 # if we asked for the children of 'foo', but had only added
732 # if we asked for the children of 'foo', but had only added
733 # self._parents, we wouldn't be able to respond ['bar'].
733 # self._parents, we wouldn't be able to respond ['bar'].
734 return _dirchildren(
734 return _dirchildren(
735 itertools.chain(self._dirs, self._roots, self._parents),
735 itertools.chain(self._dirs, self._roots, self._parents),
736 onlyinclude=self._parents,
736 onlyinclude=self._parents,
737 )
737 )
738
738
739 def visitchildrenset(self, dir):
739 def visitchildrenset(self, dir):
740 if self._prefix and dir in self._roots:
740 if self._prefix and dir in self._roots:
741 return b'all'
741 return b'all'
742 # Note: this does *not* include the 'dir in self._parents' case from
742 # Note: this does *not* include the 'dir in self._parents' case from
743 # visitdir, that's handled below.
743 # visitdir, that's handled below.
744 if (
744 if (
745 b'' in self._roots
745 b'' in self._roots
746 or dir in self._dirs
746 or dir in self._dirs
747 or path_or_parents_in_set(dir, self._roots)
747 or path_or_parents_in_set(dir, self._roots)
748 ):
748 ):
749 return b'this'
749 return b'this'
750
750
751 if dir in self._parents:
751 if dir in self._parents:
752 return self._allparentschildren.get(dir) or set()
752 return self._allparentschildren.get(dir) or set()
753 return set()
753 return set()
754
754
755 @encoding.strmethod
755 @encoding.strmethod
756 def __repr__(self):
756 def __repr__(self):
757 return b'<includematcher includes=%r>' % pycompat.bytestr(self._pats)
757 return b'<includematcher includes=%r>' % pycompat.bytestr(self._pats)
758
758
759
759
760 class exactmatcher(basematcher):
760 class exactmatcher(basematcher):
761 r"""Matches the input files exactly. They are interpreted as paths, not
761 r"""Matches the input files exactly. They are interpreted as paths, not
762 patterns (so no kind-prefixes).
762 patterns (so no kind-prefixes).
763
763
764 >>> m = exactmatcher([b'a.txt', br're:.*\.c$'])
764 >>> m = exactmatcher([b'a.txt', br're:.*\.c$'])
765 >>> m(b'a.txt')
765 >>> m(b'a.txt')
766 True
766 True
767 >>> m(b'b.txt')
767 >>> m(b'b.txt')
768 False
768 False
769
769
770 Input files that would be matched are exactly those returned by .files()
770 Input files that would be matched are exactly those returned by .files()
771 >>> m.files()
771 >>> m.files()
772 ['a.txt', 're:.*\\.c$']
772 ['a.txt', 're:.*\\.c$']
773
773
774 So pattern 're:.*\.c$' is not considered as a regex, but as a file name
774 So pattern 're:.*\.c$' is not considered as a regex, but as a file name
775 >>> m(b'main.c')
775 >>> m(b'main.c')
776 False
776 False
777 >>> m(br're:.*\.c$')
777 >>> m(br're:.*\.c$')
778 True
778 True
779 """
779 """
780
780
781 def __init__(self, files, badfn=None):
781 def __init__(self, files, badfn=None):
782 super(exactmatcher, self).__init__(badfn)
782 super(exactmatcher, self).__init__(badfn)
783
783
784 if isinstance(files, list):
784 if isinstance(files, list):
785 self._files = files
785 self._files = files
786 else:
786 else:
787 self._files = list(files)
787 self._files = list(files)
788
788
789 matchfn = basematcher.exact
789 matchfn = basematcher.exact
790
790
791 @propertycache
791 @propertycache
792 def _dirs(self):
792 def _dirs(self):
793 return set(pathutil.dirs(self._fileset))
793 return set(pathutil.dirs(self._fileset))
794
794
795 def visitdir(self, dir):
795 def visitdir(self, dir):
796 return dir in self._dirs
796 return dir in self._dirs
797
797
798 @propertycache
798 @propertycache
799 def _visitchildrenset_candidates(self):
799 def _visitchildrenset_candidates(self):
800 """A memoized set of candidates for visitchildrenset."""
800 """A memoized set of candidates for visitchildrenset."""
801 return self._fileset | self._dirs - {b''}
801 return self._fileset | self._dirs - {b''}
802
802
803 @propertycache
803 @propertycache
804 def _sorted_visitchildrenset_candidates(self):
804 def _sorted_visitchildrenset_candidates(self):
805 """A memoized sorted list of candidates for visitchildrenset."""
805 """A memoized sorted list of candidates for visitchildrenset."""
806 return sorted(self._visitchildrenset_candidates)
806 return sorted(self._visitchildrenset_candidates)
807
807
808 def visitchildrenset(self, dir):
808 def visitchildrenset(self, dir):
809 if not self._fileset or dir not in self._dirs:
809 if not self._fileset or dir not in self._dirs:
810 return set()
810 return set()
811
811
812 if dir == b'':
812 if dir == b'':
813 candidates = self._visitchildrenset_candidates
813 candidates = self._visitchildrenset_candidates
814 else:
814 else:
815 candidates = self._sorted_visitchildrenset_candidates
815 candidates = self._sorted_visitchildrenset_candidates
816 d = dir + b'/'
816 d = dir + b'/'
817 # Use bisect to find the first element potentially starting with d
817 # Use bisect to find the first element potentially starting with d
818 # (i.e. >= d). This should always find at least one element (we'll
818 # (i.e. >= d). This should always find at least one element (we'll
819 # assert later if this is not the case).
819 # assert later if this is not the case).
820 first = bisect.bisect_left(candidates, d)
820 first = bisect.bisect_left(candidates, d)
821 # We need a representation of the first element that is > d that
821 # We need a representation of the first element that is > d that
822 # does not start with d, so since we added a `/` on the end of dir,
822 # does not start with d, so since we added a `/` on the end of dir,
823 # we'll add whatever comes after slash (we could probably assume
823 # we'll add whatever comes after slash (we could probably assume
824 # that `0` is after `/`, but let's not) to the end of dir instead.
824 # that `0` is after `/`, but let's not) to the end of dir instead.
825 dnext = dir + encoding.strtolocal(chr(ord(b'/') + 1))
825 dnext = dir + encoding.strtolocal(chr(ord(b'/') + 1))
826 # Use bisect to find the first element >= d_next
826 # Use bisect to find the first element >= d_next
827 last = bisect.bisect_left(candidates, dnext, lo=first)
827 last = bisect.bisect_left(candidates, dnext, lo=first)
828 dlen = len(d)
828 dlen = len(d)
829 candidates = {c[dlen:] for c in candidates[first:last]}
829 candidates = {c[dlen:] for c in candidates[first:last]}
830 # self._dirs includes all of the directories, recursively, so if
830 # self._dirs includes all of the directories, recursively, so if
831 # we're attempting to match foo/bar/baz.txt, it'll have '', 'foo',
831 # we're attempting to match foo/bar/baz.txt, it'll have '', 'foo',
832 # 'foo/bar' in it. Thus we can safely ignore a candidate that has a
832 # 'foo/bar' in it. Thus we can safely ignore a candidate that has a
833 # '/' in it, indicating a it's for a subdir-of-a-subdir; the
833 # '/' in it, indicating a it's for a subdir-of-a-subdir; the
834 # immediate subdir will be in there without a slash.
834 # immediate subdir will be in there without a slash.
835 ret = {c for c in candidates if b'/' not in c}
835 ret = {c for c in candidates if b'/' not in c}
836 # We really do not expect ret to be empty, since that would imply that
836 # We really do not expect ret to be empty, since that would imply that
837 # there's something in _dirs that didn't have a file in _fileset.
837 # there's something in _dirs that didn't have a file in _fileset.
838 assert ret
838 assert ret
839 return ret
839 return ret
840
840
841 def isexact(self):
841 def isexact(self):
842 return True
842 return True
843
843
844 @encoding.strmethod
844 @encoding.strmethod
845 def __repr__(self):
845 def __repr__(self):
846 return b'<exactmatcher files=%r>' % self._files
846 return b'<exactmatcher files=%r>' % self._files
847
847
848
848
849 class differencematcher(basematcher):
849 class differencematcher(basematcher):
850 """Composes two matchers by matching if the first matches and the second
850 """Composes two matchers by matching if the first matches and the second
851 does not.
851 does not.
852
852
853 The second matcher's non-matching-attributes (bad, traversedir) are ignored.
853 The second matcher's non-matching-attributes (bad, traversedir) are ignored.
854 """
854 """
855
855
856 def __init__(self, m1, m2):
856 def __init__(self, m1, m2):
857 super(differencematcher, self).__init__()
857 super(differencematcher, self).__init__()
858 self._m1 = m1
858 self._m1 = m1
859 self._m2 = m2
859 self._m2 = m2
860 self.bad = m1.bad
860 self.bad = m1.bad
861 self.traversedir = m1.traversedir
861 self.traversedir = m1.traversedir
862
862
863 def matchfn(self, f):
863 def matchfn(self, f):
864 return self._m1(f) and not self._m2(f)
864 return self._m1(f) and not self._m2(f)
865
865
866 @propertycache
866 @propertycache
867 def _files(self):
867 def _files(self):
868 if self.isexact():
868 if self.isexact():
869 return [f for f in self._m1.files() if self(f)]
869 return [f for f in self._m1.files() if self(f)]
870 # If m1 is not an exact matcher, we can't easily figure out the set of
870 # If m1 is not an exact matcher, we can't easily figure out the set of
871 # files, because its files() are not always files. For example, if
871 # files, because its files() are not always files. For example, if
872 # m1 is "path:dir" and m2 is "rootfileins:.", we don't
872 # m1 is "path:dir" and m2 is "rootfileins:.", we don't
873 # want to remove "dir" from the set even though it would match m2,
873 # want to remove "dir" from the set even though it would match m2,
874 # because the "dir" in m1 may not be a file.
874 # because the "dir" in m1 may not be a file.
875 return self._m1.files()
875 return self._m1.files()
876
876
877 def visitdir(self, dir):
877 def visitdir(self, dir):
878 if self._m2.visitdir(dir) == b'all':
878 if self._m2.visitdir(dir) == b'all':
879 return False
879 return False
880 elif not self._m2.visitdir(dir):
880 elif not self._m2.visitdir(dir):
881 # m2 does not match dir, we can return 'all' here if possible
881 # m2 does not match dir, we can return 'all' here if possible
882 return self._m1.visitdir(dir)
882 return self._m1.visitdir(dir)
883 return bool(self._m1.visitdir(dir))
883 return bool(self._m1.visitdir(dir))
884
884
885 def visitchildrenset(self, dir):
885 def visitchildrenset(self, dir):
886 m2_set = self._m2.visitchildrenset(dir)
886 m2_set = self._m2.visitchildrenset(dir)
887 if m2_set == b'all':
887 if m2_set == b'all':
888 return set()
888 return set()
889 m1_set = self._m1.visitchildrenset(dir)
889 m1_set = self._m1.visitchildrenset(dir)
890 # Possible values for m1: 'all', 'this', set(...), set()
890 # Possible values for m1: 'all', 'this', set(...), set()
891 # Possible values for m2: 'this', set(...), set()
891 # Possible values for m2: 'this', set(...), set()
892 # If m2 has nothing under here that we care about, return m1, even if
892 # If m2 has nothing under here that we care about, return m1, even if
893 # it's 'all'. This is a change in behavior from visitdir, which would
893 # it's 'all'. This is a change in behavior from visitdir, which would
894 # return True, not 'all', for some reason.
894 # return True, not 'all', for some reason.
895 if not m2_set:
895 if not m2_set:
896 return m1_set
896 return m1_set
897 if m1_set in [b'all', b'this']:
897 if m1_set in [b'all', b'this']:
898 # Never return 'all' here if m2_set is any kind of non-empty (either
898 # Never return 'all' here if m2_set is any kind of non-empty (either
899 # 'this' or set(foo)), since m2 might return set() for a
899 # 'this' or set(foo)), since m2 might return set() for a
900 # subdirectory.
900 # subdirectory.
901 return b'this'
901 return b'this'
902 # Possible values for m1: set(...), set()
902 # Possible values for m1: set(...), set()
903 # Possible values for m2: 'this', set(...)
903 # Possible values for m2: 'this', set(...)
904 # We ignore m2's set results. They're possibly incorrect:
904 # We ignore m2's set results. They're possibly incorrect:
905 # m1 = path:dir/subdir, m2=rootfilesin:dir, visitchildrenset(''):
905 # m1 = path:dir/subdir, m2=rootfilesin:dir, visitchildrenset(''):
906 # m1 returns {'dir'}, m2 returns {'dir'}, if we subtracted we'd
906 # m1 returns {'dir'}, m2 returns {'dir'}, if we subtracted we'd
907 # return set(), which is *not* correct, we still need to visit 'dir'!
907 # return set(), which is *not* correct, we still need to visit 'dir'!
908 return m1_set
908 return m1_set
909
909
910 def isexact(self):
910 def isexact(self):
911 return self._m1.isexact()
911 return self._m1.isexact()
912
912
913 @encoding.strmethod
913 @encoding.strmethod
914 def __repr__(self):
914 def __repr__(self):
915 return b'<differencematcher m1=%r, m2=%r>' % (self._m1, self._m2)
915 return b'<differencematcher m1=%r, m2=%r>' % (self._m1, self._m2)
916
916
917
917
918 def intersectmatchers(m1, m2):
918 def intersectmatchers(m1, m2):
919 """Composes two matchers by matching if both of them match.
919 """Composes two matchers by matching if both of them match.
920
920
921 The second matcher's non-matching-attributes (bad, traversedir) are ignored.
921 The second matcher's non-matching-attributes (bad, traversedir) are ignored.
922 """
922 """
923 if m1 is None or m2 is None:
923 if m1 is None or m2 is None:
924 return m1 or m2
924 return m1 or m2
925 if m1.always():
925 if m1.always():
926 m = copy.copy(m2)
926 m = copy.copy(m2)
927 # TODO: Consider encapsulating these things in a class so there's only
927 # TODO: Consider encapsulating these things in a class so there's only
928 # one thing to copy from m1.
928 # one thing to copy from m1.
929 m.bad = m1.bad
929 m.bad = m1.bad
930 m.traversedir = m1.traversedir
930 m.traversedir = m1.traversedir
931 return m
931 return m
932 if m2.always():
932 if m2.always():
933 m = copy.copy(m1)
933 m = copy.copy(m1)
934 return m
934 return m
935 return intersectionmatcher(m1, m2)
935 return intersectionmatcher(m1, m2)
936
936
937
937
938 class intersectionmatcher(basematcher):
938 class intersectionmatcher(basematcher):
939 def __init__(self, m1, m2):
939 def __init__(self, m1, m2):
940 super(intersectionmatcher, self).__init__()
940 super(intersectionmatcher, self).__init__()
941 self._m1 = m1
941 self._m1 = m1
942 self._m2 = m2
942 self._m2 = m2
943 self.bad = m1.bad
943 self.bad = m1.bad
944 self.traversedir = m1.traversedir
944 self.traversedir = m1.traversedir
945
945
946 @propertycache
946 @propertycache
947 def _files(self):
947 def _files(self):
948 if self.isexact():
948 if self.isexact():
949 m1, m2 = self._m1, self._m2
949 m1, m2 = self._m1, self._m2
950 if not m1.isexact():
950 if not m1.isexact():
951 m1, m2 = m2, m1
951 m1, m2 = m2, m1
952 return [f for f in m1.files() if m2(f)]
952 return [f for f in m1.files() if m2(f)]
953 # It neither m1 nor m2 is an exact matcher, we can't easily intersect
953 # It neither m1 nor m2 is an exact matcher, we can't easily intersect
954 # the set of files, because their files() are not always files. For
954 # the set of files, because their files() are not always files. For
955 # example, if intersecting a matcher "-I glob:foo.txt" with matcher of
955 # example, if intersecting a matcher "-I glob:foo.txt" with matcher of
956 # "path:dir2", we don't want to remove "dir2" from the set.
956 # "path:dir2", we don't want to remove "dir2" from the set.
957 return self._m1.files() + self._m2.files()
957 return self._m1.files() + self._m2.files()
958
958
959 def matchfn(self, f):
959 def matchfn(self, f):
960 return self._m1(f) and self._m2(f)
960 return self._m1(f) and self._m2(f)
961
961
962 def visitdir(self, dir):
962 def visitdir(self, dir):
963 visit1 = self._m1.visitdir(dir)
963 visit1 = self._m1.visitdir(dir)
964 if visit1 == b'all':
964 if visit1 == b'all':
965 return self._m2.visitdir(dir)
965 return self._m2.visitdir(dir)
966 # bool() because visit1=True + visit2='all' should not be 'all'
966 # bool() because visit1=True + visit2='all' should not be 'all'
967 return bool(visit1 and self._m2.visitdir(dir))
967 return bool(visit1 and self._m2.visitdir(dir))
968
968
969 def visitchildrenset(self, dir):
969 def visitchildrenset(self, dir):
970 m1_set = self._m1.visitchildrenset(dir)
970 m1_set = self._m1.visitchildrenset(dir)
971 if not m1_set:
971 if not m1_set:
972 return set()
972 return set()
973 m2_set = self._m2.visitchildrenset(dir)
973 m2_set = self._m2.visitchildrenset(dir)
974 if not m2_set:
974 if not m2_set:
975 return set()
975 return set()
976
976
977 if m1_set == b'all':
977 if m1_set == b'all':
978 return m2_set
978 return m2_set
979 elif m2_set == b'all':
979 elif m2_set == b'all':
980 return m1_set
980 return m1_set
981
981
982 if m1_set == b'this' or m2_set == b'this':
982 if m1_set == b'this' or m2_set == b'this':
983 return b'this'
983 return b'this'
984
984
985 assert isinstance(m1_set, set) and isinstance(m2_set, set)
985 assert isinstance(m1_set, set) and isinstance(m2_set, set)
986 return m1_set.intersection(m2_set)
986 return m1_set.intersection(m2_set)
987
987
988 def always(self):
988 def always(self):
989 return self._m1.always() and self._m2.always()
989 return self._m1.always() and self._m2.always()
990
990
991 def isexact(self):
991 def isexact(self):
992 return self._m1.isexact() or self._m2.isexact()
992 return self._m1.isexact() or self._m2.isexact()
993
993
994 @encoding.strmethod
994 @encoding.strmethod
995 def __repr__(self):
995 def __repr__(self):
996 return b'<intersectionmatcher m1=%r, m2=%r>' % (self._m1, self._m2)
996 return b'<intersectionmatcher m1=%r, m2=%r>' % (self._m1, self._m2)
997
997
998
998
999 class subdirmatcher(basematcher):
999 class subdirmatcher(basematcher):
1000 """Adapt a matcher to work on a subdirectory only.
1000 """Adapt a matcher to work on a subdirectory only.
1001
1001
1002 The paths are remapped to remove/insert the path as needed:
1002 The paths are remapped to remove/insert the path as needed:
1003
1003
1004 >>> from . import pycompat
1004 >>> from . import pycompat
1005 >>> m1 = match(util.localpath(b'/root'), b'', [b'a.txt', b'sub/b.txt'], auditor=lambda name: None)
1005 >>> m1 = match(util.localpath(b'/root'), b'', [b'a.txt', b'sub/b.txt'], auditor=lambda name: None)
1006 >>> m2 = subdirmatcher(b'sub', m1)
1006 >>> m2 = subdirmatcher(b'sub', m1)
1007 >>> m2(b'a.txt')
1007 >>> m2(b'a.txt')
1008 False
1008 False
1009 >>> m2(b'b.txt')
1009 >>> m2(b'b.txt')
1010 True
1010 True
1011 >>> m2.matchfn(b'a.txt')
1011 >>> m2.matchfn(b'a.txt')
1012 False
1012 False
1013 >>> m2.matchfn(b'b.txt')
1013 >>> m2.matchfn(b'b.txt')
1014 True
1014 True
1015 >>> m2.files()
1015 >>> m2.files()
1016 ['b.txt']
1016 ['b.txt']
1017 >>> m2.exact(b'b.txt')
1017 >>> m2.exact(b'b.txt')
1018 True
1018 True
1019 >>> def bad(f, msg):
1019 >>> def bad(f, msg):
1020 ... print(pycompat.sysstr(b"%s: %s" % (f, msg)))
1020 ... print(pycompat.sysstr(b"%s: %s" % (f, msg)))
1021 >>> m1.bad = bad
1021 >>> m1.bad = bad
1022 >>> m2.bad(b'x.txt', b'No such file')
1022 >>> m2.bad(b'x.txt', b'No such file')
1023 sub/x.txt: No such file
1023 sub/x.txt: No such file
1024 """
1024 """
1025
1025
1026 def __init__(self, path, matcher):
1026 def __init__(self, path, matcher):
1027 super(subdirmatcher, self).__init__()
1027 super(subdirmatcher, self).__init__()
1028 self._path = path
1028 self._path = path
1029 self._matcher = matcher
1029 self._matcher = matcher
1030 self._always = matcher.always()
1030 self._always = matcher.always()
1031
1031
1032 self._files = [
1032 self._files = [
1033 f[len(path) + 1 :]
1033 f[len(path) + 1 :]
1034 for f in matcher._files
1034 for f in matcher._files
1035 if f.startswith(path + b"/")
1035 if f.startswith(path + b"/")
1036 ]
1036 ]
1037
1037
1038 # If the parent repo had a path to this subrepo and the matcher is
1038 # If the parent repo had a path to this subrepo and the matcher is
1039 # a prefix matcher, this submatcher always matches.
1039 # a prefix matcher, this submatcher always matches.
1040 if matcher.prefix():
1040 if matcher.prefix():
1041 self._always = any(f == path for f in matcher._files)
1041 self._always = any(f == path for f in matcher._files)
1042
1042
1043 def bad(self, f, msg):
1043 def bad(self, f, msg):
1044 self._matcher.bad(self._path + b"/" + f, msg)
1044 self._matcher.bad(self._path + b"/" + f, msg)
1045
1045
1046 def matchfn(self, f):
1046 def matchfn(self, f):
1047 # Some information is lost in the superclass's constructor, so we
1047 # Some information is lost in the superclass's constructor, so we
1048 # can not accurately create the matching function for the subdirectory
1048 # can not accurately create the matching function for the subdirectory
1049 # from the inputs. Instead, we override matchfn() and visitdir() to
1049 # from the inputs. Instead, we override matchfn() and visitdir() to
1050 # call the original matcher with the subdirectory path prepended.
1050 # call the original matcher with the subdirectory path prepended.
1051 return self._matcher.matchfn(self._path + b"/" + f)
1051 return self._matcher.matchfn(self._path + b"/" + f)
1052
1052
1053 def visitdir(self, dir):
1053 def visitdir(self, dir):
1054 if dir == b'':
1054 if dir == b'':
1055 dir = self._path
1055 dir = self._path
1056 else:
1056 else:
1057 dir = self._path + b"/" + dir
1057 dir = self._path + b"/" + dir
1058 return self._matcher.visitdir(dir)
1058 return self._matcher.visitdir(dir)
1059
1059
1060 def visitchildrenset(self, dir):
1060 def visitchildrenset(self, dir):
1061 if dir == b'':
1061 if dir == b'':
1062 dir = self._path
1062 dir = self._path
1063 else:
1063 else:
1064 dir = self._path + b"/" + dir
1064 dir = self._path + b"/" + dir
1065 return self._matcher.visitchildrenset(dir)
1065 return self._matcher.visitchildrenset(dir)
1066
1066
1067 def always(self):
1067 def always(self):
1068 return self._always
1068 return self._always
1069
1069
1070 def prefix(self):
1070 def prefix(self):
1071 return self._matcher.prefix() and not self._always
1071 return self._matcher.prefix() and not self._always
1072
1072
1073 @encoding.strmethod
1073 @encoding.strmethod
1074 def __repr__(self):
1074 def __repr__(self):
1075 return b'<subdirmatcher path=%r, matcher=%r>' % (
1075 return b'<subdirmatcher path=%r, matcher=%r>' % (
1076 self._path,
1076 self._path,
1077 self._matcher,
1077 self._matcher,
1078 )
1078 )
1079
1079
1080
1080
1081 class prefixdirmatcher(basematcher):
1081 class prefixdirmatcher(basematcher):
1082 """Adapt a matcher to work on a parent directory.
1082 """Adapt a matcher to work on a parent directory.
1083
1083
1084 The matcher's non-matching-attributes (bad, traversedir) are ignored.
1084 The matcher's non-matching-attributes (bad, traversedir) are ignored.
1085
1085
1086 The prefix path should usually be the relative path from the root of
1086 The prefix path should usually be the relative path from the root of
1087 this matcher to the root of the wrapped matcher.
1087 this matcher to the root of the wrapped matcher.
1088
1088
1089 >>> m1 = match(util.localpath(b'/root/d/e'), b'f', [b'../a.txt', b'b.txt'], auditor=lambda name: None)
1089 >>> m1 = match(util.localpath(b'/root/d/e'), b'f', [b'../a.txt', b'b.txt'], auditor=lambda name: None)
1090 >>> m2 = prefixdirmatcher(b'd/e', m1)
1090 >>> m2 = prefixdirmatcher(b'd/e', m1)
1091 >>> m2(b'a.txt')
1091 >>> m2(b'a.txt')
1092 False
1092 False
1093 >>> m2(b'd/e/a.txt')
1093 >>> m2(b'd/e/a.txt')
1094 True
1094 True
1095 >>> m2(b'd/e/b.txt')
1095 >>> m2(b'd/e/b.txt')
1096 False
1096 False
1097 >>> m2.files()
1097 >>> m2.files()
1098 ['d/e/a.txt', 'd/e/f/b.txt']
1098 ['d/e/a.txt', 'd/e/f/b.txt']
1099 >>> m2.exact(b'd/e/a.txt')
1099 >>> m2.exact(b'd/e/a.txt')
1100 True
1100 True
1101 >>> m2.visitdir(b'd')
1101 >>> m2.visitdir(b'd')
1102 True
1102 True
1103 >>> m2.visitdir(b'd/e')
1103 >>> m2.visitdir(b'd/e')
1104 True
1104 True
1105 >>> m2.visitdir(b'd/e/f')
1105 >>> m2.visitdir(b'd/e/f')
1106 True
1106 True
1107 >>> m2.visitdir(b'd/e/g')
1107 >>> m2.visitdir(b'd/e/g')
1108 False
1108 False
1109 >>> m2.visitdir(b'd/ef')
1109 >>> m2.visitdir(b'd/ef')
1110 False
1110 False
1111 """
1111 """
1112
1112
1113 def __init__(self, path, matcher, badfn=None):
1113 def __init__(self, path, matcher, badfn=None):
1114 super(prefixdirmatcher, self).__init__(badfn)
1114 super(prefixdirmatcher, self).__init__(badfn)
1115 if not path:
1115 if not path:
1116 raise error.ProgrammingError(b'prefix path must not be empty')
1116 raise error.ProgrammingError(b'prefix path must not be empty')
1117 self._path = path
1117 self._path = path
1118 self._pathprefix = path + b'/'
1118 self._pathprefix = path + b'/'
1119 self._matcher = matcher
1119 self._matcher = matcher
1120
1120
1121 @propertycache
1121 @propertycache
1122 def _files(self):
1122 def _files(self):
1123 return [self._pathprefix + f for f in self._matcher._files]
1123 return [self._pathprefix + f for f in self._matcher._files]
1124
1124
1125 def matchfn(self, f):
1125 def matchfn(self, f):
1126 if not f.startswith(self._pathprefix):
1126 if not f.startswith(self._pathprefix):
1127 return False
1127 return False
1128 return self._matcher.matchfn(f[len(self._pathprefix) :])
1128 return self._matcher.matchfn(f[len(self._pathprefix) :])
1129
1129
1130 @propertycache
1130 @propertycache
1131 def _pathdirs(self):
1131 def _pathdirs(self):
1132 return set(pathutil.finddirs(self._path))
1132 return set(pathutil.finddirs(self._path))
1133
1133
1134 def visitdir(self, dir):
1134 def visitdir(self, dir):
1135 if dir == self._path:
1135 if dir == self._path:
1136 return self._matcher.visitdir(b'')
1136 return self._matcher.visitdir(b'')
1137 if dir.startswith(self._pathprefix):
1137 if dir.startswith(self._pathprefix):
1138 return self._matcher.visitdir(dir[len(self._pathprefix) :])
1138 return self._matcher.visitdir(dir[len(self._pathprefix) :])
1139 return dir in self._pathdirs
1139 return dir in self._pathdirs
1140
1140
1141 def visitchildrenset(self, dir):
1141 def visitchildrenset(self, dir):
1142 if dir == self._path:
1142 if dir == self._path:
1143 return self._matcher.visitchildrenset(b'')
1143 return self._matcher.visitchildrenset(b'')
1144 if dir.startswith(self._pathprefix):
1144 if dir.startswith(self._pathprefix):
1145 return self._matcher.visitchildrenset(dir[len(self._pathprefix) :])
1145 return self._matcher.visitchildrenset(dir[len(self._pathprefix) :])
1146 if dir in self._pathdirs:
1146 if dir in self._pathdirs:
1147 return b'this'
1147 return b'this'
1148 return set()
1148 return set()
1149
1149
1150 def isexact(self):
1150 def isexact(self):
1151 return self._matcher.isexact()
1151 return self._matcher.isexact()
1152
1152
1153 def prefix(self):
1153 def prefix(self):
1154 return self._matcher.prefix()
1154 return self._matcher.prefix()
1155
1155
1156 @encoding.strmethod
1156 @encoding.strmethod
1157 def __repr__(self):
1157 def __repr__(self):
1158 return b'<prefixdirmatcher path=%r, matcher=%r>' % (
1158 return b'<prefixdirmatcher path=%r, matcher=%r>' % (
1159 pycompat.bytestr(self._path),
1159 pycompat.bytestr(self._path),
1160 self._matcher,
1160 self._matcher,
1161 )
1161 )
1162
1162
1163
1163
1164 class unionmatcher(basematcher):
1164 class unionmatcher(basematcher):
1165 """A matcher that is the union of several matchers.
1165 """A matcher that is the union of several matchers.
1166
1166
1167 The non-matching-attributes (bad, traversedir) are taken from the first
1167 The non-matching-attributes (bad, traversedir) are taken from the first
1168 matcher.
1168 matcher.
1169 """
1169 """
1170
1170
1171 def __init__(self, matchers):
1171 def __init__(self, matchers):
1172 m1 = matchers[0]
1172 m1 = matchers[0]
1173 super(unionmatcher, self).__init__()
1173 super(unionmatcher, self).__init__()
1174 self.traversedir = m1.traversedir
1174 self.traversedir = m1.traversedir
1175 self._matchers = matchers
1175 self._matchers = matchers
1176
1176
1177 def matchfn(self, f):
1177 def matchfn(self, f):
1178 for match in self._matchers:
1178 for match in self._matchers:
1179 if match(f):
1179 if match(f):
1180 return True
1180 return True
1181 return False
1181 return False
1182
1182
1183 def visitdir(self, dir):
1183 def visitdir(self, dir):
1184 r = False
1184 r = False
1185 for m in self._matchers:
1185 for m in self._matchers:
1186 v = m.visitdir(dir)
1186 v = m.visitdir(dir)
1187 if v == b'all':
1187 if v == b'all':
1188 return v
1188 return v
1189 r |= v
1189 r |= v
1190 return r
1190 return r
1191
1191
1192 def visitchildrenset(self, dir):
1192 def visitchildrenset(self, dir):
1193 r = set()
1193 r = set()
1194 this = False
1194 this = False
1195 for m in self._matchers:
1195 for m in self._matchers:
1196 v = m.visitchildrenset(dir)
1196 v = m.visitchildrenset(dir)
1197 if not v:
1197 if not v:
1198 continue
1198 continue
1199 if v == b'all':
1199 if v == b'all':
1200 return v
1200 return v
1201 if this or v == b'this':
1201 if this or v == b'this':
1202 this = True
1202 this = True
1203 # don't break, we might have an 'all' in here.
1203 # don't break, we might have an 'all' in here.
1204 continue
1204 continue
1205 assert isinstance(v, set)
1205 assert isinstance(v, set)
1206 r = r.union(v)
1206 r = r.union(v)
1207 if this:
1207 if this:
1208 return b'this'
1208 return b'this'
1209 return r
1209 return r
1210
1210
1211 @encoding.strmethod
1211 @encoding.strmethod
1212 def __repr__(self):
1212 def __repr__(self):
1213 return b'<unionmatcher matchers=%r>' % self._matchers
1213 return b'<unionmatcher matchers=%r>' % self._matchers
1214
1214
1215
1215
1216 def patkind(pattern, default=None):
1216 def patkind(pattern, default=None):
1217 r"""If pattern is 'kind:pat' with a known kind, return kind.
1217 r"""If pattern is 'kind:pat' with a known kind, return kind.
1218
1218
1219 >>> patkind(br're:.*\.c$')
1219 >>> patkind(br're:.*\.c$')
1220 're'
1220 're'
1221 >>> patkind(b'glob:*.c')
1221 >>> patkind(b'glob:*.c')
1222 'glob'
1222 'glob'
1223 >>> patkind(b'relpath:test.py')
1223 >>> patkind(b'relpath:test.py')
1224 'relpath'
1224 'relpath'
1225 >>> patkind(b'main.py')
1225 >>> patkind(b'main.py')
1226 >>> patkind(b'main.py', default=b're')
1226 >>> patkind(b'main.py', default=b're')
1227 're'
1227 're'
1228 """
1228 """
1229 return _patsplit(pattern, default)[0]
1229 return _patsplit(pattern, default)[0]
1230
1230
1231
1231
1232 def _patsplit(pattern, default):
1232 def _patsplit(pattern, default):
1233 """Split a string into the optional pattern kind prefix and the actual
1233 """Split a string into the optional pattern kind prefix and the actual
1234 pattern."""
1234 pattern."""
1235 if b':' in pattern:
1235 if b':' in pattern:
1236 kind, pat = pattern.split(b':', 1)
1236 kind, pat = pattern.split(b':', 1)
1237 if kind in allpatternkinds:
1237 if kind in allpatternkinds:
1238 return kind, pat
1238 return kind, pat
1239 return default, pattern
1239 return default, pattern
1240
1240
1241
1241
1242 def _globre(pat):
1242 def _globre(pat):
1243 r"""Convert an extended glob string to a regexp string.
1243 r"""Convert an extended glob string to a regexp string.
1244
1244
1245 >>> from . import pycompat
1245 >>> from . import pycompat
1246 >>> def bprint(s):
1246 >>> def bprint(s):
1247 ... print(pycompat.sysstr(s))
1247 ... print(pycompat.sysstr(s))
1248 >>> bprint(_globre(br'?'))
1248 >>> bprint(_globre(br'?'))
1249 .
1249 .
1250 >>> bprint(_globre(br'*'))
1250 >>> bprint(_globre(br'*'))
1251 [^/]*
1251 [^/]*
1252 >>> bprint(_globre(br'**'))
1252 >>> bprint(_globre(br'**'))
1253 .*
1253 .*
1254 >>> bprint(_globre(br'**/a'))
1254 >>> bprint(_globre(br'**/a'))
1255 (?:.*/)?a
1255 (?:.*/)?a
1256 >>> bprint(_globre(br'a/**/b'))
1256 >>> bprint(_globre(br'a/**/b'))
1257 a/(?:.*/)?b
1257 a/(?:.*/)?b
1258 >>> bprint(_globre(br'[a*?!^][^b][!c]'))
1258 >>> bprint(_globre(br'[a*?!^][^b][!c]'))
1259 [a*?!^][\^b][^c]
1259 [a*?!^][\^b][^c]
1260 >>> bprint(_globre(br'{a,b}'))
1260 >>> bprint(_globre(br'{a,b}'))
1261 (?:a|b)
1261 (?:a|b)
1262 >>> bprint(_globre(br'.\*\?'))
1262 >>> bprint(_globre(br'.\*\?'))
1263 \.\*\?
1263 \.\*\?
1264 """
1264 """
1265 i, n = 0, len(pat)
1265 i, n = 0, len(pat)
1266 res = b''
1266 res = b''
1267 group = 0
1267 group = 0
1268 escape = util.stringutil.regexbytesescapemap.get
1268 escape = util.stringutil.regexbytesescapemap.get
1269
1269
1270 def peek():
1270 def peek():
1271 return i < n and pat[i : i + 1]
1271 return i < n and pat[i : i + 1]
1272
1272
1273 while i < n:
1273 while i < n:
1274 c = pat[i : i + 1]
1274 c = pat[i : i + 1]
1275 i += 1
1275 i += 1
1276 if c not in b'*?[{},\\':
1276 if c not in b'*?[{},\\':
1277 res += escape(c, c)
1277 res += escape(c, c)
1278 elif c == b'*':
1278 elif c == b'*':
1279 if peek() == b'*':
1279 if peek() == b'*':
1280 i += 1
1280 i += 1
1281 if peek() == b'/':
1281 if peek() == b'/':
1282 i += 1
1282 i += 1
1283 res += b'(?:.*/)?'
1283 res += b'(?:.*/)?'
1284 else:
1284 else:
1285 res += b'.*'
1285 res += b'.*'
1286 else:
1286 else:
1287 res += b'[^/]*'
1287 res += b'[^/]*'
1288 elif c == b'?':
1288 elif c == b'?':
1289 res += b'.'
1289 res += b'.'
1290 elif c == b'[':
1290 elif c == b'[':
1291 j = i
1291 j = i
1292 if j < n and pat[j : j + 1] in b'!]':
1292 if j < n and pat[j : j + 1] in b'!]':
1293 j += 1
1293 j += 1
1294 while j < n and pat[j : j + 1] != b']':
1294 while j < n and pat[j : j + 1] != b']':
1295 j += 1
1295 j += 1
1296 if j >= n:
1296 if j >= n:
1297 res += b'\\['
1297 res += b'\\['
1298 else:
1298 else:
1299 stuff = pat[i:j].replace(b'\\', b'\\\\')
1299 stuff = pat[i:j].replace(b'\\', b'\\\\')
1300 i = j + 1
1300 i = j + 1
1301 if stuff[0:1] == b'!':
1301 if stuff[0:1] == b'!':
1302 stuff = b'^' + stuff[1:]
1302 stuff = b'^' + stuff[1:]
1303 elif stuff[0:1] == b'^':
1303 elif stuff[0:1] == b'^':
1304 stuff = b'\\' + stuff
1304 stuff = b'\\' + stuff
1305 res = b'%s[%s]' % (res, stuff)
1305 res = b'%s[%s]' % (res, stuff)
1306 elif c == b'{':
1306 elif c == b'{':
1307 group += 1
1307 group += 1
1308 res += b'(?:'
1308 res += b'(?:'
1309 elif c == b'}' and group:
1309 elif c == b'}' and group:
1310 res += b')'
1310 res += b')'
1311 group -= 1
1311 group -= 1
1312 elif c == b',' and group:
1312 elif c == b',' and group:
1313 res += b'|'
1313 res += b'|'
1314 elif c == b'\\':
1314 elif c == b'\\':
1315 p = peek()
1315 p = peek()
1316 if p:
1316 if p:
1317 i += 1
1317 i += 1
1318 res += escape(p, p)
1318 res += escape(p, p)
1319 else:
1319 else:
1320 res += escape(c, c)
1320 res += escape(c, c)
1321 else:
1321 else:
1322 res += escape(c, c)
1322 res += escape(c, c)
1323 return res
1323 return res
1324
1324
1325
1325
1326 FLAG_RE = util.re.compile(b'^\(\?([aiLmsux]+)\)(.*)')
1326 FLAG_RE = util.re.compile(br'^\(\?([aiLmsux]+)\)(.*)')
1327
1327
1328
1328
1329 def _regex(kind, pat, globsuffix):
1329 def _regex(kind, pat, globsuffix):
1330 """Convert a (normalized) pattern of any kind into a
1330 """Convert a (normalized) pattern of any kind into a
1331 regular expression.
1331 regular expression.
1332 globsuffix is appended to the regexp of globs."""
1332 globsuffix is appended to the regexp of globs."""
1333 if not pat and kind in (b'glob', b'relpath'):
1333 if not pat and kind in (b'glob', b'relpath'):
1334 return b''
1334 return b''
1335 if kind == b're':
1335 if kind == b're':
1336 return pat
1336 return pat
1337 if kind in (b'path', b'relpath'):
1337 if kind in (b'path', b'relpath'):
1338 if pat == b'.':
1338 if pat == b'.':
1339 return b''
1339 return b''
1340 return util.stringutil.reescape(pat) + b'(?:/|$)'
1340 return util.stringutil.reescape(pat) + b'(?:/|$)'
1341 if kind == b'rootfilesin':
1341 if kind == b'rootfilesin':
1342 if pat == b'.':
1342 if pat == b'.':
1343 escaped = b''
1343 escaped = b''
1344 else:
1344 else:
1345 # Pattern is a directory name.
1345 # Pattern is a directory name.
1346 escaped = util.stringutil.reescape(pat) + b'/'
1346 escaped = util.stringutil.reescape(pat) + b'/'
1347 # Anything after the pattern must be a non-directory.
1347 # Anything after the pattern must be a non-directory.
1348 return escaped + b'[^/]+$'
1348 return escaped + b'[^/]+$'
1349 if kind == b'relglob':
1349 if kind == b'relglob':
1350 globre = _globre(pat)
1350 globre = _globre(pat)
1351 if globre.startswith(b'[^/]*'):
1351 if globre.startswith(b'[^/]*'):
1352 # When pat has the form *XYZ (common), make the returned regex more
1352 # When pat has the form *XYZ (common), make the returned regex more
1353 # legible by returning the regex for **XYZ instead of **/*XYZ.
1353 # legible by returning the regex for **XYZ instead of **/*XYZ.
1354 return b'.*' + globre[len(b'[^/]*') :] + globsuffix
1354 return b'.*' + globre[len(b'[^/]*') :] + globsuffix
1355 return b'(?:|.*/)' + globre + globsuffix
1355 return b'(?:|.*/)' + globre + globsuffix
1356 if kind == b'relre':
1356 if kind == b'relre':
1357 flag = None
1357 flag = None
1358 m = FLAG_RE.match(pat)
1358 m = FLAG_RE.match(pat)
1359 if m:
1359 if m:
1360 flag, pat = m.groups()
1360 flag, pat = m.groups()
1361 if not pat.startswith(b'^'):
1361 if not pat.startswith(b'^'):
1362 pat = b'.*' + pat
1362 pat = b'.*' + pat
1363 if flag is not None:
1363 if flag is not None:
1364 pat = br'(?%s:%s)' % (flag, pat)
1364 pat = br'(?%s:%s)' % (flag, pat)
1365 return pat
1365 return pat
1366 if kind in (b'glob', b'rootglob'):
1366 if kind in (b'glob', b'rootglob'):
1367 return _globre(pat) + globsuffix
1367 return _globre(pat) + globsuffix
1368 raise error.ProgrammingError(b'not a regex pattern: %s:%s' % (kind, pat))
1368 raise error.ProgrammingError(b'not a regex pattern: %s:%s' % (kind, pat))
1369
1369
1370
1370
1371 def _buildmatch(kindpats, globsuffix, root):
1371 def _buildmatch(kindpats, globsuffix, root):
1372 """Return regexp string and a matcher function for kindpats.
1372 """Return regexp string and a matcher function for kindpats.
1373 globsuffix is appended to the regexp of globs."""
1373 globsuffix is appended to the regexp of globs."""
1374 matchfuncs = []
1374 matchfuncs = []
1375
1375
1376 subincludes, kindpats = _expandsubinclude(kindpats, root)
1376 subincludes, kindpats = _expandsubinclude(kindpats, root)
1377 if subincludes:
1377 if subincludes:
1378 submatchers = {}
1378 submatchers = {}
1379
1379
1380 def matchsubinclude(f):
1380 def matchsubinclude(f):
1381 for prefix, matcherargs in subincludes:
1381 for prefix, matcherargs in subincludes:
1382 if f.startswith(prefix):
1382 if f.startswith(prefix):
1383 mf = submatchers.get(prefix)
1383 mf = submatchers.get(prefix)
1384 if mf is None:
1384 if mf is None:
1385 mf = match(*matcherargs)
1385 mf = match(*matcherargs)
1386 submatchers[prefix] = mf
1386 submatchers[prefix] = mf
1387
1387
1388 if mf(f[len(prefix) :]):
1388 if mf(f[len(prefix) :]):
1389 return True
1389 return True
1390 return False
1390 return False
1391
1391
1392 matchfuncs.append(matchsubinclude)
1392 matchfuncs.append(matchsubinclude)
1393
1393
1394 regex = b''
1394 regex = b''
1395 if kindpats:
1395 if kindpats:
1396 if all(k == b'rootfilesin' for k, p, s in kindpats):
1396 if all(k == b'rootfilesin' for k, p, s in kindpats):
1397 dirs = {p for k, p, s in kindpats}
1397 dirs = {p for k, p, s in kindpats}
1398
1398
1399 def mf(f):
1399 def mf(f):
1400 i = f.rfind(b'/')
1400 i = f.rfind(b'/')
1401 if i >= 0:
1401 if i >= 0:
1402 dir = f[:i]
1402 dir = f[:i]
1403 else:
1403 else:
1404 dir = b'.'
1404 dir = b'.'
1405 return dir in dirs
1405 return dir in dirs
1406
1406
1407 regex = b'rootfilesin: %s' % stringutil.pprint(list(sorted(dirs)))
1407 regex = b'rootfilesin: %s' % stringutil.pprint(list(sorted(dirs)))
1408 matchfuncs.append(mf)
1408 matchfuncs.append(mf)
1409 else:
1409 else:
1410 regex, mf = _buildregexmatch(kindpats, globsuffix)
1410 regex, mf = _buildregexmatch(kindpats, globsuffix)
1411 matchfuncs.append(mf)
1411 matchfuncs.append(mf)
1412
1412
1413 if len(matchfuncs) == 1:
1413 if len(matchfuncs) == 1:
1414 return regex, matchfuncs[0]
1414 return regex, matchfuncs[0]
1415 else:
1415 else:
1416 return regex, lambda f: any(mf(f) for mf in matchfuncs)
1416 return regex, lambda f: any(mf(f) for mf in matchfuncs)
1417
1417
1418
1418
1419 MAX_RE_SIZE = 20000
1419 MAX_RE_SIZE = 20000
1420
1420
1421
1421
1422 def _joinregexes(regexps):
1422 def _joinregexes(regexps):
1423 """gather multiple regular expressions into a single one"""
1423 """gather multiple regular expressions into a single one"""
1424 return b'|'.join(regexps)
1424 return b'|'.join(regexps)
1425
1425
1426
1426
1427 def _buildregexmatch(kindpats, globsuffix):
1427 def _buildregexmatch(kindpats, globsuffix):
1428 """Build a match function from a list of kinds and kindpats,
1428 """Build a match function from a list of kinds and kindpats,
1429 return regexp string and a matcher function.
1429 return regexp string and a matcher function.
1430
1430
1431 Test too large input
1431 Test too large input
1432 >>> _buildregexmatch([
1432 >>> _buildregexmatch([
1433 ... (b'relglob', b'?' * MAX_RE_SIZE, b'')
1433 ... (b'relglob', b'?' * MAX_RE_SIZE, b'')
1434 ... ], b'$')
1434 ... ], b'$')
1435 Traceback (most recent call last):
1435 Traceback (most recent call last):
1436 ...
1436 ...
1437 Abort: matcher pattern is too long (20009 bytes)
1437 Abort: matcher pattern is too long (20009 bytes)
1438 """
1438 """
1439 try:
1439 try:
1440 allgroups = []
1440 allgroups = []
1441 regexps = [_regex(k, p, globsuffix) for (k, p, s) in kindpats]
1441 regexps = [_regex(k, p, globsuffix) for (k, p, s) in kindpats]
1442 fullregexp = _joinregexes(regexps)
1442 fullregexp = _joinregexes(regexps)
1443
1443
1444 startidx = 0
1444 startidx = 0
1445 groupsize = 0
1445 groupsize = 0
1446 for idx, r in enumerate(regexps):
1446 for idx, r in enumerate(regexps):
1447 piecesize = len(r)
1447 piecesize = len(r)
1448 if piecesize > MAX_RE_SIZE:
1448 if piecesize > MAX_RE_SIZE:
1449 msg = _(b"matcher pattern is too long (%d bytes)") % piecesize
1449 msg = _(b"matcher pattern is too long (%d bytes)") % piecesize
1450 raise error.Abort(msg)
1450 raise error.Abort(msg)
1451 elif (groupsize + piecesize) > MAX_RE_SIZE:
1451 elif (groupsize + piecesize) > MAX_RE_SIZE:
1452 group = regexps[startidx:idx]
1452 group = regexps[startidx:idx]
1453 allgroups.append(_joinregexes(group))
1453 allgroups.append(_joinregexes(group))
1454 startidx = idx
1454 startidx = idx
1455 groupsize = 0
1455 groupsize = 0
1456 groupsize += piecesize + 1
1456 groupsize += piecesize + 1
1457
1457
1458 if startidx == 0:
1458 if startidx == 0:
1459 matcher = _rematcher(fullregexp)
1459 matcher = _rematcher(fullregexp)
1460 func = lambda s: bool(matcher(s))
1460 func = lambda s: bool(matcher(s))
1461 else:
1461 else:
1462 group = regexps[startidx:]
1462 group = regexps[startidx:]
1463 allgroups.append(_joinregexes(group))
1463 allgroups.append(_joinregexes(group))
1464 allmatchers = [_rematcher(g) for g in allgroups]
1464 allmatchers = [_rematcher(g) for g in allgroups]
1465 func = lambda s: any(m(s) for m in allmatchers)
1465 func = lambda s: any(m(s) for m in allmatchers)
1466 return fullregexp, func
1466 return fullregexp, func
1467 except re.error:
1467 except re.error:
1468 for k, p, s in kindpats:
1468 for k, p, s in kindpats:
1469 try:
1469 try:
1470 _rematcher(_regex(k, p, globsuffix))
1470 _rematcher(_regex(k, p, globsuffix))
1471 except re.error:
1471 except re.error:
1472 if s:
1472 if s:
1473 raise error.Abort(
1473 raise error.Abort(
1474 _(b"%s: invalid pattern (%s): %s") % (s, k, p)
1474 _(b"%s: invalid pattern (%s): %s") % (s, k, p)
1475 )
1475 )
1476 else:
1476 else:
1477 raise error.Abort(_(b"invalid pattern (%s): %s") % (k, p))
1477 raise error.Abort(_(b"invalid pattern (%s): %s") % (k, p))
1478 raise error.Abort(_(b"invalid pattern"))
1478 raise error.Abort(_(b"invalid pattern"))
1479
1479
1480
1480
1481 def _patternrootsanddirs(kindpats):
1481 def _patternrootsanddirs(kindpats):
1482 """Returns roots and directories corresponding to each pattern.
1482 """Returns roots and directories corresponding to each pattern.
1483
1483
1484 This calculates the roots and directories exactly matching the patterns and
1484 This calculates the roots and directories exactly matching the patterns and
1485 returns a tuple of (roots, dirs) for each. It does not return other
1485 returns a tuple of (roots, dirs) for each. It does not return other
1486 directories which may also need to be considered, like the parent
1486 directories which may also need to be considered, like the parent
1487 directories.
1487 directories.
1488 """
1488 """
1489 r = []
1489 r = []
1490 d = []
1490 d = []
1491 for kind, pat, source in kindpats:
1491 for kind, pat, source in kindpats:
1492 if kind in (b'glob', b'rootglob'): # find the non-glob prefix
1492 if kind in (b'glob', b'rootglob'): # find the non-glob prefix
1493 root = []
1493 root = []
1494 for p in pat.split(b'/'):
1494 for p in pat.split(b'/'):
1495 if b'[' in p or b'{' in p or b'*' in p or b'?' in p:
1495 if b'[' in p or b'{' in p or b'*' in p or b'?' in p:
1496 break
1496 break
1497 root.append(p)
1497 root.append(p)
1498 r.append(b'/'.join(root))
1498 r.append(b'/'.join(root))
1499 elif kind in (b'relpath', b'path'):
1499 elif kind in (b'relpath', b'path'):
1500 if pat == b'.':
1500 if pat == b'.':
1501 pat = b''
1501 pat = b''
1502 r.append(pat)
1502 r.append(pat)
1503 elif kind in (b'rootfilesin',):
1503 elif kind in (b'rootfilesin',):
1504 if pat == b'.':
1504 if pat == b'.':
1505 pat = b''
1505 pat = b''
1506 d.append(pat)
1506 d.append(pat)
1507 else: # relglob, re, relre
1507 else: # relglob, re, relre
1508 r.append(b'')
1508 r.append(b'')
1509 return r, d
1509 return r, d
1510
1510
1511
1511
1512 def _roots(kindpats):
1512 def _roots(kindpats):
1513 '''Returns root directories to match recursively from the given patterns.'''
1513 '''Returns root directories to match recursively from the given patterns.'''
1514 roots, dirs = _patternrootsanddirs(kindpats)
1514 roots, dirs = _patternrootsanddirs(kindpats)
1515 return roots
1515 return roots
1516
1516
1517
1517
1518 def _rootsdirsandparents(kindpats):
1518 def _rootsdirsandparents(kindpats):
1519 """Returns roots and exact directories from patterns.
1519 """Returns roots and exact directories from patterns.
1520
1520
1521 `roots` are directories to match recursively, `dirs` should
1521 `roots` are directories to match recursively, `dirs` should
1522 be matched non-recursively, and `parents` are the implicitly required
1522 be matched non-recursively, and `parents` are the implicitly required
1523 directories to walk to items in either roots or dirs.
1523 directories to walk to items in either roots or dirs.
1524
1524
1525 Returns a tuple of (roots, dirs, parents).
1525 Returns a tuple of (roots, dirs, parents).
1526
1526
1527 >>> r = _rootsdirsandparents(
1527 >>> r = _rootsdirsandparents(
1528 ... [(b'glob', b'g/h/*', b''), (b'glob', b'g/h', b''),
1528 ... [(b'glob', b'g/h/*', b''), (b'glob', b'g/h', b''),
1529 ... (b'glob', b'g*', b'')])
1529 ... (b'glob', b'g*', b'')])
1530 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1530 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1531 (['g/h', 'g/h', ''], []) ['', 'g']
1531 (['g/h', 'g/h', ''], []) ['', 'g']
1532 >>> r = _rootsdirsandparents(
1532 >>> r = _rootsdirsandparents(
1533 ... [(b'rootfilesin', b'g/h', b''), (b'rootfilesin', b'', b'')])
1533 ... [(b'rootfilesin', b'g/h', b''), (b'rootfilesin', b'', b'')])
1534 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1534 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1535 ([], ['g/h', '']) ['', 'g']
1535 ([], ['g/h', '']) ['', 'g']
1536 >>> r = _rootsdirsandparents(
1536 >>> r = _rootsdirsandparents(
1537 ... [(b'relpath', b'r', b''), (b'path', b'p/p', b''),
1537 ... [(b'relpath', b'r', b''), (b'path', b'p/p', b''),
1538 ... (b'path', b'', b'')])
1538 ... (b'path', b'', b'')])
1539 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1539 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1540 (['r', 'p/p', ''], []) ['', 'p']
1540 (['r', 'p/p', ''], []) ['', 'p']
1541 >>> r = _rootsdirsandparents(
1541 >>> r = _rootsdirsandparents(
1542 ... [(b'relglob', b'rg*', b''), (b're', b're/', b''),
1542 ... [(b'relglob', b'rg*', b''), (b're', b're/', b''),
1543 ... (b'relre', b'rr', b'')])
1543 ... (b'relre', b'rr', b'')])
1544 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1544 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1545 (['', '', ''], []) ['']
1545 (['', '', ''], []) ['']
1546 """
1546 """
1547 r, d = _patternrootsanddirs(kindpats)
1547 r, d = _patternrootsanddirs(kindpats)
1548
1548
1549 p = set()
1549 p = set()
1550 # Add the parents as non-recursive/exact directories, since they must be
1550 # Add the parents as non-recursive/exact directories, since they must be
1551 # scanned to get to either the roots or the other exact directories.
1551 # scanned to get to either the roots or the other exact directories.
1552 p.update(pathutil.dirs(d))
1552 p.update(pathutil.dirs(d))
1553 p.update(pathutil.dirs(r))
1553 p.update(pathutil.dirs(r))
1554
1554
1555 # FIXME: all uses of this function convert these to sets, do so before
1555 # FIXME: all uses of this function convert these to sets, do so before
1556 # returning.
1556 # returning.
1557 # FIXME: all uses of this function do not need anything in 'roots' and
1557 # FIXME: all uses of this function do not need anything in 'roots' and
1558 # 'dirs' to also be in 'parents', consider removing them before returning.
1558 # 'dirs' to also be in 'parents', consider removing them before returning.
1559 return r, d, p
1559 return r, d, p
1560
1560
1561
1561
1562 def _explicitfiles(kindpats):
1562 def _explicitfiles(kindpats):
1563 """Returns the potential explicit filenames from the patterns.
1563 """Returns the potential explicit filenames from the patterns.
1564
1564
1565 >>> _explicitfiles([(b'path', b'foo/bar', b'')])
1565 >>> _explicitfiles([(b'path', b'foo/bar', b'')])
1566 ['foo/bar']
1566 ['foo/bar']
1567 >>> _explicitfiles([(b'rootfilesin', b'foo/bar', b'')])
1567 >>> _explicitfiles([(b'rootfilesin', b'foo/bar', b'')])
1568 []
1568 []
1569 """
1569 """
1570 # Keep only the pattern kinds where one can specify filenames (vs only
1570 # Keep only the pattern kinds where one can specify filenames (vs only
1571 # directory names).
1571 # directory names).
1572 filable = [kp for kp in kindpats if kp[0] not in (b'rootfilesin',)]
1572 filable = [kp for kp in kindpats if kp[0] not in (b'rootfilesin',)]
1573 return _roots(filable)
1573 return _roots(filable)
1574
1574
1575
1575
1576 def _prefix(kindpats):
1576 def _prefix(kindpats):
1577 '''Whether all the patterns match a prefix (i.e. recursively)'''
1577 '''Whether all the patterns match a prefix (i.e. recursively)'''
1578 for kind, pat, source in kindpats:
1578 for kind, pat, source in kindpats:
1579 if kind not in (b'path', b'relpath'):
1579 if kind not in (b'path', b'relpath'):
1580 return False
1580 return False
1581 return True
1581 return True
1582
1582
1583
1583
1584 _commentre = None
1584 _commentre = None
1585
1585
1586
1586
1587 def readpatternfile(filepath, warn, sourceinfo=False):
1587 def readpatternfile(filepath, warn, sourceinfo=False):
1588 """parse a pattern file, returning a list of
1588 """parse a pattern file, returning a list of
1589 patterns. These patterns should be given to compile()
1589 patterns. These patterns should be given to compile()
1590 to be validated and converted into a match function.
1590 to be validated and converted into a match function.
1591
1591
1592 trailing white space is dropped.
1592 trailing white space is dropped.
1593 the escape character is backslash.
1593 the escape character is backslash.
1594 comments start with #.
1594 comments start with #.
1595 empty lines are skipped.
1595 empty lines are skipped.
1596
1596
1597 lines can be of the following formats:
1597 lines can be of the following formats:
1598
1598
1599 syntax: regexp # defaults following lines to non-rooted regexps
1599 syntax: regexp # defaults following lines to non-rooted regexps
1600 syntax: glob # defaults following lines to non-rooted globs
1600 syntax: glob # defaults following lines to non-rooted globs
1601 re:pattern # non-rooted regular expression
1601 re:pattern # non-rooted regular expression
1602 glob:pattern # non-rooted glob
1602 glob:pattern # non-rooted glob
1603 rootglob:pat # rooted glob (same root as ^ in regexps)
1603 rootglob:pat # rooted glob (same root as ^ in regexps)
1604 pattern # pattern of the current default type
1604 pattern # pattern of the current default type
1605
1605
1606 if sourceinfo is set, returns a list of tuples:
1606 if sourceinfo is set, returns a list of tuples:
1607 (pattern, lineno, originalline).
1607 (pattern, lineno, originalline).
1608 This is useful to debug ignore patterns.
1608 This is useful to debug ignore patterns.
1609 """
1609 """
1610
1610
1611 syntaxes = {
1611 syntaxes = {
1612 b're': b'relre:',
1612 b're': b'relre:',
1613 b'regexp': b'relre:',
1613 b'regexp': b'relre:',
1614 b'glob': b'relglob:',
1614 b'glob': b'relglob:',
1615 b'rootglob': b'rootglob:',
1615 b'rootglob': b'rootglob:',
1616 b'include': b'include',
1616 b'include': b'include',
1617 b'subinclude': b'subinclude',
1617 b'subinclude': b'subinclude',
1618 }
1618 }
1619 syntax = b'relre:'
1619 syntax = b'relre:'
1620 patterns = []
1620 patterns = []
1621
1621
1622 fp = open(filepath, b'rb')
1622 fp = open(filepath, b'rb')
1623 for lineno, line in enumerate(fp, start=1):
1623 for lineno, line in enumerate(fp, start=1):
1624 if b"#" in line:
1624 if b"#" in line:
1625 global _commentre
1625 global _commentre
1626 if not _commentre:
1626 if not _commentre:
1627 _commentre = util.re.compile(br'((?:^|[^\\])(?:\\\\)*)#.*')
1627 _commentre = util.re.compile(br'((?:^|[^\\])(?:\\\\)*)#.*')
1628 # remove comments prefixed by an even number of escapes
1628 # remove comments prefixed by an even number of escapes
1629 m = _commentre.search(line)
1629 m = _commentre.search(line)
1630 if m:
1630 if m:
1631 line = line[: m.end(1)]
1631 line = line[: m.end(1)]
1632 # fixup properly escaped comments that survived the above
1632 # fixup properly escaped comments that survived the above
1633 line = line.replace(b"\\#", b"#")
1633 line = line.replace(b"\\#", b"#")
1634 line = line.rstrip()
1634 line = line.rstrip()
1635 if not line:
1635 if not line:
1636 continue
1636 continue
1637
1637
1638 if line.startswith(b'syntax:'):
1638 if line.startswith(b'syntax:'):
1639 s = line[7:].strip()
1639 s = line[7:].strip()
1640 try:
1640 try:
1641 syntax = syntaxes[s]
1641 syntax = syntaxes[s]
1642 except KeyError:
1642 except KeyError:
1643 if warn:
1643 if warn:
1644 warn(
1644 warn(
1645 _(b"%s: ignoring invalid syntax '%s'\n") % (filepath, s)
1645 _(b"%s: ignoring invalid syntax '%s'\n") % (filepath, s)
1646 )
1646 )
1647 continue
1647 continue
1648
1648
1649 linesyntax = syntax
1649 linesyntax = syntax
1650 for s, rels in syntaxes.items():
1650 for s, rels in syntaxes.items():
1651 if line.startswith(rels):
1651 if line.startswith(rels):
1652 linesyntax = rels
1652 linesyntax = rels
1653 line = line[len(rels) :]
1653 line = line[len(rels) :]
1654 break
1654 break
1655 elif line.startswith(s + b':'):
1655 elif line.startswith(s + b':'):
1656 linesyntax = rels
1656 linesyntax = rels
1657 line = line[len(s) + 1 :]
1657 line = line[len(s) + 1 :]
1658 break
1658 break
1659 if sourceinfo:
1659 if sourceinfo:
1660 patterns.append((linesyntax + line, lineno, line))
1660 patterns.append((linesyntax + line, lineno, line))
1661 else:
1661 else:
1662 patterns.append(linesyntax + line)
1662 patterns.append(linesyntax + line)
1663 fp.close()
1663 fp.close()
1664 return patterns
1664 return patterns
@@ -1,526 +1,526 b''
1 # setdiscovery.py - improved discovery of common nodeset for mercurial
1 # setdiscovery.py - improved discovery of common nodeset for mercurial
2 #
2 #
3 # Copyright 2010 Benoit Boissinot <bboissin@gmail.com>
3 # Copyright 2010 Benoit Boissinot <bboissin@gmail.com>
4 # and Peter Arrenbrecht <peter@arrenbrecht.ch>
4 # and Peter Arrenbrecht <peter@arrenbrecht.ch>
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 Algorithm works in the following way. You have two repository: local and
9 Algorithm works in the following way. You have two repository: local and
10 remote. They both contains a DAG of changelists.
10 remote. They both contains a DAG of changelists.
11
11
12 The goal of the discovery protocol is to find one set of node *common*,
12 The goal of the discovery protocol is to find one set of node *common*,
13 the set of nodes shared by local and remote.
13 the set of nodes shared by local and remote.
14
14
15 One of the issue with the original protocol was latency, it could
15 One of the issue with the original protocol was latency, it could
16 potentially require lots of roundtrips to discover that the local repo was a
16 potentially require lots of roundtrips to discover that the local repo was a
17 subset of remote (which is a very common case, you usually have few changes
17 subset of remote (which is a very common case, you usually have few changes
18 compared to upstream, while upstream probably had lots of development).
18 compared to upstream, while upstream probably had lots of development).
19
19
20 The new protocol only requires one interface for the remote repo: `known()`,
20 The new protocol only requires one interface for the remote repo: `known()`,
21 which given a set of changelists tells you if they are present in the DAG.
21 which given a set of changelists tells you if they are present in the DAG.
22
22
23 The algorithm then works as follow:
23 The algorithm then works as follow:
24
24
25 - We will be using three sets, `common`, `missing`, `unknown`. Originally
25 - We will be using three sets, `common`, `missing`, `unknown`. Originally
26 all nodes are in `unknown`.
26 all nodes are in `unknown`.
27 - Take a sample from `unknown`, call `remote.known(sample)`
27 - Take a sample from `unknown`, call `remote.known(sample)`
28 - For each node that remote knows, move it and all its ancestors to `common`
28 - For each node that remote knows, move it and all its ancestors to `common`
29 - For each node that remote doesn't know, move it and all its descendants
29 - For each node that remote doesn't know, move it and all its descendants
30 to `missing`
30 to `missing`
31 - Iterate until `unknown` is empty
31 - Iterate until `unknown` is empty
32
32
33 There are a couple optimizations, first is instead of starting with a random
33 There are a couple optimizations, first is instead of starting with a random
34 sample of missing, start by sending all heads, in the case where the local
34 sample of missing, start by sending all heads, in the case where the local
35 repo is a subset, you computed the answer in one round trip.
35 repo is a subset, you computed the answer in one round trip.
36
36
37 Then you can do something similar to the bisecting strategy used when
37 Then you can do something similar to the bisecting strategy used when
38 finding faulty changesets. Instead of random samples, you can try picking
38 finding faulty changesets. Instead of random samples, you can try picking
39 nodes that will maximize the number of nodes that will be
39 nodes that will maximize the number of nodes that will be
40 classified with it (since all ancestors or descendants will be marked as well).
40 classified with it (since all ancestors or descendants will be marked as well).
41 """
41 """
42
42
43
43
44 import collections
44 import collections
45 import random
45 import random
46
46
47 from .i18n import _
47 from .i18n import _
48 from .node import nullrev
48 from .node import nullrev
49 from . import (
49 from . import (
50 error,
50 error,
51 policy,
51 policy,
52 util,
52 util,
53 )
53 )
54
54
55
55
56 def _updatesample(revs, heads, sample, parentfn, quicksamplesize=0):
56 def _updatesample(revs, heads, sample, parentfn, quicksamplesize=0):
57 """update an existing sample to match the expected size
57 """update an existing sample to match the expected size
58
58
59 The sample is updated with revs exponentially distant from each head of the
59 The sample is updated with revs exponentially distant from each head of the
60 <revs> set. (H~1, H~2, H~4, H~8, etc).
60 <revs> set. (H~1, H~2, H~4, H~8, etc).
61
61
62 If a target size is specified, the sampling will stop once this size is
62 If a target size is specified, the sampling will stop once this size is
63 reached. Otherwise sampling will happen until roots of the <revs> set are
63 reached. Otherwise sampling will happen until roots of the <revs> set are
64 reached.
64 reached.
65
65
66 :revs: set of revs we want to discover (if None, assume the whole dag)
66 :revs: set of revs we want to discover (if None, assume the whole dag)
67 :heads: set of DAG head revs
67 :heads: set of DAG head revs
68 :sample: a sample to update
68 :sample: a sample to update
69 :parentfn: a callable to resolve parents for a revision
69 :parentfn: a callable to resolve parents for a revision
70 :quicksamplesize: optional target size of the sample"""
70 :quicksamplesize: optional target size of the sample"""
71 dist = {}
71 dist = {}
72 visit = collections.deque(heads)
72 visit = collections.deque(heads)
73 seen = set()
73 seen = set()
74 factor = 1
74 factor = 1
75 while visit:
75 while visit:
76 curr = visit.popleft()
76 curr = visit.popleft()
77 if curr in seen:
77 if curr in seen:
78 continue
78 continue
79 d = dist.setdefault(curr, 1)
79 d = dist.setdefault(curr, 1)
80 if d > factor:
80 if d > factor:
81 factor *= 2
81 factor *= 2
82 if d == factor:
82 if d == factor:
83 sample.add(curr)
83 sample.add(curr)
84 if quicksamplesize and (len(sample) >= quicksamplesize):
84 if quicksamplesize and (len(sample) >= quicksamplesize):
85 return
85 return
86 seen.add(curr)
86 seen.add(curr)
87
87
88 for p in parentfn(curr):
88 for p in parentfn(curr):
89 if p != nullrev and (not revs or p in revs):
89 if p != nullrev and (not revs or p in revs):
90 dist.setdefault(p, d + 1)
90 dist.setdefault(p, d + 1)
91 visit.append(p)
91 visit.append(p)
92
92
93
93
94 def _limitsample(sample, desiredlen, randomize=True):
94 def _limitsample(sample, desiredlen, randomize=True):
95 """return a random subset of sample of at most desiredlen item.
95 """return a random subset of sample of at most desiredlen item.
96
96
97 If randomize is False, though, a deterministic subset is returned.
97 If randomize is False, though, a deterministic subset is returned.
98 This is meant for integration tests.
98 This is meant for integration tests.
99 """
99 """
100 if len(sample) <= desiredlen:
100 if len(sample) <= desiredlen:
101 return sample
101 return sample
102 sample = list(sample)
102 if randomize:
103 if randomize:
103 return set(random.sample(sample, desiredlen))
104 return set(random.sample(sample, desiredlen))
104 sample = list(sample)
105 sample.sort()
105 sample.sort()
106 return set(sample[:desiredlen])
106 return set(sample[:desiredlen])
107
107
108
108
109 class partialdiscovery:
109 class partialdiscovery:
110 """an object representing ongoing discovery
110 """an object representing ongoing discovery
111
111
112 Feed with data from the remote repository, this object keep track of the
112 Feed with data from the remote repository, this object keep track of the
113 current set of changeset in various states:
113 current set of changeset in various states:
114
114
115 - common: revs also known remotely
115 - common: revs also known remotely
116 - undecided: revs we don't have information on yet
116 - undecided: revs we don't have information on yet
117 - missing: revs missing remotely
117 - missing: revs missing remotely
118 (all tracked revisions are known locally)
118 (all tracked revisions are known locally)
119 """
119 """
120
120
121 def __init__(self, repo, targetheads, respectsize, randomize=True):
121 def __init__(self, repo, targetheads, respectsize, randomize=True):
122 self._repo = repo
122 self._repo = repo
123 self._targetheads = targetheads
123 self._targetheads = targetheads
124 self._common = repo.changelog.incrementalmissingrevs()
124 self._common = repo.changelog.incrementalmissingrevs()
125 self._undecided = None
125 self._undecided = None
126 self.missing = set()
126 self.missing = set()
127 self._childrenmap = None
127 self._childrenmap = None
128 self._respectsize = respectsize
128 self._respectsize = respectsize
129 self.randomize = randomize
129 self.randomize = randomize
130
130
131 def addcommons(self, commons):
131 def addcommons(self, commons):
132 """register nodes known as common"""
132 """register nodes known as common"""
133 self._common.addbases(commons)
133 self._common.addbases(commons)
134 if self._undecided is not None:
134 if self._undecided is not None:
135 self._common.removeancestorsfrom(self._undecided)
135 self._common.removeancestorsfrom(self._undecided)
136
136
137 def addmissings(self, missings):
137 def addmissings(self, missings):
138 """register some nodes as missing"""
138 """register some nodes as missing"""
139 newmissing = self._repo.revs(b'%ld::%ld', missings, self.undecided)
139 newmissing = self._repo.revs(b'%ld::%ld', missings, self.undecided)
140 if newmissing:
140 if newmissing:
141 self.missing.update(newmissing)
141 self.missing.update(newmissing)
142 self.undecided.difference_update(newmissing)
142 self.undecided.difference_update(newmissing)
143
143
144 def addinfo(self, sample):
144 def addinfo(self, sample):
145 """consume an iterable of (rev, known) tuples"""
145 """consume an iterable of (rev, known) tuples"""
146 common = set()
146 common = set()
147 missing = set()
147 missing = set()
148 for rev, known in sample:
148 for rev, known in sample:
149 if known:
149 if known:
150 common.add(rev)
150 common.add(rev)
151 else:
151 else:
152 missing.add(rev)
152 missing.add(rev)
153 if common:
153 if common:
154 self.addcommons(common)
154 self.addcommons(common)
155 if missing:
155 if missing:
156 self.addmissings(missing)
156 self.addmissings(missing)
157
157
158 def hasinfo(self):
158 def hasinfo(self):
159 """return True is we have any clue about the remote state"""
159 """return True is we have any clue about the remote state"""
160 return self._common.hasbases()
160 return self._common.hasbases()
161
161
162 def iscomplete(self):
162 def iscomplete(self):
163 """True if all the necessary data have been gathered"""
163 """True if all the necessary data have been gathered"""
164 return self._undecided is not None and not self._undecided
164 return self._undecided is not None and not self._undecided
165
165
166 @property
166 @property
167 def undecided(self):
167 def undecided(self):
168 if self._undecided is not None:
168 if self._undecided is not None:
169 return self._undecided
169 return self._undecided
170 self._undecided = set(self._common.missingancestors(self._targetheads))
170 self._undecided = set(self._common.missingancestors(self._targetheads))
171 return self._undecided
171 return self._undecided
172
172
173 def stats(self):
173 def stats(self):
174 return {
174 return {
175 'undecided': len(self.undecided),
175 'undecided': len(self.undecided),
176 }
176 }
177
177
178 def commonheads(self):
178 def commonheads(self):
179 """the heads of the known common set"""
179 """the heads of the known common set"""
180 # heads(common) == heads(common.bases) since common represents
180 # heads(common) == heads(common.bases) since common represents
181 # common.bases and all its ancestors
181 # common.bases and all its ancestors
182 return self._common.basesheads()
182 return self._common.basesheads()
183
183
184 def _parentsgetter(self):
184 def _parentsgetter(self):
185 getrev = self._repo.changelog.index.__getitem__
185 getrev = self._repo.changelog.index.__getitem__
186
186
187 def getparents(r):
187 def getparents(r):
188 return getrev(r)[5:7]
188 return getrev(r)[5:7]
189
189
190 return getparents
190 return getparents
191
191
192 def _childrengetter(self):
192 def _childrengetter(self):
193
193
194 if self._childrenmap is not None:
194 if self._childrenmap is not None:
195 # During discovery, the `undecided` set keep shrinking.
195 # During discovery, the `undecided` set keep shrinking.
196 # Therefore, the map computed for an iteration N will be
196 # Therefore, the map computed for an iteration N will be
197 # valid for iteration N+1. Instead of computing the same
197 # valid for iteration N+1. Instead of computing the same
198 # data over and over we cached it the first time.
198 # data over and over we cached it the first time.
199 return self._childrenmap.__getitem__
199 return self._childrenmap.__getitem__
200
200
201 # _updatesample() essentially does interaction over revisions to look
201 # _updatesample() essentially does interaction over revisions to look
202 # up their children. This lookup is expensive and doing it in a loop is
202 # up their children. This lookup is expensive and doing it in a loop is
203 # quadratic. We precompute the children for all relevant revisions and
203 # quadratic. We precompute the children for all relevant revisions and
204 # make the lookup in _updatesample() a simple dict lookup.
204 # make the lookup in _updatesample() a simple dict lookup.
205 self._childrenmap = children = {}
205 self._childrenmap = children = {}
206
206
207 parentrevs = self._parentsgetter()
207 parentrevs = self._parentsgetter()
208 revs = self.undecided
208 revs = self.undecided
209
209
210 for rev in sorted(revs):
210 for rev in sorted(revs):
211 # Always ensure revision has an entry so we don't need to worry
211 # Always ensure revision has an entry so we don't need to worry
212 # about missing keys.
212 # about missing keys.
213 children[rev] = []
213 children[rev] = []
214 for prev in parentrevs(rev):
214 for prev in parentrevs(rev):
215 if prev == nullrev:
215 if prev == nullrev:
216 continue
216 continue
217 c = children.get(prev)
217 c = children.get(prev)
218 if c is not None:
218 if c is not None:
219 c.append(rev)
219 c.append(rev)
220 return children.__getitem__
220 return children.__getitem__
221
221
222 def takequicksample(self, headrevs, size):
222 def takequicksample(self, headrevs, size):
223 """takes a quick sample of size <size>
223 """takes a quick sample of size <size>
224
224
225 It is meant for initial sampling and focuses on querying heads and close
225 It is meant for initial sampling and focuses on querying heads and close
226 ancestors of heads.
226 ancestors of heads.
227
227
228 :headrevs: set of head revisions in local DAG to consider
228 :headrevs: set of head revisions in local DAG to consider
229 :size: the maximum size of the sample"""
229 :size: the maximum size of the sample"""
230 revs = self.undecided
230 revs = self.undecided
231 if len(revs) <= size:
231 if len(revs) <= size:
232 return list(revs)
232 return list(revs)
233 sample = set(self._repo.revs(b'heads(%ld)', revs))
233 sample = set(self._repo.revs(b'heads(%ld)', revs))
234
234
235 if len(sample) >= size:
235 if len(sample) >= size:
236 return _limitsample(sample, size, randomize=self.randomize)
236 return _limitsample(sample, size, randomize=self.randomize)
237
237
238 _updatesample(
238 _updatesample(
239 None, headrevs, sample, self._parentsgetter(), quicksamplesize=size
239 None, headrevs, sample, self._parentsgetter(), quicksamplesize=size
240 )
240 )
241 return sample
241 return sample
242
242
243 def takefullsample(self, headrevs, size):
243 def takefullsample(self, headrevs, size):
244 revs = self.undecided
244 revs = self.undecided
245 if len(revs) <= size:
245 if len(revs) <= size:
246 return list(revs)
246 return list(revs)
247 repo = self._repo
247 repo = self._repo
248 sample = set(repo.revs(b'heads(%ld)', revs))
248 sample = set(repo.revs(b'heads(%ld)', revs))
249 parentrevs = self._parentsgetter()
249 parentrevs = self._parentsgetter()
250
250
251 # update from heads
251 # update from heads
252 revsheads = sample.copy()
252 revsheads = sample.copy()
253 _updatesample(revs, revsheads, sample, parentrevs)
253 _updatesample(revs, revsheads, sample, parentrevs)
254
254
255 # update from roots
255 # update from roots
256 revsroots = set(repo.revs(b'roots(%ld)', revs))
256 revsroots = set(repo.revs(b'roots(%ld)', revs))
257 childrenrevs = self._childrengetter()
257 childrenrevs = self._childrengetter()
258 _updatesample(revs, revsroots, sample, childrenrevs)
258 _updatesample(revs, revsroots, sample, childrenrevs)
259 assert sample
259 assert sample
260
260
261 if not self._respectsize:
261 if not self._respectsize:
262 size = max(size, min(len(revsroots), len(revsheads)))
262 size = max(size, min(len(revsroots), len(revsheads)))
263
263
264 sample = _limitsample(sample, size, randomize=self.randomize)
264 sample = _limitsample(sample, size, randomize=self.randomize)
265 if len(sample) < size:
265 if len(sample) < size:
266 more = size - len(sample)
266 more = size - len(sample)
267 takefrom = list(revs - sample)
267 takefrom = list(revs - sample)
268 if self.randomize:
268 if self.randomize:
269 sample.update(random.sample(takefrom, more))
269 sample.update(random.sample(takefrom, more))
270 else:
270 else:
271 takefrom.sort()
271 takefrom.sort()
272 sample.update(takefrom[:more])
272 sample.update(takefrom[:more])
273 return sample
273 return sample
274
274
275
275
276 pure_partialdiscovery = partialdiscovery
276 pure_partialdiscovery = partialdiscovery
277
277
278 partialdiscovery = policy.importrust(
278 partialdiscovery = policy.importrust(
279 'discovery', member='PartialDiscovery', default=partialdiscovery
279 'discovery', member='PartialDiscovery', default=partialdiscovery
280 )
280 )
281
281
282
282
283 def findcommonheads(
283 def findcommonheads(
284 ui,
284 ui,
285 local,
285 local,
286 remote,
286 remote,
287 abortwhenunrelated=True,
287 abortwhenunrelated=True,
288 ancestorsof=None,
288 ancestorsof=None,
289 audit=None,
289 audit=None,
290 ):
290 ):
291 """Return a tuple (common, anyincoming, remoteheads) used to identify
291 """Return a tuple (common, anyincoming, remoteheads) used to identify
292 missing nodes from or in remote.
292 missing nodes from or in remote.
293
293
294 The audit argument is an optional dictionnary that a caller can pass. it
294 The audit argument is an optional dictionnary that a caller can pass. it
295 will be updated with extra data about the discovery, this is useful for
295 will be updated with extra data about the discovery, this is useful for
296 debug.
296 debug.
297 """
297 """
298
298
299 samplegrowth = float(ui.config(b'devel', b'discovery.grow-sample.rate'))
299 samplegrowth = float(ui.config(b'devel', b'discovery.grow-sample.rate'))
300
300
301 if audit is not None:
301 if audit is not None:
302 audit[b'total-queries'] = 0
302 audit[b'total-queries'] = 0
303
303
304 start = util.timer()
304 start = util.timer()
305
305
306 roundtrips = 0
306 roundtrips = 0
307 cl = local.changelog
307 cl = local.changelog
308 clnode = cl.node
308 clnode = cl.node
309 clrev = cl.rev
309 clrev = cl.rev
310
310
311 if ancestorsof is not None:
311 if ancestorsof is not None:
312 ownheads = [clrev(n) for n in ancestorsof]
312 ownheads = [clrev(n) for n in ancestorsof]
313 else:
313 else:
314 ownheads = [rev for rev in cl.headrevs() if rev != nullrev]
314 ownheads = [rev for rev in cl.headrevs() if rev != nullrev]
315
315
316 initial_head_exchange = ui.configbool(b'devel', b'discovery.exchange-heads')
316 initial_head_exchange = ui.configbool(b'devel', b'discovery.exchange-heads')
317 initialsamplesize = ui.configint(b'devel', b'discovery.sample-size.initial')
317 initialsamplesize = ui.configint(b'devel', b'discovery.sample-size.initial')
318 fullsamplesize = ui.configint(b'devel', b'discovery.sample-size')
318 fullsamplesize = ui.configint(b'devel', b'discovery.sample-size')
319 # We also ask remote about all the local heads. That set can be arbitrarily
319 # We also ask remote about all the local heads. That set can be arbitrarily
320 # large, so we used to limit it size to `initialsamplesize`. We no longer
320 # large, so we used to limit it size to `initialsamplesize`. We no longer
321 # do as it proved counter productive. The skipped heads could lead to a
321 # do as it proved counter productive. The skipped heads could lead to a
322 # large "undecided" set, slower to be clarified than if we asked the
322 # large "undecided" set, slower to be clarified than if we asked the
323 # question for all heads right away.
323 # question for all heads right away.
324 #
324 #
325 # We are already fetching all server heads using the `heads` commands,
325 # We are already fetching all server heads using the `heads` commands,
326 # sending a equivalent number of heads the other way should not have a
326 # sending a equivalent number of heads the other way should not have a
327 # significant impact. In addition, it is very likely that we are going to
327 # significant impact. In addition, it is very likely that we are going to
328 # have to issue "known" request for an equivalent amount of revisions in
328 # have to issue "known" request for an equivalent amount of revisions in
329 # order to decide if theses heads are common or missing.
329 # order to decide if theses heads are common or missing.
330 #
330 #
331 # find a detailled analysis below.
331 # find a detailled analysis below.
332 #
332 #
333 # Case A: local and server both has few heads
333 # Case A: local and server both has few heads
334 #
334 #
335 # Ownheads is below initialsamplesize, limit would not have any effect.
335 # Ownheads is below initialsamplesize, limit would not have any effect.
336 #
336 #
337 # Case B: local has few heads and server has many
337 # Case B: local has few heads and server has many
338 #
338 #
339 # Ownheads is below initialsamplesize, limit would not have any effect.
339 # Ownheads is below initialsamplesize, limit would not have any effect.
340 #
340 #
341 # Case C: local and server both has many heads
341 # Case C: local and server both has many heads
342 #
342 #
343 # We now transfert some more data, but not significantly more than is
343 # We now transfert some more data, but not significantly more than is
344 # already transfered to carry the server heads.
344 # already transfered to carry the server heads.
345 #
345 #
346 # Case D: local has many heads, server has few
346 # Case D: local has many heads, server has few
347 #
347 #
348 # D.1 local heads are mostly known remotely
348 # D.1 local heads are mostly known remotely
349 #
349 #
350 # All the known head will have be part of a `known` request at some
350 # All the known head will have be part of a `known` request at some
351 # point for the discovery to finish. Sending them all earlier is
351 # point for the discovery to finish. Sending them all earlier is
352 # actually helping.
352 # actually helping.
353 #
353 #
354 # (This case is fairly unlikely, it requires the numerous heads to all
354 # (This case is fairly unlikely, it requires the numerous heads to all
355 # be merged server side in only a few heads)
355 # be merged server side in only a few heads)
356 #
356 #
357 # D.2 local heads are mostly missing remotely
357 # D.2 local heads are mostly missing remotely
358 #
358 #
359 # To determine that the heads are missing, we'll have to issue `known`
359 # To determine that the heads are missing, we'll have to issue `known`
360 # request for them or one of their ancestors. This amount of `known`
360 # request for them or one of their ancestors. This amount of `known`
361 # request will likely be in the same order of magnitude than the amount
361 # request will likely be in the same order of magnitude than the amount
362 # of local heads.
362 # of local heads.
363 #
363 #
364 # The only case where we can be more efficient using `known` request on
364 # The only case where we can be more efficient using `known` request on
365 # ancestors are case were all the "missing" local heads are based on a
365 # ancestors are case were all the "missing" local heads are based on a
366 # few changeset, also "missing". This means we would have a "complex"
366 # few changeset, also "missing". This means we would have a "complex"
367 # graph (with many heads) attached to, but very independant to a the
367 # graph (with many heads) attached to, but very independant to a the
368 # "simple" graph on the server. This is a fairly usual case and have
368 # "simple" graph on the server. This is a fairly usual case and have
369 # not been met in the wild so far.
369 # not been met in the wild so far.
370 if initial_head_exchange:
370 if initial_head_exchange:
371 if remote.limitedarguments:
371 if remote.limitedarguments:
372 sample = _limitsample(ownheads, initialsamplesize)
372 sample = _limitsample(ownheads, initialsamplesize)
373 # indices between sample and externalized version must match
373 # indices between sample and externalized version must match
374 sample = list(sample)
374 sample = list(sample)
375 else:
375 else:
376 sample = ownheads
376 sample = ownheads
377
377
378 ui.debug(b"query 1; heads\n")
378 ui.debug(b"query 1; heads\n")
379 roundtrips += 1
379 roundtrips += 1
380 with remote.commandexecutor() as e:
380 with remote.commandexecutor() as e:
381 fheads = e.callcommand(b'heads', {})
381 fheads = e.callcommand(b'heads', {})
382 if audit is not None:
382 if audit is not None:
383 audit[b'total-queries'] += len(sample)
383 audit[b'total-queries'] += len(sample)
384 fknown = e.callcommand(
384 fknown = e.callcommand(
385 b'known',
385 b'known',
386 {
386 {
387 b'nodes': [clnode(r) for r in sample],
387 b'nodes': [clnode(r) for r in sample],
388 },
388 },
389 )
389 )
390
390
391 srvheadhashes, yesno = fheads.result(), fknown.result()
391 srvheadhashes, yesno = fheads.result(), fknown.result()
392
392
393 if audit is not None:
393 if audit is not None:
394 audit[b'total-roundtrips'] = 1
394 audit[b'total-roundtrips'] = 1
395
395
396 if cl.tiprev() == nullrev:
396 if cl.tiprev() == nullrev:
397 if srvheadhashes != [cl.nullid]:
397 if srvheadhashes != [cl.nullid]:
398 return [cl.nullid], True, srvheadhashes
398 return [cl.nullid], True, srvheadhashes
399 return [cl.nullid], False, []
399 return [cl.nullid], False, []
400 else:
400 else:
401 # we still need the remote head for the function return
401 # we still need the remote head for the function return
402 with remote.commandexecutor() as e:
402 with remote.commandexecutor() as e:
403 fheads = e.callcommand(b'heads', {})
403 fheads = e.callcommand(b'heads', {})
404 srvheadhashes = fheads.result()
404 srvheadhashes = fheads.result()
405
405
406 # start actual discovery (we note this before the next "if" for
406 # start actual discovery (we note this before the next "if" for
407 # compatibility reasons)
407 # compatibility reasons)
408 ui.status(_(b"searching for changes\n"))
408 ui.status(_(b"searching for changes\n"))
409
409
410 knownsrvheads = [] # revnos of remote heads that are known locally
410 knownsrvheads = [] # revnos of remote heads that are known locally
411 for node in srvheadhashes:
411 for node in srvheadhashes:
412 if node == cl.nullid:
412 if node == cl.nullid:
413 continue
413 continue
414
414
415 try:
415 try:
416 knownsrvheads.append(clrev(node))
416 knownsrvheads.append(clrev(node))
417 # Catches unknown and filtered nodes.
417 # Catches unknown and filtered nodes.
418 except error.LookupError:
418 except error.LookupError:
419 continue
419 continue
420
420
421 if initial_head_exchange:
421 if initial_head_exchange:
422 # early exit if we know all the specified remote heads already
422 # early exit if we know all the specified remote heads already
423 if len(knownsrvheads) == len(srvheadhashes):
423 if len(knownsrvheads) == len(srvheadhashes):
424 ui.debug(b"all remote heads known locally\n")
424 ui.debug(b"all remote heads known locally\n")
425 return srvheadhashes, False, srvheadhashes
425 return srvheadhashes, False, srvheadhashes
426
426
427 if len(sample) == len(ownheads) and all(yesno):
427 if len(sample) == len(ownheads) and all(yesno):
428 ui.note(_(b"all local changesets known remotely\n"))
428 ui.note(_(b"all local changesets known remotely\n"))
429 ownheadhashes = [clnode(r) for r in ownheads]
429 ownheadhashes = [clnode(r) for r in ownheads]
430 return ownheadhashes, True, srvheadhashes
430 return ownheadhashes, True, srvheadhashes
431
431
432 # full blown discovery
432 # full blown discovery
433
433
434 # if the server has a limit to its arguments size, we can't grow the sample.
434 # if the server has a limit to its arguments size, we can't grow the sample.
435 configbool = local.ui.configbool
435 configbool = local.ui.configbool
436 grow_sample = configbool(b'devel', b'discovery.grow-sample')
436 grow_sample = configbool(b'devel', b'discovery.grow-sample')
437 grow_sample = grow_sample and not remote.limitedarguments
437 grow_sample = grow_sample and not remote.limitedarguments
438
438
439 dynamic_sample = configbool(b'devel', b'discovery.grow-sample.dynamic')
439 dynamic_sample = configbool(b'devel', b'discovery.grow-sample.dynamic')
440 hard_limit_sample = not (dynamic_sample or remote.limitedarguments)
440 hard_limit_sample = not (dynamic_sample or remote.limitedarguments)
441
441
442 randomize = ui.configbool(b'devel', b'discovery.randomize')
442 randomize = ui.configbool(b'devel', b'discovery.randomize')
443 if cl.index.rust_ext_compat:
443 if cl.index.rust_ext_compat:
444 pd = partialdiscovery
444 pd = partialdiscovery
445 else:
445 else:
446 pd = pure_partialdiscovery
446 pd = pure_partialdiscovery
447 disco = pd(local, ownheads, hard_limit_sample, randomize=randomize)
447 disco = pd(local, ownheads, hard_limit_sample, randomize=randomize)
448 if initial_head_exchange:
448 if initial_head_exchange:
449 # treat remote heads (and maybe own heads) as a first implicit sample
449 # treat remote heads (and maybe own heads) as a first implicit sample
450 # response
450 # response
451 disco.addcommons(knownsrvheads)
451 disco.addcommons(knownsrvheads)
452 disco.addinfo(zip(sample, yesno))
452 disco.addinfo(zip(sample, yesno))
453
453
454 full = not initial_head_exchange
454 full = not initial_head_exchange
455 progress = ui.makeprogress(_(b'searching'), unit=_(b'queries'))
455 progress = ui.makeprogress(_(b'searching'), unit=_(b'queries'))
456 while not disco.iscomplete():
456 while not disco.iscomplete():
457
457
458 if full or disco.hasinfo():
458 if full or disco.hasinfo():
459 if full:
459 if full:
460 ui.note(_(b"sampling from both directions\n"))
460 ui.note(_(b"sampling from both directions\n"))
461 else:
461 else:
462 ui.debug(b"taking initial sample\n")
462 ui.debug(b"taking initial sample\n")
463 samplefunc = disco.takefullsample
463 samplefunc = disco.takefullsample
464 targetsize = fullsamplesize
464 targetsize = fullsamplesize
465 if grow_sample:
465 if grow_sample:
466 fullsamplesize = int(fullsamplesize * samplegrowth)
466 fullsamplesize = int(fullsamplesize * samplegrowth)
467 else:
467 else:
468 # use even cheaper initial sample
468 # use even cheaper initial sample
469 ui.debug(b"taking quick initial sample\n")
469 ui.debug(b"taking quick initial sample\n")
470 samplefunc = disco.takequicksample
470 samplefunc = disco.takequicksample
471 targetsize = initialsamplesize
471 targetsize = initialsamplesize
472 sample = samplefunc(ownheads, targetsize)
472 sample = samplefunc(ownheads, targetsize)
473
473
474 roundtrips += 1
474 roundtrips += 1
475 progress.update(roundtrips)
475 progress.update(roundtrips)
476 stats = disco.stats()
476 stats = disco.stats()
477 ui.debug(
477 ui.debug(
478 b"query %i; still undecided: %i, sample size is: %i\n"
478 b"query %i; still undecided: %i, sample size is: %i\n"
479 % (roundtrips, stats['undecided'], len(sample))
479 % (roundtrips, stats['undecided'], len(sample))
480 )
480 )
481
481
482 # indices between sample and externalized version must match
482 # indices between sample and externalized version must match
483 sample = list(sample)
483 sample = list(sample)
484
484
485 with remote.commandexecutor() as e:
485 with remote.commandexecutor() as e:
486 if audit is not None:
486 if audit is not None:
487 audit[b'total-queries'] += len(sample)
487 audit[b'total-queries'] += len(sample)
488 yesno = e.callcommand(
488 yesno = e.callcommand(
489 b'known',
489 b'known',
490 {
490 {
491 b'nodes': [clnode(r) for r in sample],
491 b'nodes': [clnode(r) for r in sample],
492 },
492 },
493 ).result()
493 ).result()
494
494
495 full = True
495 full = True
496
496
497 disco.addinfo(zip(sample, yesno))
497 disco.addinfo(zip(sample, yesno))
498
498
499 result = disco.commonheads()
499 result = disco.commonheads()
500 elapsed = util.timer() - start
500 elapsed = util.timer() - start
501 progress.complete()
501 progress.complete()
502 ui.debug(b"%d total queries in %.4fs\n" % (roundtrips, elapsed))
502 ui.debug(b"%d total queries in %.4fs\n" % (roundtrips, elapsed))
503 msg = (
503 msg = (
504 b'found %d common and %d unknown server heads,'
504 b'found %d common and %d unknown server heads,'
505 b' %d roundtrips in %.4fs\n'
505 b' %d roundtrips in %.4fs\n'
506 )
506 )
507 missing = set(result) - set(knownsrvheads)
507 missing = set(result) - set(knownsrvheads)
508 ui.log(b'discovery', msg, len(result), len(missing), roundtrips, elapsed)
508 ui.log(b'discovery', msg, len(result), len(missing), roundtrips, elapsed)
509
509
510 if audit is not None:
510 if audit is not None:
511 audit[b'total-roundtrips'] = roundtrips
511 audit[b'total-roundtrips'] = roundtrips
512
512
513 if not result and srvheadhashes != [cl.nullid]:
513 if not result and srvheadhashes != [cl.nullid]:
514 if abortwhenunrelated:
514 if abortwhenunrelated:
515 raise error.Abort(_(b"repository is unrelated"))
515 raise error.Abort(_(b"repository is unrelated"))
516 else:
516 else:
517 ui.warn(_(b"warning: repository is unrelated\n"))
517 ui.warn(_(b"warning: repository is unrelated\n"))
518 return (
518 return (
519 {cl.nullid},
519 {cl.nullid},
520 True,
520 True,
521 srvheadhashes,
521 srvheadhashes,
522 )
522 )
523
523
524 anyincoming = srvheadhashes != [cl.nullid]
524 anyincoming = srvheadhashes != [cl.nullid]
525 result = {clnode(r) for r in result}
525 result = {clnode(r) for r in result}
526 return result, anyincoming, srvheadhashes
526 return result, anyincoming, srvheadhashes
@@ -1,1892 +1,1900 b''
1 ============================================================================================
1 ============================================================================================
2 Test cases where there are race condition between two clients pushing to the same repository
2 Test cases where there are race condition between two clients pushing to the same repository
3 ============================================================================================
3 ============================================================================================
4
4
5 This file tests cases where two clients push to a server at the same time. The
5 This file tests cases where two clients push to a server at the same time. The
6 "raced" client is done preparing it push bundle when the "racing" client
6 "raced" client is done preparing it push bundle when the "racing" client
7 perform its push. The "raced" client starts its actual push after the "racing"
7 perform its push. The "raced" client starts its actual push after the "racing"
8 client push is fully complete.
8 client push is fully complete.
9
9
10 A set of extension and shell functions ensures this scheduling.
10 A set of extension and shell functions ensures this scheduling.
11
11
12 $ cat >> delaypush.py << EOF
12 $ cat >> delaypush.py << EOF
13 > """small extension orchestrate push race
13 > """small extension orchestrate push race
14 >
14 >
15 > Client with the extensions will create a file when ready and get stuck until
15 > Client with the extensions will create a file when ready and get stuck until
16 > a file is created."""
16 > a file is created."""
17 >
17 >
18 > import errno
18 > import errno
19 > import os
19 > import os
20 > import time
20 > import time
21 >
21 >
22 > from mercurial import (
22 > from mercurial import (
23 > exchange,
23 > exchange,
24 > extensions,
24 > extensions,
25 > registrar,
25 > registrar,
26 > )
26 > )
27 >
27 >
28 > configtable = {}
28 > configtable = {}
29 > configitem = registrar.configitem(configtable)
29 > configitem = registrar.configitem(configtable)
30 >
30 >
31 > configitem(b'delaypush', b'ready-path',
31 > configitem(b'delaypush', b'ready-path',
32 > default=None,
32 > default=None,
33 > )
33 > )
34 > configitem(b'delaypush', b'release-path',
34 > configitem(b'delaypush', b'release-path',
35 > default=None,
35 > default=None,
36 > )
36 > )
37 >
37 >
38 > def delaypush(orig, pushop):
38 > def delaypush(orig, pushop):
39 > # notify we are done preparing
39 > # notify we are done preparing
40 > ui = pushop.repo.ui
40 > ui = pushop.repo.ui
41 > readypath = ui.config(b'delaypush', b'ready-path')
41 > readypath = ui.config(b'delaypush', b'ready-path')
42 > if readypath is not None:
42 > if readypath is not None:
43 > with open(readypath, 'w') as r:
43 > with open(readypath, 'w') as r:
44 > r.write('foo')
44 > r.write('foo')
45 > ui.status(b'wrote ready: %s\n' % readypath)
45 > ui.status(b'wrote ready: %s\n' % readypath)
46 > # now wait for the other process to be done
46 > # now wait for the other process to be done
47 > watchpath = ui.config(b'delaypush', b'release-path')
47 > watchpath = ui.config(b'delaypush', b'release-path')
48 > if watchpath is not None:
48 > if watchpath is not None:
49 > ui.status(b'waiting on: %s\n' % watchpath)
49 > ui.status(b'waiting on: %s\n' % watchpath)
50 > limit = 100
50 > limit = 100
51 > test_default_timeout = os.environ.get('HGTEST_TIMEOUT_DEFAULT')
52 > test_timeout = os.environ.get('HGTEST_TIMEOUT')
53 > if (
54 > test_default_timeout is not None
55 > and test_timeout is not None
56 > and test_default_timeout < test_timeout
57 > ):
58 > limit = int(limit * (test_timeout / test_default_timeout))
51 > while 0 < limit and not os.path.exists(watchpath):
59 > while 0 < limit and not os.path.exists(watchpath):
52 > limit -= 1
60 > limit -= 1
53 > time.sleep(0.1)
61 > time.sleep(0.1)
54 > if limit <= 0:
62 > if limit <= 0:
55 > ui.warn(b'exiting without watchfile: %s' % watchpath)
63 > ui.warn(b'exiting without watchfile: %s' % watchpath)
56 > else:
64 > else:
57 > # delete the file at the end of the push
65 > # delete the file at the end of the push
58 > def delete():
66 > def delete():
59 > try:
67 > try:
60 > os.unlink(watchpath)
68 > os.unlink(watchpath)
61 > except FileNotFoundError:
69 > except FileNotFoundError:
62 > pass
70 > pass
63 > ui.atexit(delete)
71 > ui.atexit(delete)
64 > return orig(pushop)
72 > return orig(pushop)
65 >
73 >
66 > def uisetup(ui):
74 > def uisetup(ui):
67 > extensions.wrapfunction(exchange, b'_pushbundle2', delaypush)
75 > extensions.wrapfunction(exchange, b'_pushbundle2', delaypush)
68 > EOF
76 > EOF
69
77
70 $ waiton () {
78 $ waiton () {
71 > # wait for a file to be created (then delete it)
79 > # wait for a file to be created (then delete it)
72 > count=100
80 > count=100
73 > while [ ! -f $1 ] ;
81 > while [ ! -f $1 ] ;
74 > do
82 > do
75 > sleep 0.1;
83 > sleep 0.1;
76 > count=`expr $count - 1`;
84 > count=`expr $count - 1`;
77 > if [ $count -lt 0 ];
85 > if [ $count -lt 0 ];
78 > then
86 > then
79 > break
87 > break
80 > fi;
88 > fi;
81 > done
89 > done
82 > [ -f $1 ] || echo "ready file still missing: $1"
90 > [ -f $1 ] || echo "ready file still missing: $1"
83 > rm -f $1
91 > rm -f $1
84 > }
92 > }
85
93
86 $ release () {
94 $ release () {
87 > # create a file and wait for it be deleted
95 > # create a file and wait for it be deleted
88 > count=100
96 > count=100
89 > touch $1
97 > touch $1
90 > while [ -f $1 ] ;
98 > while [ -f $1 ] ;
91 > do
99 > do
92 > sleep 0.1;
100 > sleep 0.1;
93 > count=`expr $count - 1`;
101 > count=`expr $count - 1`;
94 > if [ $count -lt 0 ];
102 > if [ $count -lt 0 ];
95 > then
103 > then
96 > break
104 > break
97 > fi;
105 > fi;
98 > done
106 > done
99 > [ ! -f $1 ] || echo "delay file still exist: $1"
107 > [ ! -f $1 ] || echo "delay file still exist: $1"
100 > }
108 > }
101
109
102 $ cat >> $HGRCPATH << EOF
110 $ cat >> $HGRCPATH << EOF
103 > [ui]
111 > [ui]
104 > # simplify output
112 > # simplify output
105 > logtemplate = {node|short} {desc} ({branch})
113 > logtemplate = {node|short} {desc} ({branch})
106 > [phases]
114 > [phases]
107 > publish = no
115 > publish = no
108 > [experimental]
116 > [experimental]
109 > evolution=true
117 > evolution=true
110 > [alias]
118 > [alias]
111 > graph = log -G --rev 'sort(all(), "topo")'
119 > graph = log -G --rev 'sort(all(), "topo")'
112 > EOF
120 > EOF
113
121
114 We tests multiple cases:
122 We tests multiple cases:
115 * strict: no race detected,
123 * strict: no race detected,
116 * unrelated: race on unrelated heads are allowed.
124 * unrelated: race on unrelated heads are allowed.
117
125
118 #testcases strict unrelated
126 #testcases strict unrelated
119
127
120 #if strict
128 #if strict
121
129
122 $ cat >> $HGRCPATH << EOF
130 $ cat >> $HGRCPATH << EOF
123 > [server]
131 > [server]
124 > concurrent-push-mode = strict
132 > concurrent-push-mode = strict
125 > EOF
133 > EOF
126
134
127 #endif
135 #endif
128
136
129 Setup
137 Setup
130 -----
138 -----
131
139
132 create a repo with one root
140 create a repo with one root
133
141
134 $ hg init server
142 $ hg init server
135 $ cd server
143 $ cd server
136 $ echo root > root
144 $ echo root > root
137 $ hg ci -Am "C-ROOT"
145 $ hg ci -Am "C-ROOT"
138 adding root
146 adding root
139 $ cd ..
147 $ cd ..
140
148
141 clone it in two clients
149 clone it in two clients
142
150
143 $ hg clone ssh://user@dummy/server client-racy
151 $ hg clone ssh://user@dummy/server client-racy
144 requesting all changes
152 requesting all changes
145 adding changesets
153 adding changesets
146 adding manifests
154 adding manifests
147 adding file changes
155 adding file changes
148 added 1 changesets with 1 changes to 1 files
156 added 1 changesets with 1 changes to 1 files
149 new changesets 842e2fac6304 (1 drafts)
157 new changesets 842e2fac6304 (1 drafts)
150 updating to branch default
158 updating to branch default
151 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
159 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
152 $ hg clone ssh://user@dummy/server client-other
160 $ hg clone ssh://user@dummy/server client-other
153 requesting all changes
161 requesting all changes
154 adding changesets
162 adding changesets
155 adding manifests
163 adding manifests
156 adding file changes
164 adding file changes
157 added 1 changesets with 1 changes to 1 files
165 added 1 changesets with 1 changes to 1 files
158 new changesets 842e2fac6304 (1 drafts)
166 new changesets 842e2fac6304 (1 drafts)
159 updating to branch default
167 updating to branch default
160 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
168 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
161
169
162 setup one to allow race on push
170 setup one to allow race on push
163
171
164 $ cat >> client-racy/.hg/hgrc << EOF
172 $ cat >> client-racy/.hg/hgrc << EOF
165 > [extensions]
173 > [extensions]
166 > delaypush = $TESTTMP/delaypush.py
174 > delaypush = $TESTTMP/delaypush.py
167 > [delaypush]
175 > [delaypush]
168 > ready-path = $TESTTMP/readyfile
176 > ready-path = $TESTTMP/readyfile
169 > release-path = $TESTTMP/watchfile
177 > release-path = $TESTTMP/watchfile
170 > EOF
178 > EOF
171
179
172 Simple race, both try to push to the server at the same time
180 Simple race, both try to push to the server at the same time
173 ------------------------------------------------------------
181 ------------------------------------------------------------
174
182
175 Both try to replace the same head
183 Both try to replace the same head
176
184
177 # a
185 # a
178 # | b
186 # | b
179 # |/
187 # |/
180 # *
188 # *
181
189
182 Creating changesets
190 Creating changesets
183
191
184 $ echo b > client-other/a
192 $ echo b > client-other/a
185 $ hg -R client-other/ add client-other/a
193 $ hg -R client-other/ add client-other/a
186 $ hg -R client-other/ commit -m "C-A"
194 $ hg -R client-other/ commit -m "C-A"
187 $ echo b > client-racy/b
195 $ echo b > client-racy/b
188 $ hg -R client-racy/ add client-racy/b
196 $ hg -R client-racy/ add client-racy/b
189 $ hg -R client-racy/ commit -m "C-B"
197 $ hg -R client-racy/ commit -m "C-B"
190
198
191 Pushing
199 Pushing
192
200
193 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
201 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
194
202
195 $ waiton $TESTTMP/readyfile
203 $ waiton $TESTTMP/readyfile
196
204
197 $ hg -R client-other push -r 'tip'
205 $ hg -R client-other push -r 'tip'
198 pushing to ssh://user@dummy/server
206 pushing to ssh://user@dummy/server
199 searching for changes
207 searching for changes
200 remote: adding changesets
208 remote: adding changesets
201 remote: adding manifests
209 remote: adding manifests
202 remote: adding file changes
210 remote: adding file changes
203 remote: added 1 changesets with 1 changes to 1 files
211 remote: added 1 changesets with 1 changes to 1 files
204
212
205 $ release $TESTTMP/watchfile
213 $ release $TESTTMP/watchfile
206
214
207 Check the result of the push
215 Check the result of the push
208
216
209 $ cat ./push-log
217 $ cat ./push-log
210 pushing to ssh://user@dummy/server
218 pushing to ssh://user@dummy/server
211 searching for changes
219 searching for changes
212 wrote ready: $TESTTMP/readyfile
220 wrote ready: $TESTTMP/readyfile
213 waiting on: $TESTTMP/watchfile
221 waiting on: $TESTTMP/watchfile
214 abort: push failed:
222 abort: push failed:
215 'remote repository changed while pushing - please try again'
223 'remote repository changed while pushing - please try again'
216
224
217 $ hg -R server graph
225 $ hg -R server graph
218 o 98217d5a1659 C-A (default)
226 o 98217d5a1659 C-A (default)
219 |
227 |
220 @ 842e2fac6304 C-ROOT (default)
228 @ 842e2fac6304 C-ROOT (default)
221
229
222
230
223 Pushing on two different heads
231 Pushing on two different heads
224 ------------------------------
232 ------------------------------
225
233
226 Both try to replace a different head
234 Both try to replace a different head
227
235
228 # a b
236 # a b
229 # | |
237 # | |
230 # * *
238 # * *
231 # |/
239 # |/
232 # *
240 # *
233
241
234 (resync-all)
242 (resync-all)
235
243
236 $ hg -R ./server pull ./client-racy
244 $ hg -R ./server pull ./client-racy
237 pulling from ./client-racy
245 pulling from ./client-racy
238 searching for changes
246 searching for changes
239 adding changesets
247 adding changesets
240 adding manifests
248 adding manifests
241 adding file changes
249 adding file changes
242 added 1 changesets with 1 changes to 1 files (+1 heads)
250 added 1 changesets with 1 changes to 1 files (+1 heads)
243 new changesets a9149a1428e2 (1 drafts)
251 new changesets a9149a1428e2 (1 drafts)
244 (run 'hg heads' to see heads, 'hg merge' to merge)
252 (run 'hg heads' to see heads, 'hg merge' to merge)
245 $ hg -R ./client-other pull
253 $ hg -R ./client-other pull
246 pulling from ssh://user@dummy/server
254 pulling from ssh://user@dummy/server
247 searching for changes
255 searching for changes
248 adding changesets
256 adding changesets
249 adding manifests
257 adding manifests
250 adding file changes
258 adding file changes
251 added 1 changesets with 1 changes to 1 files (+1 heads)
259 added 1 changesets with 1 changes to 1 files (+1 heads)
252 new changesets a9149a1428e2 (1 drafts)
260 new changesets a9149a1428e2 (1 drafts)
253 (run 'hg heads' to see heads, 'hg merge' to merge)
261 (run 'hg heads' to see heads, 'hg merge' to merge)
254 $ hg -R ./client-racy pull
262 $ hg -R ./client-racy pull
255 pulling from ssh://user@dummy/server
263 pulling from ssh://user@dummy/server
256 searching for changes
264 searching for changes
257 adding changesets
265 adding changesets
258 adding manifests
266 adding manifests
259 adding file changes
267 adding file changes
260 added 1 changesets with 1 changes to 1 files (+1 heads)
268 added 1 changesets with 1 changes to 1 files (+1 heads)
261 new changesets 98217d5a1659 (1 drafts)
269 new changesets 98217d5a1659 (1 drafts)
262 (run 'hg heads' to see heads, 'hg merge' to merge)
270 (run 'hg heads' to see heads, 'hg merge' to merge)
263
271
264 $ hg -R server graph
272 $ hg -R server graph
265 o a9149a1428e2 C-B (default)
273 o a9149a1428e2 C-B (default)
266 |
274 |
267 | o 98217d5a1659 C-A (default)
275 | o 98217d5a1659 C-A (default)
268 |/
276 |/
269 @ 842e2fac6304 C-ROOT (default)
277 @ 842e2fac6304 C-ROOT (default)
270
278
271
279
272 Creating changesets
280 Creating changesets
273
281
274 $ echo aa >> client-other/a
282 $ echo aa >> client-other/a
275 $ hg -R client-other/ commit -m "C-C"
283 $ hg -R client-other/ commit -m "C-C"
276 $ echo bb >> client-racy/b
284 $ echo bb >> client-racy/b
277 $ hg -R client-racy/ commit -m "C-D"
285 $ hg -R client-racy/ commit -m "C-D"
278
286
279 Pushing
287 Pushing
280
288
281 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
289 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
282
290
283 $ waiton $TESTTMP/readyfile
291 $ waiton $TESTTMP/readyfile
284
292
285 $ hg -R client-other push -r 'tip'
293 $ hg -R client-other push -r 'tip'
286 pushing to ssh://user@dummy/server
294 pushing to ssh://user@dummy/server
287 searching for changes
295 searching for changes
288 remote: adding changesets
296 remote: adding changesets
289 remote: adding manifests
297 remote: adding manifests
290 remote: adding file changes
298 remote: adding file changes
291 remote: added 1 changesets with 1 changes to 1 files
299 remote: added 1 changesets with 1 changes to 1 files
292
300
293 $ release $TESTTMP/watchfile
301 $ release $TESTTMP/watchfile
294
302
295 Check the result of the push
303 Check the result of the push
296
304
297 #if strict
305 #if strict
298 $ cat ./push-log
306 $ cat ./push-log
299 pushing to ssh://user@dummy/server
307 pushing to ssh://user@dummy/server
300 searching for changes
308 searching for changes
301 wrote ready: $TESTTMP/readyfile
309 wrote ready: $TESTTMP/readyfile
302 waiting on: $TESTTMP/watchfile
310 waiting on: $TESTTMP/watchfile
303 abort: push failed:
311 abort: push failed:
304 'remote repository changed while pushing - please try again'
312 'remote repository changed while pushing - please try again'
305
313
306 $ hg -R server graph
314 $ hg -R server graph
307 o 51c544a58128 C-C (default)
315 o 51c544a58128 C-C (default)
308 |
316 |
309 o 98217d5a1659 C-A (default)
317 o 98217d5a1659 C-A (default)
310 |
318 |
311 | o a9149a1428e2 C-B (default)
319 | o a9149a1428e2 C-B (default)
312 |/
320 |/
313 @ 842e2fac6304 C-ROOT (default)
321 @ 842e2fac6304 C-ROOT (default)
314
322
315 #endif
323 #endif
316 #if unrelated
324 #if unrelated
317
325
318 (The two heads are unrelated, push should be allowed)
326 (The two heads are unrelated, push should be allowed)
319
327
320 $ cat ./push-log
328 $ cat ./push-log
321 pushing to ssh://user@dummy/server
329 pushing to ssh://user@dummy/server
322 searching for changes
330 searching for changes
323 wrote ready: $TESTTMP/readyfile
331 wrote ready: $TESTTMP/readyfile
324 waiting on: $TESTTMP/watchfile
332 waiting on: $TESTTMP/watchfile
325 remote: adding changesets
333 remote: adding changesets
326 remote: adding manifests
334 remote: adding manifests
327 remote: adding file changes
335 remote: adding file changes
328 remote: added 1 changesets with 1 changes to 1 files
336 remote: added 1 changesets with 1 changes to 1 files
329
337
330 $ hg -R server graph
338 $ hg -R server graph
331 o 59e76faf78bd C-D (default)
339 o 59e76faf78bd C-D (default)
332 |
340 |
333 o a9149a1428e2 C-B (default)
341 o a9149a1428e2 C-B (default)
334 |
342 |
335 | o 51c544a58128 C-C (default)
343 | o 51c544a58128 C-C (default)
336 | |
344 | |
337 | o 98217d5a1659 C-A (default)
345 | o 98217d5a1659 C-A (default)
338 |/
346 |/
339 @ 842e2fac6304 C-ROOT (default)
347 @ 842e2fac6304 C-ROOT (default)
340
348
341 #endif
349 #endif
342
350
343 Pushing while someone creates a new head
351 Pushing while someone creates a new head
344 -----------------------------------------
352 -----------------------------------------
345
353
346 Pushing a new changeset while someone creates a new branch.
354 Pushing a new changeset while someone creates a new branch.
347
355
348 # a (raced)
356 # a (raced)
349 # |
357 # |
350 # * b
358 # * b
351 # |/
359 # |/
352 # *
360 # *
353
361
354 (resync-all)
362 (resync-all)
355
363
356 #if strict
364 #if strict
357
365
358 $ hg -R ./server pull ./client-racy
366 $ hg -R ./server pull ./client-racy
359 pulling from ./client-racy
367 pulling from ./client-racy
360 searching for changes
368 searching for changes
361 adding changesets
369 adding changesets
362 adding manifests
370 adding manifests
363 adding file changes
371 adding file changes
364 added 1 changesets with 1 changes to 1 files
372 added 1 changesets with 1 changes to 1 files
365 new changesets 59e76faf78bd (1 drafts)
373 new changesets 59e76faf78bd (1 drafts)
366 (run 'hg update' to get a working copy)
374 (run 'hg update' to get a working copy)
367
375
368 #endif
376 #endif
369 #if unrelated
377 #if unrelated
370
378
371 $ hg -R ./server pull ./client-racy
379 $ hg -R ./server pull ./client-racy
372 pulling from ./client-racy
380 pulling from ./client-racy
373 searching for changes
381 searching for changes
374 no changes found
382 no changes found
375
383
376 #endif
384 #endif
377
385
378 $ hg -R ./client-other pull
386 $ hg -R ./client-other pull
379 pulling from ssh://user@dummy/server
387 pulling from ssh://user@dummy/server
380 searching for changes
388 searching for changes
381 adding changesets
389 adding changesets
382 adding manifests
390 adding manifests
383 adding file changes
391 adding file changes
384 added 1 changesets with 1 changes to 1 files
392 added 1 changesets with 1 changes to 1 files
385 new changesets 59e76faf78bd (1 drafts)
393 new changesets 59e76faf78bd (1 drafts)
386 (run 'hg update' to get a working copy)
394 (run 'hg update' to get a working copy)
387 $ hg -R ./client-racy pull
395 $ hg -R ./client-racy pull
388 pulling from ssh://user@dummy/server
396 pulling from ssh://user@dummy/server
389 searching for changes
397 searching for changes
390 adding changesets
398 adding changesets
391 adding manifests
399 adding manifests
392 adding file changes
400 adding file changes
393 added 1 changesets with 1 changes to 1 files
401 added 1 changesets with 1 changes to 1 files
394 new changesets 51c544a58128 (1 drafts)
402 new changesets 51c544a58128 (1 drafts)
395 (run 'hg update' to get a working copy)
403 (run 'hg update' to get a working copy)
396
404
397 $ hg -R server graph
405 $ hg -R server graph
398 o 59e76faf78bd C-D (default)
406 o 59e76faf78bd C-D (default)
399 |
407 |
400 o a9149a1428e2 C-B (default)
408 o a9149a1428e2 C-B (default)
401 |
409 |
402 | o 51c544a58128 C-C (default)
410 | o 51c544a58128 C-C (default)
403 | |
411 | |
404 | o 98217d5a1659 C-A (default)
412 | o 98217d5a1659 C-A (default)
405 |/
413 |/
406 @ 842e2fac6304 C-ROOT (default)
414 @ 842e2fac6304 C-ROOT (default)
407
415
408
416
409 Creating changesets
417 Creating changesets
410
418
411 (new head)
419 (new head)
412
420
413 $ hg -R client-other/ up 'desc("C-A")'
421 $ hg -R client-other/ up 'desc("C-A")'
414 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
422 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
415 $ echo aaa >> client-other/a
423 $ echo aaa >> client-other/a
416 $ hg -R client-other/ commit -m "C-E"
424 $ hg -R client-other/ commit -m "C-E"
417 created new head
425 created new head
418
426
419 (children of existing head)
427 (children of existing head)
420
428
421 $ hg -R client-racy/ up 'desc("C-C")'
429 $ hg -R client-racy/ up 'desc("C-C")'
422 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
430 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
423 $ echo bbb >> client-racy/a
431 $ echo bbb >> client-racy/a
424 $ hg -R client-racy/ commit -m "C-F"
432 $ hg -R client-racy/ commit -m "C-F"
425
433
426 Pushing
434 Pushing
427
435
428 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
436 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
429
437
430 $ waiton $TESTTMP/readyfile
438 $ waiton $TESTTMP/readyfile
431
439
432 $ hg -R client-other push -fr 'tip'
440 $ hg -R client-other push -fr 'tip'
433 pushing to ssh://user@dummy/server
441 pushing to ssh://user@dummy/server
434 searching for changes
442 searching for changes
435 remote: adding changesets
443 remote: adding changesets
436 remote: adding manifests
444 remote: adding manifests
437 remote: adding file changes
445 remote: adding file changes
438 remote: added 1 changesets with 1 changes to 1 files (+1 heads)
446 remote: added 1 changesets with 1 changes to 1 files (+1 heads)
439
447
440 $ release $TESTTMP/watchfile
448 $ release $TESTTMP/watchfile
441
449
442 Check the result of the push
450 Check the result of the push
443
451
444 #if strict
452 #if strict
445
453
446 $ cat ./push-log
454 $ cat ./push-log
447 pushing to ssh://user@dummy/server
455 pushing to ssh://user@dummy/server
448 searching for changes
456 searching for changes
449 wrote ready: $TESTTMP/readyfile
457 wrote ready: $TESTTMP/readyfile
450 waiting on: $TESTTMP/watchfile
458 waiting on: $TESTTMP/watchfile
451 abort: push failed:
459 abort: push failed:
452 'remote repository changed while pushing - please try again'
460 'remote repository changed while pushing - please try again'
453
461
454 $ hg -R server graph
462 $ hg -R server graph
455 o d603e2c0cdd7 C-E (default)
463 o d603e2c0cdd7 C-E (default)
456 |
464 |
457 | o 51c544a58128 C-C (default)
465 | o 51c544a58128 C-C (default)
458 |/
466 |/
459 o 98217d5a1659 C-A (default)
467 o 98217d5a1659 C-A (default)
460 |
468 |
461 | o 59e76faf78bd C-D (default)
469 | o 59e76faf78bd C-D (default)
462 | |
470 | |
463 | o a9149a1428e2 C-B (default)
471 | o a9149a1428e2 C-B (default)
464 |/
472 |/
465 @ 842e2fac6304 C-ROOT (default)
473 @ 842e2fac6304 C-ROOT (default)
466
474
467
475
468 #endif
476 #endif
469
477
470 #if unrelated
478 #if unrelated
471
479
472 (The racing new head do not affect existing heads, push should go through)
480 (The racing new head do not affect existing heads, push should go through)
473
481
474 $ cat ./push-log
482 $ cat ./push-log
475 pushing to ssh://user@dummy/server
483 pushing to ssh://user@dummy/server
476 searching for changes
484 searching for changes
477 wrote ready: $TESTTMP/readyfile
485 wrote ready: $TESTTMP/readyfile
478 waiting on: $TESTTMP/watchfile
486 waiting on: $TESTTMP/watchfile
479 remote: adding changesets
487 remote: adding changesets
480 remote: adding manifests
488 remote: adding manifests
481 remote: adding file changes
489 remote: adding file changes
482 remote: added 1 changesets with 1 changes to 1 files
490 remote: added 1 changesets with 1 changes to 1 files
483
491
484 $ hg -R server graph
492 $ hg -R server graph
485 o d9e379a8c432 C-F (default)
493 o d9e379a8c432 C-F (default)
486 |
494 |
487 o 51c544a58128 C-C (default)
495 o 51c544a58128 C-C (default)
488 |
496 |
489 | o d603e2c0cdd7 C-E (default)
497 | o d603e2c0cdd7 C-E (default)
490 |/
498 |/
491 o 98217d5a1659 C-A (default)
499 o 98217d5a1659 C-A (default)
492 |
500 |
493 | o 59e76faf78bd C-D (default)
501 | o 59e76faf78bd C-D (default)
494 | |
502 | |
495 | o a9149a1428e2 C-B (default)
503 | o a9149a1428e2 C-B (default)
496 |/
504 |/
497 @ 842e2fac6304 C-ROOT (default)
505 @ 842e2fac6304 C-ROOT (default)
498
506
499 #endif
507 #endif
500
508
501 Pushing touching different named branch (same topo): new branch raced
509 Pushing touching different named branch (same topo): new branch raced
502 ---------------------------------------------------------------------
510 ---------------------------------------------------------------------
503
511
504 Pushing two children on the same head, one is a different named branch
512 Pushing two children on the same head, one is a different named branch
505
513
506 # a (raced, branch-a)
514 # a (raced, branch-a)
507 # |
515 # |
508 # | b (default branch)
516 # | b (default branch)
509 # |/
517 # |/
510 # *
518 # *
511
519
512 (resync-all)
520 (resync-all)
513
521
514 #if strict
522 #if strict
515
523
516 $ hg -R ./server pull ./client-racy
524 $ hg -R ./server pull ./client-racy
517 pulling from ./client-racy
525 pulling from ./client-racy
518 searching for changes
526 searching for changes
519 adding changesets
527 adding changesets
520 adding manifests
528 adding manifests
521 adding file changes
529 adding file changes
522 added 1 changesets with 1 changes to 1 files
530 added 1 changesets with 1 changes to 1 files
523 new changesets d9e379a8c432 (1 drafts)
531 new changesets d9e379a8c432 (1 drafts)
524 (run 'hg update' to get a working copy)
532 (run 'hg update' to get a working copy)
525
533
526 #endif
534 #endif
527 #if unrelated
535 #if unrelated
528
536
529 $ hg -R ./server pull ./client-racy
537 $ hg -R ./server pull ./client-racy
530 pulling from ./client-racy
538 pulling from ./client-racy
531 searching for changes
539 searching for changes
532 no changes found
540 no changes found
533
541
534 #endif
542 #endif
535
543
536 $ hg -R ./client-other pull
544 $ hg -R ./client-other pull
537 pulling from ssh://user@dummy/server
545 pulling from ssh://user@dummy/server
538 searching for changes
546 searching for changes
539 adding changesets
547 adding changesets
540 adding manifests
548 adding manifests
541 adding file changes
549 adding file changes
542 added 1 changesets with 1 changes to 1 files
550 added 1 changesets with 1 changes to 1 files
543 new changesets d9e379a8c432 (1 drafts)
551 new changesets d9e379a8c432 (1 drafts)
544 (run 'hg update' to get a working copy)
552 (run 'hg update' to get a working copy)
545 $ hg -R ./client-racy pull
553 $ hg -R ./client-racy pull
546 pulling from ssh://user@dummy/server
554 pulling from ssh://user@dummy/server
547 searching for changes
555 searching for changes
548 adding changesets
556 adding changesets
549 adding manifests
557 adding manifests
550 adding file changes
558 adding file changes
551 added 1 changesets with 1 changes to 1 files (+1 heads)
559 added 1 changesets with 1 changes to 1 files (+1 heads)
552 new changesets d603e2c0cdd7 (1 drafts)
560 new changesets d603e2c0cdd7 (1 drafts)
553 (run 'hg heads .' to see heads, 'hg merge' to merge)
561 (run 'hg heads .' to see heads, 'hg merge' to merge)
554
562
555 $ hg -R server graph
563 $ hg -R server graph
556 o d9e379a8c432 C-F (default)
564 o d9e379a8c432 C-F (default)
557 |
565 |
558 o 51c544a58128 C-C (default)
566 o 51c544a58128 C-C (default)
559 |
567 |
560 | o d603e2c0cdd7 C-E (default)
568 | o d603e2c0cdd7 C-E (default)
561 |/
569 |/
562 o 98217d5a1659 C-A (default)
570 o 98217d5a1659 C-A (default)
563 |
571 |
564 | o 59e76faf78bd C-D (default)
572 | o 59e76faf78bd C-D (default)
565 | |
573 | |
566 | o a9149a1428e2 C-B (default)
574 | o a9149a1428e2 C-B (default)
567 |/
575 |/
568 @ 842e2fac6304 C-ROOT (default)
576 @ 842e2fac6304 C-ROOT (default)
569
577
570
578
571 Creating changesets
579 Creating changesets
572
580
573 (update existing head)
581 (update existing head)
574
582
575 $ hg -R client-other/ up 'desc("C-F")'
583 $ hg -R client-other/ up 'desc("C-F")'
576 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
584 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
577 $ echo aaa >> client-other/a
585 $ echo aaa >> client-other/a
578 $ hg -R client-other/ commit -m "C-G"
586 $ hg -R client-other/ commit -m "C-G"
579
587
580 (new named branch from that existing head)
588 (new named branch from that existing head)
581
589
582 $ hg -R client-racy/ up 'desc("C-F")'
590 $ hg -R client-racy/ up 'desc("C-F")'
583 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
591 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
584 $ echo bbb >> client-racy/a
592 $ echo bbb >> client-racy/a
585 $ hg -R client-racy/ branch my-first-test-branch
593 $ hg -R client-racy/ branch my-first-test-branch
586 marked working directory as branch my-first-test-branch
594 marked working directory as branch my-first-test-branch
587 (branches are permanent and global, did you want a bookmark?)
595 (branches are permanent and global, did you want a bookmark?)
588 $ hg -R client-racy/ commit -m "C-H"
596 $ hg -R client-racy/ commit -m "C-H"
589
597
590 Pushing
598 Pushing
591
599
592 $ hg -R client-racy push -r 'tip' --new-branch > ./push-log 2>&1 &
600 $ hg -R client-racy push -r 'tip' --new-branch > ./push-log 2>&1 &
593
601
594 $ waiton $TESTTMP/readyfile
602 $ waiton $TESTTMP/readyfile
595
603
596 $ hg -R client-other push -fr 'tip'
604 $ hg -R client-other push -fr 'tip'
597 pushing to ssh://user@dummy/server
605 pushing to ssh://user@dummy/server
598 searching for changes
606 searching for changes
599 remote: adding changesets
607 remote: adding changesets
600 remote: adding manifests
608 remote: adding manifests
601 remote: adding file changes
609 remote: adding file changes
602 remote: added 1 changesets with 1 changes to 1 files
610 remote: added 1 changesets with 1 changes to 1 files
603
611
604 $ release $TESTTMP/watchfile
612 $ release $TESTTMP/watchfile
605
613
606 Check the result of the push
614 Check the result of the push
607
615
608 #if strict
616 #if strict
609 $ cat ./push-log
617 $ cat ./push-log
610 pushing to ssh://user@dummy/server
618 pushing to ssh://user@dummy/server
611 searching for changes
619 searching for changes
612 wrote ready: $TESTTMP/readyfile
620 wrote ready: $TESTTMP/readyfile
613 waiting on: $TESTTMP/watchfile
621 waiting on: $TESTTMP/watchfile
614 abort: push failed:
622 abort: push failed:
615 'remote repository changed while pushing - please try again'
623 'remote repository changed while pushing - please try again'
616
624
617 $ hg -R server graph
625 $ hg -R server graph
618 o 75d69cba5402 C-G (default)
626 o 75d69cba5402 C-G (default)
619 |
627 |
620 o d9e379a8c432 C-F (default)
628 o d9e379a8c432 C-F (default)
621 |
629 |
622 o 51c544a58128 C-C (default)
630 o 51c544a58128 C-C (default)
623 |
631 |
624 | o d603e2c0cdd7 C-E (default)
632 | o d603e2c0cdd7 C-E (default)
625 |/
633 |/
626 o 98217d5a1659 C-A (default)
634 o 98217d5a1659 C-A (default)
627 |
635 |
628 | o 59e76faf78bd C-D (default)
636 | o 59e76faf78bd C-D (default)
629 | |
637 | |
630 | o a9149a1428e2 C-B (default)
638 | o a9149a1428e2 C-B (default)
631 |/
639 |/
632 @ 842e2fac6304 C-ROOT (default)
640 @ 842e2fac6304 C-ROOT (default)
633
641
634 #endif
642 #endif
635 #if unrelated
643 #if unrelated
636
644
637 (unrelated named branches are unrelated)
645 (unrelated named branches are unrelated)
638
646
639 $ cat ./push-log
647 $ cat ./push-log
640 pushing to ssh://user@dummy/server
648 pushing to ssh://user@dummy/server
641 searching for changes
649 searching for changes
642 wrote ready: $TESTTMP/readyfile
650 wrote ready: $TESTTMP/readyfile
643 waiting on: $TESTTMP/watchfile
651 waiting on: $TESTTMP/watchfile
644 remote: adding changesets
652 remote: adding changesets
645 remote: adding manifests
653 remote: adding manifests
646 remote: adding file changes
654 remote: adding file changes
647 remote: added 1 changesets with 1 changes to 1 files (+1 heads)
655 remote: added 1 changesets with 1 changes to 1 files (+1 heads)
648
656
649 $ hg -R server graph
657 $ hg -R server graph
650 o 833be552cfe6 C-H (my-first-test-branch)
658 o 833be552cfe6 C-H (my-first-test-branch)
651 |
659 |
652 | o 75d69cba5402 C-G (default)
660 | o 75d69cba5402 C-G (default)
653 |/
661 |/
654 o d9e379a8c432 C-F (default)
662 o d9e379a8c432 C-F (default)
655 |
663 |
656 o 51c544a58128 C-C (default)
664 o 51c544a58128 C-C (default)
657 |
665 |
658 | o d603e2c0cdd7 C-E (default)
666 | o d603e2c0cdd7 C-E (default)
659 |/
667 |/
660 o 98217d5a1659 C-A (default)
668 o 98217d5a1659 C-A (default)
661 |
669 |
662 | o 59e76faf78bd C-D (default)
670 | o 59e76faf78bd C-D (default)
663 | |
671 | |
664 | o a9149a1428e2 C-B (default)
672 | o a9149a1428e2 C-B (default)
665 |/
673 |/
666 @ 842e2fac6304 C-ROOT (default)
674 @ 842e2fac6304 C-ROOT (default)
667
675
668 #endif
676 #endif
669
677
670 The racing new head do not affect existing heads, push should go through
678 The racing new head do not affect existing heads, push should go through
671
679
672 pushing touching different named branch (same topo): old branch raced
680 pushing touching different named branch (same topo): old branch raced
673 ---------------------------------------------------------------------
681 ---------------------------------------------------------------------
674
682
675 Pushing two children on the same head, one is a different named branch
683 Pushing two children on the same head, one is a different named branch
676
684
677 # a (raced, default-branch)
685 # a (raced, default-branch)
678 # |
686 # |
679 # | b (new branch)
687 # | b (new branch)
680 # |/
688 # |/
681 # * (default-branch)
689 # * (default-branch)
682
690
683 (resync-all)
691 (resync-all)
684
692
685 #if strict
693 #if strict
686
694
687 $ hg -R ./server pull ./client-racy
695 $ hg -R ./server pull ./client-racy
688 pulling from ./client-racy
696 pulling from ./client-racy
689 searching for changes
697 searching for changes
690 adding changesets
698 adding changesets
691 adding manifests
699 adding manifests
692 adding file changes
700 adding file changes
693 added 1 changesets with 1 changes to 1 files (+1 heads)
701 added 1 changesets with 1 changes to 1 files (+1 heads)
694 new changesets 833be552cfe6 (1 drafts)
702 new changesets 833be552cfe6 (1 drafts)
695 (run 'hg heads .' to see heads, 'hg merge' to merge)
703 (run 'hg heads .' to see heads, 'hg merge' to merge)
696
704
697 #endif
705 #endif
698 #if unrelated
706 #if unrelated
699
707
700 $ hg -R ./server pull ./client-racy
708 $ hg -R ./server pull ./client-racy
701 pulling from ./client-racy
709 pulling from ./client-racy
702 searching for changes
710 searching for changes
703 no changes found
711 no changes found
704
712
705 #endif
713 #endif
706
714
707 $ hg -R ./client-other pull
715 $ hg -R ./client-other pull
708 pulling from ssh://user@dummy/server
716 pulling from ssh://user@dummy/server
709 searching for changes
717 searching for changes
710 adding changesets
718 adding changesets
711 adding manifests
719 adding manifests
712 adding file changes
720 adding file changes
713 added 1 changesets with 1 changes to 1 files (+1 heads)
721 added 1 changesets with 1 changes to 1 files (+1 heads)
714 new changesets 833be552cfe6 (1 drafts)
722 new changesets 833be552cfe6 (1 drafts)
715 (run 'hg heads .' to see heads, 'hg merge' to merge)
723 (run 'hg heads .' to see heads, 'hg merge' to merge)
716 $ hg -R ./client-racy pull
724 $ hg -R ./client-racy pull
717 pulling from ssh://user@dummy/server
725 pulling from ssh://user@dummy/server
718 searching for changes
726 searching for changes
719 adding changesets
727 adding changesets
720 adding manifests
728 adding manifests
721 adding file changes
729 adding file changes
722 added 1 changesets with 1 changes to 1 files (+1 heads)
730 added 1 changesets with 1 changes to 1 files (+1 heads)
723 new changesets 75d69cba5402 (1 drafts)
731 new changesets 75d69cba5402 (1 drafts)
724 (run 'hg heads' to see heads)
732 (run 'hg heads' to see heads)
725
733
726 $ hg -R server graph
734 $ hg -R server graph
727 o 833be552cfe6 C-H (my-first-test-branch)
735 o 833be552cfe6 C-H (my-first-test-branch)
728 |
736 |
729 | o 75d69cba5402 C-G (default)
737 | o 75d69cba5402 C-G (default)
730 |/
738 |/
731 o d9e379a8c432 C-F (default)
739 o d9e379a8c432 C-F (default)
732 |
740 |
733 o 51c544a58128 C-C (default)
741 o 51c544a58128 C-C (default)
734 |
742 |
735 | o d603e2c0cdd7 C-E (default)
743 | o d603e2c0cdd7 C-E (default)
736 |/
744 |/
737 o 98217d5a1659 C-A (default)
745 o 98217d5a1659 C-A (default)
738 |
746 |
739 | o 59e76faf78bd C-D (default)
747 | o 59e76faf78bd C-D (default)
740 | |
748 | |
741 | o a9149a1428e2 C-B (default)
749 | o a9149a1428e2 C-B (default)
742 |/
750 |/
743 @ 842e2fac6304 C-ROOT (default)
751 @ 842e2fac6304 C-ROOT (default)
744
752
745
753
746 Creating changesets
754 Creating changesets
747
755
748 (new named branch from one head)
756 (new named branch from one head)
749
757
750 $ hg -R client-other/ up 'desc("C-G")'
758 $ hg -R client-other/ up 'desc("C-G")'
751 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
759 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
752 $ echo aaa >> client-other/a
760 $ echo aaa >> client-other/a
753 $ hg -R client-other/ branch my-second-test-branch
761 $ hg -R client-other/ branch my-second-test-branch
754 marked working directory as branch my-second-test-branch
762 marked working directory as branch my-second-test-branch
755 $ hg -R client-other/ commit -m "C-I"
763 $ hg -R client-other/ commit -m "C-I"
756
764
757 (children "updating" that same head)
765 (children "updating" that same head)
758
766
759 $ hg -R client-racy/ up 'desc("C-G")'
767 $ hg -R client-racy/ up 'desc("C-G")'
760 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
768 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
761 $ echo bbb >> client-racy/a
769 $ echo bbb >> client-racy/a
762 $ hg -R client-racy/ commit -m "C-J"
770 $ hg -R client-racy/ commit -m "C-J"
763
771
764 Pushing
772 Pushing
765
773
766 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
774 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
767
775
768 $ waiton $TESTTMP/readyfile
776 $ waiton $TESTTMP/readyfile
769
777
770 $ hg -R client-other push -fr 'tip' --new-branch
778 $ hg -R client-other push -fr 'tip' --new-branch
771 pushing to ssh://user@dummy/server
779 pushing to ssh://user@dummy/server
772 searching for changes
780 searching for changes
773 remote: adding changesets
781 remote: adding changesets
774 remote: adding manifests
782 remote: adding manifests
775 remote: adding file changes
783 remote: adding file changes
776 remote: added 1 changesets with 1 changes to 1 files
784 remote: added 1 changesets with 1 changes to 1 files
777
785
778 $ release $TESTTMP/watchfile
786 $ release $TESTTMP/watchfile
779
787
780 Check the result of the push
788 Check the result of the push
781
789
782 #if strict
790 #if strict
783
791
784 $ cat ./push-log
792 $ cat ./push-log
785 pushing to ssh://user@dummy/server
793 pushing to ssh://user@dummy/server
786 searching for changes
794 searching for changes
787 wrote ready: $TESTTMP/readyfile
795 wrote ready: $TESTTMP/readyfile
788 waiting on: $TESTTMP/watchfile
796 waiting on: $TESTTMP/watchfile
789 abort: push failed:
797 abort: push failed:
790 'remote repository changed while pushing - please try again'
798 'remote repository changed while pushing - please try again'
791
799
792 $ hg -R server graph
800 $ hg -R server graph
793 o b35ed749f288 C-I (my-second-test-branch)
801 o b35ed749f288 C-I (my-second-test-branch)
794 |
802 |
795 o 75d69cba5402 C-G (default)
803 o 75d69cba5402 C-G (default)
796 |
804 |
797 | o 833be552cfe6 C-H (my-first-test-branch)
805 | o 833be552cfe6 C-H (my-first-test-branch)
798 |/
806 |/
799 o d9e379a8c432 C-F (default)
807 o d9e379a8c432 C-F (default)
800 |
808 |
801 o 51c544a58128 C-C (default)
809 o 51c544a58128 C-C (default)
802 |
810 |
803 | o d603e2c0cdd7 C-E (default)
811 | o d603e2c0cdd7 C-E (default)
804 |/
812 |/
805 o 98217d5a1659 C-A (default)
813 o 98217d5a1659 C-A (default)
806 |
814 |
807 | o 59e76faf78bd C-D (default)
815 | o 59e76faf78bd C-D (default)
808 | |
816 | |
809 | o a9149a1428e2 C-B (default)
817 | o a9149a1428e2 C-B (default)
810 |/
818 |/
811 @ 842e2fac6304 C-ROOT (default)
819 @ 842e2fac6304 C-ROOT (default)
812
820
813
821
814 #endif
822 #endif
815
823
816 #if unrelated
824 #if unrelated
817
825
818 (unrelated named branches are unrelated)
826 (unrelated named branches are unrelated)
819
827
820 $ cat ./push-log
828 $ cat ./push-log
821 pushing to ssh://user@dummy/server
829 pushing to ssh://user@dummy/server
822 searching for changes
830 searching for changes
823 wrote ready: $TESTTMP/readyfile
831 wrote ready: $TESTTMP/readyfile
824 waiting on: $TESTTMP/watchfile
832 waiting on: $TESTTMP/watchfile
825 remote: adding changesets
833 remote: adding changesets
826 remote: adding manifests
834 remote: adding manifests
827 remote: adding file changes
835 remote: adding file changes
828 remote: added 1 changesets with 1 changes to 1 files (+1 heads)
836 remote: added 1 changesets with 1 changes to 1 files (+1 heads)
829
837
830 $ hg -R server graph
838 $ hg -R server graph
831 o 89420bf00fae C-J (default)
839 o 89420bf00fae C-J (default)
832 |
840 |
833 | o b35ed749f288 C-I (my-second-test-branch)
841 | o b35ed749f288 C-I (my-second-test-branch)
834 |/
842 |/
835 o 75d69cba5402 C-G (default)
843 o 75d69cba5402 C-G (default)
836 |
844 |
837 | o 833be552cfe6 C-H (my-first-test-branch)
845 | o 833be552cfe6 C-H (my-first-test-branch)
838 |/
846 |/
839 o d9e379a8c432 C-F (default)
847 o d9e379a8c432 C-F (default)
840 |
848 |
841 o 51c544a58128 C-C (default)
849 o 51c544a58128 C-C (default)
842 |
850 |
843 | o d603e2c0cdd7 C-E (default)
851 | o d603e2c0cdd7 C-E (default)
844 |/
852 |/
845 o 98217d5a1659 C-A (default)
853 o 98217d5a1659 C-A (default)
846 |
854 |
847 | o 59e76faf78bd C-D (default)
855 | o 59e76faf78bd C-D (default)
848 | |
856 | |
849 | o a9149a1428e2 C-B (default)
857 | o a9149a1428e2 C-B (default)
850 |/
858 |/
851 @ 842e2fac6304 C-ROOT (default)
859 @ 842e2fac6304 C-ROOT (default)
852
860
853
861
854 #endif
862 #endif
855
863
856 pushing racing push touch multiple heads
864 pushing racing push touch multiple heads
857 ----------------------------------------
865 ----------------------------------------
858
866
859 There are multiple heads, but the racing push touch all of them
867 There are multiple heads, but the racing push touch all of them
860
868
861 # a (raced)
869 # a (raced)
862 # | b
870 # | b
863 # |/|
871 # |/|
864 # * *
872 # * *
865 # |/
873 # |/
866 # *
874 # *
867
875
868 (resync-all)
876 (resync-all)
869
877
870 #if strict
878 #if strict
871
879
872 $ hg -R ./server pull ./client-racy
880 $ hg -R ./server pull ./client-racy
873 pulling from ./client-racy
881 pulling from ./client-racy
874 searching for changes
882 searching for changes
875 adding changesets
883 adding changesets
876 adding manifests
884 adding manifests
877 adding file changes
885 adding file changes
878 added 1 changesets with 1 changes to 1 files (+1 heads)
886 added 1 changesets with 1 changes to 1 files (+1 heads)
879 new changesets 89420bf00fae (1 drafts)
887 new changesets 89420bf00fae (1 drafts)
880 (run 'hg heads .' to see heads, 'hg merge' to merge)
888 (run 'hg heads .' to see heads, 'hg merge' to merge)
881
889
882 #endif
890 #endif
883
891
884 #if unrelated
892 #if unrelated
885
893
886 $ hg -R ./server pull ./client-racy
894 $ hg -R ./server pull ./client-racy
887 pulling from ./client-racy
895 pulling from ./client-racy
888 searching for changes
896 searching for changes
889 no changes found
897 no changes found
890
898
891 #endif
899 #endif
892
900
893 $ hg -R ./client-other pull
901 $ hg -R ./client-other pull
894 pulling from ssh://user@dummy/server
902 pulling from ssh://user@dummy/server
895 searching for changes
903 searching for changes
896 adding changesets
904 adding changesets
897 adding manifests
905 adding manifests
898 adding file changes
906 adding file changes
899 added 1 changesets with 1 changes to 1 files (+1 heads)
907 added 1 changesets with 1 changes to 1 files (+1 heads)
900 new changesets 89420bf00fae (1 drafts)
908 new changesets 89420bf00fae (1 drafts)
901 (run 'hg heads' to see heads)
909 (run 'hg heads' to see heads)
902 $ hg -R ./client-racy pull
910 $ hg -R ./client-racy pull
903 pulling from ssh://user@dummy/server
911 pulling from ssh://user@dummy/server
904 searching for changes
912 searching for changes
905 adding changesets
913 adding changesets
906 adding manifests
914 adding manifests
907 adding file changes
915 adding file changes
908 added 1 changesets with 1 changes to 1 files (+1 heads)
916 added 1 changesets with 1 changes to 1 files (+1 heads)
909 new changesets b35ed749f288 (1 drafts)
917 new changesets b35ed749f288 (1 drafts)
910 (run 'hg heads .' to see heads, 'hg merge' to merge)
918 (run 'hg heads .' to see heads, 'hg merge' to merge)
911
919
912 $ hg -R server graph
920 $ hg -R server graph
913 o 89420bf00fae C-J (default)
921 o 89420bf00fae C-J (default)
914 |
922 |
915 | o b35ed749f288 C-I (my-second-test-branch)
923 | o b35ed749f288 C-I (my-second-test-branch)
916 |/
924 |/
917 o 75d69cba5402 C-G (default)
925 o 75d69cba5402 C-G (default)
918 |
926 |
919 | o 833be552cfe6 C-H (my-first-test-branch)
927 | o 833be552cfe6 C-H (my-first-test-branch)
920 |/
928 |/
921 o d9e379a8c432 C-F (default)
929 o d9e379a8c432 C-F (default)
922 |
930 |
923 o 51c544a58128 C-C (default)
931 o 51c544a58128 C-C (default)
924 |
932 |
925 | o d603e2c0cdd7 C-E (default)
933 | o d603e2c0cdd7 C-E (default)
926 |/
934 |/
927 o 98217d5a1659 C-A (default)
935 o 98217d5a1659 C-A (default)
928 |
936 |
929 | o 59e76faf78bd C-D (default)
937 | o 59e76faf78bd C-D (default)
930 | |
938 | |
931 | o a9149a1428e2 C-B (default)
939 | o a9149a1428e2 C-B (default)
932 |/
940 |/
933 @ 842e2fac6304 C-ROOT (default)
941 @ 842e2fac6304 C-ROOT (default)
934
942
935
943
936 Creating changesets
944 Creating changesets
937
945
938 (merges heads)
946 (merges heads)
939
947
940 $ hg -R client-other/ up 'desc("C-E")'
948 $ hg -R client-other/ up 'desc("C-E")'
941 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
949 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
942 $ hg -R client-other/ merge 'desc("C-D")'
950 $ hg -R client-other/ merge 'desc("C-D")'
943 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
951 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
944 (branch merge, don't forget to commit)
952 (branch merge, don't forget to commit)
945 $ hg -R client-other/ commit -m "C-K"
953 $ hg -R client-other/ commit -m "C-K"
946
954
947 (update one head)
955 (update one head)
948
956
949 $ hg -R client-racy/ up 'desc("C-D")'
957 $ hg -R client-racy/ up 'desc("C-D")'
950 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
958 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
951 $ echo bbb >> client-racy/b
959 $ echo bbb >> client-racy/b
952 $ hg -R client-racy/ commit -m "C-L"
960 $ hg -R client-racy/ commit -m "C-L"
953
961
954 Pushing
962 Pushing
955
963
956 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
964 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
957
965
958 $ waiton $TESTTMP/readyfile
966 $ waiton $TESTTMP/readyfile
959
967
960 $ hg -R client-other push -fr 'tip' --new-branch
968 $ hg -R client-other push -fr 'tip' --new-branch
961 pushing to ssh://user@dummy/server
969 pushing to ssh://user@dummy/server
962 searching for changes
970 searching for changes
963 remote: adding changesets
971 remote: adding changesets
964 remote: adding manifests
972 remote: adding manifests
965 remote: adding file changes
973 remote: adding file changes
966 remote: added 1 changesets with 0 changes to 0 files (-1 heads)
974 remote: added 1 changesets with 0 changes to 0 files (-1 heads)
967
975
968 $ release $TESTTMP/watchfile
976 $ release $TESTTMP/watchfile
969
977
970 Check the result of the push
978 Check the result of the push
971
979
972 $ cat ./push-log
980 $ cat ./push-log
973 pushing to ssh://user@dummy/server
981 pushing to ssh://user@dummy/server
974 searching for changes
982 searching for changes
975 wrote ready: $TESTTMP/readyfile
983 wrote ready: $TESTTMP/readyfile
976 waiting on: $TESTTMP/watchfile
984 waiting on: $TESTTMP/watchfile
977 abort: push failed:
985 abort: push failed:
978 'remote repository changed while pushing - please try again'
986 'remote repository changed while pushing - please try again'
979
987
980 $ hg -R server graph
988 $ hg -R server graph
981 o be705100c623 C-K (default)
989 o be705100c623 C-K (default)
982 |\
990 |\
983 | o d603e2c0cdd7 C-E (default)
991 | o d603e2c0cdd7 C-E (default)
984 | |
992 | |
985 o | 59e76faf78bd C-D (default)
993 o | 59e76faf78bd C-D (default)
986 | |
994 | |
987 | | o 89420bf00fae C-J (default)
995 | | o 89420bf00fae C-J (default)
988 | | |
996 | | |
989 | | | o b35ed749f288 C-I (my-second-test-branch)
997 | | | o b35ed749f288 C-I (my-second-test-branch)
990 | | |/
998 | | |/
991 | | o 75d69cba5402 C-G (default)
999 | | o 75d69cba5402 C-G (default)
992 | | |
1000 | | |
993 | | | o 833be552cfe6 C-H (my-first-test-branch)
1001 | | | o 833be552cfe6 C-H (my-first-test-branch)
994 | | |/
1002 | | |/
995 | | o d9e379a8c432 C-F (default)
1003 | | o d9e379a8c432 C-F (default)
996 | | |
1004 | | |
997 | | o 51c544a58128 C-C (default)
1005 | | o 51c544a58128 C-C (default)
998 | |/
1006 | |/
999 o | a9149a1428e2 C-B (default)
1007 o | a9149a1428e2 C-B (default)
1000 | |
1008 | |
1001 | o 98217d5a1659 C-A (default)
1009 | o 98217d5a1659 C-A (default)
1002 |/
1010 |/
1003 @ 842e2fac6304 C-ROOT (default)
1011 @ 842e2fac6304 C-ROOT (default)
1004
1012
1005
1013
1006 pushing raced push touch multiple heads
1014 pushing raced push touch multiple heads
1007 ---------------------------------------
1015 ---------------------------------------
1008
1016
1009 There are multiple heads, the raced push touch all of them
1017 There are multiple heads, the raced push touch all of them
1010
1018
1011 # b
1019 # b
1012 # | a (raced)
1020 # | a (raced)
1013 # |/|
1021 # |/|
1014 # * *
1022 # * *
1015 # |/
1023 # |/
1016 # *
1024 # *
1017
1025
1018 (resync-all)
1026 (resync-all)
1019
1027
1020 $ hg -R ./server pull ./client-racy
1028 $ hg -R ./server pull ./client-racy
1021 pulling from ./client-racy
1029 pulling from ./client-racy
1022 searching for changes
1030 searching for changes
1023 adding changesets
1031 adding changesets
1024 adding manifests
1032 adding manifests
1025 adding file changes
1033 adding file changes
1026 added 1 changesets with 1 changes to 1 files (+1 heads)
1034 added 1 changesets with 1 changes to 1 files (+1 heads)
1027 new changesets cac2cead0ff0 (1 drafts)
1035 new changesets cac2cead0ff0 (1 drafts)
1028 (run 'hg heads .' to see heads, 'hg merge' to merge)
1036 (run 'hg heads .' to see heads, 'hg merge' to merge)
1029 $ hg -R ./client-other pull
1037 $ hg -R ./client-other pull
1030 pulling from ssh://user@dummy/server
1038 pulling from ssh://user@dummy/server
1031 searching for changes
1039 searching for changes
1032 adding changesets
1040 adding changesets
1033 adding manifests
1041 adding manifests
1034 adding file changes
1042 adding file changes
1035 added 1 changesets with 1 changes to 1 files (+1 heads)
1043 added 1 changesets with 1 changes to 1 files (+1 heads)
1036 new changesets cac2cead0ff0 (1 drafts)
1044 new changesets cac2cead0ff0 (1 drafts)
1037 (run 'hg heads .' to see heads, 'hg merge' to merge)
1045 (run 'hg heads .' to see heads, 'hg merge' to merge)
1038 $ hg -R ./client-racy pull
1046 $ hg -R ./client-racy pull
1039 pulling from ssh://user@dummy/server
1047 pulling from ssh://user@dummy/server
1040 searching for changes
1048 searching for changes
1041 adding changesets
1049 adding changesets
1042 adding manifests
1050 adding manifests
1043 adding file changes
1051 adding file changes
1044 added 1 changesets with 0 changes to 0 files
1052 added 1 changesets with 0 changes to 0 files
1045 new changesets be705100c623 (1 drafts)
1053 new changesets be705100c623 (1 drafts)
1046 (run 'hg update' to get a working copy)
1054 (run 'hg update' to get a working copy)
1047
1055
1048 $ hg -R server graph
1056 $ hg -R server graph
1049 o cac2cead0ff0 C-L (default)
1057 o cac2cead0ff0 C-L (default)
1050 |
1058 |
1051 | o be705100c623 C-K (default)
1059 | o be705100c623 C-K (default)
1052 |/|
1060 |/|
1053 | o d603e2c0cdd7 C-E (default)
1061 | o d603e2c0cdd7 C-E (default)
1054 | |
1062 | |
1055 o | 59e76faf78bd C-D (default)
1063 o | 59e76faf78bd C-D (default)
1056 | |
1064 | |
1057 | | o 89420bf00fae C-J (default)
1065 | | o 89420bf00fae C-J (default)
1058 | | |
1066 | | |
1059 | | | o b35ed749f288 C-I (my-second-test-branch)
1067 | | | o b35ed749f288 C-I (my-second-test-branch)
1060 | | |/
1068 | | |/
1061 | | o 75d69cba5402 C-G (default)
1069 | | o 75d69cba5402 C-G (default)
1062 | | |
1070 | | |
1063 | | | o 833be552cfe6 C-H (my-first-test-branch)
1071 | | | o 833be552cfe6 C-H (my-first-test-branch)
1064 | | |/
1072 | | |/
1065 | | o d9e379a8c432 C-F (default)
1073 | | o d9e379a8c432 C-F (default)
1066 | | |
1074 | | |
1067 | | o 51c544a58128 C-C (default)
1075 | | o 51c544a58128 C-C (default)
1068 | |/
1076 | |/
1069 o | a9149a1428e2 C-B (default)
1077 o | a9149a1428e2 C-B (default)
1070 | |
1078 | |
1071 | o 98217d5a1659 C-A (default)
1079 | o 98217d5a1659 C-A (default)
1072 |/
1080 |/
1073 @ 842e2fac6304 C-ROOT (default)
1081 @ 842e2fac6304 C-ROOT (default)
1074
1082
1075
1083
1076 Creating changesets
1084 Creating changesets
1077
1085
1078 (update existing head)
1086 (update existing head)
1079
1087
1080 $ echo aaa >> client-other/a
1088 $ echo aaa >> client-other/a
1081 $ hg -R client-other/ commit -m "C-M"
1089 $ hg -R client-other/ commit -m "C-M"
1082
1090
1083 (merge heads)
1091 (merge heads)
1084
1092
1085 $ hg -R client-racy/ merge 'desc("C-K")'
1093 $ hg -R client-racy/ merge 'desc("C-K")'
1086 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1094 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1087 (branch merge, don't forget to commit)
1095 (branch merge, don't forget to commit)
1088 $ hg -R client-racy/ commit -m "C-N"
1096 $ hg -R client-racy/ commit -m "C-N"
1089
1097
1090 Pushing
1098 Pushing
1091
1099
1092 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
1100 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
1093
1101
1094 $ waiton $TESTTMP/readyfile
1102 $ waiton $TESTTMP/readyfile
1095
1103
1096 $ hg -R client-other push -fr 'tip' --new-branch
1104 $ hg -R client-other push -fr 'tip' --new-branch
1097 pushing to ssh://user@dummy/server
1105 pushing to ssh://user@dummy/server
1098 searching for changes
1106 searching for changes
1099 remote: adding changesets
1107 remote: adding changesets
1100 remote: adding manifests
1108 remote: adding manifests
1101 remote: adding file changes
1109 remote: adding file changes
1102 remote: added 1 changesets with 1 changes to 1 files
1110 remote: added 1 changesets with 1 changes to 1 files
1103
1111
1104 $ release $TESTTMP/watchfile
1112 $ release $TESTTMP/watchfile
1105
1113
1106 Check the result of the push
1114 Check the result of the push
1107
1115
1108 $ cat ./push-log
1116 $ cat ./push-log
1109 pushing to ssh://user@dummy/server
1117 pushing to ssh://user@dummy/server
1110 searching for changes
1118 searching for changes
1111 wrote ready: $TESTTMP/readyfile
1119 wrote ready: $TESTTMP/readyfile
1112 waiting on: $TESTTMP/watchfile
1120 waiting on: $TESTTMP/watchfile
1113 abort: push failed:
1121 abort: push failed:
1114 'remote repository changed while pushing - please try again'
1122 'remote repository changed while pushing - please try again'
1115
1123
1116 $ hg -R server graph
1124 $ hg -R server graph
1117 o 6fd3090135df C-M (default)
1125 o 6fd3090135df C-M (default)
1118 |
1126 |
1119 o be705100c623 C-K (default)
1127 o be705100c623 C-K (default)
1120 |\
1128 |\
1121 | o d603e2c0cdd7 C-E (default)
1129 | o d603e2c0cdd7 C-E (default)
1122 | |
1130 | |
1123 +---o cac2cead0ff0 C-L (default)
1131 +---o cac2cead0ff0 C-L (default)
1124 | |
1132 | |
1125 o | 59e76faf78bd C-D (default)
1133 o | 59e76faf78bd C-D (default)
1126 | |
1134 | |
1127 | | o 89420bf00fae C-J (default)
1135 | | o 89420bf00fae C-J (default)
1128 | | |
1136 | | |
1129 | | | o b35ed749f288 C-I (my-second-test-branch)
1137 | | | o b35ed749f288 C-I (my-second-test-branch)
1130 | | |/
1138 | | |/
1131 | | o 75d69cba5402 C-G (default)
1139 | | o 75d69cba5402 C-G (default)
1132 | | |
1140 | | |
1133 | | | o 833be552cfe6 C-H (my-first-test-branch)
1141 | | | o 833be552cfe6 C-H (my-first-test-branch)
1134 | | |/
1142 | | |/
1135 | | o d9e379a8c432 C-F (default)
1143 | | o d9e379a8c432 C-F (default)
1136 | | |
1144 | | |
1137 | | o 51c544a58128 C-C (default)
1145 | | o 51c544a58128 C-C (default)
1138 | |/
1146 | |/
1139 o | a9149a1428e2 C-B (default)
1147 o | a9149a1428e2 C-B (default)
1140 | |
1148 | |
1141 | o 98217d5a1659 C-A (default)
1149 | o 98217d5a1659 C-A (default)
1142 |/
1150 |/
1143 @ 842e2fac6304 C-ROOT (default)
1151 @ 842e2fac6304 C-ROOT (default)
1144
1152
1145
1153
1146 racing commit push a new head behind another named branch
1154 racing commit push a new head behind another named branch
1147 ---------------------------------------------------------
1155 ---------------------------------------------------------
1148
1156
1149 non-continuous branch are valid case, we tests for them.
1157 non-continuous branch are valid case, we tests for them.
1150
1158
1151 # b (branch default)
1159 # b (branch default)
1152 # |
1160 # |
1153 # o (branch foo)
1161 # o (branch foo)
1154 # |
1162 # |
1155 # | a (raced, branch default)
1163 # | a (raced, branch default)
1156 # |/
1164 # |/
1157 # * (branch foo)
1165 # * (branch foo)
1158 # |
1166 # |
1159 # * (branch default)
1167 # * (branch default)
1160
1168
1161 (resync-all + other branch)
1169 (resync-all + other branch)
1162
1170
1163 $ hg -R ./server pull ./client-racy
1171 $ hg -R ./server pull ./client-racy
1164 pulling from ./client-racy
1172 pulling from ./client-racy
1165 searching for changes
1173 searching for changes
1166 adding changesets
1174 adding changesets
1167 adding manifests
1175 adding manifests
1168 adding file changes
1176 adding file changes
1169 added 1 changesets with 0 changes to 0 files
1177 added 1 changesets with 0 changes to 0 files
1170 new changesets 866a66e18630 (1 drafts)
1178 new changesets 866a66e18630 (1 drafts)
1171 (run 'hg update' to get a working copy)
1179 (run 'hg update' to get a working copy)
1172
1180
1173 (creates named branch on head)
1181 (creates named branch on head)
1174
1182
1175 $ hg -R ./server/ up 'desc("C-N")'
1183 $ hg -R ./server/ up 'desc("C-N")'
1176 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
1184 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
1177 $ hg -R ./server/ branch other
1185 $ hg -R ./server/ branch other
1178 marked working directory as branch other
1186 marked working directory as branch other
1179 $ hg -R ./server/ ci -m "C-Z"
1187 $ hg -R ./server/ ci -m "C-Z"
1180 $ hg -R ./server/ up null
1188 $ hg -R ./server/ up null
1181 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
1189 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
1182
1190
1183 (sync client)
1191 (sync client)
1184
1192
1185 $ hg -R ./client-other pull
1193 $ hg -R ./client-other pull
1186 pulling from ssh://user@dummy/server
1194 pulling from ssh://user@dummy/server
1187 searching for changes
1195 searching for changes
1188 adding changesets
1196 adding changesets
1189 adding manifests
1197 adding manifests
1190 adding file changes
1198 adding file changes
1191 added 2 changesets with 0 changes to 0 files
1199 added 2 changesets with 0 changes to 0 files
1192 new changesets 866a66e18630:55a6f1c01b48 (2 drafts)
1200 new changesets 866a66e18630:55a6f1c01b48 (2 drafts)
1193 (run 'hg update' to get a working copy)
1201 (run 'hg update' to get a working copy)
1194 $ hg -R ./client-racy pull
1202 $ hg -R ./client-racy pull
1195 pulling from ssh://user@dummy/server
1203 pulling from ssh://user@dummy/server
1196 searching for changes
1204 searching for changes
1197 adding changesets
1205 adding changesets
1198 adding manifests
1206 adding manifests
1199 adding file changes
1207 adding file changes
1200 added 2 changesets with 1 changes to 1 files (+1 heads)
1208 added 2 changesets with 1 changes to 1 files (+1 heads)
1201 new changesets 6fd3090135df:55a6f1c01b48 (2 drafts)
1209 new changesets 6fd3090135df:55a6f1c01b48 (2 drafts)
1202 (run 'hg heads .' to see heads, 'hg merge' to merge)
1210 (run 'hg heads .' to see heads, 'hg merge' to merge)
1203
1211
1204 $ hg -R server graph
1212 $ hg -R server graph
1205 o 55a6f1c01b48 C-Z (other)
1213 o 55a6f1c01b48 C-Z (other)
1206 |
1214 |
1207 o 866a66e18630 C-N (default)
1215 o 866a66e18630 C-N (default)
1208 |\
1216 |\
1209 +---o 6fd3090135df C-M (default)
1217 +---o 6fd3090135df C-M (default)
1210 | |
1218 | |
1211 | o cac2cead0ff0 C-L (default)
1219 | o cac2cead0ff0 C-L (default)
1212 | |
1220 | |
1213 o | be705100c623 C-K (default)
1221 o | be705100c623 C-K (default)
1214 |\|
1222 |\|
1215 o | d603e2c0cdd7 C-E (default)
1223 o | d603e2c0cdd7 C-E (default)
1216 | |
1224 | |
1217 | o 59e76faf78bd C-D (default)
1225 | o 59e76faf78bd C-D (default)
1218 | |
1226 | |
1219 | | o 89420bf00fae C-J (default)
1227 | | o 89420bf00fae C-J (default)
1220 | | |
1228 | | |
1221 | | | o b35ed749f288 C-I (my-second-test-branch)
1229 | | | o b35ed749f288 C-I (my-second-test-branch)
1222 | | |/
1230 | | |/
1223 | | o 75d69cba5402 C-G (default)
1231 | | o 75d69cba5402 C-G (default)
1224 | | |
1232 | | |
1225 | | | o 833be552cfe6 C-H (my-first-test-branch)
1233 | | | o 833be552cfe6 C-H (my-first-test-branch)
1226 | | |/
1234 | | |/
1227 | | o d9e379a8c432 C-F (default)
1235 | | o d9e379a8c432 C-F (default)
1228 | | |
1236 | | |
1229 +---o 51c544a58128 C-C (default)
1237 +---o 51c544a58128 C-C (default)
1230 | |
1238 | |
1231 | o a9149a1428e2 C-B (default)
1239 | o a9149a1428e2 C-B (default)
1232 | |
1240 | |
1233 o | 98217d5a1659 C-A (default)
1241 o | 98217d5a1659 C-A (default)
1234 |/
1242 |/
1235 o 842e2fac6304 C-ROOT (default)
1243 o 842e2fac6304 C-ROOT (default)
1236
1244
1237
1245
1238 Creating changesets
1246 Creating changesets
1239
1247
1240 (update default head through another named branch one)
1248 (update default head through another named branch one)
1241
1249
1242 $ hg -R client-other/ up 'desc("C-Z")'
1250 $ hg -R client-other/ up 'desc("C-Z")'
1243 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
1251 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
1244 $ echo aaa >> client-other/a
1252 $ echo aaa >> client-other/a
1245 $ hg -R client-other/ commit -m "C-O"
1253 $ hg -R client-other/ commit -m "C-O"
1246 $ echo aaa >> client-other/a
1254 $ echo aaa >> client-other/a
1247 $ hg -R client-other/ branch --force default
1255 $ hg -R client-other/ branch --force default
1248 marked working directory as branch default
1256 marked working directory as branch default
1249 $ hg -R client-other/ commit -m "C-P"
1257 $ hg -R client-other/ commit -m "C-P"
1250 created new head
1258 created new head
1251
1259
1252 (update default head)
1260 (update default head)
1253
1261
1254 $ hg -R client-racy/ up 'desc("C-Z")'
1262 $ hg -R client-racy/ up 'desc("C-Z")'
1255 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1263 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1256 $ echo bbb >> client-other/a
1264 $ echo bbb >> client-other/a
1257 $ hg -R client-racy/ branch --force default
1265 $ hg -R client-racy/ branch --force default
1258 marked working directory as branch default
1266 marked working directory as branch default
1259 $ hg -R client-racy/ commit -m "C-Q"
1267 $ hg -R client-racy/ commit -m "C-Q"
1260 created new head
1268 created new head
1261
1269
1262 Pushing
1270 Pushing
1263
1271
1264 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
1272 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
1265
1273
1266 $ waiton $TESTTMP/readyfile
1274 $ waiton $TESTTMP/readyfile
1267
1275
1268 $ hg -R client-other push -fr 'tip' --new-branch
1276 $ hg -R client-other push -fr 'tip' --new-branch
1269 pushing to ssh://user@dummy/server
1277 pushing to ssh://user@dummy/server
1270 searching for changes
1278 searching for changes
1271 remote: adding changesets
1279 remote: adding changesets
1272 remote: adding manifests
1280 remote: adding manifests
1273 remote: adding file changes
1281 remote: adding file changes
1274 remote: added 2 changesets with 1 changes to 1 files
1282 remote: added 2 changesets with 1 changes to 1 files
1275
1283
1276 $ release $TESTTMP/watchfile
1284 $ release $TESTTMP/watchfile
1277
1285
1278 Check the result of the push
1286 Check the result of the push
1279
1287
1280 $ cat ./push-log
1288 $ cat ./push-log
1281 pushing to ssh://user@dummy/server
1289 pushing to ssh://user@dummy/server
1282 searching for changes
1290 searching for changes
1283 wrote ready: $TESTTMP/readyfile
1291 wrote ready: $TESTTMP/readyfile
1284 waiting on: $TESTTMP/watchfile
1292 waiting on: $TESTTMP/watchfile
1285 abort: push failed:
1293 abort: push failed:
1286 'remote repository changed while pushing - please try again'
1294 'remote repository changed while pushing - please try again'
1287
1295
1288 $ hg -R server graph
1296 $ hg -R server graph
1289 o 1b58ee3f79e5 C-P (default)
1297 o 1b58ee3f79e5 C-P (default)
1290 |
1298 |
1291 o d0a85b2252a9 C-O (other)
1299 o d0a85b2252a9 C-O (other)
1292 |
1300 |
1293 o 55a6f1c01b48 C-Z (other)
1301 o 55a6f1c01b48 C-Z (other)
1294 |
1302 |
1295 o 866a66e18630 C-N (default)
1303 o 866a66e18630 C-N (default)
1296 |\
1304 |\
1297 +---o 6fd3090135df C-M (default)
1305 +---o 6fd3090135df C-M (default)
1298 | |
1306 | |
1299 | o cac2cead0ff0 C-L (default)
1307 | o cac2cead0ff0 C-L (default)
1300 | |
1308 | |
1301 o | be705100c623 C-K (default)
1309 o | be705100c623 C-K (default)
1302 |\|
1310 |\|
1303 o | d603e2c0cdd7 C-E (default)
1311 o | d603e2c0cdd7 C-E (default)
1304 | |
1312 | |
1305 | o 59e76faf78bd C-D (default)
1313 | o 59e76faf78bd C-D (default)
1306 | |
1314 | |
1307 | | o 89420bf00fae C-J (default)
1315 | | o 89420bf00fae C-J (default)
1308 | | |
1316 | | |
1309 | | | o b35ed749f288 C-I (my-second-test-branch)
1317 | | | o b35ed749f288 C-I (my-second-test-branch)
1310 | | |/
1318 | | |/
1311 | | o 75d69cba5402 C-G (default)
1319 | | o 75d69cba5402 C-G (default)
1312 | | |
1320 | | |
1313 | | | o 833be552cfe6 C-H (my-first-test-branch)
1321 | | | o 833be552cfe6 C-H (my-first-test-branch)
1314 | | |/
1322 | | |/
1315 | | o d9e379a8c432 C-F (default)
1323 | | o d9e379a8c432 C-F (default)
1316 | | |
1324 | | |
1317 +---o 51c544a58128 C-C (default)
1325 +---o 51c544a58128 C-C (default)
1318 | |
1326 | |
1319 | o a9149a1428e2 C-B (default)
1327 | o a9149a1428e2 C-B (default)
1320 | |
1328 | |
1321 o | 98217d5a1659 C-A (default)
1329 o | 98217d5a1659 C-A (default)
1322 |/
1330 |/
1323 o 842e2fac6304 C-ROOT (default)
1331 o 842e2fac6304 C-ROOT (default)
1324
1332
1325
1333
1326 raced commit push a new head behind another named branch
1334 raced commit push a new head behind another named branch
1327 ---------------------------------------------------------
1335 ---------------------------------------------------------
1328
1336
1329 non-continuous branch are valid case, we tests for them.
1337 non-continuous branch are valid case, we tests for them.
1330
1338
1331 # b (raced branch default)
1339 # b (raced branch default)
1332 # |
1340 # |
1333 # o (branch foo)
1341 # o (branch foo)
1334 # |
1342 # |
1335 # | a (branch default)
1343 # | a (branch default)
1336 # |/
1344 # |/
1337 # * (branch foo)
1345 # * (branch foo)
1338 # |
1346 # |
1339 # * (branch default)
1347 # * (branch default)
1340
1348
1341 (resync-all)
1349 (resync-all)
1342
1350
1343 $ hg -R ./server pull ./client-racy
1351 $ hg -R ./server pull ./client-racy
1344 pulling from ./client-racy
1352 pulling from ./client-racy
1345 searching for changes
1353 searching for changes
1346 adding changesets
1354 adding changesets
1347 adding manifests
1355 adding manifests
1348 adding file changes
1356 adding file changes
1349 added 1 changesets with 0 changes to 0 files (+1 heads)
1357 added 1 changesets with 0 changes to 0 files (+1 heads)
1350 new changesets b0ee3d6f51bc (1 drafts)
1358 new changesets b0ee3d6f51bc (1 drafts)
1351 (run 'hg heads .' to see heads, 'hg merge' to merge)
1359 (run 'hg heads .' to see heads, 'hg merge' to merge)
1352 $ hg -R ./client-other pull
1360 $ hg -R ./client-other pull
1353 pulling from ssh://user@dummy/server
1361 pulling from ssh://user@dummy/server
1354 searching for changes
1362 searching for changes
1355 adding changesets
1363 adding changesets
1356 adding manifests
1364 adding manifests
1357 adding file changes
1365 adding file changes
1358 added 1 changesets with 0 changes to 0 files (+1 heads)
1366 added 1 changesets with 0 changes to 0 files (+1 heads)
1359 new changesets b0ee3d6f51bc (1 drafts)
1367 new changesets b0ee3d6f51bc (1 drafts)
1360 (run 'hg heads .' to see heads, 'hg merge' to merge)
1368 (run 'hg heads .' to see heads, 'hg merge' to merge)
1361 $ hg -R ./client-racy pull
1369 $ hg -R ./client-racy pull
1362 pulling from ssh://user@dummy/server
1370 pulling from ssh://user@dummy/server
1363 searching for changes
1371 searching for changes
1364 adding changesets
1372 adding changesets
1365 adding manifests
1373 adding manifests
1366 adding file changes
1374 adding file changes
1367 added 2 changesets with 1 changes to 1 files (+1 heads)
1375 added 2 changesets with 1 changes to 1 files (+1 heads)
1368 new changesets d0a85b2252a9:1b58ee3f79e5 (2 drafts)
1376 new changesets d0a85b2252a9:1b58ee3f79e5 (2 drafts)
1369 (run 'hg heads .' to see heads, 'hg merge' to merge)
1377 (run 'hg heads .' to see heads, 'hg merge' to merge)
1370
1378
1371 $ hg -R server graph
1379 $ hg -R server graph
1372 o b0ee3d6f51bc C-Q (default)
1380 o b0ee3d6f51bc C-Q (default)
1373 |
1381 |
1374 | o 1b58ee3f79e5 C-P (default)
1382 | o 1b58ee3f79e5 C-P (default)
1375 | |
1383 | |
1376 | o d0a85b2252a9 C-O (other)
1384 | o d0a85b2252a9 C-O (other)
1377 |/
1385 |/
1378 o 55a6f1c01b48 C-Z (other)
1386 o 55a6f1c01b48 C-Z (other)
1379 |
1387 |
1380 o 866a66e18630 C-N (default)
1388 o 866a66e18630 C-N (default)
1381 |\
1389 |\
1382 +---o 6fd3090135df C-M (default)
1390 +---o 6fd3090135df C-M (default)
1383 | |
1391 | |
1384 | o cac2cead0ff0 C-L (default)
1392 | o cac2cead0ff0 C-L (default)
1385 | |
1393 | |
1386 o | be705100c623 C-K (default)
1394 o | be705100c623 C-K (default)
1387 |\|
1395 |\|
1388 o | d603e2c0cdd7 C-E (default)
1396 o | d603e2c0cdd7 C-E (default)
1389 | |
1397 | |
1390 | o 59e76faf78bd C-D (default)
1398 | o 59e76faf78bd C-D (default)
1391 | |
1399 | |
1392 | | o 89420bf00fae C-J (default)
1400 | | o 89420bf00fae C-J (default)
1393 | | |
1401 | | |
1394 | | | o b35ed749f288 C-I (my-second-test-branch)
1402 | | | o b35ed749f288 C-I (my-second-test-branch)
1395 | | |/
1403 | | |/
1396 | | o 75d69cba5402 C-G (default)
1404 | | o 75d69cba5402 C-G (default)
1397 | | |
1405 | | |
1398 | | | o 833be552cfe6 C-H (my-first-test-branch)
1406 | | | o 833be552cfe6 C-H (my-first-test-branch)
1399 | | |/
1407 | | |/
1400 | | o d9e379a8c432 C-F (default)
1408 | | o d9e379a8c432 C-F (default)
1401 | | |
1409 | | |
1402 +---o 51c544a58128 C-C (default)
1410 +---o 51c544a58128 C-C (default)
1403 | |
1411 | |
1404 | o a9149a1428e2 C-B (default)
1412 | o a9149a1428e2 C-B (default)
1405 | |
1413 | |
1406 o | 98217d5a1659 C-A (default)
1414 o | 98217d5a1659 C-A (default)
1407 |/
1415 |/
1408 o 842e2fac6304 C-ROOT (default)
1416 o 842e2fac6304 C-ROOT (default)
1409
1417
1410
1418
1411 Creating changesets
1419 Creating changesets
1412
1420
1413 (update 'other' named branch head)
1421 (update 'other' named branch head)
1414
1422
1415 $ hg -R client-other/ up 'desc("C-P")'
1423 $ hg -R client-other/ up 'desc("C-P")'
1416 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1424 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1417 $ echo aaa >> client-other/a
1425 $ echo aaa >> client-other/a
1418 $ hg -R client-other/ branch --force other
1426 $ hg -R client-other/ branch --force other
1419 marked working directory as branch other
1427 marked working directory as branch other
1420 $ hg -R client-other/ commit -m "C-R"
1428 $ hg -R client-other/ commit -m "C-R"
1421 created new head
1429 created new head
1422
1430
1423 (update 'other named brnach through a 'default' changeset')
1431 (update 'other named brnach through a 'default' changeset')
1424
1432
1425 $ hg -R client-racy/ up 'desc("C-P")'
1433 $ hg -R client-racy/ up 'desc("C-P")'
1426 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1434 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1427 $ echo bbb >> client-racy/a
1435 $ echo bbb >> client-racy/a
1428 $ hg -R client-racy/ commit -m "C-S"
1436 $ hg -R client-racy/ commit -m "C-S"
1429 $ echo bbb >> client-racy/a
1437 $ echo bbb >> client-racy/a
1430 $ hg -R client-racy/ branch --force other
1438 $ hg -R client-racy/ branch --force other
1431 marked working directory as branch other
1439 marked working directory as branch other
1432 $ hg -R client-racy/ commit -m "C-T"
1440 $ hg -R client-racy/ commit -m "C-T"
1433 created new head
1441 created new head
1434
1442
1435 Pushing
1443 Pushing
1436
1444
1437 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
1445 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
1438
1446
1439 $ waiton $TESTTMP/readyfile
1447 $ waiton $TESTTMP/readyfile
1440
1448
1441 $ hg -R client-other push -fr 'tip' --new-branch
1449 $ hg -R client-other push -fr 'tip' --new-branch
1442 pushing to ssh://user@dummy/server
1450 pushing to ssh://user@dummy/server
1443 searching for changes
1451 searching for changes
1444 remote: adding changesets
1452 remote: adding changesets
1445 remote: adding manifests
1453 remote: adding manifests
1446 remote: adding file changes
1454 remote: adding file changes
1447 remote: added 1 changesets with 1 changes to 1 files
1455 remote: added 1 changesets with 1 changes to 1 files
1448
1456
1449 $ release $TESTTMP/watchfile
1457 $ release $TESTTMP/watchfile
1450
1458
1451 Check the result of the push
1459 Check the result of the push
1452
1460
1453 $ cat ./push-log
1461 $ cat ./push-log
1454 pushing to ssh://user@dummy/server
1462 pushing to ssh://user@dummy/server
1455 searching for changes
1463 searching for changes
1456 wrote ready: $TESTTMP/readyfile
1464 wrote ready: $TESTTMP/readyfile
1457 waiting on: $TESTTMP/watchfile
1465 waiting on: $TESTTMP/watchfile
1458 abort: push failed:
1466 abort: push failed:
1459 'remote repository changed while pushing - please try again'
1467 'remote repository changed while pushing - please try again'
1460
1468
1461 $ hg -R server graph
1469 $ hg -R server graph
1462 o de7b9e2ba3f6 C-R (other)
1470 o de7b9e2ba3f6 C-R (other)
1463 |
1471 |
1464 o 1b58ee3f79e5 C-P (default)
1472 o 1b58ee3f79e5 C-P (default)
1465 |
1473 |
1466 o d0a85b2252a9 C-O (other)
1474 o d0a85b2252a9 C-O (other)
1467 |
1475 |
1468 | o b0ee3d6f51bc C-Q (default)
1476 | o b0ee3d6f51bc C-Q (default)
1469 |/
1477 |/
1470 o 55a6f1c01b48 C-Z (other)
1478 o 55a6f1c01b48 C-Z (other)
1471 |
1479 |
1472 o 866a66e18630 C-N (default)
1480 o 866a66e18630 C-N (default)
1473 |\
1481 |\
1474 +---o 6fd3090135df C-M (default)
1482 +---o 6fd3090135df C-M (default)
1475 | |
1483 | |
1476 | o cac2cead0ff0 C-L (default)
1484 | o cac2cead0ff0 C-L (default)
1477 | |
1485 | |
1478 o | be705100c623 C-K (default)
1486 o | be705100c623 C-K (default)
1479 |\|
1487 |\|
1480 o | d603e2c0cdd7 C-E (default)
1488 o | d603e2c0cdd7 C-E (default)
1481 | |
1489 | |
1482 | o 59e76faf78bd C-D (default)
1490 | o 59e76faf78bd C-D (default)
1483 | |
1491 | |
1484 | | o 89420bf00fae C-J (default)
1492 | | o 89420bf00fae C-J (default)
1485 | | |
1493 | | |
1486 | | | o b35ed749f288 C-I (my-second-test-branch)
1494 | | | o b35ed749f288 C-I (my-second-test-branch)
1487 | | |/
1495 | | |/
1488 | | o 75d69cba5402 C-G (default)
1496 | | o 75d69cba5402 C-G (default)
1489 | | |
1497 | | |
1490 | | | o 833be552cfe6 C-H (my-first-test-branch)
1498 | | | o 833be552cfe6 C-H (my-first-test-branch)
1491 | | |/
1499 | | |/
1492 | | o d9e379a8c432 C-F (default)
1500 | | o d9e379a8c432 C-F (default)
1493 | | |
1501 | | |
1494 +---o 51c544a58128 C-C (default)
1502 +---o 51c544a58128 C-C (default)
1495 | |
1503 | |
1496 | o a9149a1428e2 C-B (default)
1504 | o a9149a1428e2 C-B (default)
1497 | |
1505 | |
1498 o | 98217d5a1659 C-A (default)
1506 o | 98217d5a1659 C-A (default)
1499 |/
1507 |/
1500 o 842e2fac6304 C-ROOT (default)
1508 o 842e2fac6304 C-ROOT (default)
1501
1509
1502
1510
1503 raced commit push a new head obsoleting the one touched by the racing push
1511 raced commit push a new head obsoleting the one touched by the racing push
1504 --------------------------------------------------------------------------
1512 --------------------------------------------------------------------------
1505
1513
1506 # b (racing)
1514 # b (racing)
1507 # |
1515 # |
1508 # ø⇠◔ a (raced)
1516 # ø⇠◔ a (raced)
1509 # |/
1517 # |/
1510 # *
1518 # *
1511
1519
1512 (resync-all)
1520 (resync-all)
1513
1521
1514 $ hg -R ./server pull ./client-racy
1522 $ hg -R ./server pull ./client-racy
1515 pulling from ./client-racy
1523 pulling from ./client-racy
1516 searching for changes
1524 searching for changes
1517 adding changesets
1525 adding changesets
1518 adding manifests
1526 adding manifests
1519 adding file changes
1527 adding file changes
1520 added 2 changesets with 2 changes to 1 files (+1 heads)
1528 added 2 changesets with 2 changes to 1 files (+1 heads)
1521 new changesets 2efd43f7b5ba:3d57ed3c1091 (2 drafts)
1529 new changesets 2efd43f7b5ba:3d57ed3c1091 (2 drafts)
1522 (run 'hg heads .' to see heads, 'hg merge' to merge)
1530 (run 'hg heads .' to see heads, 'hg merge' to merge)
1523 $ hg -R ./client-other pull
1531 $ hg -R ./client-other pull
1524 pulling from ssh://user@dummy/server
1532 pulling from ssh://user@dummy/server
1525 searching for changes
1533 searching for changes
1526 adding changesets
1534 adding changesets
1527 adding manifests
1535 adding manifests
1528 adding file changes
1536 adding file changes
1529 added 2 changesets with 2 changes to 1 files (+1 heads)
1537 added 2 changesets with 2 changes to 1 files (+1 heads)
1530 new changesets 2efd43f7b5ba:3d57ed3c1091 (2 drafts)
1538 new changesets 2efd43f7b5ba:3d57ed3c1091 (2 drafts)
1531 (run 'hg heads' to see heads, 'hg merge' to merge)
1539 (run 'hg heads' to see heads, 'hg merge' to merge)
1532 $ hg -R ./client-racy pull
1540 $ hg -R ./client-racy pull
1533 pulling from ssh://user@dummy/server
1541 pulling from ssh://user@dummy/server
1534 searching for changes
1542 searching for changes
1535 adding changesets
1543 adding changesets
1536 adding manifests
1544 adding manifests
1537 adding file changes
1545 adding file changes
1538 added 1 changesets with 1 changes to 1 files (+1 heads)
1546 added 1 changesets with 1 changes to 1 files (+1 heads)
1539 new changesets de7b9e2ba3f6 (1 drafts)
1547 new changesets de7b9e2ba3f6 (1 drafts)
1540 (run 'hg heads' to see heads, 'hg merge' to merge)
1548 (run 'hg heads' to see heads, 'hg merge' to merge)
1541
1549
1542 $ hg -R server graph
1550 $ hg -R server graph
1543 o 3d57ed3c1091 C-T (other)
1551 o 3d57ed3c1091 C-T (other)
1544 |
1552 |
1545 o 2efd43f7b5ba C-S (default)
1553 o 2efd43f7b5ba C-S (default)
1546 |
1554 |
1547 | o de7b9e2ba3f6 C-R (other)
1555 | o de7b9e2ba3f6 C-R (other)
1548 |/
1556 |/
1549 o 1b58ee3f79e5 C-P (default)
1557 o 1b58ee3f79e5 C-P (default)
1550 |
1558 |
1551 o d0a85b2252a9 C-O (other)
1559 o d0a85b2252a9 C-O (other)
1552 |
1560 |
1553 | o b0ee3d6f51bc C-Q (default)
1561 | o b0ee3d6f51bc C-Q (default)
1554 |/
1562 |/
1555 o 55a6f1c01b48 C-Z (other)
1563 o 55a6f1c01b48 C-Z (other)
1556 |
1564 |
1557 o 866a66e18630 C-N (default)
1565 o 866a66e18630 C-N (default)
1558 |\
1566 |\
1559 +---o 6fd3090135df C-M (default)
1567 +---o 6fd3090135df C-M (default)
1560 | |
1568 | |
1561 | o cac2cead0ff0 C-L (default)
1569 | o cac2cead0ff0 C-L (default)
1562 | |
1570 | |
1563 o | be705100c623 C-K (default)
1571 o | be705100c623 C-K (default)
1564 |\|
1572 |\|
1565 o | d603e2c0cdd7 C-E (default)
1573 o | d603e2c0cdd7 C-E (default)
1566 | |
1574 | |
1567 | o 59e76faf78bd C-D (default)
1575 | o 59e76faf78bd C-D (default)
1568 | |
1576 | |
1569 | | o 89420bf00fae C-J (default)
1577 | | o 89420bf00fae C-J (default)
1570 | | |
1578 | | |
1571 | | | o b35ed749f288 C-I (my-second-test-branch)
1579 | | | o b35ed749f288 C-I (my-second-test-branch)
1572 | | |/
1580 | | |/
1573 | | o 75d69cba5402 C-G (default)
1581 | | o 75d69cba5402 C-G (default)
1574 | | |
1582 | | |
1575 | | | o 833be552cfe6 C-H (my-first-test-branch)
1583 | | | o 833be552cfe6 C-H (my-first-test-branch)
1576 | | |/
1584 | | |/
1577 | | o d9e379a8c432 C-F (default)
1585 | | o d9e379a8c432 C-F (default)
1578 | | |
1586 | | |
1579 +---o 51c544a58128 C-C (default)
1587 +---o 51c544a58128 C-C (default)
1580 | |
1588 | |
1581 | o a9149a1428e2 C-B (default)
1589 | o a9149a1428e2 C-B (default)
1582 | |
1590 | |
1583 o | 98217d5a1659 C-A (default)
1591 o | 98217d5a1659 C-A (default)
1584 |/
1592 |/
1585 o 842e2fac6304 C-ROOT (default)
1593 o 842e2fac6304 C-ROOT (default)
1586
1594
1587
1595
1588 Creating changesets and markers
1596 Creating changesets and markers
1589
1597
1590 (continue existing head)
1598 (continue existing head)
1591
1599
1592 $ hg -R client-other/ up 'desc("C-Q")'
1600 $ hg -R client-other/ up 'desc("C-Q")'
1593 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1601 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1594 $ echo aaa >> client-other/a
1602 $ echo aaa >> client-other/a
1595 $ hg -R client-other/ commit -m "C-U"
1603 $ hg -R client-other/ commit -m "C-U"
1596
1604
1597 (new topo branch obsoleting that same head)
1605 (new topo branch obsoleting that same head)
1598
1606
1599 $ hg -R client-racy/ up 'desc("C-Z")'
1607 $ hg -R client-racy/ up 'desc("C-Z")'
1600 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1608 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1601 $ echo bbb >> client-racy/a
1609 $ echo bbb >> client-racy/a
1602 $ hg -R client-racy/ branch --force default
1610 $ hg -R client-racy/ branch --force default
1603 marked working directory as branch default
1611 marked working directory as branch default
1604 $ hg -R client-racy/ commit -m "C-V"
1612 $ hg -R client-racy/ commit -m "C-V"
1605 created new head
1613 created new head
1606 $ ID_Q=`hg -R client-racy log -T '{node}\n' -r 'desc("C-Q")'`
1614 $ ID_Q=`hg -R client-racy log -T '{node}\n' -r 'desc("C-Q")'`
1607 $ ID_V=`hg -R client-racy log -T '{node}\n' -r 'desc("C-V")'`
1615 $ ID_V=`hg -R client-racy log -T '{node}\n' -r 'desc("C-V")'`
1608 $ hg -R client-racy debugobsolete $ID_Q $ID_V
1616 $ hg -R client-racy debugobsolete $ID_Q $ID_V
1609 1 new obsolescence markers
1617 1 new obsolescence markers
1610 obsoleted 1 changesets
1618 obsoleted 1 changesets
1611
1619
1612 Pushing
1620 Pushing
1613
1621
1614 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
1622 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
1615
1623
1616 $ waiton $TESTTMP/readyfile
1624 $ waiton $TESTTMP/readyfile
1617
1625
1618 $ hg -R client-other push -fr 'tip' --new-branch
1626 $ hg -R client-other push -fr 'tip' --new-branch
1619 pushing to ssh://user@dummy/server
1627 pushing to ssh://user@dummy/server
1620 searching for changes
1628 searching for changes
1621 remote: adding changesets
1629 remote: adding changesets
1622 remote: adding manifests
1630 remote: adding manifests
1623 remote: adding file changes
1631 remote: adding file changes
1624 remote: added 1 changesets with 0 changes to 0 files
1632 remote: added 1 changesets with 0 changes to 0 files
1625
1633
1626 $ release $TESTTMP/watchfile
1634 $ release $TESTTMP/watchfile
1627
1635
1628 Check the result of the push
1636 Check the result of the push
1629
1637
1630 $ cat ./push-log
1638 $ cat ./push-log
1631 pushing to ssh://user@dummy/server
1639 pushing to ssh://user@dummy/server
1632 searching for changes
1640 searching for changes
1633 wrote ready: $TESTTMP/readyfile
1641 wrote ready: $TESTTMP/readyfile
1634 waiting on: $TESTTMP/watchfile
1642 waiting on: $TESTTMP/watchfile
1635 abort: push failed:
1643 abort: push failed:
1636 'remote repository changed while pushing - please try again'
1644 'remote repository changed while pushing - please try again'
1637
1645
1638 $ hg -R server debugobsolete
1646 $ hg -R server debugobsolete
1639 $ hg -R server graph
1647 $ hg -R server graph
1640 o a98a47d8b85b C-U (default)
1648 o a98a47d8b85b C-U (default)
1641 |
1649 |
1642 o b0ee3d6f51bc C-Q (default)
1650 o b0ee3d6f51bc C-Q (default)
1643 |
1651 |
1644 | o 3d57ed3c1091 C-T (other)
1652 | o 3d57ed3c1091 C-T (other)
1645 | |
1653 | |
1646 | o 2efd43f7b5ba C-S (default)
1654 | o 2efd43f7b5ba C-S (default)
1647 | |
1655 | |
1648 | | o de7b9e2ba3f6 C-R (other)
1656 | | o de7b9e2ba3f6 C-R (other)
1649 | |/
1657 | |/
1650 | o 1b58ee3f79e5 C-P (default)
1658 | o 1b58ee3f79e5 C-P (default)
1651 | |
1659 | |
1652 | o d0a85b2252a9 C-O (other)
1660 | o d0a85b2252a9 C-O (other)
1653 |/
1661 |/
1654 o 55a6f1c01b48 C-Z (other)
1662 o 55a6f1c01b48 C-Z (other)
1655 |
1663 |
1656 o 866a66e18630 C-N (default)
1664 o 866a66e18630 C-N (default)
1657 |\
1665 |\
1658 +---o 6fd3090135df C-M (default)
1666 +---o 6fd3090135df C-M (default)
1659 | |
1667 | |
1660 | o cac2cead0ff0 C-L (default)
1668 | o cac2cead0ff0 C-L (default)
1661 | |
1669 | |
1662 o | be705100c623 C-K (default)
1670 o | be705100c623 C-K (default)
1663 |\|
1671 |\|
1664 o | d603e2c0cdd7 C-E (default)
1672 o | d603e2c0cdd7 C-E (default)
1665 | |
1673 | |
1666 | o 59e76faf78bd C-D (default)
1674 | o 59e76faf78bd C-D (default)
1667 | |
1675 | |
1668 | | o 89420bf00fae C-J (default)
1676 | | o 89420bf00fae C-J (default)
1669 | | |
1677 | | |
1670 | | | o b35ed749f288 C-I (my-second-test-branch)
1678 | | | o b35ed749f288 C-I (my-second-test-branch)
1671 | | |/
1679 | | |/
1672 | | o 75d69cba5402 C-G (default)
1680 | | o 75d69cba5402 C-G (default)
1673 | | |
1681 | | |
1674 | | | o 833be552cfe6 C-H (my-first-test-branch)
1682 | | | o 833be552cfe6 C-H (my-first-test-branch)
1675 | | |/
1683 | | |/
1676 | | o d9e379a8c432 C-F (default)
1684 | | o d9e379a8c432 C-F (default)
1677 | | |
1685 | | |
1678 +---o 51c544a58128 C-C (default)
1686 +---o 51c544a58128 C-C (default)
1679 | |
1687 | |
1680 | o a9149a1428e2 C-B (default)
1688 | o a9149a1428e2 C-B (default)
1681 | |
1689 | |
1682 o | 98217d5a1659 C-A (default)
1690 o | 98217d5a1659 C-A (default)
1683 |/
1691 |/
1684 o 842e2fac6304 C-ROOT (default)
1692 o 842e2fac6304 C-ROOT (default)
1685
1693
1686
1694
1687 racing commit push a new head obsoleting the one touched by the raced push
1695 racing commit push a new head obsoleting the one touched by the raced push
1688 --------------------------------------------------------------------------
1696 --------------------------------------------------------------------------
1689
1697
1690 (mirror test case of the previous one
1698 (mirror test case of the previous one
1691
1699
1692 # a (raced branch default)
1700 # a (raced branch default)
1693 # |
1701 # |
1694 # ø⇠◔ b (racing)
1702 # ø⇠◔ b (racing)
1695 # |/
1703 # |/
1696 # *
1704 # *
1697
1705
1698 (resync-all)
1706 (resync-all)
1699
1707
1700 $ hg -R ./server pull ./client-racy
1708 $ hg -R ./server pull ./client-racy
1701 pulling from ./client-racy
1709 pulling from ./client-racy
1702 searching for changes
1710 searching for changes
1703 adding changesets
1711 adding changesets
1704 adding manifests
1712 adding manifests
1705 adding file changes
1713 adding file changes
1706 added 1 changesets with 1 changes to 1 files (+1 heads)
1714 added 1 changesets with 1 changes to 1 files (+1 heads)
1707 1 new obsolescence markers
1715 1 new obsolescence markers
1708 obsoleted 1 changesets
1716 obsoleted 1 changesets
1709 1 new orphan changesets
1717 1 new orphan changesets
1710 new changesets 720c5163ecf6 (1 drafts)
1718 new changesets 720c5163ecf6 (1 drafts)
1711 (run 'hg heads .' to see heads, 'hg merge' to merge)
1719 (run 'hg heads .' to see heads, 'hg merge' to merge)
1712 $ hg -R ./client-other pull
1720 $ hg -R ./client-other pull
1713 pulling from ssh://user@dummy/server
1721 pulling from ssh://user@dummy/server
1714 searching for changes
1722 searching for changes
1715 adding changesets
1723 adding changesets
1716 adding manifests
1724 adding manifests
1717 adding file changes
1725 adding file changes
1718 added 1 changesets with 1 changes to 1 files (+1 heads)
1726 added 1 changesets with 1 changes to 1 files (+1 heads)
1719 1 new obsolescence markers
1727 1 new obsolescence markers
1720 obsoleted 1 changesets
1728 obsoleted 1 changesets
1721 1 new orphan changesets
1729 1 new orphan changesets
1722 new changesets 720c5163ecf6 (1 drafts)
1730 new changesets 720c5163ecf6 (1 drafts)
1723 (run 'hg heads .' to see heads, 'hg merge' to merge)
1731 (run 'hg heads .' to see heads, 'hg merge' to merge)
1724 $ hg -R ./client-racy pull
1732 $ hg -R ./client-racy pull
1725 pulling from ssh://user@dummy/server
1733 pulling from ssh://user@dummy/server
1726 searching for changes
1734 searching for changes
1727 adding changesets
1735 adding changesets
1728 adding manifests
1736 adding manifests
1729 adding file changes
1737 adding file changes
1730 added 1 changesets with 0 changes to 0 files
1738 added 1 changesets with 0 changes to 0 files
1731 1 new orphan changesets
1739 1 new orphan changesets
1732 new changesets a98a47d8b85b (1 drafts)
1740 new changesets a98a47d8b85b (1 drafts)
1733 (run 'hg update' to get a working copy)
1741 (run 'hg update' to get a working copy)
1734
1742
1735 $ hg -R server debugobsolete
1743 $ hg -R server debugobsolete
1736 b0ee3d6f51bc4c0ca6d4f2907708027a6c376233 720c5163ecf64dcc6216bee2d62bf3edb1882499 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
1744 b0ee3d6f51bc4c0ca6d4f2907708027a6c376233 720c5163ecf64dcc6216bee2d62bf3edb1882499 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
1737 $ hg -R server graph
1745 $ hg -R server graph
1738 o 720c5163ecf6 C-V (default)
1746 o 720c5163ecf6 C-V (default)
1739 |
1747 |
1740 | * a98a47d8b85b C-U (default)
1748 | * a98a47d8b85b C-U (default)
1741 | |
1749 | |
1742 | x b0ee3d6f51bc C-Q (default)
1750 | x b0ee3d6f51bc C-Q (default)
1743 |/
1751 |/
1744 | o 3d57ed3c1091 C-T (other)
1752 | o 3d57ed3c1091 C-T (other)
1745 | |
1753 | |
1746 | o 2efd43f7b5ba C-S (default)
1754 | o 2efd43f7b5ba C-S (default)
1747 | |
1755 | |
1748 | | o de7b9e2ba3f6 C-R (other)
1756 | | o de7b9e2ba3f6 C-R (other)
1749 | |/
1757 | |/
1750 | o 1b58ee3f79e5 C-P (default)
1758 | o 1b58ee3f79e5 C-P (default)
1751 | |
1759 | |
1752 | o d0a85b2252a9 C-O (other)
1760 | o d0a85b2252a9 C-O (other)
1753 |/
1761 |/
1754 o 55a6f1c01b48 C-Z (other)
1762 o 55a6f1c01b48 C-Z (other)
1755 |
1763 |
1756 o 866a66e18630 C-N (default)
1764 o 866a66e18630 C-N (default)
1757 |\
1765 |\
1758 +---o 6fd3090135df C-M (default)
1766 +---o 6fd3090135df C-M (default)
1759 | |
1767 | |
1760 | o cac2cead0ff0 C-L (default)
1768 | o cac2cead0ff0 C-L (default)
1761 | |
1769 | |
1762 o | be705100c623 C-K (default)
1770 o | be705100c623 C-K (default)
1763 |\|
1771 |\|
1764 o | d603e2c0cdd7 C-E (default)
1772 o | d603e2c0cdd7 C-E (default)
1765 | |
1773 | |
1766 | o 59e76faf78bd C-D (default)
1774 | o 59e76faf78bd C-D (default)
1767 | |
1775 | |
1768 | | o 89420bf00fae C-J (default)
1776 | | o 89420bf00fae C-J (default)
1769 | | |
1777 | | |
1770 | | | o b35ed749f288 C-I (my-second-test-branch)
1778 | | | o b35ed749f288 C-I (my-second-test-branch)
1771 | | |/
1779 | | |/
1772 | | o 75d69cba5402 C-G (default)
1780 | | o 75d69cba5402 C-G (default)
1773 | | |
1781 | | |
1774 | | | o 833be552cfe6 C-H (my-first-test-branch)
1782 | | | o 833be552cfe6 C-H (my-first-test-branch)
1775 | | |/
1783 | | |/
1776 | | o d9e379a8c432 C-F (default)
1784 | | o d9e379a8c432 C-F (default)
1777 | | |
1785 | | |
1778 +---o 51c544a58128 C-C (default)
1786 +---o 51c544a58128 C-C (default)
1779 | |
1787 | |
1780 | o a9149a1428e2 C-B (default)
1788 | o a9149a1428e2 C-B (default)
1781 | |
1789 | |
1782 o | 98217d5a1659 C-A (default)
1790 o | 98217d5a1659 C-A (default)
1783 |/
1791 |/
1784 o 842e2fac6304 C-ROOT (default)
1792 o 842e2fac6304 C-ROOT (default)
1785
1793
1786
1794
1787 Creating changesets and markers
1795 Creating changesets and markers
1788
1796
1789 (new topo branch obsoleting that same head)
1797 (new topo branch obsoleting that same head)
1790
1798
1791 $ hg -R client-other/ up 'desc("C-Q")'
1799 $ hg -R client-other/ up 'desc("C-Q")'
1792 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1800 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1793 $ echo bbb >> client-other/a
1801 $ echo bbb >> client-other/a
1794 $ hg -R client-other/ branch --force default
1802 $ hg -R client-other/ branch --force default
1795 marked working directory as branch default
1803 marked working directory as branch default
1796 $ hg -R client-other/ commit -m "C-W"
1804 $ hg -R client-other/ commit -m "C-W"
1797 1 new orphan changesets
1805 1 new orphan changesets
1798 created new head
1806 created new head
1799 $ ID_V=`hg -R client-other log -T '{node}\n' -r 'desc("C-V")'`
1807 $ ID_V=`hg -R client-other log -T '{node}\n' -r 'desc("C-V")'`
1800 $ ID_W=`hg -R client-other log -T '{node}\n' -r 'desc("C-W")'`
1808 $ ID_W=`hg -R client-other log -T '{node}\n' -r 'desc("C-W")'`
1801 $ hg -R client-other debugobsolete $ID_V $ID_W
1809 $ hg -R client-other debugobsolete $ID_V $ID_W
1802 1 new obsolescence markers
1810 1 new obsolescence markers
1803 obsoleted 1 changesets
1811 obsoleted 1 changesets
1804
1812
1805 (continue the same head)
1813 (continue the same head)
1806
1814
1807 $ echo aaa >> client-racy/a
1815 $ echo aaa >> client-racy/a
1808 $ hg -R client-racy/ commit -m "C-X"
1816 $ hg -R client-racy/ commit -m "C-X"
1809
1817
1810 Pushing
1818 Pushing
1811
1819
1812 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
1820 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
1813
1821
1814 $ waiton $TESTTMP/readyfile
1822 $ waiton $TESTTMP/readyfile
1815
1823
1816 $ hg -R client-other push -fr 'tip' --new-branch
1824 $ hg -R client-other push -fr 'tip' --new-branch
1817 pushing to ssh://user@dummy/server
1825 pushing to ssh://user@dummy/server
1818 searching for changes
1826 searching for changes
1819 remote: adding changesets
1827 remote: adding changesets
1820 remote: adding manifests
1828 remote: adding manifests
1821 remote: adding file changes
1829 remote: adding file changes
1822 remote: added 1 changesets with 0 changes to 1 files (+1 heads)
1830 remote: added 1 changesets with 0 changes to 1 files (+1 heads)
1823 remote: 1 new obsolescence markers
1831 remote: 1 new obsolescence markers
1824 remote: obsoleted 1 changesets
1832 remote: obsoleted 1 changesets
1825 remote: 1 new orphan changesets
1833 remote: 1 new orphan changesets
1826
1834
1827 $ release $TESTTMP/watchfile
1835 $ release $TESTTMP/watchfile
1828
1836
1829 Check the result of the push
1837 Check the result of the push
1830
1838
1831 $ cat ./push-log
1839 $ cat ./push-log
1832 pushing to ssh://user@dummy/server
1840 pushing to ssh://user@dummy/server
1833 searching for changes
1841 searching for changes
1834 wrote ready: $TESTTMP/readyfile
1842 wrote ready: $TESTTMP/readyfile
1835 waiting on: $TESTTMP/watchfile
1843 waiting on: $TESTTMP/watchfile
1836 abort: push failed:
1844 abort: push failed:
1837 'remote repository changed while pushing - please try again'
1845 'remote repository changed while pushing - please try again'
1838
1846
1839 $ hg -R server debugobsolete
1847 $ hg -R server debugobsolete
1840 b0ee3d6f51bc4c0ca6d4f2907708027a6c376233 720c5163ecf64dcc6216bee2d62bf3edb1882499 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
1848 b0ee3d6f51bc4c0ca6d4f2907708027a6c376233 720c5163ecf64dcc6216bee2d62bf3edb1882499 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
1841 720c5163ecf64dcc6216bee2d62bf3edb1882499 39bc0598afe90ab18da460bafecc0fa953b77596 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
1849 720c5163ecf64dcc6216bee2d62bf3edb1882499 39bc0598afe90ab18da460bafecc0fa953b77596 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
1842 $ hg -R server graph --hidden
1850 $ hg -R server graph --hidden
1843 * 39bc0598afe9 C-W (default)
1851 * 39bc0598afe9 C-W (default)
1844 |
1852 |
1845 | * a98a47d8b85b C-U (default)
1853 | * a98a47d8b85b C-U (default)
1846 |/
1854 |/
1847 x b0ee3d6f51bc C-Q (default)
1855 x b0ee3d6f51bc C-Q (default)
1848 |
1856 |
1849 | o 3d57ed3c1091 C-T (other)
1857 | o 3d57ed3c1091 C-T (other)
1850 | |
1858 | |
1851 | o 2efd43f7b5ba C-S (default)
1859 | o 2efd43f7b5ba C-S (default)
1852 | |
1860 | |
1853 | | o de7b9e2ba3f6 C-R (other)
1861 | | o de7b9e2ba3f6 C-R (other)
1854 | |/
1862 | |/
1855 | o 1b58ee3f79e5 C-P (default)
1863 | o 1b58ee3f79e5 C-P (default)
1856 | |
1864 | |
1857 | o d0a85b2252a9 C-O (other)
1865 | o d0a85b2252a9 C-O (other)
1858 |/
1866 |/
1859 | x 720c5163ecf6 C-V (default)
1867 | x 720c5163ecf6 C-V (default)
1860 |/
1868 |/
1861 o 55a6f1c01b48 C-Z (other)
1869 o 55a6f1c01b48 C-Z (other)
1862 |
1870 |
1863 o 866a66e18630 C-N (default)
1871 o 866a66e18630 C-N (default)
1864 |\
1872 |\
1865 +---o 6fd3090135df C-M (default)
1873 +---o 6fd3090135df C-M (default)
1866 | |
1874 | |
1867 | o cac2cead0ff0 C-L (default)
1875 | o cac2cead0ff0 C-L (default)
1868 | |
1876 | |
1869 o | be705100c623 C-K (default)
1877 o | be705100c623 C-K (default)
1870 |\|
1878 |\|
1871 o | d603e2c0cdd7 C-E (default)
1879 o | d603e2c0cdd7 C-E (default)
1872 | |
1880 | |
1873 | o 59e76faf78bd C-D (default)
1881 | o 59e76faf78bd C-D (default)
1874 | |
1882 | |
1875 | | o 89420bf00fae C-J (default)
1883 | | o 89420bf00fae C-J (default)
1876 | | |
1884 | | |
1877 | | | o b35ed749f288 C-I (my-second-test-branch)
1885 | | | o b35ed749f288 C-I (my-second-test-branch)
1878 | | |/
1886 | | |/
1879 | | o 75d69cba5402 C-G (default)
1887 | | o 75d69cba5402 C-G (default)
1880 | | |
1888 | | |
1881 | | | o 833be552cfe6 C-H (my-first-test-branch)
1889 | | | o 833be552cfe6 C-H (my-first-test-branch)
1882 | | |/
1890 | | |/
1883 | | o d9e379a8c432 C-F (default)
1891 | | o d9e379a8c432 C-F (default)
1884 | | |
1892 | | |
1885 +---o 51c544a58128 C-C (default)
1893 +---o 51c544a58128 C-C (default)
1886 | |
1894 | |
1887 | o a9149a1428e2 C-B (default)
1895 | o a9149a1428e2 C-B (default)
1888 | |
1896 | |
1889 o | 98217d5a1659 C-A (default)
1897 o | 98217d5a1659 C-A (default)
1890 |/
1898 |/
1891 o 842e2fac6304 C-ROOT (default)
1899 o 842e2fac6304 C-ROOT (default)
1892
1900
General Comments 0
You need to be logged in to leave comments. Login now