##// END OF EJS Templates
comments: fix minor spelling issues found with spell checker
Mads Kiilerich -
r20549:2025315c default
parent child Browse files
Show More
@@ -1,706 +1,706 b''
1 # minirst.py - minimal reStructuredText parser
1 # minirst.py - minimal reStructuredText parser
2 #
2 #
3 # Copyright 2009, 2010 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2009, 2010 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """simplified reStructuredText parser.
8 """simplified reStructuredText parser.
9
9
10 This parser knows just enough about reStructuredText to parse the
10 This parser knows just enough about reStructuredText to parse the
11 Mercurial docstrings.
11 Mercurial docstrings.
12
12
13 It cheats in a major way: nested blocks are not really nested. They
13 It cheats in a major way: nested blocks are not really nested. They
14 are just indented blocks that look like they are nested. This relies
14 are just indented blocks that look like they are nested. This relies
15 on the user to keep the right indentation for the blocks.
15 on the user to keep the right indentation for the blocks.
16
16
17 Remember to update http://mercurial.selenic.com/wiki/HelpStyleGuide
17 Remember to update http://mercurial.selenic.com/wiki/HelpStyleGuide
18 when adding support for new constructs.
18 when adding support for new constructs.
19 """
19 """
20
20
21 import re
21 import re
22 import util, encoding
22 import util, encoding
23 from i18n import _
23 from i18n import _
24
24
25 import cgi
25 import cgi
26
26
27 def section(s):
27 def section(s):
28 return "%s\n%s\n\n" % (s, "\"" * encoding.colwidth(s))
28 return "%s\n%s\n\n" % (s, "\"" * encoding.colwidth(s))
29
29
30 def subsection(s):
30 def subsection(s):
31 return "%s\n%s\n\n" % (s, '=' * encoding.colwidth(s))
31 return "%s\n%s\n\n" % (s, '=' * encoding.colwidth(s))
32
32
33 def subsubsection(s):
33 def subsubsection(s):
34 return "%s\n%s\n\n" % (s, "-" * encoding.colwidth(s))
34 return "%s\n%s\n\n" % (s, "-" * encoding.colwidth(s))
35
35
36 def subsubsubsection(s):
36 def subsubsubsection(s):
37 return "%s\n%s\n\n" % (s, "." * encoding.colwidth(s))
37 return "%s\n%s\n\n" % (s, "." * encoding.colwidth(s))
38
38
39 def replace(text, substs):
39 def replace(text, substs):
40 '''
40 '''
41 Apply a list of (find, replace) pairs to a text.
41 Apply a list of (find, replace) pairs to a text.
42
42
43 >>> replace("foo bar", [('f', 'F'), ('b', 'B')])
43 >>> replace("foo bar", [('f', 'F'), ('b', 'B')])
44 'Foo Bar'
44 'Foo Bar'
45 >>> encoding.encoding = 'latin1'
45 >>> encoding.encoding = 'latin1'
46 >>> replace('\\x81\\\\', [('\\\\', '/')])
46 >>> replace('\\x81\\\\', [('\\\\', '/')])
47 '\\x81/'
47 '\\x81/'
48 >>> encoding.encoding = 'shiftjis'
48 >>> encoding.encoding = 'shiftjis'
49 >>> replace('\\x81\\\\', [('\\\\', '/')])
49 >>> replace('\\x81\\\\', [('\\\\', '/')])
50 '\\x81\\\\'
50 '\\x81\\\\'
51 '''
51 '''
52
52
53 # some character encodings (cp932 for Japanese, at least) use
53 # some character encodings (cp932 for Japanese, at least) use
54 # ASCII characters other than control/alphabet/digit as a part of
54 # ASCII characters other than control/alphabet/digit as a part of
55 # multi-bytes characters, so direct replacing with such characters
55 # multi-bytes characters, so direct replacing with such characters
56 # on strings in local encoding causes invalid byte sequences.
56 # on strings in local encoding causes invalid byte sequences.
57 utext = text.decode(encoding.encoding)
57 utext = text.decode(encoding.encoding)
58 for f, t in substs:
58 for f, t in substs:
59 utext = utext.replace(f, t)
59 utext = utext.replace(f, t)
60 return utext.encode(encoding.encoding)
60 return utext.encode(encoding.encoding)
61
61
62 _blockre = re.compile(r"\n(?:\s*\n)+")
62 _blockre = re.compile(r"\n(?:\s*\n)+")
63
63
64 def findblocks(text):
64 def findblocks(text):
65 """Find continuous blocks of lines in text.
65 """Find continuous blocks of lines in text.
66
66
67 Returns a list of dictionaries representing the blocks. Each block
67 Returns a list of dictionaries representing the blocks. Each block
68 has an 'indent' field and a 'lines' field.
68 has an 'indent' field and a 'lines' field.
69 """
69 """
70 blocks = []
70 blocks = []
71 for b in _blockre.split(text.lstrip('\n').rstrip()):
71 for b in _blockre.split(text.lstrip('\n').rstrip()):
72 lines = b.splitlines()
72 lines = b.splitlines()
73 if lines:
73 if lines:
74 indent = min((len(l) - len(l.lstrip())) for l in lines)
74 indent = min((len(l) - len(l.lstrip())) for l in lines)
75 lines = [l[indent:] for l in lines]
75 lines = [l[indent:] for l in lines]
76 blocks.append(dict(indent=indent, lines=lines))
76 blocks.append(dict(indent=indent, lines=lines))
77 return blocks
77 return blocks
78
78
79 def findliteralblocks(blocks):
79 def findliteralblocks(blocks):
80 """Finds literal blocks and adds a 'type' field to the blocks.
80 """Finds literal blocks and adds a 'type' field to the blocks.
81
81
82 Literal blocks are given the type 'literal', all other blocks are
82 Literal blocks are given the type 'literal', all other blocks are
83 given type the 'paragraph'.
83 given type the 'paragraph'.
84 """
84 """
85 i = 0
85 i = 0
86 while i < len(blocks):
86 while i < len(blocks):
87 # Searching for a block that looks like this:
87 # Searching for a block that looks like this:
88 #
88 #
89 # +------------------------------+
89 # +------------------------------+
90 # | paragraph |
90 # | paragraph |
91 # | (ends with "::") |
91 # | (ends with "::") |
92 # +------------------------------+
92 # +------------------------------+
93 # +---------------------------+
93 # +---------------------------+
94 # | indented literal block |
94 # | indented literal block |
95 # +---------------------------+
95 # +---------------------------+
96 blocks[i]['type'] = 'paragraph'
96 blocks[i]['type'] = 'paragraph'
97 if blocks[i]['lines'][-1].endswith('::') and i + 1 < len(blocks):
97 if blocks[i]['lines'][-1].endswith('::') and i + 1 < len(blocks):
98 indent = blocks[i]['indent']
98 indent = blocks[i]['indent']
99 adjustment = blocks[i + 1]['indent'] - indent
99 adjustment = blocks[i + 1]['indent'] - indent
100
100
101 if blocks[i]['lines'] == ['::']:
101 if blocks[i]['lines'] == ['::']:
102 # Expanded form: remove block
102 # Expanded form: remove block
103 del blocks[i]
103 del blocks[i]
104 i -= 1
104 i -= 1
105 elif blocks[i]['lines'][-1].endswith(' ::'):
105 elif blocks[i]['lines'][-1].endswith(' ::'):
106 # Partially minimized form: remove space and both
106 # Partially minimized form: remove space and both
107 # colons.
107 # colons.
108 blocks[i]['lines'][-1] = blocks[i]['lines'][-1][:-3]
108 blocks[i]['lines'][-1] = blocks[i]['lines'][-1][:-3]
109 elif len(blocks[i]['lines']) == 1 and \
109 elif len(blocks[i]['lines']) == 1 and \
110 blocks[i]['lines'][0].lstrip(' ').startswith('.. ') and \
110 blocks[i]['lines'][0].lstrip(' ').startswith('.. ') and \
111 blocks[i]['lines'][0].find(' ', 3) == -1:
111 blocks[i]['lines'][0].find(' ', 3) == -1:
112 # directive on its onw line, not a literal block
112 # directive on its own line, not a literal block
113 i += 1
113 i += 1
114 continue
114 continue
115 else:
115 else:
116 # Fully minimized form: remove just one colon.
116 # Fully minimized form: remove just one colon.
117 blocks[i]['lines'][-1] = blocks[i]['lines'][-1][:-1]
117 blocks[i]['lines'][-1] = blocks[i]['lines'][-1][:-1]
118
118
119 # List items are formatted with a hanging indent. We must
119 # List items are formatted with a hanging indent. We must
120 # correct for this here while we still have the original
120 # correct for this here while we still have the original
121 # information on the indentation of the subsequent literal
121 # information on the indentation of the subsequent literal
122 # blocks available.
122 # blocks available.
123 m = _bulletre.match(blocks[i]['lines'][0])
123 m = _bulletre.match(blocks[i]['lines'][0])
124 if m:
124 if m:
125 indent += m.end()
125 indent += m.end()
126 adjustment -= m.end()
126 adjustment -= m.end()
127
127
128 # Mark the following indented blocks.
128 # Mark the following indented blocks.
129 while i + 1 < len(blocks) and blocks[i + 1]['indent'] > indent:
129 while i + 1 < len(blocks) and blocks[i + 1]['indent'] > indent:
130 blocks[i + 1]['type'] = 'literal'
130 blocks[i + 1]['type'] = 'literal'
131 blocks[i + 1]['indent'] -= adjustment
131 blocks[i + 1]['indent'] -= adjustment
132 i += 1
132 i += 1
133 i += 1
133 i += 1
134 return blocks
134 return blocks
135
135
136 _bulletre = re.compile(r'(-|[0-9A-Za-z]+\.|\(?[0-9A-Za-z]+\)|\|) ')
136 _bulletre = re.compile(r'(-|[0-9A-Za-z]+\.|\(?[0-9A-Za-z]+\)|\|) ')
137 _optionre = re.compile(r'^(-([a-zA-Z0-9]), )?(--[a-z0-9-]+)'
137 _optionre = re.compile(r'^(-([a-zA-Z0-9]), )?(--[a-z0-9-]+)'
138 r'((.*) +)(.*)$')
138 r'((.*) +)(.*)$')
139 _fieldre = re.compile(r':(?![: ])([^:]*)(?<! ):[ ]+(.*)')
139 _fieldre = re.compile(r':(?![: ])([^:]*)(?<! ):[ ]+(.*)')
140 _definitionre = re.compile(r'[^ ]')
140 _definitionre = re.compile(r'[^ ]')
141 _tablere = re.compile(r'(=+\s+)*=+')
141 _tablere = re.compile(r'(=+\s+)*=+')
142
142
143 def splitparagraphs(blocks):
143 def splitparagraphs(blocks):
144 """Split paragraphs into lists."""
144 """Split paragraphs into lists."""
145 # Tuples with (list type, item regexp, single line items?). Order
145 # Tuples with (list type, item regexp, single line items?). Order
146 # matters: definition lists has the least specific regexp and must
146 # matters: definition lists has the least specific regexp and must
147 # come last.
147 # come last.
148 listtypes = [('bullet', _bulletre, True),
148 listtypes = [('bullet', _bulletre, True),
149 ('option', _optionre, True),
149 ('option', _optionre, True),
150 ('field', _fieldre, True),
150 ('field', _fieldre, True),
151 ('definition', _definitionre, False)]
151 ('definition', _definitionre, False)]
152
152
153 def match(lines, i, itemre, singleline):
153 def match(lines, i, itemre, singleline):
154 """Does itemre match an item at line i?
154 """Does itemre match an item at line i?
155
155
156 A list item can be followed by an indented line or another list
156 A list item can be followed by an indented line or another list
157 item (but only if singleline is True).
157 item (but only if singleline is True).
158 """
158 """
159 line1 = lines[i]
159 line1 = lines[i]
160 line2 = i + 1 < len(lines) and lines[i + 1] or ''
160 line2 = i + 1 < len(lines) and lines[i + 1] or ''
161 if not itemre.match(line1):
161 if not itemre.match(line1):
162 return False
162 return False
163 if singleline:
163 if singleline:
164 return line2 == '' or line2[0] == ' ' or itemre.match(line2)
164 return line2 == '' or line2[0] == ' ' or itemre.match(line2)
165 else:
165 else:
166 return line2.startswith(' ')
166 return line2.startswith(' ')
167
167
168 i = 0
168 i = 0
169 while i < len(blocks):
169 while i < len(blocks):
170 if blocks[i]['type'] == 'paragraph':
170 if blocks[i]['type'] == 'paragraph':
171 lines = blocks[i]['lines']
171 lines = blocks[i]['lines']
172 for type, itemre, singleline in listtypes:
172 for type, itemre, singleline in listtypes:
173 if match(lines, 0, itemre, singleline):
173 if match(lines, 0, itemre, singleline):
174 items = []
174 items = []
175 for j, line in enumerate(lines):
175 for j, line in enumerate(lines):
176 if match(lines, j, itemre, singleline):
176 if match(lines, j, itemre, singleline):
177 items.append(dict(type=type, lines=[],
177 items.append(dict(type=type, lines=[],
178 indent=blocks[i]['indent']))
178 indent=blocks[i]['indent']))
179 items[-1]['lines'].append(line)
179 items[-1]['lines'].append(line)
180 blocks[i:i + 1] = items
180 blocks[i:i + 1] = items
181 break
181 break
182 i += 1
182 i += 1
183 return blocks
183 return blocks
184
184
185 _fieldwidth = 14
185 _fieldwidth = 14
186
186
187 def updatefieldlists(blocks):
187 def updatefieldlists(blocks):
188 """Find key for field lists."""
188 """Find key for field lists."""
189 i = 0
189 i = 0
190 while i < len(blocks):
190 while i < len(blocks):
191 if blocks[i]['type'] != 'field':
191 if blocks[i]['type'] != 'field':
192 i += 1
192 i += 1
193 continue
193 continue
194
194
195 j = i
195 j = i
196 while j < len(blocks) and blocks[j]['type'] == 'field':
196 while j < len(blocks) and blocks[j]['type'] == 'field':
197 m = _fieldre.match(blocks[j]['lines'][0])
197 m = _fieldre.match(blocks[j]['lines'][0])
198 key, rest = m.groups()
198 key, rest = m.groups()
199 blocks[j]['lines'][0] = rest
199 blocks[j]['lines'][0] = rest
200 blocks[j]['key'] = key
200 blocks[j]['key'] = key
201 j += 1
201 j += 1
202
202
203 i = j + 1
203 i = j + 1
204
204
205 return blocks
205 return blocks
206
206
207 def updateoptionlists(blocks):
207 def updateoptionlists(blocks):
208 i = 0
208 i = 0
209 while i < len(blocks):
209 while i < len(blocks):
210 if blocks[i]['type'] != 'option':
210 if blocks[i]['type'] != 'option':
211 i += 1
211 i += 1
212 continue
212 continue
213
213
214 optstrwidth = 0
214 optstrwidth = 0
215 j = i
215 j = i
216 while j < len(blocks) and blocks[j]['type'] == 'option':
216 while j < len(blocks) and blocks[j]['type'] == 'option':
217 m = _optionre.match(blocks[j]['lines'][0])
217 m = _optionre.match(blocks[j]['lines'][0])
218
218
219 shortoption = m.group(2)
219 shortoption = m.group(2)
220 group3 = m.group(3)
220 group3 = m.group(3)
221 longoption = group3[2:].strip()
221 longoption = group3[2:].strip()
222 desc = m.group(6).strip()
222 desc = m.group(6).strip()
223 longoptionarg = m.group(5).strip()
223 longoptionarg = m.group(5).strip()
224 blocks[j]['lines'][0] = desc
224 blocks[j]['lines'][0] = desc
225
225
226 noshortop = ''
226 noshortop = ''
227 if not shortoption:
227 if not shortoption:
228 noshortop = ' '
228 noshortop = ' '
229
229
230 opt = "%s%s" % (shortoption and "-%s " % shortoption or '',
230 opt = "%s%s" % (shortoption and "-%s " % shortoption or '',
231 ("%s--%s %s") % (noshortop, longoption,
231 ("%s--%s %s") % (noshortop, longoption,
232 longoptionarg))
232 longoptionarg))
233 opt = opt.rstrip()
233 opt = opt.rstrip()
234 blocks[j]['optstr'] = opt
234 blocks[j]['optstr'] = opt
235 optstrwidth = max(optstrwidth, encoding.colwidth(opt))
235 optstrwidth = max(optstrwidth, encoding.colwidth(opt))
236 j += 1
236 j += 1
237
237
238 for block in blocks[i:j]:
238 for block in blocks[i:j]:
239 block['optstrwidth'] = optstrwidth
239 block['optstrwidth'] = optstrwidth
240 i = j + 1
240 i = j + 1
241 return blocks
241 return blocks
242
242
243 def prunecontainers(blocks, keep):
243 def prunecontainers(blocks, keep):
244 """Prune unwanted containers.
244 """Prune unwanted containers.
245
245
246 The blocks must have a 'type' field, i.e., they should have been
246 The blocks must have a 'type' field, i.e., they should have been
247 run through findliteralblocks first.
247 run through findliteralblocks first.
248 """
248 """
249 pruned = []
249 pruned = []
250 i = 0
250 i = 0
251 while i + 1 < len(blocks):
251 while i + 1 < len(blocks):
252 # Searching for a block that looks like this:
252 # Searching for a block that looks like this:
253 #
253 #
254 # +-------+---------------------------+
254 # +-------+---------------------------+
255 # | ".. container ::" type |
255 # | ".. container ::" type |
256 # +---+ |
256 # +---+ |
257 # | blocks |
257 # | blocks |
258 # +-------------------------------+
258 # +-------------------------------+
259 if (blocks[i]['type'] == 'paragraph' and
259 if (blocks[i]['type'] == 'paragraph' and
260 blocks[i]['lines'][0].startswith('.. container::')):
260 blocks[i]['lines'][0].startswith('.. container::')):
261 indent = blocks[i]['indent']
261 indent = blocks[i]['indent']
262 adjustment = blocks[i + 1]['indent'] - indent
262 adjustment = blocks[i + 1]['indent'] - indent
263 containertype = blocks[i]['lines'][0][15:]
263 containertype = blocks[i]['lines'][0][15:]
264 prune = containertype not in keep
264 prune = containertype not in keep
265 if prune:
265 if prune:
266 pruned.append(containertype)
266 pruned.append(containertype)
267
267
268 # Always delete "..container:: type" block
268 # Always delete "..container:: type" block
269 del blocks[i]
269 del blocks[i]
270 j = i
270 j = i
271 i -= 1
271 i -= 1
272 while j < len(blocks) and blocks[j]['indent'] > indent:
272 while j < len(blocks) and blocks[j]['indent'] > indent:
273 if prune:
273 if prune:
274 del blocks[j]
274 del blocks[j]
275 else:
275 else:
276 blocks[j]['indent'] -= adjustment
276 blocks[j]['indent'] -= adjustment
277 j += 1
277 j += 1
278 i += 1
278 i += 1
279 return blocks, pruned
279 return blocks, pruned
280
280
281 _sectionre = re.compile(r"""^([-=`:.'"~^_*+#])\1+$""")
281 _sectionre = re.compile(r"""^([-=`:.'"~^_*+#])\1+$""")
282
282
283 def findtables(blocks):
283 def findtables(blocks):
284 '''Find simple tables
284 '''Find simple tables
285
285
286 Only simple one-line table elements are supported
286 Only simple one-line table elements are supported
287 '''
287 '''
288
288
289 for block in blocks:
289 for block in blocks:
290 # Searching for a block that looks like this:
290 # Searching for a block that looks like this:
291 #
291 #
292 # === ==== ===
292 # === ==== ===
293 # A B C
293 # A B C
294 # === ==== === <- optional
294 # === ==== === <- optional
295 # 1 2 3
295 # 1 2 3
296 # x y z
296 # x y z
297 # === ==== ===
297 # === ==== ===
298 if (block['type'] == 'paragraph' and
298 if (block['type'] == 'paragraph' and
299 len(block['lines']) > 2 and
299 len(block['lines']) > 2 and
300 _tablere.match(block['lines'][0]) and
300 _tablere.match(block['lines'][0]) and
301 block['lines'][0] == block['lines'][-1]):
301 block['lines'][0] == block['lines'][-1]):
302 block['type'] = 'table'
302 block['type'] = 'table'
303 block['header'] = False
303 block['header'] = False
304 div = block['lines'][0]
304 div = block['lines'][0]
305
305
306 # column markers are ASCII so we can calculate column
306 # column markers are ASCII so we can calculate column
307 # position in bytes
307 # position in bytes
308 columns = [x for x in xrange(len(div))
308 columns = [x for x in xrange(len(div))
309 if div[x] == '=' and (x == 0 or div[x - 1] == ' ')]
309 if div[x] == '=' and (x == 0 or div[x - 1] == ' ')]
310 rows = []
310 rows = []
311 for l in block['lines'][1:-1]:
311 for l in block['lines'][1:-1]:
312 if l == div:
312 if l == div:
313 block['header'] = True
313 block['header'] = True
314 continue
314 continue
315 row = []
315 row = []
316 # we measure columns not in bytes or characters but in
316 # we measure columns not in bytes or characters but in
317 # colwidth which makes things tricky
317 # colwidth which makes things tricky
318 pos = columns[0] # leading whitespace is bytes
318 pos = columns[0] # leading whitespace is bytes
319 for n, start in enumerate(columns):
319 for n, start in enumerate(columns):
320 if n + 1 < len(columns):
320 if n + 1 < len(columns):
321 width = columns[n + 1] - start
321 width = columns[n + 1] - start
322 v = encoding.getcols(l, pos, width) # gather columns
322 v = encoding.getcols(l, pos, width) # gather columns
323 pos += len(v) # calculate byte position of end
323 pos += len(v) # calculate byte position of end
324 row.append(v.strip())
324 row.append(v.strip())
325 else:
325 else:
326 row.append(l[pos:].strip())
326 row.append(l[pos:].strip())
327 rows.append(row)
327 rows.append(row)
328
328
329 block['table'] = rows
329 block['table'] = rows
330
330
331 return blocks
331 return blocks
332
332
333 def findsections(blocks):
333 def findsections(blocks):
334 """Finds sections.
334 """Finds sections.
335
335
336 The blocks must have a 'type' field, i.e., they should have been
336 The blocks must have a 'type' field, i.e., they should have been
337 run through findliteralblocks first.
337 run through findliteralblocks first.
338 """
338 """
339 for block in blocks:
339 for block in blocks:
340 # Searching for a block that looks like this:
340 # Searching for a block that looks like this:
341 #
341 #
342 # +------------------------------+
342 # +------------------------------+
343 # | Section title |
343 # | Section title |
344 # | ------------- |
344 # | ------------- |
345 # +------------------------------+
345 # +------------------------------+
346 if (block['type'] == 'paragraph' and
346 if (block['type'] == 'paragraph' and
347 len(block['lines']) == 2 and
347 len(block['lines']) == 2 and
348 encoding.colwidth(block['lines'][0]) == len(block['lines'][1]) and
348 encoding.colwidth(block['lines'][0]) == len(block['lines'][1]) and
349 _sectionre.match(block['lines'][1])):
349 _sectionre.match(block['lines'][1])):
350 block['underline'] = block['lines'][1][0]
350 block['underline'] = block['lines'][1][0]
351 block['type'] = 'section'
351 block['type'] = 'section'
352 del block['lines'][1]
352 del block['lines'][1]
353 return blocks
353 return blocks
354
354
355 def inlineliterals(blocks):
355 def inlineliterals(blocks):
356 substs = [('``', '"')]
356 substs = [('``', '"')]
357 for b in blocks:
357 for b in blocks:
358 if b['type'] in ('paragraph', 'section'):
358 if b['type'] in ('paragraph', 'section'):
359 b['lines'] = [replace(l, substs) for l in b['lines']]
359 b['lines'] = [replace(l, substs) for l in b['lines']]
360 return blocks
360 return blocks
361
361
362 def hgrole(blocks):
362 def hgrole(blocks):
363 substs = [(':hg:`', '"hg '), ('`', '"')]
363 substs = [(':hg:`', '"hg '), ('`', '"')]
364 for b in blocks:
364 for b in blocks:
365 if b['type'] in ('paragraph', 'section'):
365 if b['type'] in ('paragraph', 'section'):
366 # Turn :hg:`command` into "hg command". This also works
366 # Turn :hg:`command` into "hg command". This also works
367 # when there is a line break in the command and relies on
367 # when there is a line break in the command and relies on
368 # the fact that we have no stray back-quotes in the input
368 # the fact that we have no stray back-quotes in the input
369 # (run the blocks through inlineliterals first).
369 # (run the blocks through inlineliterals first).
370 b['lines'] = [replace(l, substs) for l in b['lines']]
370 b['lines'] = [replace(l, substs) for l in b['lines']]
371 return blocks
371 return blocks
372
372
373 def addmargins(blocks):
373 def addmargins(blocks):
374 """Adds empty blocks for vertical spacing.
374 """Adds empty blocks for vertical spacing.
375
375
376 This groups bullets, options, and definitions together with no vertical
376 This groups bullets, options, and definitions together with no vertical
377 space between them, and adds an empty block between all other blocks.
377 space between them, and adds an empty block between all other blocks.
378 """
378 """
379 i = 1
379 i = 1
380 while i < len(blocks):
380 while i < len(blocks):
381 if (blocks[i]['type'] == blocks[i - 1]['type'] and
381 if (blocks[i]['type'] == blocks[i - 1]['type'] and
382 blocks[i]['type'] in ('bullet', 'option', 'field')):
382 blocks[i]['type'] in ('bullet', 'option', 'field')):
383 i += 1
383 i += 1
384 elif not blocks[i - 1]['lines']:
384 elif not blocks[i - 1]['lines']:
385 # no lines in previous block, do not seperate
385 # no lines in previous block, do not separate
386 i += 1
386 i += 1
387 else:
387 else:
388 blocks.insert(i, dict(lines=[''], indent=0, type='margin'))
388 blocks.insert(i, dict(lines=[''], indent=0, type='margin'))
389 i += 2
389 i += 2
390 return blocks
390 return blocks
391
391
392 def prunecomments(blocks):
392 def prunecomments(blocks):
393 """Remove comments."""
393 """Remove comments."""
394 i = 0
394 i = 0
395 while i < len(blocks):
395 while i < len(blocks):
396 b = blocks[i]
396 b = blocks[i]
397 if b['type'] == 'paragraph' and (b['lines'][0].startswith('.. ') or
397 if b['type'] == 'paragraph' and (b['lines'][0].startswith('.. ') or
398 b['lines'] == ['..']):
398 b['lines'] == ['..']):
399 del blocks[i]
399 del blocks[i]
400 if i < len(blocks) and blocks[i]['type'] == 'margin':
400 if i < len(blocks) and blocks[i]['type'] == 'margin':
401 del blocks[i]
401 del blocks[i]
402 else:
402 else:
403 i += 1
403 i += 1
404 return blocks
404 return blocks
405
405
406 _admonitionre = re.compile(r"\.\. (admonition|attention|caution|danger|"
406 _admonitionre = re.compile(r"\.\. (admonition|attention|caution|danger|"
407 r"error|hint|important|note|tip|warning)::",
407 r"error|hint|important|note|tip|warning)::",
408 flags=re.IGNORECASE)
408 flags=re.IGNORECASE)
409
409
410 def findadmonitions(blocks):
410 def findadmonitions(blocks):
411 """
411 """
412 Makes the type of the block an admonition block if
412 Makes the type of the block an admonition block if
413 the first line is an admonition directive
413 the first line is an admonition directive
414 """
414 """
415 i = 0
415 i = 0
416 while i < len(blocks):
416 while i < len(blocks):
417 m = _admonitionre.match(blocks[i]['lines'][0])
417 m = _admonitionre.match(blocks[i]['lines'][0])
418 if m:
418 if m:
419 blocks[i]['type'] = 'admonition'
419 blocks[i]['type'] = 'admonition'
420 admonitiontitle = blocks[i]['lines'][0][3:m.end() - 2].lower()
420 admonitiontitle = blocks[i]['lines'][0][3:m.end() - 2].lower()
421
421
422 firstline = blocks[i]['lines'][0][m.end() + 1:]
422 firstline = blocks[i]['lines'][0][m.end() + 1:]
423 if firstline:
423 if firstline:
424 blocks[i]['lines'].insert(1, ' ' + firstline)
424 blocks[i]['lines'].insert(1, ' ' + firstline)
425
425
426 blocks[i]['admonitiontitle'] = admonitiontitle
426 blocks[i]['admonitiontitle'] = admonitiontitle
427 del blocks[i]['lines'][0]
427 del blocks[i]['lines'][0]
428 i = i + 1
428 i = i + 1
429 return blocks
429 return blocks
430
430
431 _admonitiontitles = {'attention': _('Attention:'),
431 _admonitiontitles = {'attention': _('Attention:'),
432 'caution': _('Caution:'),
432 'caution': _('Caution:'),
433 'danger': _('!Danger!') ,
433 'danger': _('!Danger!') ,
434 'error': _('Error:'),
434 'error': _('Error:'),
435 'hint': _('Hint:'),
435 'hint': _('Hint:'),
436 'important': _('Important:'),
436 'important': _('Important:'),
437 'note': _('Note:'),
437 'note': _('Note:'),
438 'tip': _('Tip:'),
438 'tip': _('Tip:'),
439 'warning': _('Warning!')}
439 'warning': _('Warning!')}
440
440
441 def formatoption(block, width):
441 def formatoption(block, width):
442 desc = ' '.join(map(str.strip, block['lines']))
442 desc = ' '.join(map(str.strip, block['lines']))
443 colwidth = encoding.colwidth(block['optstr'])
443 colwidth = encoding.colwidth(block['optstr'])
444 usablewidth = width - 1
444 usablewidth = width - 1
445 hanging = block['optstrwidth']
445 hanging = block['optstrwidth']
446 initindent = '%s%s ' % (block['optstr'], ' ' * ((hanging - colwidth)))
446 initindent = '%s%s ' % (block['optstr'], ' ' * ((hanging - colwidth)))
447 hangindent = ' ' * (encoding.colwidth(initindent) + 1)
447 hangindent = ' ' * (encoding.colwidth(initindent) + 1)
448 return ' %s\n' % (util.wrap(desc, usablewidth,
448 return ' %s\n' % (util.wrap(desc, usablewidth,
449 initindent=initindent,
449 initindent=initindent,
450 hangindent=hangindent))
450 hangindent=hangindent))
451
451
452 def formatblock(block, width):
452 def formatblock(block, width):
453 """Format a block according to width."""
453 """Format a block according to width."""
454 if width <= 0:
454 if width <= 0:
455 width = 78
455 width = 78
456 indent = ' ' * block['indent']
456 indent = ' ' * block['indent']
457 if block['type'] == 'admonition':
457 if block['type'] == 'admonition':
458 admonition = _admonitiontitles[block['admonitiontitle']]
458 admonition = _admonitiontitles[block['admonitiontitle']]
459 if not block['lines']:
459 if not block['lines']:
460 return indent + admonition + '\n'
460 return indent + admonition + '\n'
461 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
461 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
462
462
463 defindent = indent + hang * ' '
463 defindent = indent + hang * ' '
464 text = ' '.join(map(str.strip, block['lines']))
464 text = ' '.join(map(str.strip, block['lines']))
465 return '%s\n%s\n' % (indent + admonition,
465 return '%s\n%s\n' % (indent + admonition,
466 util.wrap(text, width=width,
466 util.wrap(text, width=width,
467 initindent=defindent,
467 initindent=defindent,
468 hangindent=defindent))
468 hangindent=defindent))
469 if block['type'] == 'margin':
469 if block['type'] == 'margin':
470 return '\n'
470 return '\n'
471 if block['type'] == 'literal':
471 if block['type'] == 'literal':
472 indent += ' '
472 indent += ' '
473 return indent + ('\n' + indent).join(block['lines']) + '\n'
473 return indent + ('\n' + indent).join(block['lines']) + '\n'
474 if block['type'] == 'section':
474 if block['type'] == 'section':
475 underline = encoding.colwidth(block['lines'][0]) * block['underline']
475 underline = encoding.colwidth(block['lines'][0]) * block['underline']
476 return "%s%s\n%s%s\n" % (indent, block['lines'][0],indent, underline)
476 return "%s%s\n%s%s\n" % (indent, block['lines'][0],indent, underline)
477 if block['type'] == 'table':
477 if block['type'] == 'table':
478 table = block['table']
478 table = block['table']
479 # compute column widths
479 # compute column widths
480 widths = [max([encoding.colwidth(e) for e in c]) for c in zip(*table)]
480 widths = [max([encoding.colwidth(e) for e in c]) for c in zip(*table)]
481 text = ''
481 text = ''
482 span = sum(widths) + len(widths) - 1
482 span = sum(widths) + len(widths) - 1
483 indent = ' ' * block['indent']
483 indent = ' ' * block['indent']
484 hang = ' ' * (len(indent) + span - widths[-1])
484 hang = ' ' * (len(indent) + span - widths[-1])
485
485
486 for row in table:
486 for row in table:
487 l = []
487 l = []
488 for w, v in zip(widths, row):
488 for w, v in zip(widths, row):
489 pad = ' ' * (w - encoding.colwidth(v))
489 pad = ' ' * (w - encoding.colwidth(v))
490 l.append(v + pad)
490 l.append(v + pad)
491 l = ' '.join(l)
491 l = ' '.join(l)
492 l = util.wrap(l, width=width, initindent=indent, hangindent=hang)
492 l = util.wrap(l, width=width, initindent=indent, hangindent=hang)
493 if not text and block['header']:
493 if not text and block['header']:
494 text = l + '\n' + indent + '-' * (min(width, span)) + '\n'
494 text = l + '\n' + indent + '-' * (min(width, span)) + '\n'
495 else:
495 else:
496 text += l + "\n"
496 text += l + "\n"
497 return text
497 return text
498 if block['type'] == 'definition':
498 if block['type'] == 'definition':
499 term = indent + block['lines'][0]
499 term = indent + block['lines'][0]
500 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
500 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
501 defindent = indent + hang * ' '
501 defindent = indent + hang * ' '
502 text = ' '.join(map(str.strip, block['lines'][1:]))
502 text = ' '.join(map(str.strip, block['lines'][1:]))
503 return '%s\n%s\n' % (term, util.wrap(text, width=width,
503 return '%s\n%s\n' % (term, util.wrap(text, width=width,
504 initindent=defindent,
504 initindent=defindent,
505 hangindent=defindent))
505 hangindent=defindent))
506 subindent = indent
506 subindent = indent
507 if block['type'] == 'bullet':
507 if block['type'] == 'bullet':
508 if block['lines'][0].startswith('| '):
508 if block['lines'][0].startswith('| '):
509 # Remove bullet for line blocks and add no extra
509 # Remove bullet for line blocks and add no extra
510 # indention.
510 # indention.
511 block['lines'][0] = block['lines'][0][2:]
511 block['lines'][0] = block['lines'][0][2:]
512 else:
512 else:
513 m = _bulletre.match(block['lines'][0])
513 m = _bulletre.match(block['lines'][0])
514 subindent = indent + m.end() * ' '
514 subindent = indent + m.end() * ' '
515 elif block['type'] == 'field':
515 elif block['type'] == 'field':
516 key = block['key']
516 key = block['key']
517 subindent = indent + _fieldwidth * ' '
517 subindent = indent + _fieldwidth * ' '
518 if len(key) + 2 > _fieldwidth:
518 if len(key) + 2 > _fieldwidth:
519 # key too large, use full line width
519 # key too large, use full line width
520 key = key.ljust(width)
520 key = key.ljust(width)
521 else:
521 else:
522 # key fits within field width
522 # key fits within field width
523 key = key.ljust(_fieldwidth)
523 key = key.ljust(_fieldwidth)
524 block['lines'][0] = key + block['lines'][0]
524 block['lines'][0] = key + block['lines'][0]
525 elif block['type'] == 'option':
525 elif block['type'] == 'option':
526 return formatoption(block, width)
526 return formatoption(block, width)
527
527
528 text = ' '.join(map(str.strip, block['lines']))
528 text = ' '.join(map(str.strip, block['lines']))
529 return util.wrap(text, width=width,
529 return util.wrap(text, width=width,
530 initindent=indent,
530 initindent=indent,
531 hangindent=subindent) + '\n'
531 hangindent=subindent) + '\n'
532
532
533 def formathtml(blocks):
533 def formathtml(blocks):
534 """Format RST blocks as HTML"""
534 """Format RST blocks as HTML"""
535
535
536 out = []
536 out = []
537 headernest = ''
537 headernest = ''
538 listnest = []
538 listnest = []
539
539
540 def escape(s):
540 def escape(s):
541 return cgi.escape(s, True)
541 return cgi.escape(s, True)
542
542
543 def openlist(start, level):
543 def openlist(start, level):
544 if not listnest or listnest[-1][0] != start:
544 if not listnest or listnest[-1][0] != start:
545 listnest.append((start, level))
545 listnest.append((start, level))
546 out.append('<%s>\n' % start)
546 out.append('<%s>\n' % start)
547
547
548 blocks = [b for b in blocks if b['type'] != 'margin']
548 blocks = [b for b in blocks if b['type'] != 'margin']
549
549
550 for pos, b in enumerate(blocks):
550 for pos, b in enumerate(blocks):
551 btype = b['type']
551 btype = b['type']
552 level = b['indent']
552 level = b['indent']
553 lines = b['lines']
553 lines = b['lines']
554
554
555 if btype == 'admonition':
555 if btype == 'admonition':
556 admonition = escape(_admonitiontitles[b['admonitiontitle']])
556 admonition = escape(_admonitiontitles[b['admonitiontitle']])
557 text = escape(' '.join(map(str.strip, lines)))
557 text = escape(' '.join(map(str.strip, lines)))
558 out.append('<p>\n<b>%s</b> %s\n</p>\n' % (admonition, text))
558 out.append('<p>\n<b>%s</b> %s\n</p>\n' % (admonition, text))
559 elif btype == 'paragraph':
559 elif btype == 'paragraph':
560 out.append('<p>\n%s\n</p>\n' % escape('\n'.join(lines)))
560 out.append('<p>\n%s\n</p>\n' % escape('\n'.join(lines)))
561 elif btype == 'margin':
561 elif btype == 'margin':
562 pass
562 pass
563 elif btype == 'literal':
563 elif btype == 'literal':
564 out.append('<pre>\n%s\n</pre>\n' % escape('\n'.join(lines)))
564 out.append('<pre>\n%s\n</pre>\n' % escape('\n'.join(lines)))
565 elif btype == 'section':
565 elif btype == 'section':
566 i = b['underline']
566 i = b['underline']
567 if i not in headernest:
567 if i not in headernest:
568 headernest += i
568 headernest += i
569 level = headernest.index(i) + 1
569 level = headernest.index(i) + 1
570 out.append('<h%d>%s</h%d>\n' % (level, escape(lines[0]), level))
570 out.append('<h%d>%s</h%d>\n' % (level, escape(lines[0]), level))
571 elif btype == 'table':
571 elif btype == 'table':
572 table = b['table']
572 table = b['table']
573 out.append('<table>\n')
573 out.append('<table>\n')
574 for row in table:
574 for row in table:
575 out.append('<tr>')
575 out.append('<tr>')
576 for v in row:
576 for v in row:
577 out.append('<td>')
577 out.append('<td>')
578 out.append(escape(v))
578 out.append(escape(v))
579 out.append('</td>')
579 out.append('</td>')
580 out.append('\n')
580 out.append('\n')
581 out.pop()
581 out.pop()
582 out.append('</tr>\n')
582 out.append('</tr>\n')
583 out.append('</table>\n')
583 out.append('</table>\n')
584 elif btype == 'definition':
584 elif btype == 'definition':
585 openlist('dl', level)
585 openlist('dl', level)
586 term = escape(lines[0])
586 term = escape(lines[0])
587 text = escape(' '.join(map(str.strip, lines[1:])))
587 text = escape(' '.join(map(str.strip, lines[1:])))
588 out.append(' <dt>%s\n <dd>%s\n' % (term, text))
588 out.append(' <dt>%s\n <dd>%s\n' % (term, text))
589 elif btype == 'bullet':
589 elif btype == 'bullet':
590 bullet, head = lines[0].split(' ', 1)
590 bullet, head = lines[0].split(' ', 1)
591 if bullet == '-':
591 if bullet == '-':
592 openlist('ul', level)
592 openlist('ul', level)
593 else:
593 else:
594 openlist('ol', level)
594 openlist('ol', level)
595 out.append(' <li> %s\n' % escape(' '.join([head] + lines[1:])))
595 out.append(' <li> %s\n' % escape(' '.join([head] + lines[1:])))
596 elif btype == 'field':
596 elif btype == 'field':
597 openlist('dl', level)
597 openlist('dl', level)
598 key = escape(b['key'])
598 key = escape(b['key'])
599 text = escape(' '.join(map(str.strip, lines)))
599 text = escape(' '.join(map(str.strip, lines)))
600 out.append(' <dt>%s\n <dd>%s\n' % (key, text))
600 out.append(' <dt>%s\n <dd>%s\n' % (key, text))
601 elif btype == 'option':
601 elif btype == 'option':
602 openlist('dl', level)
602 openlist('dl', level)
603 opt = escape(b['optstr'])
603 opt = escape(b['optstr'])
604 desc = escape(' '.join(map(str.strip, lines)))
604 desc = escape(' '.join(map(str.strip, lines)))
605 out.append(' <dt>%s\n <dd>%s\n' % (opt, desc))
605 out.append(' <dt>%s\n <dd>%s\n' % (opt, desc))
606
606
607 # close lists if indent level of next block is lower
607 # close lists if indent level of next block is lower
608 if listnest:
608 if listnest:
609 start, level = listnest[-1]
609 start, level = listnest[-1]
610 if pos == len(blocks) - 1:
610 if pos == len(blocks) - 1:
611 out.append('</%s>\n' % start)
611 out.append('</%s>\n' % start)
612 listnest.pop()
612 listnest.pop()
613 else:
613 else:
614 nb = blocks[pos + 1]
614 nb = blocks[pos + 1]
615 ni = nb['indent']
615 ni = nb['indent']
616 if (ni < level or
616 if (ni < level or
617 (ni == level and
617 (ni == level and
618 nb['type'] not in 'definition bullet field option')):
618 nb['type'] not in 'definition bullet field option')):
619 out.append('</%s>\n' % start)
619 out.append('</%s>\n' % start)
620 listnest.pop()
620 listnest.pop()
621
621
622 return ''.join(out)
622 return ''.join(out)
623
623
624 def parse(text, indent=0, keep=None):
624 def parse(text, indent=0, keep=None):
625 """Parse text into a list of blocks"""
625 """Parse text into a list of blocks"""
626 pruned = []
626 pruned = []
627 blocks = findblocks(text)
627 blocks = findblocks(text)
628 for b in blocks:
628 for b in blocks:
629 b['indent'] += indent
629 b['indent'] += indent
630 blocks = findliteralblocks(blocks)
630 blocks = findliteralblocks(blocks)
631 blocks = findtables(blocks)
631 blocks = findtables(blocks)
632 blocks, pruned = prunecontainers(blocks, keep or [])
632 blocks, pruned = prunecontainers(blocks, keep or [])
633 blocks = findsections(blocks)
633 blocks = findsections(blocks)
634 blocks = inlineliterals(blocks)
634 blocks = inlineliterals(blocks)
635 blocks = hgrole(blocks)
635 blocks = hgrole(blocks)
636 blocks = splitparagraphs(blocks)
636 blocks = splitparagraphs(blocks)
637 blocks = updatefieldlists(blocks)
637 blocks = updatefieldlists(blocks)
638 blocks = updateoptionlists(blocks)
638 blocks = updateoptionlists(blocks)
639 blocks = findadmonitions(blocks)
639 blocks = findadmonitions(blocks)
640 blocks = addmargins(blocks)
640 blocks = addmargins(blocks)
641 blocks = prunecomments(blocks)
641 blocks = prunecomments(blocks)
642 return blocks, pruned
642 return blocks, pruned
643
643
644 def formatblocks(blocks, width):
644 def formatblocks(blocks, width):
645 text = ''.join(formatblock(b, width) for b in blocks)
645 text = ''.join(formatblock(b, width) for b in blocks)
646 return text
646 return text
647
647
648 def format(text, width=80, indent=0, keep=None, style='plain'):
648 def format(text, width=80, indent=0, keep=None, style='plain'):
649 """Parse and format the text according to width."""
649 """Parse and format the text according to width."""
650 blocks, pruned = parse(text, indent, keep or [])
650 blocks, pruned = parse(text, indent, keep or [])
651 if style == 'html':
651 if style == 'html':
652 text = formathtml(blocks)
652 text = formathtml(blocks)
653 else:
653 else:
654 text = ''.join(formatblock(b, width) for b in blocks)
654 text = ''.join(formatblock(b, width) for b in blocks)
655 if keep is None:
655 if keep is None:
656 return text
656 return text
657 else:
657 else:
658 return text, pruned
658 return text, pruned
659
659
660 def getsections(blocks):
660 def getsections(blocks):
661 '''return a list of (section name, nesting level, blocks) tuples'''
661 '''return a list of (section name, nesting level, blocks) tuples'''
662 nest = ""
662 nest = ""
663 level = 0
663 level = 0
664 secs = []
664 secs = []
665 for b in blocks:
665 for b in blocks:
666 if b['type'] == 'section':
666 if b['type'] == 'section':
667 i = b['underline']
667 i = b['underline']
668 if i not in nest:
668 if i not in nest:
669 nest += i
669 nest += i
670 level = nest.index(i) + 1
670 level = nest.index(i) + 1
671 nest = nest[:level]
671 nest = nest[:level]
672 secs.append((b['lines'][0], level, [b]))
672 secs.append((b['lines'][0], level, [b]))
673 else:
673 else:
674 if not secs:
674 if not secs:
675 # add an initial empty section
675 # add an initial empty section
676 secs = [('', 0, [])]
676 secs = [('', 0, [])]
677 secs[-1][2].append(b)
677 secs[-1][2].append(b)
678 return secs
678 return secs
679
679
680 def decorateblocks(blocks, width):
680 def decorateblocks(blocks, width):
681 '''generate a list of (section name, line text) pairs for search'''
681 '''generate a list of (section name, line text) pairs for search'''
682 lines = []
682 lines = []
683 for s in getsections(blocks):
683 for s in getsections(blocks):
684 section = s[0]
684 section = s[0]
685 text = formatblocks(s[2], width)
685 text = formatblocks(s[2], width)
686 lines.append([(section, l) for l in text.splitlines(True)])
686 lines.append([(section, l) for l in text.splitlines(True)])
687 return lines
687 return lines
688
688
689 def maketable(data, indent=0, header=False):
689 def maketable(data, indent=0, header=False):
690 '''Generate an RST table for the given table data as a list of lines'''
690 '''Generate an RST table for the given table data as a list of lines'''
691
691
692 widths = [max(encoding.colwidth(e) for e in c) for c in zip(*data)]
692 widths = [max(encoding.colwidth(e) for e in c) for c in zip(*data)]
693 indent = ' ' * indent
693 indent = ' ' * indent
694 div = indent + ' '.join('=' * w for w in widths) + '\n'
694 div = indent + ' '.join('=' * w for w in widths) + '\n'
695
695
696 out = [div]
696 out = [div]
697 for row in data:
697 for row in data:
698 l = []
698 l = []
699 for w, v in zip(widths, row):
699 for w, v in zip(widths, row):
700 pad = ' ' * (w - encoding.colwidth(v))
700 pad = ' ' * (w - encoding.colwidth(v))
701 l.append(v + pad)
701 l.append(v + pad)
702 out.append(indent + ' '.join(l) + "\n")
702 out.append(indent + ' '.join(l) + "\n")
703 if header and len(data) > 1:
703 if header and len(data) > 1:
704 out.insert(2, div)
704 out.insert(2, div)
705 out.append(div)
705 out.append(div)
706 return out
706 return out
@@ -1,854 +1,854 b''
1 # obsolete.py - obsolete markers handling
1 # obsolete.py - obsolete markers handling
2 #
2 #
3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 # Logilab SA <contact@logilab.fr>
4 # Logilab SA <contact@logilab.fr>
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 """Obsolete markers handling
9 """Obsolete markers handling
10
10
11 An obsolete marker maps an old changeset to a list of new
11 An obsolete marker maps an old changeset to a list of new
12 changesets. If the list of new changesets is empty, the old changeset
12 changesets. If the list of new changesets is empty, the old changeset
13 is said to be "killed". Otherwise, the old changeset is being
13 is said to be "killed". Otherwise, the old changeset is being
14 "replaced" by the new changesets.
14 "replaced" by the new changesets.
15
15
16 Obsolete markers can be used to record and distribute changeset graph
16 Obsolete markers can be used to record and distribute changeset graph
17 transformations performed by history rewriting operations, and help
17 transformations performed by history rewriting operations, and help
18 building new tools to reconciliate conflicting rewriting actions. To
18 building new tools to reconciliate conflicting rewriting actions. To
19 facilitate conflicts resolution, markers include various annotations
19 facilitate conflicts resolution, markers include various annotations
20 besides old and news changeset identifiers, such as creation date or
20 besides old and news changeset identifiers, such as creation date or
21 author name.
21 author name.
22
22
23 The old obsoleted changeset is called "precursor" and possible replacements are
23 The old obsoleted changeset is called "precursor" and possible replacements are
24 called "successors". Markers that used changeset X as a precursors are called
24 called "successors". Markers that used changeset X as a precursors are called
25 "successor markers of X" because they hold information about the successors of
25 "successor markers of X" because they hold information about the successors of
26 X. Markers that use changeset Y as a successors are call "precursor markers of
26 X. Markers that use changeset Y as a successors are call "precursor markers of
27 Y" because they hold information about the precursors of Y.
27 Y" because they hold information about the precursors of Y.
28
28
29 Examples:
29 Examples:
30
30
31 - When changeset A is replacement by a changeset A', one marker is stored:
31 - When changeset A is replacement by a changeset A', one marker is stored:
32
32
33 (A, (A'))
33 (A, (A'))
34
34
35 - When changesets A and B are folded into a new changeset C two markers are
35 - When changesets A and B are folded into a new changeset C two markers are
36 stored:
36 stored:
37
37
38 (A, (C,)) and (B, (C,))
38 (A, (C,)) and (B, (C,))
39
39
40 - When changeset A is simply "pruned" from the graph, a marker in create:
40 - When changeset A is simply "pruned" from the graph, a marker in create:
41
41
42 (A, ())
42 (A, ())
43
43
44 - When changeset A is split into B and C, a single marker are used:
44 - When changeset A is split into B and C, a single marker are used:
45
45
46 (A, (C, C))
46 (A, (C, C))
47
47
48 We use a single marker to distinct the "split" case from the "divergence"
48 We use a single marker to distinct the "split" case from the "divergence"
49 case. If two independents operation rewrite the same changeset A in to A' and
49 case. If two independents operation rewrite the same changeset A in to A' and
50 A'' when have an error case: divergent rewriting. We can detect it because
50 A'' when have an error case: divergent rewriting. We can detect it because
51 two markers will be created independently:
51 two markers will be created independently:
52
52
53 (A, (B,)) and (A, (C,))
53 (A, (B,)) and (A, (C,))
54
54
55 Format
55 Format
56 ------
56 ------
57
57
58 Markers are stored in an append-only file stored in
58 Markers are stored in an append-only file stored in
59 '.hg/store/obsstore'.
59 '.hg/store/obsstore'.
60
60
61 The file starts with a version header:
61 The file starts with a version header:
62
62
63 - 1 unsigned byte: version number, starting at zero.
63 - 1 unsigned byte: version number, starting at zero.
64
64
65
65
66 The header is followed by the markers. Each marker is made of:
66 The header is followed by the markers. Each marker is made of:
67
67
68 - 1 unsigned byte: number of new changesets "R", could be zero.
68 - 1 unsigned byte: number of new changesets "R", could be zero.
69
69
70 - 1 unsigned 32-bits integer: metadata size "M" in bytes.
70 - 1 unsigned 32-bits integer: metadata size "M" in bytes.
71
71
72 - 1 byte: a bit field. It is reserved for flags used in obsolete
72 - 1 byte: a bit field. It is reserved for flags used in obsolete
73 markers common operations, to avoid repeated decoding of metadata
73 markers common operations, to avoid repeated decoding of metadata
74 entries.
74 entries.
75
75
76 - 20 bytes: obsoleted changeset identifier.
76 - 20 bytes: obsoleted changeset identifier.
77
77
78 - N*20 bytes: new changesets identifiers.
78 - N*20 bytes: new changesets identifiers.
79
79
80 - M bytes: metadata as a sequence of nul-terminated strings. Each
80 - M bytes: metadata as a sequence of nul-terminated strings. Each
81 string contains a key and a value, separated by a color ':', without
81 string contains a key and a value, separated by a color ':', without
82 additional encoding. Keys cannot contain '\0' or ':' and values
82 additional encoding. Keys cannot contain '\0' or ':' and values
83 cannot contain '\0'.
83 cannot contain '\0'.
84 """
84 """
85 import struct
85 import struct
86 import util, base85, node
86 import util, base85, node
87 import phases
87 import phases
88 from i18n import _
88 from i18n import _
89
89
90 _pack = struct.pack
90 _pack = struct.pack
91 _unpack = struct.unpack
91 _unpack = struct.unpack
92
92
93 _SEEK_END = 2 # os.SEEK_END was introduced in Python 2.5
93 _SEEK_END = 2 # os.SEEK_END was introduced in Python 2.5
94
94
95 # the obsolete feature is not mature enough to be enabled by default.
95 # the obsolete feature is not mature enough to be enabled by default.
96 # you have to rely on third party extension extension to enable this.
96 # you have to rely on third party extension extension to enable this.
97 _enabled = False
97 _enabled = False
98
98
99 # data used for parsing and writing
99 # data used for parsing and writing
100 _fmversion = 0
100 _fmversion = 0
101 _fmfixed = '>BIB20s'
101 _fmfixed = '>BIB20s'
102 _fmnode = '20s'
102 _fmnode = '20s'
103 _fmfsize = struct.calcsize(_fmfixed)
103 _fmfsize = struct.calcsize(_fmfixed)
104 _fnodesize = struct.calcsize(_fmnode)
104 _fnodesize = struct.calcsize(_fmnode)
105
105
106 ### obsolescence marker flag
106 ### obsolescence marker flag
107
107
108 ## bumpedfix flag
108 ## bumpedfix flag
109 #
109 #
110 # When a changeset A' succeed to a changeset A which became public, we call A'
110 # When a changeset A' succeed to a changeset A which became public, we call A'
111 # "bumped" because it's a successors of a public changesets
111 # "bumped" because it's a successors of a public changesets
112 #
112 #
113 # o A' (bumped)
113 # o A' (bumped)
114 # |`:
114 # |`:
115 # | o A
115 # | o A
116 # |/
116 # |/
117 # o Z
117 # o Z
118 #
118 #
119 # The way to solve this situation is to create a new changeset Ad as children
119 # The way to solve this situation is to create a new changeset Ad as children
120 # of A. This changeset have the same content than A'. So the diff from A to A'
120 # of A. This changeset have the same content than A'. So the diff from A to A'
121 # is the same than the diff from A to Ad. Ad is marked as a successors of A'
121 # is the same than the diff from A to Ad. Ad is marked as a successors of A'
122 #
122 #
123 # o Ad
123 # o Ad
124 # |`:
124 # |`:
125 # | x A'
125 # | x A'
126 # |'|
126 # |'|
127 # o | A
127 # o | A
128 # |/
128 # |/
129 # o Z
129 # o Z
130 #
130 #
131 # But by transitivity Ad is also a successors of A. To avoid having Ad marked
131 # But by transitivity Ad is also a successors of A. To avoid having Ad marked
132 # as bumped too, we add the `bumpedfix` flag to the marker. <A', (Ad,)>.
132 # as bumped too, we add the `bumpedfix` flag to the marker. <A', (Ad,)>.
133 # This flag mean that the successors express the changes between the public and
133 # This flag mean that the successors express the changes between the public and
134 # bumped version and fix the situation, breaking the transitivity of
134 # bumped version and fix the situation, breaking the transitivity of
135 # "bumped" here.
135 # "bumped" here.
136 bumpedfix = 1
136 bumpedfix = 1
137
137
138 def _readmarkers(data):
138 def _readmarkers(data):
139 """Read and enumerate markers from raw data"""
139 """Read and enumerate markers from raw data"""
140 off = 0
140 off = 0
141 diskversion = _unpack('>B', data[off:off + 1])[0]
141 diskversion = _unpack('>B', data[off:off + 1])[0]
142 off += 1
142 off += 1
143 if diskversion != _fmversion:
143 if diskversion != _fmversion:
144 raise util.Abort(_('parsing obsolete marker: unknown version %r')
144 raise util.Abort(_('parsing obsolete marker: unknown version %r')
145 % diskversion)
145 % diskversion)
146
146
147 # Loop on markers
147 # Loop on markers
148 l = len(data)
148 l = len(data)
149 while off + _fmfsize <= l:
149 while off + _fmfsize <= l:
150 # read fixed part
150 # read fixed part
151 cur = data[off:off + _fmfsize]
151 cur = data[off:off + _fmfsize]
152 off += _fmfsize
152 off += _fmfsize
153 nbsuc, mdsize, flags, pre = _unpack(_fmfixed, cur)
153 nbsuc, mdsize, flags, pre = _unpack(_fmfixed, cur)
154 # read replacement
154 # read replacement
155 sucs = ()
155 sucs = ()
156 if nbsuc:
156 if nbsuc:
157 s = (_fnodesize * nbsuc)
157 s = (_fnodesize * nbsuc)
158 cur = data[off:off + s]
158 cur = data[off:off + s]
159 sucs = _unpack(_fmnode * nbsuc, cur)
159 sucs = _unpack(_fmnode * nbsuc, cur)
160 off += s
160 off += s
161 # read metadata
161 # read metadata
162 # (metadata will be decoded on demand)
162 # (metadata will be decoded on demand)
163 metadata = data[off:off + mdsize]
163 metadata = data[off:off + mdsize]
164 if len(metadata) != mdsize:
164 if len(metadata) != mdsize:
165 raise util.Abort(_('parsing obsolete marker: metadata is too '
165 raise util.Abort(_('parsing obsolete marker: metadata is too '
166 'short, %d bytes expected, got %d')
166 'short, %d bytes expected, got %d')
167 % (mdsize, len(metadata)))
167 % (mdsize, len(metadata)))
168 off += mdsize
168 off += mdsize
169 yield (pre, sucs, flags, metadata)
169 yield (pre, sucs, flags, metadata)
170
170
171 def encodemeta(meta):
171 def encodemeta(meta):
172 """Return encoded metadata string to string mapping.
172 """Return encoded metadata string to string mapping.
173
173
174 Assume no ':' in key and no '\0' in both key and value."""
174 Assume no ':' in key and no '\0' in both key and value."""
175 for key, value in meta.iteritems():
175 for key, value in meta.iteritems():
176 if ':' in key or '\0' in key:
176 if ':' in key or '\0' in key:
177 raise ValueError("':' and '\0' are forbidden in metadata key'")
177 raise ValueError("':' and '\0' are forbidden in metadata key'")
178 if '\0' in value:
178 if '\0' in value:
179 raise ValueError("':' are forbidden in metadata value'")
179 raise ValueError("':' are forbidden in metadata value'")
180 return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
180 return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
181
181
182 def decodemeta(data):
182 def decodemeta(data):
183 """Return string to string dictionary from encoded version."""
183 """Return string to string dictionary from encoded version."""
184 d = {}
184 d = {}
185 for l in data.split('\0'):
185 for l in data.split('\0'):
186 if l:
186 if l:
187 key, value = l.split(':')
187 key, value = l.split(':')
188 d[key] = value
188 d[key] = value
189 return d
189 return d
190
190
191 class marker(object):
191 class marker(object):
192 """Wrap obsolete marker raw data"""
192 """Wrap obsolete marker raw data"""
193
193
194 def __init__(self, repo, data):
194 def __init__(self, repo, data):
195 # the repo argument will be used to create changectx in later version
195 # the repo argument will be used to create changectx in later version
196 self._repo = repo
196 self._repo = repo
197 self._data = data
197 self._data = data
198 self._decodedmeta = None
198 self._decodedmeta = None
199
199
200 def __hash__(self):
200 def __hash__(self):
201 return hash(self._data)
201 return hash(self._data)
202
202
203 def __eq__(self, other):
203 def __eq__(self, other):
204 if type(other) != type(self):
204 if type(other) != type(self):
205 return False
205 return False
206 return self._data == other._data
206 return self._data == other._data
207
207
208 def precnode(self):
208 def precnode(self):
209 """Precursor changeset node identifier"""
209 """Precursor changeset node identifier"""
210 return self._data[0]
210 return self._data[0]
211
211
212 def succnodes(self):
212 def succnodes(self):
213 """List of successor changesets node identifiers"""
213 """List of successor changesets node identifiers"""
214 return self._data[1]
214 return self._data[1]
215
215
216 def metadata(self):
216 def metadata(self):
217 """Decoded metadata dictionary"""
217 """Decoded metadata dictionary"""
218 if self._decodedmeta is None:
218 if self._decodedmeta is None:
219 self._decodedmeta = decodemeta(self._data[3])
219 self._decodedmeta = decodemeta(self._data[3])
220 return self._decodedmeta
220 return self._decodedmeta
221
221
222 def date(self):
222 def date(self):
223 """Creation date as (unixtime, offset)"""
223 """Creation date as (unixtime, offset)"""
224 parts = self.metadata()['date'].split(' ')
224 parts = self.metadata()['date'].split(' ')
225 return (float(parts[0]), int(parts[1]))
225 return (float(parts[0]), int(parts[1]))
226
226
227 class obsstore(object):
227 class obsstore(object):
228 """Store obsolete markers
228 """Store obsolete markers
229
229
230 Markers can be accessed with two mappings:
230 Markers can be accessed with two mappings:
231 - precursors[x] -> set(markers on precursors edges of x)
231 - precursors[x] -> set(markers on precursors edges of x)
232 - successors[x] -> set(markers on successors edges of x)
232 - successors[x] -> set(markers on successors edges of x)
233 """
233 """
234
234
235 def __init__(self, sopener):
235 def __init__(self, sopener):
236 # caches for various obsolescence related cache
236 # caches for various obsolescence related cache
237 self.caches = {}
237 self.caches = {}
238 self._all = []
238 self._all = []
239 # new markers to serialize
239 # new markers to serialize
240 self.precursors = {}
240 self.precursors = {}
241 self.successors = {}
241 self.successors = {}
242 self.sopener = sopener
242 self.sopener = sopener
243 data = sopener.tryread('obsstore')
243 data = sopener.tryread('obsstore')
244 if data:
244 if data:
245 self._load(_readmarkers(data))
245 self._load(_readmarkers(data))
246
246
247 def __iter__(self):
247 def __iter__(self):
248 return iter(self._all)
248 return iter(self._all)
249
249
250 def __nonzero__(self):
250 def __nonzero__(self):
251 return bool(self._all)
251 return bool(self._all)
252
252
253 def create(self, transaction, prec, succs=(), flag=0, metadata=None):
253 def create(self, transaction, prec, succs=(), flag=0, metadata=None):
254 """obsolete: add a new obsolete marker
254 """obsolete: add a new obsolete marker
255
255
256 * ensuring it is hashable
256 * ensuring it is hashable
257 * check mandatory metadata
257 * check mandatory metadata
258 * encode metadata
258 * encode metadata
259
259
260 If you are a human writing code creating marker you want to use the
260 If you are a human writing code creating marker you want to use the
261 `createmarkers` function in this module instead.
261 `createmarkers` function in this module instead.
262 """
262 """
263 if metadata is None:
263 if metadata is None:
264 metadata = {}
264 metadata = {}
265 if 'date' not in metadata:
265 if 'date' not in metadata:
266 metadata['date'] = "%d %d" % util.makedate()
266 metadata['date'] = "%d %d" % util.makedate()
267 if len(prec) != 20:
267 if len(prec) != 20:
268 raise ValueError(prec)
268 raise ValueError(prec)
269 for succ in succs:
269 for succ in succs:
270 if len(succ) != 20:
270 if len(succ) != 20:
271 raise ValueError(succ)
271 raise ValueError(succ)
272 marker = (str(prec), tuple(succs), int(flag), encodemeta(metadata))
272 marker = (str(prec), tuple(succs), int(flag), encodemeta(metadata))
273 self.add(transaction, [marker])
273 self.add(transaction, [marker])
274
274
275 def add(self, transaction, markers):
275 def add(self, transaction, markers):
276 """Add new markers to the store
276 """Add new markers to the store
277
277
278 Take care of filtering duplicate.
278 Take care of filtering duplicate.
279 Return the number of new marker."""
279 Return the number of new marker."""
280 if not _enabled:
280 if not _enabled:
281 raise util.Abort('obsolete feature is not enabled on this repo')
281 raise util.Abort('obsolete feature is not enabled on this repo')
282 known = set(self._all)
282 known = set(self._all)
283 new = []
283 new = []
284 for m in markers:
284 for m in markers:
285 if m not in known:
285 if m not in known:
286 known.add(m)
286 known.add(m)
287 new.append(m)
287 new.append(m)
288 if new:
288 if new:
289 f = self.sopener('obsstore', 'ab')
289 f = self.sopener('obsstore', 'ab')
290 try:
290 try:
291 # Whether the file's current position is at the begin or at
291 # Whether the file's current position is at the begin or at
292 # the end after opening a file for appending is implementation
292 # the end after opening a file for appending is implementation
293 # defined. So we must seek to the end before calling tell(),
293 # defined. So we must seek to the end before calling tell(),
294 # or we may get a zero offset for non-zero sized files on
294 # or we may get a zero offset for non-zero sized files on
295 # some platforms (issue3543).
295 # some platforms (issue3543).
296 f.seek(0, _SEEK_END)
296 f.seek(0, _SEEK_END)
297 offset = f.tell()
297 offset = f.tell()
298 transaction.add('obsstore', offset)
298 transaction.add('obsstore', offset)
299 # offset == 0: new file - add the version header
299 # offset == 0: new file - add the version header
300 for bytes in _encodemarkers(new, offset == 0):
300 for bytes in _encodemarkers(new, offset == 0):
301 f.write(bytes)
301 f.write(bytes)
302 finally:
302 finally:
303 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
303 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
304 # call 'filecacheentry.refresh()' here
304 # call 'filecacheentry.refresh()' here
305 f.close()
305 f.close()
306 self._load(new)
306 self._load(new)
307 # new marker *may* have changed several set. invalidate the cache.
307 # new marker *may* have changed several set. invalidate the cache.
308 self.caches.clear()
308 self.caches.clear()
309 return len(new)
309 return len(new)
310
310
311 def mergemarkers(self, transaction, data):
311 def mergemarkers(self, transaction, data):
312 markers = _readmarkers(data)
312 markers = _readmarkers(data)
313 self.add(transaction, markers)
313 self.add(transaction, markers)
314
314
315 def _load(self, markers):
315 def _load(self, markers):
316 for mark in markers:
316 for mark in markers:
317 self._all.append(mark)
317 self._all.append(mark)
318 pre, sucs = mark[:2]
318 pre, sucs = mark[:2]
319 self.successors.setdefault(pre, set()).add(mark)
319 self.successors.setdefault(pre, set()).add(mark)
320 for suc in sucs:
320 for suc in sucs:
321 self.precursors.setdefault(suc, set()).add(mark)
321 self.precursors.setdefault(suc, set()).add(mark)
322 if node.nullid in self.precursors:
322 if node.nullid in self.precursors:
323 raise util.Abort(_('bad obsolescence marker detected: '
323 raise util.Abort(_('bad obsolescence marker detected: '
324 'invalid successors nullid'))
324 'invalid successors nullid'))
325
325
326 def _encodemarkers(markers, addheader=False):
326 def _encodemarkers(markers, addheader=False):
327 # Kept separate from flushmarkers(), it will be reused for
327 # Kept separate from flushmarkers(), it will be reused for
328 # markers exchange.
328 # markers exchange.
329 if addheader:
329 if addheader:
330 yield _pack('>B', _fmversion)
330 yield _pack('>B', _fmversion)
331 for marker in markers:
331 for marker in markers:
332 yield _encodeonemarker(marker)
332 yield _encodeonemarker(marker)
333
333
334
334
335 def _encodeonemarker(marker):
335 def _encodeonemarker(marker):
336 pre, sucs, flags, metadata = marker
336 pre, sucs, flags, metadata = marker
337 nbsuc = len(sucs)
337 nbsuc = len(sucs)
338 format = _fmfixed + (_fmnode * nbsuc)
338 format = _fmfixed + (_fmnode * nbsuc)
339 data = [nbsuc, len(metadata), flags, pre]
339 data = [nbsuc, len(metadata), flags, pre]
340 data.extend(sucs)
340 data.extend(sucs)
341 return _pack(format, *data) + metadata
341 return _pack(format, *data) + metadata
342
342
343 # arbitrary picked to fit into 8K limit from HTTP server
343 # arbitrary picked to fit into 8K limit from HTTP server
344 # you have to take in account:
344 # you have to take in account:
345 # - the version header
345 # - the version header
346 # - the base85 encoding
346 # - the base85 encoding
347 _maxpayload = 5300
347 _maxpayload = 5300
348
348
349 def listmarkers(repo):
349 def listmarkers(repo):
350 """List markers over pushkey"""
350 """List markers over pushkey"""
351 if not repo.obsstore:
351 if not repo.obsstore:
352 return {}
352 return {}
353 keys = {}
353 keys = {}
354 parts = []
354 parts = []
355 currentlen = _maxpayload * 2 # ensure we create a new part
355 currentlen = _maxpayload * 2 # ensure we create a new part
356 for marker in repo.obsstore:
356 for marker in repo.obsstore:
357 nextdata = _encodeonemarker(marker)
357 nextdata = _encodeonemarker(marker)
358 if (len(nextdata) + currentlen > _maxpayload):
358 if (len(nextdata) + currentlen > _maxpayload):
359 currentpart = []
359 currentpart = []
360 currentlen = 0
360 currentlen = 0
361 parts.append(currentpart)
361 parts.append(currentpart)
362 currentpart.append(nextdata)
362 currentpart.append(nextdata)
363 currentlen += len(nextdata)
363 currentlen += len(nextdata)
364 for idx, part in enumerate(reversed(parts)):
364 for idx, part in enumerate(reversed(parts)):
365 data = ''.join([_pack('>B', _fmversion)] + part)
365 data = ''.join([_pack('>B', _fmversion)] + part)
366 keys['dump%i' % idx] = base85.b85encode(data)
366 keys['dump%i' % idx] = base85.b85encode(data)
367 return keys
367 return keys
368
368
369 def pushmarker(repo, key, old, new):
369 def pushmarker(repo, key, old, new):
370 """Push markers over pushkey"""
370 """Push markers over pushkey"""
371 if not key.startswith('dump'):
371 if not key.startswith('dump'):
372 repo.ui.warn(_('unknown key: %r') % key)
372 repo.ui.warn(_('unknown key: %r') % key)
373 return 0
373 return 0
374 if old:
374 if old:
375 repo.ui.warn(_('unexpected old value') % key)
375 repo.ui.warn(_('unexpected old value') % key)
376 return 0
376 return 0
377 data = base85.b85decode(new)
377 data = base85.b85decode(new)
378 lock = repo.lock()
378 lock = repo.lock()
379 try:
379 try:
380 tr = repo.transaction('pushkey: obsolete markers')
380 tr = repo.transaction('pushkey: obsolete markers')
381 try:
381 try:
382 repo.obsstore.mergemarkers(tr, data)
382 repo.obsstore.mergemarkers(tr, data)
383 tr.close()
383 tr.close()
384 return 1
384 return 1
385 finally:
385 finally:
386 tr.release()
386 tr.release()
387 finally:
387 finally:
388 lock.release()
388 lock.release()
389
389
390 def allmarkers(repo):
390 def allmarkers(repo):
391 """all obsolete markers known in a repository"""
391 """all obsolete markers known in a repository"""
392 for markerdata in repo.obsstore:
392 for markerdata in repo.obsstore:
393 yield marker(repo, markerdata)
393 yield marker(repo, markerdata)
394
394
395 def precursormarkers(ctx):
395 def precursormarkers(ctx):
396 """obsolete marker marking this changeset as a successors"""
396 """obsolete marker marking this changeset as a successors"""
397 for data in ctx._repo.obsstore.precursors.get(ctx.node(), ()):
397 for data in ctx._repo.obsstore.precursors.get(ctx.node(), ()):
398 yield marker(ctx._repo, data)
398 yield marker(ctx._repo, data)
399
399
400 def successormarkers(ctx):
400 def successormarkers(ctx):
401 """obsolete marker making this changeset obsolete"""
401 """obsolete marker making this changeset obsolete"""
402 for data in ctx._repo.obsstore.successors.get(ctx.node(), ()):
402 for data in ctx._repo.obsstore.successors.get(ctx.node(), ()):
403 yield marker(ctx._repo, data)
403 yield marker(ctx._repo, data)
404
404
405 def allsuccessors(obsstore, nodes, ignoreflags=0):
405 def allsuccessors(obsstore, nodes, ignoreflags=0):
406 """Yield node for every successor of <nodes>.
406 """Yield node for every successor of <nodes>.
407
407
408 Some successors may be unknown locally.
408 Some successors may be unknown locally.
409
409
410 This is a linear yield unsuited to detecting split changesets. It includes
410 This is a linear yield unsuited to detecting split changesets. It includes
411 initial nodes too."""
411 initial nodes too."""
412 remaining = set(nodes)
412 remaining = set(nodes)
413 seen = set(remaining)
413 seen = set(remaining)
414 while remaining:
414 while remaining:
415 current = remaining.pop()
415 current = remaining.pop()
416 yield current
416 yield current
417 for mark in obsstore.successors.get(current, ()):
417 for mark in obsstore.successors.get(current, ()):
418 # ignore marker flagged with specified flag
418 # ignore marker flagged with specified flag
419 if mark[2] & ignoreflags:
419 if mark[2] & ignoreflags:
420 continue
420 continue
421 for suc in mark[1]:
421 for suc in mark[1]:
422 if suc not in seen:
422 if suc not in seen:
423 seen.add(suc)
423 seen.add(suc)
424 remaining.add(suc)
424 remaining.add(suc)
425
425
426 def allprecursors(obsstore, nodes, ignoreflags=0):
426 def allprecursors(obsstore, nodes, ignoreflags=0):
427 """Yield node for every precursors of <nodes>.
427 """Yield node for every precursors of <nodes>.
428
428
429 Some precursors may be unknown locally.
429 Some precursors may be unknown locally.
430
430
431 This is a linear yield unsuited to detecting folded changesets. It includes
431 This is a linear yield unsuited to detecting folded changesets. It includes
432 initial nodes too."""
432 initial nodes too."""
433
433
434 remaining = set(nodes)
434 remaining = set(nodes)
435 seen = set(remaining)
435 seen = set(remaining)
436 while remaining:
436 while remaining:
437 current = remaining.pop()
437 current = remaining.pop()
438 yield current
438 yield current
439 for mark in obsstore.precursors.get(current, ()):
439 for mark in obsstore.precursors.get(current, ()):
440 # ignore marker flagged with specified flag
440 # ignore marker flagged with specified flag
441 if mark[2] & ignoreflags:
441 if mark[2] & ignoreflags:
442 continue
442 continue
443 suc = mark[0]
443 suc = mark[0]
444 if suc not in seen:
444 if suc not in seen:
445 seen.add(suc)
445 seen.add(suc)
446 remaining.add(suc)
446 remaining.add(suc)
447
447
448 def foreground(repo, nodes):
448 def foreground(repo, nodes):
449 """return all nodes in the "foreground" of other node
449 """return all nodes in the "foreground" of other node
450
450
451 The foreground of a revision is anything reachable using parent -> children
451 The foreground of a revision is anything reachable using parent -> children
452 or precursor -> successor relation. It is very similar to "descendant" but
452 or precursor -> successor relation. It is very similar to "descendant" but
453 augmented with obsolescence information.
453 augmented with obsolescence information.
454
454
455 Beware that possible obsolescence cycle may result if complex situation.
455 Beware that possible obsolescence cycle may result if complex situation.
456 """
456 """
457 repo = repo.unfiltered()
457 repo = repo.unfiltered()
458 foreground = set(repo.set('%ln::', nodes))
458 foreground = set(repo.set('%ln::', nodes))
459 if repo.obsstore:
459 if repo.obsstore:
460 # We only need this complicated logic if there is obsolescence
460 # We only need this complicated logic if there is obsolescence
461 # XXX will probably deserve an optimised revset.
461 # XXX will probably deserve an optimised revset.
462 nm = repo.changelog.nodemap
462 nm = repo.changelog.nodemap
463 plen = -1
463 plen = -1
464 # compute the whole set of successors or descendants
464 # compute the whole set of successors or descendants
465 while len(foreground) != plen:
465 while len(foreground) != plen:
466 plen = len(foreground)
466 plen = len(foreground)
467 succs = set(c.node() for c in foreground)
467 succs = set(c.node() for c in foreground)
468 mutable = [c.node() for c in foreground if c.mutable()]
468 mutable = [c.node() for c in foreground if c.mutable()]
469 succs.update(allsuccessors(repo.obsstore, mutable))
469 succs.update(allsuccessors(repo.obsstore, mutable))
470 known = (n for n in succs if n in nm)
470 known = (n for n in succs if n in nm)
471 foreground = set(repo.set('%ln::', known))
471 foreground = set(repo.set('%ln::', known))
472 return set(c.node() for c in foreground)
472 return set(c.node() for c in foreground)
473
473
474
474
475 def successorssets(repo, initialnode, cache=None):
475 def successorssets(repo, initialnode, cache=None):
476 """Return all set of successors of initial nodes
476 """Return all set of successors of initial nodes
477
477
478 The successors set of a changeset A are a group of revisions that succeed
478 The successors set of a changeset A are a group of revisions that succeed
479 A. It succeeds A as a consistent whole, each revision being only a partial
479 A. It succeeds A as a consistent whole, each revision being only a partial
480 replacement. The successors set contains non-obsolete changesets only.
480 replacement. The successors set contains non-obsolete changesets only.
481
481
482 This function returns the full list of successor sets which is why it
482 This function returns the full list of successor sets which is why it
483 returns a list of tuples and not just a single tuple. Each tuple is a valid
483 returns a list of tuples and not just a single tuple. Each tuple is a valid
484 successors set. Not that (A,) may be a valid successors set for changeset A
484 successors set. Not that (A,) may be a valid successors set for changeset A
485 (see below).
485 (see below).
486
486
487 In most cases, a changeset A will have a single element (e.g. the changeset
487 In most cases, a changeset A will have a single element (e.g. the changeset
488 A is replaced by A') in its successors set. Though, it is also common for a
488 A is replaced by A') in its successors set. Though, it is also common for a
489 changeset A to have no elements in its successor set (e.g. the changeset
489 changeset A to have no elements in its successor set (e.g. the changeset
490 has been pruned). Therefore, the returned list of successors sets will be
490 has been pruned). Therefore, the returned list of successors sets will be
491 [(A',)] or [], respectively.
491 [(A',)] or [], respectively.
492
492
493 When a changeset A is split into A' and B', however, it will result in a
493 When a changeset A is split into A' and B', however, it will result in a
494 successors set containing more than a single element, i.e. [(A',B')].
494 successors set containing more than a single element, i.e. [(A',B')].
495 Divergent changesets will result in multiple successors sets, i.e. [(A',),
495 Divergent changesets will result in multiple successors sets, i.e. [(A',),
496 (A'')].
496 (A'')].
497
497
498 If a changeset A is not obsolete, then it will conceptually have no
498 If a changeset A is not obsolete, then it will conceptually have no
499 successors set. To distinguish this from a pruned changeset, the successor
499 successors set. To distinguish this from a pruned changeset, the successor
500 set will only contain itself, i.e. [(A,)].
500 set will only contain itself, i.e. [(A,)].
501
501
502 Finally, successors unknown locally are considered to be pruned (obsoleted
502 Finally, successors unknown locally are considered to be pruned (obsoleted
503 without any successors).
503 without any successors).
504
504
505 The optional `cache` parameter is a dictionary that may contain precomputed
505 The optional `cache` parameter is a dictionary that may contain precomputed
506 successors sets. It is meant to reuse the computation of a previous call to
506 successors sets. It is meant to reuse the computation of a previous call to
507 `successorssets` when multiple calls are made at the same time. The cache
507 `successorssets` when multiple calls are made at the same time. The cache
508 dictionary is updated in place. The caller is responsible for its live
508 dictionary is updated in place. The caller is responsible for its live
509 spawn. Code that makes multiple calls to `successorssets` *must* use this
509 spawn. Code that makes multiple calls to `successorssets` *must* use this
510 cache mechanism or suffer terrible performances.
510 cache mechanism or suffer terrible performances.
511
511
512 """
512 """
513
513
514 succmarkers = repo.obsstore.successors
514 succmarkers = repo.obsstore.successors
515
515
516 # Stack of nodes we search successors sets for
516 # Stack of nodes we search successors sets for
517 toproceed = [initialnode]
517 toproceed = [initialnode]
518 # set version of above list for fast loop detection
518 # set version of above list for fast loop detection
519 # element added to "toproceed" must be added here
519 # element added to "toproceed" must be added here
520 stackedset = set(toproceed)
520 stackedset = set(toproceed)
521 if cache is None:
521 if cache is None:
522 cache = {}
522 cache = {}
523
523
524 # This while loop is the flattened version of a recursive search for
524 # This while loop is the flattened version of a recursive search for
525 # successors sets
525 # successors sets
526 #
526 #
527 # def successorssets(x):
527 # def successorssets(x):
528 # successors = directsuccessors(x)
528 # successors = directsuccessors(x)
529 # ss = [[]]
529 # ss = [[]]
530 # for succ in directsuccessors(x):
530 # for succ in directsuccessors(x):
531 # # product as in itertools cartesian product
531 # # product as in itertools cartesian product
532 # ss = product(ss, successorssets(succ))
532 # ss = product(ss, successorssets(succ))
533 # return ss
533 # return ss
534 #
534 #
535 # But we can not use plain recursive calls here:
535 # But we can not use plain recursive calls here:
536 # - that would blow the python call stack
536 # - that would blow the python call stack
537 # - obsolescence markers may have cycles, we need to handle them.
537 # - obsolescence markers may have cycles, we need to handle them.
538 #
538 #
539 # The `toproceed` list act as our call stack. Every node we search
539 # The `toproceed` list act as our call stack. Every node we search
540 # successors set for are stacked there.
540 # successors set for are stacked there.
541 #
541 #
542 # The `stackedset` is set version of this stack used to check if a node is
542 # The `stackedset` is set version of this stack used to check if a node is
543 # already stacked. This check is used to detect cycles and prevent infinite
543 # already stacked. This check is used to detect cycles and prevent infinite
544 # loop.
544 # loop.
545 #
545 #
546 # successors set of all nodes are stored in the `cache` dictionary.
546 # successors set of all nodes are stored in the `cache` dictionary.
547 #
547 #
548 # After this while loop ends we use the cache to return the successors sets
548 # After this while loop ends we use the cache to return the successors sets
549 # for the node requested by the caller.
549 # for the node requested by the caller.
550 while toproceed:
550 while toproceed:
551 # Every iteration tries to compute the successors sets of the topmost
551 # Every iteration tries to compute the successors sets of the topmost
552 # node of the stack: CURRENT.
552 # node of the stack: CURRENT.
553 #
553 #
554 # There are four possible outcomes:
554 # There are four possible outcomes:
555 #
555 #
556 # 1) We already know the successors sets of CURRENT:
556 # 1) We already know the successors sets of CURRENT:
557 # -> mission accomplished, pop it from the stack.
557 # -> mission accomplished, pop it from the stack.
558 # 2) Node is not obsolete:
558 # 2) Node is not obsolete:
559 # -> the node is its own successors sets. Add it to the cache.
559 # -> the node is its own successors sets. Add it to the cache.
560 # 3) We do not know successors set of direct successors of CURRENT:
560 # 3) We do not know successors set of direct successors of CURRENT:
561 # -> We add those successors to the stack.
561 # -> We add those successors to the stack.
562 # 4) We know successors sets of all direct successors of CURRENT:
562 # 4) We know successors sets of all direct successors of CURRENT:
563 # -> We can compute CURRENT successors set and add it to the
563 # -> We can compute CURRENT successors set and add it to the
564 # cache.
564 # cache.
565 #
565 #
566 current = toproceed[-1]
566 current = toproceed[-1]
567 if current in cache:
567 if current in cache:
568 # case (1): We already know the successors sets
568 # case (1): We already know the successors sets
569 stackedset.remove(toproceed.pop())
569 stackedset.remove(toproceed.pop())
570 elif current not in succmarkers:
570 elif current not in succmarkers:
571 # case (2): The node is not obsolete.
571 # case (2): The node is not obsolete.
572 if current in repo:
572 if current in repo:
573 # We have a valid last successors.
573 # We have a valid last successors.
574 cache[current] = [(current,)]
574 cache[current] = [(current,)]
575 else:
575 else:
576 # Final obsolete version is unknown locally.
576 # Final obsolete version is unknown locally.
577 # Do not count that as a valid successors
577 # Do not count that as a valid successors
578 cache[current] = []
578 cache[current] = []
579 else:
579 else:
580 # cases (3) and (4)
580 # cases (3) and (4)
581 #
581 #
582 # We proceed in two phases. Phase 1 aims to distinguish case (3)
582 # We proceed in two phases. Phase 1 aims to distinguish case (3)
583 # from case (4):
583 # from case (4):
584 #
584 #
585 # For each direct successors of CURRENT, we check whether its
585 # For each direct successors of CURRENT, we check whether its
586 # successors sets are known. If they are not, we stack the
586 # successors sets are known. If they are not, we stack the
587 # unknown node and proceed to the next iteration of the while
587 # unknown node and proceed to the next iteration of the while
588 # loop. (case 3)
588 # loop. (case 3)
589 #
589 #
590 # During this step, we may detect obsolescence cycles: a node
590 # During this step, we may detect obsolescence cycles: a node
591 # with unknown successors sets but already in the call stack.
591 # with unknown successors sets but already in the call stack.
592 # In such a situation, we arbitrary set the successors sets of
592 # In such a situation, we arbitrary set the successors sets of
593 # the node to nothing (node pruned) to break the cycle.
593 # the node to nothing (node pruned) to break the cycle.
594 #
594 #
595 # If no break was encountered we proceed to phase 2.
595 # If no break was encountered we proceed to phase 2.
596 #
596 #
597 # Phase 2 computes successors sets of CURRENT (case 4); see details
597 # Phase 2 computes successors sets of CURRENT (case 4); see details
598 # in phase 2 itself.
598 # in phase 2 itself.
599 #
599 #
600 # Note the two levels of iteration in each phase.
600 # Note the two levels of iteration in each phase.
601 # - The first one handles obsolescence markers using CURRENT as
601 # - The first one handles obsolescence markers using CURRENT as
602 # precursor (successors markers of CURRENT).
602 # precursor (successors markers of CURRENT).
603 #
603 #
604 # Having multiple entry here means divergence.
604 # Having multiple entry here means divergence.
605 #
605 #
606 # - The second one handles successors defined in each marker.
606 # - The second one handles successors defined in each marker.
607 #
607 #
608 # Having none means pruned node, multiple successors means split,
608 # Having none means pruned node, multiple successors means split,
609 # single successors are standard replacement.
609 # single successors are standard replacement.
610 #
610 #
611 for mark in sorted(succmarkers[current]):
611 for mark in sorted(succmarkers[current]):
612 for suc in mark[1]:
612 for suc in mark[1]:
613 if suc not in cache:
613 if suc not in cache:
614 if suc in stackedset:
614 if suc in stackedset:
615 # cycle breaking
615 # cycle breaking
616 cache[suc] = []
616 cache[suc] = []
617 else:
617 else:
618 # case (3) If we have not computed successors sets
618 # case (3) If we have not computed successors sets
619 # of one of those successors we add it to the
619 # of one of those successors we add it to the
620 # `toproceed` stack and stop all work for this
620 # `toproceed` stack and stop all work for this
621 # iteration.
621 # iteration.
622 toproceed.append(suc)
622 toproceed.append(suc)
623 stackedset.add(suc)
623 stackedset.add(suc)
624 break
624 break
625 else:
625 else:
626 continue
626 continue
627 break
627 break
628 else:
628 else:
629 # case (4): we know all successors sets of all direct
629 # case (4): we know all successors sets of all direct
630 # successors
630 # successors
631 #
631 #
632 # Successors set contributed by each marker depends on the
632 # Successors set contributed by each marker depends on the
633 # successors sets of all its "successors" node.
633 # successors sets of all its "successors" node.
634 #
634 #
635 # Each different marker is a divergence in the obsolescence
635 # Each different marker is a divergence in the obsolescence
636 # history. It contributes successors sets distinct from other
636 # history. It contributes successors sets distinct from other
637 # markers.
637 # markers.
638 #
638 #
639 # Within a marker, a successor may have divergent successors
639 # Within a marker, a successor may have divergent successors
640 # sets. In such a case, the marker will contribute multiple
640 # sets. In such a case, the marker will contribute multiple
641 # divergent successors sets. If multiple successors have
641 # divergent successors sets. If multiple successors have
642 # divergent successors sets, a cartesian product is used.
642 # divergent successors sets, a cartesian product is used.
643 #
643 #
644 # At the end we post-process successors sets to remove
644 # At the end we post-process successors sets to remove
645 # duplicated entry and successors set that are strict subset of
645 # duplicated entry and successors set that are strict subset of
646 # another one.
646 # another one.
647 succssets = []
647 succssets = []
648 for mark in sorted(succmarkers[current]):
648 for mark in sorted(succmarkers[current]):
649 # successors sets contributed by this marker
649 # successors sets contributed by this marker
650 markss = [[]]
650 markss = [[]]
651 for suc in mark[1]:
651 for suc in mark[1]:
652 # cardinal product with previous successors
652 # cardinal product with previous successors
653 productresult = []
653 productresult = []
654 for prefix in markss:
654 for prefix in markss:
655 for suffix in cache[suc]:
655 for suffix in cache[suc]:
656 newss = list(prefix)
656 newss = list(prefix)
657 for part in suffix:
657 for part in suffix:
658 # do not duplicated entry in successors set
658 # do not duplicated entry in successors set
659 # first entry wins.
659 # first entry wins.
660 if part not in newss:
660 if part not in newss:
661 newss.append(part)
661 newss.append(part)
662 productresult.append(newss)
662 productresult.append(newss)
663 markss = productresult
663 markss = productresult
664 succssets.extend(markss)
664 succssets.extend(markss)
665 # remove duplicated and subset
665 # remove duplicated and subset
666 seen = []
666 seen = []
667 final = []
667 final = []
668 candidate = sorted(((set(s), s) for s in succssets if s),
668 candidate = sorted(((set(s), s) for s in succssets if s),
669 key=lambda x: len(x[1]), reverse=True)
669 key=lambda x: len(x[1]), reverse=True)
670 for setversion, listversion in candidate:
670 for setversion, listversion in candidate:
671 for seenset in seen:
671 for seenset in seen:
672 if setversion.issubset(seenset):
672 if setversion.issubset(seenset):
673 break
673 break
674 else:
674 else:
675 final.append(listversion)
675 final.append(listversion)
676 seen.append(setversion)
676 seen.append(setversion)
677 final.reverse() # put small successors set first
677 final.reverse() # put small successors set first
678 cache[current] = final
678 cache[current] = final
679 return cache[initialnode]
679 return cache[initialnode]
680
680
681 def _knownrevs(repo, nodes):
681 def _knownrevs(repo, nodes):
682 """yield revision numbers of known nodes passed in parameters
682 """yield revision numbers of known nodes passed in parameters
683
683
684 Unknown revisions are silently ignored."""
684 Unknown revisions are silently ignored."""
685 torev = repo.changelog.nodemap.get
685 torev = repo.changelog.nodemap.get
686 for n in nodes:
686 for n in nodes:
687 rev = torev(n)
687 rev = torev(n)
688 if rev is not None:
688 if rev is not None:
689 yield rev
689 yield rev
690
690
691 # mapping of 'set-name' -> <function to compute this set>
691 # mapping of 'set-name' -> <function to compute this set>
692 cachefuncs = {}
692 cachefuncs = {}
693 def cachefor(name):
693 def cachefor(name):
694 """Decorator to register a function as computing the cache for a set"""
694 """Decorator to register a function as computing the cache for a set"""
695 def decorator(func):
695 def decorator(func):
696 assert name not in cachefuncs
696 assert name not in cachefuncs
697 cachefuncs[name] = func
697 cachefuncs[name] = func
698 return func
698 return func
699 return decorator
699 return decorator
700
700
701 def getrevs(repo, name):
701 def getrevs(repo, name):
702 """Return the set of revision that belong to the <name> set
702 """Return the set of revision that belong to the <name> set
703
703
704 Such access may compute the set and cache it for future use"""
704 Such access may compute the set and cache it for future use"""
705 repo = repo.unfiltered()
705 repo = repo.unfiltered()
706 if not repo.obsstore:
706 if not repo.obsstore:
707 return ()
707 return ()
708 if name not in repo.obsstore.caches:
708 if name not in repo.obsstore.caches:
709 repo.obsstore.caches[name] = cachefuncs[name](repo)
709 repo.obsstore.caches[name] = cachefuncs[name](repo)
710 return repo.obsstore.caches[name]
710 return repo.obsstore.caches[name]
711
711
712 # To be simple we need to invalidate obsolescence cache when:
712 # To be simple we need to invalidate obsolescence cache when:
713 #
713 #
714 # - new changeset is added:
714 # - new changeset is added:
715 # - public phase is changed
715 # - public phase is changed
716 # - obsolescence marker are added
716 # - obsolescence marker are added
717 # - strip is used a repo
717 # - strip is used a repo
718 def clearobscaches(repo):
718 def clearobscaches(repo):
719 """Remove all obsolescence related cache from a repo
719 """Remove all obsolescence related cache from a repo
720
720
721 This remove all cache in obsstore is the obsstore already exist on the
721 This remove all cache in obsstore is the obsstore already exist on the
722 repo.
722 repo.
723
723
724 (We could be smarter here given the exact event that trigger the cache
724 (We could be smarter here given the exact event that trigger the cache
725 clearing)"""
725 clearing)"""
726 # only clear cache is there is obsstore data in this repo
726 # only clear cache is there is obsstore data in this repo
727 if 'obsstore' in repo._filecache:
727 if 'obsstore' in repo._filecache:
728 repo.obsstore.caches.clear()
728 repo.obsstore.caches.clear()
729
729
730 @cachefor('obsolete')
730 @cachefor('obsolete')
731 def _computeobsoleteset(repo):
731 def _computeobsoleteset(repo):
732 """the set of obsolete revisions"""
732 """the set of obsolete revisions"""
733 obs = set()
733 obs = set()
734 getrev = repo.changelog.nodemap.get
734 getrev = repo.changelog.nodemap.get
735 getphase = repo._phasecache.phase
735 getphase = repo._phasecache.phase
736 for node in repo.obsstore.successors:
736 for node in repo.obsstore.successors:
737 rev = getrev(node)
737 rev = getrev(node)
738 if rev is not None and getphase(repo, rev):
738 if rev is not None and getphase(repo, rev):
739 obs.add(rev)
739 obs.add(rev)
740 return obs
740 return obs
741
741
742 @cachefor('unstable')
742 @cachefor('unstable')
743 def _computeunstableset(repo):
743 def _computeunstableset(repo):
744 """the set of non obsolete revisions with obsolete parents"""
744 """the set of non obsolete revisions with obsolete parents"""
745 # revset is not efficient enough here
745 # revset is not efficient enough here
746 # we do (obsolete()::) - obsolete() by hand
746 # we do (obsolete()::) - obsolete() by hand
747 obs = getrevs(repo, 'obsolete')
747 obs = getrevs(repo, 'obsolete')
748 if not obs:
748 if not obs:
749 return set()
749 return set()
750 cl = repo.changelog
750 cl = repo.changelog
751 return set(r for r in cl.descendants(obs) if r not in obs)
751 return set(r for r in cl.descendants(obs) if r not in obs)
752
752
753 @cachefor('suspended')
753 @cachefor('suspended')
754 def _computesuspendedset(repo):
754 def _computesuspendedset(repo):
755 """the set of obsolete parents with non obsolete descendants"""
755 """the set of obsolete parents with non obsolete descendants"""
756 suspended = repo.changelog.ancestors(getrevs(repo, 'unstable'))
756 suspended = repo.changelog.ancestors(getrevs(repo, 'unstable'))
757 return set(r for r in getrevs(repo, 'obsolete') if r in suspended)
757 return set(r for r in getrevs(repo, 'obsolete') if r in suspended)
758
758
759 @cachefor('extinct')
759 @cachefor('extinct')
760 def _computeextinctset(repo):
760 def _computeextinctset(repo):
761 """the set of obsolete parents without non obsolete descendants"""
761 """the set of obsolete parents without non obsolete descendants"""
762 return getrevs(repo, 'obsolete') - getrevs(repo, 'suspended')
762 return getrevs(repo, 'obsolete') - getrevs(repo, 'suspended')
763
763
764
764
765 @cachefor('bumped')
765 @cachefor('bumped')
766 def _computebumpedset(repo):
766 def _computebumpedset(repo):
767 """the set of revs trying to obsolete public revisions"""
767 """the set of revs trying to obsolete public revisions"""
768 bumped = set()
768 bumped = set()
769 # utils function (avoid attribut lookup in the loop)
769 # utils function (avoid attribute lookup in the loop)
770 phase = repo._phasecache.phase # would be faster to grab the full list
770 phase = repo._phasecache.phase # would be faster to grab the full list
771 public = phases.public
771 public = phases.public
772 cl = repo.changelog
772 cl = repo.changelog
773 torev = cl.nodemap.get
773 torev = cl.nodemap.get
774 obs = getrevs(repo, 'obsolete')
774 obs = getrevs(repo, 'obsolete')
775 for rev in repo:
775 for rev in repo:
776 # We only evaluate mutable, non-obsolete revision
776 # We only evaluate mutable, non-obsolete revision
777 if (public < phase(repo, rev)) and (rev not in obs):
777 if (public < phase(repo, rev)) and (rev not in obs):
778 node = cl.node(rev)
778 node = cl.node(rev)
779 # (future) A cache of precursors may worth if split is very common
779 # (future) A cache of precursors may worth if split is very common
780 for pnode in allprecursors(repo.obsstore, [node],
780 for pnode in allprecursors(repo.obsstore, [node],
781 ignoreflags=bumpedfix):
781 ignoreflags=bumpedfix):
782 prev = torev(pnode) # unfiltered! but so is phasecache
782 prev = torev(pnode) # unfiltered! but so is phasecache
783 if (prev is not None) and (phase(repo, prev) <= public):
783 if (prev is not None) and (phase(repo, prev) <= public):
784 # we have a public precursors
784 # we have a public precursors
785 bumped.add(rev)
785 bumped.add(rev)
786 break # Next draft!
786 break # Next draft!
787 return bumped
787 return bumped
788
788
789 @cachefor('divergent')
789 @cachefor('divergent')
790 def _computedivergentset(repo):
790 def _computedivergentset(repo):
791 """the set of rev that compete to be the final successors of some revision.
791 """the set of rev that compete to be the final successors of some revision.
792 """
792 """
793 divergent = set()
793 divergent = set()
794 obsstore = repo.obsstore
794 obsstore = repo.obsstore
795 newermap = {}
795 newermap = {}
796 for ctx in repo.set('(not public()) - obsolete()'):
796 for ctx in repo.set('(not public()) - obsolete()'):
797 mark = obsstore.precursors.get(ctx.node(), ())
797 mark = obsstore.precursors.get(ctx.node(), ())
798 toprocess = set(mark)
798 toprocess = set(mark)
799 while toprocess:
799 while toprocess:
800 prec = toprocess.pop()[0]
800 prec = toprocess.pop()[0]
801 if prec not in newermap:
801 if prec not in newermap:
802 successorssets(repo, prec, newermap)
802 successorssets(repo, prec, newermap)
803 newer = [n for n in newermap[prec] if n]
803 newer = [n for n in newermap[prec] if n]
804 if len(newer) > 1:
804 if len(newer) > 1:
805 divergent.add(ctx.rev())
805 divergent.add(ctx.rev())
806 break
806 break
807 toprocess.update(obsstore.precursors.get(prec, ()))
807 toprocess.update(obsstore.precursors.get(prec, ()))
808 return divergent
808 return divergent
809
809
810
810
811 def createmarkers(repo, relations, flag=0, metadata=None):
811 def createmarkers(repo, relations, flag=0, metadata=None):
812 """Add obsolete markers between changesets in a repo
812 """Add obsolete markers between changesets in a repo
813
813
814 <relations> must be an iterable of (<old>, (<new>, ...)[,{metadata}])
814 <relations> must be an iterable of (<old>, (<new>, ...)[,{metadata}])
815 tuple. `old` and `news` are changectx. metadata is an optional dictionnary
815 tuple. `old` and `news` are changectx. metadata is an optional dictionnary
816 containing metadata for this marker only. It is merged with the global
816 containing metadata for this marker only. It is merged with the global
817 metadata specified through the `metadata` argument of this function,
817 metadata specified through the `metadata` argument of this function,
818
818
819 Trying to obsolete a public changeset will raise an exception.
819 Trying to obsolete a public changeset will raise an exception.
820
820
821 Current user and date are used except if specified otherwise in the
821 Current user and date are used except if specified otherwise in the
822 metadata attribute.
822 metadata attribute.
823
823
824 This function operates within a transaction of its own, but does
824 This function operates within a transaction of its own, but does
825 not take any lock on the repo.
825 not take any lock on the repo.
826 """
826 """
827 # prepare metadata
827 # prepare metadata
828 if metadata is None:
828 if metadata is None:
829 metadata = {}
829 metadata = {}
830 if 'date' not in metadata:
830 if 'date' not in metadata:
831 metadata['date'] = '%i %i' % util.makedate()
831 metadata['date'] = '%i %i' % util.makedate()
832 if 'user' not in metadata:
832 if 'user' not in metadata:
833 metadata['user'] = repo.ui.username()
833 metadata['user'] = repo.ui.username()
834 tr = repo.transaction('add-obsolescence-marker')
834 tr = repo.transaction('add-obsolescence-marker')
835 try:
835 try:
836 for rel in relations:
836 for rel in relations:
837 prec = rel[0]
837 prec = rel[0]
838 sucs = rel[1]
838 sucs = rel[1]
839 localmetadata = metadata.copy()
839 localmetadata = metadata.copy()
840 if 2 < len(rel):
840 if 2 < len(rel):
841 localmetadata.update(rel[2])
841 localmetadata.update(rel[2])
842
842
843 if not prec.mutable():
843 if not prec.mutable():
844 raise util.Abort("cannot obsolete immutable changeset: %s"
844 raise util.Abort("cannot obsolete immutable changeset: %s"
845 % prec)
845 % prec)
846 nprec = prec.node()
846 nprec = prec.node()
847 nsucs = tuple(s.node() for s in sucs)
847 nsucs = tuple(s.node() for s in sucs)
848 if nprec in nsucs:
848 if nprec in nsucs:
849 raise util.Abort("changeset %s cannot obsolete itself" % prec)
849 raise util.Abort("changeset %s cannot obsolete itself" % prec)
850 repo.obsstore.create(tr, nprec, nsucs, flag, localmetadata)
850 repo.obsstore.create(tr, nprec, nsucs, flag, localmetadata)
851 repo.filteredrevcache.clear()
851 repo.filteredrevcache.clear()
852 tr.close()
852 tr.close()
853 finally:
853 finally:
854 tr.release()
854 tr.release()
@@ -1,213 +1,213 b''
1 # repoview.py - Filtered view of a localrepo object
1 # repoview.py - Filtered view of a localrepo object
2 #
2 #
3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 # Logilab SA <contact@logilab.fr>
4 # Logilab SA <contact@logilab.fr>
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 import copy
9 import copy
10 import phases
10 import phases
11 import util
11 import util
12 import obsolete
12 import obsolete
13
13
14
14
15 def hideablerevs(repo):
15 def hideablerevs(repo):
16 """Revisions candidates to be hidden
16 """Revisions candidates to be hidden
17
17
18 This is a standalone function to help extensions to wrap it."""
18 This is a standalone function to help extensions to wrap it."""
19 return obsolete.getrevs(repo, 'obsolete')
19 return obsolete.getrevs(repo, 'obsolete')
20
20
21 def computehidden(repo):
21 def computehidden(repo):
22 """compute the set of hidden revision to filter
22 """compute the set of hidden revision to filter
23
23
24 During most operation hidden should be filtered."""
24 During most operation hidden should be filtered."""
25 assert not repo.changelog.filteredrevs
25 assert not repo.changelog.filteredrevs
26 hideable = hideablerevs(repo)
26 hideable = hideablerevs(repo)
27 if hideable:
27 if hideable:
28 cl = repo.changelog
28 cl = repo.changelog
29 firsthideable = min(hideable)
29 firsthideable = min(hideable)
30 revs = cl.revs(start=firsthideable)
30 revs = cl.revs(start=firsthideable)
31 tofilter = repo.revs(
31 tofilter = repo.revs(
32 '(%ld) and children(%ld)', list(revs), list(hideable))
32 '(%ld) and children(%ld)', list(revs), list(hideable))
33 blockers = [r for r in tofilter if r not in hideable]
33 blockers = [r for r in tofilter if r not in hideable]
34 for par in repo[None].parents():
34 for par in repo[None].parents():
35 blockers.append(par.rev())
35 blockers.append(par.rev())
36 for bm in repo._bookmarks.values():
36 for bm in repo._bookmarks.values():
37 blockers.append(repo[bm].rev())
37 blockers.append(repo[bm].rev())
38 blocked = cl.ancestors(blockers, inclusive=True)
38 blocked = cl.ancestors(blockers, inclusive=True)
39 return frozenset(r for r in hideable if r not in blocked)
39 return frozenset(r for r in hideable if r not in blocked)
40 return frozenset()
40 return frozenset()
41
41
42 def computeunserved(repo):
42 def computeunserved(repo):
43 """compute the set of revision that should be filtered when used a server
43 """compute the set of revision that should be filtered when used a server
44
44
45 Secret and hidden changeset should not pretend to be here."""
45 Secret and hidden changeset should not pretend to be here."""
46 assert not repo.changelog.filteredrevs
46 assert not repo.changelog.filteredrevs
47 # fast path in simple case to avoid impact of non optimised code
47 # fast path in simple case to avoid impact of non optimised code
48 hiddens = filterrevs(repo, 'visible')
48 hiddens = filterrevs(repo, 'visible')
49 if phases.hassecret(repo):
49 if phases.hassecret(repo):
50 cl = repo.changelog
50 cl = repo.changelog
51 secret = phases.secret
51 secret = phases.secret
52 getphase = repo._phasecache.phase
52 getphase = repo._phasecache.phase
53 first = min(cl.rev(n) for n in repo._phasecache.phaseroots[secret])
53 first = min(cl.rev(n) for n in repo._phasecache.phaseroots[secret])
54 revs = cl.revs(start=first)
54 revs = cl.revs(start=first)
55 secrets = set(r for r in revs if getphase(repo, r) >= secret)
55 secrets = set(r for r in revs if getphase(repo, r) >= secret)
56 return frozenset(hiddens | secrets)
56 return frozenset(hiddens | secrets)
57 else:
57 else:
58 return hiddens
58 return hiddens
59
59
60 def computemutable(repo):
60 def computemutable(repo):
61 """compute the set of revision that should be filtered when used a server
61 """compute the set of revision that should be filtered when used a server
62
62
63 Secret and hidden changeset should not pretend to be here."""
63 Secret and hidden changeset should not pretend to be here."""
64 assert not repo.changelog.filteredrevs
64 assert not repo.changelog.filteredrevs
65 # fast check to avoid revset call on huge repo
65 # fast check to avoid revset call on huge repo
66 if util.any(repo._phasecache.phaseroots[1:]):
66 if util.any(repo._phasecache.phaseroots[1:]):
67 getphase = repo._phasecache.phase
67 getphase = repo._phasecache.phase
68 maymutable = filterrevs(repo, 'base')
68 maymutable = filterrevs(repo, 'base')
69 return frozenset(r for r in maymutable if getphase(repo, r))
69 return frozenset(r for r in maymutable if getphase(repo, r))
70 return frozenset()
70 return frozenset()
71
71
72 def computeimpactable(repo):
72 def computeimpactable(repo):
73 """Everything impactable by mutable revision
73 """Everything impactable by mutable revision
74
74
75 The immutable filter still have some chance to get invalidated. This will
75 The immutable filter still have some chance to get invalidated. This will
76 happen when:
76 happen when:
77
77
78 - you garbage collect hidden changeset,
78 - you garbage collect hidden changeset,
79 - public phase is moved backward,
79 - public phase is moved backward,
80 - something is changed in the filtering (this could be fixed)
80 - something is changed in the filtering (this could be fixed)
81
81
82 This filter out any mutable changeset and any public changeset that may be
82 This filter out any mutable changeset and any public changeset that may be
83 impacted by something happening to a mutable revision.
83 impacted by something happening to a mutable revision.
84
84
85 This is achieved by filtered everything with a revision number egal or
85 This is achieved by filtered everything with a revision number egal or
86 higher than the first mutable changeset is filtered."""
86 higher than the first mutable changeset is filtered."""
87 assert not repo.changelog.filteredrevs
87 assert not repo.changelog.filteredrevs
88 cl = repo.changelog
88 cl = repo.changelog
89 firstmutable = len(cl)
89 firstmutable = len(cl)
90 for roots in repo._phasecache.phaseroots[1:]:
90 for roots in repo._phasecache.phaseroots[1:]:
91 if roots:
91 if roots:
92 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
92 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
93 # protect from nullrev root
93 # protect from nullrev root
94 firstmutable = max(0, firstmutable)
94 firstmutable = max(0, firstmutable)
95 return frozenset(xrange(firstmutable, len(cl)))
95 return frozenset(xrange(firstmutable, len(cl)))
96
96
97 # function to compute filtered set
97 # function to compute filtered set
98 #
98 #
99 # When addding a new filter you MUST update the table at:
99 # When adding a new filter you MUST update the table at:
100 # mercurial.branchmap.subsettable
100 # mercurial.branchmap.subsettable
101 # Otherwise your filter will have to recompute all its branches cache
101 # Otherwise your filter will have to recompute all its branches cache
102 # from scratch (very slow).
102 # from scratch (very slow).
103 filtertable = {'visible': computehidden,
103 filtertable = {'visible': computehidden,
104 'served': computeunserved,
104 'served': computeunserved,
105 'immutable': computemutable,
105 'immutable': computemutable,
106 'base': computeimpactable}
106 'base': computeimpactable}
107
107
108 def filterrevs(repo, filtername):
108 def filterrevs(repo, filtername):
109 """returns set of filtered revision for this filter name"""
109 """returns set of filtered revision for this filter name"""
110 if filtername not in repo.filteredrevcache:
110 if filtername not in repo.filteredrevcache:
111 func = filtertable[filtername]
111 func = filtertable[filtername]
112 repo.filteredrevcache[filtername] = func(repo.unfiltered())
112 repo.filteredrevcache[filtername] = func(repo.unfiltered())
113 return repo.filteredrevcache[filtername]
113 return repo.filteredrevcache[filtername]
114
114
115 class repoview(object):
115 class repoview(object):
116 """Provide a read/write view of a repo through a filtered changelog
116 """Provide a read/write view of a repo through a filtered changelog
117
117
118 This object is used to access a filtered version of a repository without
118 This object is used to access a filtered version of a repository without
119 altering the original repository object itself. We can not alter the
119 altering the original repository object itself. We can not alter the
120 original object for two main reasons:
120 original object for two main reasons:
121 - It prevents the use of a repo with multiple filters at the same time. In
121 - It prevents the use of a repo with multiple filters at the same time. In
122 particular when multiple threads are involved.
122 particular when multiple threads are involved.
123 - It makes scope of the filtering harder to control.
123 - It makes scope of the filtering harder to control.
124
124
125 This object behaves very closely to the original repository. All attribute
125 This object behaves very closely to the original repository. All attribute
126 operations are done on the original repository:
126 operations are done on the original repository:
127 - An access to `repoview.someattr` actually returns `repo.someattr`,
127 - An access to `repoview.someattr` actually returns `repo.someattr`,
128 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
128 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
129 - A deletion of `repoview.someattr` actually drops `someattr`
129 - A deletion of `repoview.someattr` actually drops `someattr`
130 from `repo.__dict__`.
130 from `repo.__dict__`.
131
131
132 The only exception is the `changelog` property. It is overridden to return
132 The only exception is the `changelog` property. It is overridden to return
133 a (surface) copy of `repo.changelog` with some revisions filtered. The
133 a (surface) copy of `repo.changelog` with some revisions filtered. The
134 `filtername` attribute of the view control the revisions that need to be
134 `filtername` attribute of the view control the revisions that need to be
135 filtered. (the fact the changelog is copied is an implementation detail).
135 filtered. (the fact the changelog is copied is an implementation detail).
136
136
137 Unlike attributes, this object intercepts all method calls. This means that
137 Unlike attributes, this object intercepts all method calls. This means that
138 all methods are run on the `repoview` object with the filtered `changelog`
138 all methods are run on the `repoview` object with the filtered `changelog`
139 property. For this purpose the simple `repoview` class must be mixed with
139 property. For this purpose the simple `repoview` class must be mixed with
140 the actual class of the repository. This ensures that the resulting
140 the actual class of the repository. This ensures that the resulting
141 `repoview` object have the very same methods than the repo object. This
141 `repoview` object have the very same methods than the repo object. This
142 leads to the property below.
142 leads to the property below.
143
143
144 repoview.method() --> repo.__class__.method(repoview)
144 repoview.method() --> repo.__class__.method(repoview)
145
145
146 The inheritance has to be done dynamically because `repo` can be of any
146 The inheritance has to be done dynamically because `repo` can be of any
147 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
147 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
148 """
148 """
149
149
150 def __init__(self, repo, filtername):
150 def __init__(self, repo, filtername):
151 object.__setattr__(self, '_unfilteredrepo', repo)
151 object.__setattr__(self, '_unfilteredrepo', repo)
152 object.__setattr__(self, 'filtername', filtername)
152 object.__setattr__(self, 'filtername', filtername)
153 object.__setattr__(self, '_clcachekey', None)
153 object.__setattr__(self, '_clcachekey', None)
154 object.__setattr__(self, '_clcache', None)
154 object.__setattr__(self, '_clcache', None)
155
155
156 # not a propertycache on purpose we shall implement a proper cache later
156 # not a propertycache on purpose we shall implement a proper cache later
157 @property
157 @property
158 def changelog(self):
158 def changelog(self):
159 """return a filtered version of the changeset
159 """return a filtered version of the changeset
160
160
161 this changelog must not be used for writing"""
161 this changelog must not be used for writing"""
162 # some cache may be implemented later
162 # some cache may be implemented later
163 unfi = self._unfilteredrepo
163 unfi = self._unfilteredrepo
164 unfichangelog = unfi.changelog
164 unfichangelog = unfi.changelog
165 revs = filterrevs(unfi, self.filtername)
165 revs = filterrevs(unfi, self.filtername)
166 cl = self._clcache
166 cl = self._clcache
167 newkey = (len(unfichangelog), unfichangelog.tip(), hash(revs))
167 newkey = (len(unfichangelog), unfichangelog.tip(), hash(revs))
168 if cl is not None:
168 if cl is not None:
169 # we need to check curkey too for some obscure reason.
169 # we need to check curkey too for some obscure reason.
170 # MQ test show a corruption of the underlying repo (in _clcache)
170 # MQ test show a corruption of the underlying repo (in _clcache)
171 # without change in the cachekey.
171 # without change in the cachekey.
172 oldfilter = cl.filteredrevs
172 oldfilter = cl.filteredrevs
173 try:
173 try:
174 cl.filterrevs = () # disable filtering for tip
174 cl.filterrevs = () # disable filtering for tip
175 curkey = (len(cl), cl.tip(), hash(oldfilter))
175 curkey = (len(cl), cl.tip(), hash(oldfilter))
176 finally:
176 finally:
177 cl.filteredrevs = oldfilter
177 cl.filteredrevs = oldfilter
178 if newkey != self._clcachekey or newkey != curkey:
178 if newkey != self._clcachekey or newkey != curkey:
179 cl = None
179 cl = None
180 # could have been made None by the previous if
180 # could have been made None by the previous if
181 if cl is None:
181 if cl is None:
182 cl = copy.copy(unfichangelog)
182 cl = copy.copy(unfichangelog)
183 cl.filteredrevs = revs
183 cl.filteredrevs = revs
184 object.__setattr__(self, '_clcache', cl)
184 object.__setattr__(self, '_clcache', cl)
185 object.__setattr__(self, '_clcachekey', newkey)
185 object.__setattr__(self, '_clcachekey', newkey)
186 return cl
186 return cl
187
187
188 def unfiltered(self):
188 def unfiltered(self):
189 """Return an unfiltered version of a repo"""
189 """Return an unfiltered version of a repo"""
190 return self._unfilteredrepo
190 return self._unfilteredrepo
191
191
192 def filtered(self, name):
192 def filtered(self, name):
193 """Return a filtered version of a repository"""
193 """Return a filtered version of a repository"""
194 if name == self.filtername:
194 if name == self.filtername:
195 return self
195 return self
196 return self.unfiltered().filtered(name)
196 return self.unfiltered().filtered(name)
197
197
198 # everything access are forwarded to the proxied repo
198 # everything access are forwarded to the proxied repo
199 def __getattr__(self, attr):
199 def __getattr__(self, attr):
200 return getattr(self._unfilteredrepo, attr)
200 return getattr(self._unfilteredrepo, attr)
201
201
202 def __setattr__(self, attr, value):
202 def __setattr__(self, attr, value):
203 return setattr(self._unfilteredrepo, attr, value)
203 return setattr(self._unfilteredrepo, attr, value)
204
204
205 def __delattr__(self, attr):
205 def __delattr__(self, attr):
206 return delattr(self._unfilteredrepo, attr)
206 return delattr(self._unfilteredrepo, attr)
207
207
208 # The `requirements` attribute is initialized during __init__. But
208 # The `requirements` attribute is initialized during __init__. But
209 # __getattr__ won't be called as it also exists on the class. We need
209 # __getattr__ won't be called as it also exists on the class. We need
210 # explicit forwarding to main repo here
210 # explicit forwarding to main repo here
211 @property
211 @property
212 def requirements(self):
212 def requirements(self):
213 return self._unfilteredrepo.requirements
213 return self._unfilteredrepo.requirements
General Comments 0
You need to be logged in to leave comments. Login now