##// END OF EJS Templates
py3: slice over bytes to prevent getting ascii values...
Pulkit Goyal -
r36198:34e85044 default
parent child Browse files
Show More
@@ -1,478 +1,478 b''
1 # Revision graph generator for Mercurial
1 # Revision graph generator for Mercurial
2 #
2 #
3 # Copyright 2008 Dirkjan Ochtman <dirkjan@ochtman.nl>
3 # Copyright 2008 Dirkjan Ochtman <dirkjan@ochtman.nl>
4 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
4 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
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 """supports walking the history as DAGs suitable for graphical output
9 """supports walking the history as DAGs suitable for graphical output
10
10
11 The most basic format we use is that of::
11 The most basic format we use is that of::
12
12
13 (id, type, data, [parentids])
13 (id, type, data, [parentids])
14
14
15 The node and parent ids are arbitrary integers which identify a node in the
15 The node and parent ids are arbitrary integers which identify a node in the
16 context of the graph returned. Type is a constant specifying the node type.
16 context of the graph returned. Type is a constant specifying the node type.
17 Data depends on type.
17 Data depends on type.
18 """
18 """
19
19
20 from __future__ import absolute_import
20 from __future__ import absolute_import
21
21
22 from .node import nullrev
22 from .node import nullrev
23 from . import (
23 from . import (
24 dagop,
24 dagop,
25 smartset,
25 smartset,
26 util,
26 util,
27 )
27 )
28
28
29 CHANGESET = 'C'
29 CHANGESET = 'C'
30 PARENT = 'P'
30 PARENT = 'P'
31 GRANDPARENT = 'G'
31 GRANDPARENT = 'G'
32 MISSINGPARENT = 'M'
32 MISSINGPARENT = 'M'
33 # Style of line to draw. None signals a line that ends and is removed at this
33 # Style of line to draw. None signals a line that ends and is removed at this
34 # point. A number prefix means only the last N characters of the current block
34 # point. A number prefix means only the last N characters of the current block
35 # will use that style, the rest will use the PARENT style. Add a - sign
35 # will use that style, the rest will use the PARENT style. Add a - sign
36 # (so making N negative) and all but the first N characters use that style.
36 # (so making N negative) and all but the first N characters use that style.
37 EDGES = {PARENT: '|', GRANDPARENT: ':', MISSINGPARENT: None}
37 EDGES = {PARENT: '|', GRANDPARENT: ':', MISSINGPARENT: None}
38
38
39 def dagwalker(repo, revs):
39 def dagwalker(repo, revs):
40 """cset DAG generator yielding (id, CHANGESET, ctx, [parentinfo]) tuples
40 """cset DAG generator yielding (id, CHANGESET, ctx, [parentinfo]) tuples
41
41
42 This generator function walks through revisions (which should be ordered
42 This generator function walks through revisions (which should be ordered
43 from bigger to lower). It returns a tuple for each node.
43 from bigger to lower). It returns a tuple for each node.
44
44
45 Each parentinfo entry is a tuple with (edgetype, parentid), where edgetype
45 Each parentinfo entry is a tuple with (edgetype, parentid), where edgetype
46 is one of PARENT, GRANDPARENT or MISSINGPARENT. The node and parent ids
46 is one of PARENT, GRANDPARENT or MISSINGPARENT. The node and parent ids
47 are arbitrary integers which identify a node in the context of the graph
47 are arbitrary integers which identify a node in the context of the graph
48 returned.
48 returned.
49
49
50 """
50 """
51 gpcache = {}
51 gpcache = {}
52
52
53 for rev in revs:
53 for rev in revs:
54 ctx = repo[rev]
54 ctx = repo[rev]
55 # partition into parents in the rev set and missing parents, then
55 # partition into parents in the rev set and missing parents, then
56 # augment the lists with markers, to inform graph drawing code about
56 # augment the lists with markers, to inform graph drawing code about
57 # what kind of edge to draw between nodes.
57 # what kind of edge to draw between nodes.
58 pset = set(p.rev() for p in ctx.parents() if p.rev() in revs)
58 pset = set(p.rev() for p in ctx.parents() if p.rev() in revs)
59 mpars = [p.rev() for p in ctx.parents()
59 mpars = [p.rev() for p in ctx.parents()
60 if p.rev() != nullrev and p.rev() not in pset]
60 if p.rev() != nullrev and p.rev() not in pset]
61 parents = [(PARENT, p) for p in sorted(pset)]
61 parents = [(PARENT, p) for p in sorted(pset)]
62
62
63 for mpar in mpars:
63 for mpar in mpars:
64 gp = gpcache.get(mpar)
64 gp = gpcache.get(mpar)
65 if gp is None:
65 if gp is None:
66 # precompute slow query as we know reachableroots() goes
66 # precompute slow query as we know reachableroots() goes
67 # through all revs (issue4782)
67 # through all revs (issue4782)
68 if not isinstance(revs, smartset.baseset):
68 if not isinstance(revs, smartset.baseset):
69 revs = smartset.baseset(revs)
69 revs = smartset.baseset(revs)
70 gp = gpcache[mpar] = sorted(set(dagop.reachableroots(
70 gp = gpcache[mpar] = sorted(set(dagop.reachableroots(
71 repo, revs, [mpar])))
71 repo, revs, [mpar])))
72 if not gp:
72 if not gp:
73 parents.append((MISSINGPARENT, mpar))
73 parents.append((MISSINGPARENT, mpar))
74 pset.add(mpar)
74 pset.add(mpar)
75 else:
75 else:
76 parents.extend((GRANDPARENT, g) for g in gp if g not in pset)
76 parents.extend((GRANDPARENT, g) for g in gp if g not in pset)
77 pset.update(gp)
77 pset.update(gp)
78
78
79 yield (ctx.rev(), CHANGESET, ctx, parents)
79 yield (ctx.rev(), CHANGESET, ctx, parents)
80
80
81 def nodes(repo, nodes):
81 def nodes(repo, nodes):
82 """cset DAG generator yielding (id, CHANGESET, ctx, [parentids]) tuples
82 """cset DAG generator yielding (id, CHANGESET, ctx, [parentids]) tuples
83
83
84 This generator function walks the given nodes. It only returns parents
84 This generator function walks the given nodes. It only returns parents
85 that are in nodes, too.
85 that are in nodes, too.
86 """
86 """
87 include = set(nodes)
87 include = set(nodes)
88 for node in nodes:
88 for node in nodes:
89 ctx = repo[node]
89 ctx = repo[node]
90 parents = set((PARENT, p.rev()) for p in ctx.parents()
90 parents = set((PARENT, p.rev()) for p in ctx.parents()
91 if p.node() in include)
91 if p.node() in include)
92 yield (ctx.rev(), CHANGESET, ctx, sorted(parents))
92 yield (ctx.rev(), CHANGESET, ctx, sorted(parents))
93
93
94 def colored(dag, repo):
94 def colored(dag, repo):
95 """annotates a DAG with colored edge information
95 """annotates a DAG with colored edge information
96
96
97 For each DAG node this function emits tuples::
97 For each DAG node this function emits tuples::
98
98
99 (id, type, data, (col, color), [(col, nextcol, color)])
99 (id, type, data, (col, color), [(col, nextcol, color)])
100
100
101 with the following new elements:
101 with the following new elements:
102
102
103 - Tuple (col, color) with column and color index for the current node
103 - Tuple (col, color) with column and color index for the current node
104 - A list of tuples indicating the edges between the current node and its
104 - A list of tuples indicating the edges between the current node and its
105 parents.
105 parents.
106 """
106 """
107 seen = []
107 seen = []
108 colors = {}
108 colors = {}
109 newcolor = 1
109 newcolor = 1
110 config = {}
110 config = {}
111
111
112 for key, val in repo.ui.configitems('graph'):
112 for key, val in repo.ui.configitems('graph'):
113 if '.' in key:
113 if '.' in key:
114 branch, setting = key.rsplit('.', 1)
114 branch, setting = key.rsplit('.', 1)
115 # Validation
115 # Validation
116 if setting == "width" and val.isdigit():
116 if setting == "width" and val.isdigit():
117 config.setdefault(branch, {})[setting] = int(val)
117 config.setdefault(branch, {})[setting] = int(val)
118 elif setting == "color" and val.isalnum():
118 elif setting == "color" and val.isalnum():
119 config.setdefault(branch, {})[setting] = val
119 config.setdefault(branch, {})[setting] = val
120
120
121 if config:
121 if config:
122 getconf = util.lrucachefunc(
122 getconf = util.lrucachefunc(
123 lambda rev: config.get(repo[rev].branch(), {}))
123 lambda rev: config.get(repo[rev].branch(), {}))
124 else:
124 else:
125 getconf = lambda rev: {}
125 getconf = lambda rev: {}
126
126
127 for (cur, type, data, parents) in dag:
127 for (cur, type, data, parents) in dag:
128
128
129 # Compute seen and next
129 # Compute seen and next
130 if cur not in seen:
130 if cur not in seen:
131 seen.append(cur) # new head
131 seen.append(cur) # new head
132 colors[cur] = newcolor
132 colors[cur] = newcolor
133 newcolor += 1
133 newcolor += 1
134
134
135 col = seen.index(cur)
135 col = seen.index(cur)
136 color = colors.pop(cur)
136 color = colors.pop(cur)
137 next = seen[:]
137 next = seen[:]
138
138
139 # Add parents to next
139 # Add parents to next
140 addparents = [p for pt, p in parents if p not in next]
140 addparents = [p for pt, p in parents if p not in next]
141 next[col:col + 1] = addparents
141 next[col:col + 1] = addparents
142
142
143 # Set colors for the parents
143 # Set colors for the parents
144 for i, p in enumerate(addparents):
144 for i, p in enumerate(addparents):
145 if not i:
145 if not i:
146 colors[p] = color
146 colors[p] = color
147 else:
147 else:
148 colors[p] = newcolor
148 colors[p] = newcolor
149 newcolor += 1
149 newcolor += 1
150
150
151 # Add edges to the graph
151 # Add edges to the graph
152 edges = []
152 edges = []
153 for ecol, eid in enumerate(seen):
153 for ecol, eid in enumerate(seen):
154 if eid in next:
154 if eid in next:
155 bconf = getconf(eid)
155 bconf = getconf(eid)
156 edges.append((
156 edges.append((
157 ecol, next.index(eid), colors[eid],
157 ecol, next.index(eid), colors[eid],
158 bconf.get('width', -1),
158 bconf.get('width', -1),
159 bconf.get('color', '')))
159 bconf.get('color', '')))
160 elif eid == cur:
160 elif eid == cur:
161 for ptype, p in parents:
161 for ptype, p in parents:
162 bconf = getconf(p)
162 bconf = getconf(p)
163 edges.append((
163 edges.append((
164 ecol, next.index(p), color,
164 ecol, next.index(p), color,
165 bconf.get('width', -1),
165 bconf.get('width', -1),
166 bconf.get('color', '')))
166 bconf.get('color', '')))
167
167
168 # Yield and move on
168 # Yield and move on
169 yield (cur, type, data, (col, color), edges)
169 yield (cur, type, data, (col, color), edges)
170 seen = next
170 seen = next
171
171
172 def asciiedges(type, char, state, rev, parents):
172 def asciiedges(type, char, state, rev, parents):
173 """adds edge info to changelog DAG walk suitable for ascii()"""
173 """adds edge info to changelog DAG walk suitable for ascii()"""
174 seen = state['seen']
174 seen = state['seen']
175 if rev not in seen:
175 if rev not in seen:
176 seen.append(rev)
176 seen.append(rev)
177 nodeidx = seen.index(rev)
177 nodeidx = seen.index(rev)
178
178
179 knownparents = []
179 knownparents = []
180 newparents = []
180 newparents = []
181 for ptype, parent in parents:
181 for ptype, parent in parents:
182 if parent == rev:
182 if parent == rev:
183 # self reference (should only be seen in null rev)
183 # self reference (should only be seen in null rev)
184 continue
184 continue
185 if parent in seen:
185 if parent in seen:
186 knownparents.append(parent)
186 knownparents.append(parent)
187 else:
187 else:
188 newparents.append(parent)
188 newparents.append(parent)
189 state['edges'][parent] = state['styles'].get(ptype, '|')
189 state['edges'][parent] = state['styles'].get(ptype, '|')
190
190
191 ncols = len(seen)
191 ncols = len(seen)
192 width = 1 + ncols * 2
192 width = 1 + ncols * 2
193 nextseen = seen[:]
193 nextseen = seen[:]
194 nextseen[nodeidx:nodeidx + 1] = newparents
194 nextseen[nodeidx:nodeidx + 1] = newparents
195 edges = [(nodeidx, nextseen.index(p)) for p in knownparents]
195 edges = [(nodeidx, nextseen.index(p)) for p in knownparents]
196
196
197 seen[:] = nextseen
197 seen[:] = nextseen
198 while len(newparents) > 2:
198 while len(newparents) > 2:
199 # ascii() only knows how to add or remove a single column between two
199 # ascii() only knows how to add or remove a single column between two
200 # calls. Nodes with more than two parents break this constraint so we
200 # calls. Nodes with more than two parents break this constraint so we
201 # introduce intermediate expansion lines to grow the active node list
201 # introduce intermediate expansion lines to grow the active node list
202 # slowly.
202 # slowly.
203 edges.append((nodeidx, nodeidx))
203 edges.append((nodeidx, nodeidx))
204 edges.append((nodeidx, nodeidx + 1))
204 edges.append((nodeidx, nodeidx + 1))
205 nmorecols = 1
205 nmorecols = 1
206 width += 2
206 width += 2
207 yield (type, char, width, (nodeidx, edges, ncols, nmorecols))
207 yield (type, char, width, (nodeidx, edges, ncols, nmorecols))
208 char = '\\'
208 char = '\\'
209 nodeidx += 1
209 nodeidx += 1
210 ncols += 1
210 ncols += 1
211 edges = []
211 edges = []
212 del newparents[0]
212 del newparents[0]
213
213
214 if len(newparents) > 0:
214 if len(newparents) > 0:
215 edges.append((nodeidx, nodeidx))
215 edges.append((nodeidx, nodeidx))
216 if len(newparents) > 1:
216 if len(newparents) > 1:
217 edges.append((nodeidx, nodeidx + 1))
217 edges.append((nodeidx, nodeidx + 1))
218 nmorecols = len(nextseen) - ncols
218 nmorecols = len(nextseen) - ncols
219 if nmorecols > 0:
219 if nmorecols > 0:
220 width += 2
220 width += 2
221 # remove current node from edge characters, no longer needed
221 # remove current node from edge characters, no longer needed
222 state['edges'].pop(rev, None)
222 state['edges'].pop(rev, None)
223 yield (type, char, width, (nodeidx, edges, ncols, nmorecols))
223 yield (type, char, width, (nodeidx, edges, ncols, nmorecols))
224
224
225 def _fixlongrightedges(edges):
225 def _fixlongrightedges(edges):
226 for (i, (start, end)) in enumerate(edges):
226 for (i, (start, end)) in enumerate(edges):
227 if end > start:
227 if end > start:
228 edges[i] = (start, end + 1)
228 edges[i] = (start, end + 1)
229
229
230 def _getnodelineedgestail(
230 def _getnodelineedgestail(
231 echars, idx, pidx, ncols, coldiff, pdiff, fix_tail):
231 echars, idx, pidx, ncols, coldiff, pdiff, fix_tail):
232 if fix_tail and coldiff == pdiff and coldiff != 0:
232 if fix_tail and coldiff == pdiff and coldiff != 0:
233 # Still going in the same non-vertical direction.
233 # Still going in the same non-vertical direction.
234 if coldiff == -1:
234 if coldiff == -1:
235 start = max(idx + 1, pidx)
235 start = max(idx + 1, pidx)
236 tail = echars[idx * 2:(start - 1) * 2]
236 tail = echars[idx * 2:(start - 1) * 2]
237 tail.extend(["/", " "] * (ncols - start))
237 tail.extend(["/", " "] * (ncols - start))
238 return tail
238 return tail
239 else:
239 else:
240 return ["\\", " "] * (ncols - idx - 1)
240 return ["\\", " "] * (ncols - idx - 1)
241 else:
241 else:
242 remainder = (ncols - idx - 1)
242 remainder = (ncols - idx - 1)
243 return echars[-(remainder * 2):] if remainder > 0 else []
243 return echars[-(remainder * 2):] if remainder > 0 else []
244
244
245 def _drawedges(echars, edges, nodeline, interline):
245 def _drawedges(echars, edges, nodeline, interline):
246 for (start, end) in edges:
246 for (start, end) in edges:
247 if start == end + 1:
247 if start == end + 1:
248 interline[2 * end + 1] = "/"
248 interline[2 * end + 1] = "/"
249 elif start == end - 1:
249 elif start == end - 1:
250 interline[2 * start + 1] = "\\"
250 interline[2 * start + 1] = "\\"
251 elif start == end:
251 elif start == end:
252 interline[2 * start] = echars[2 * start]
252 interline[2 * start] = echars[2 * start]
253 else:
253 else:
254 if 2 * end >= len(nodeline):
254 if 2 * end >= len(nodeline):
255 continue
255 continue
256 nodeline[2 * end] = "+"
256 nodeline[2 * end] = "+"
257 if start > end:
257 if start > end:
258 (start, end) = (end, start)
258 (start, end) = (end, start)
259 for i in range(2 * start + 1, 2 * end):
259 for i in range(2 * start + 1, 2 * end):
260 if nodeline[i] != "+":
260 if nodeline[i] != "+":
261 nodeline[i] = "-"
261 nodeline[i] = "-"
262
262
263 def _getpaddingline(echars, idx, ncols, edges):
263 def _getpaddingline(echars, idx, ncols, edges):
264 # all edges up to the current node
264 # all edges up to the current node
265 line = echars[:idx * 2]
265 line = echars[:idx * 2]
266 # an edge for the current node, if there is one
266 # an edge for the current node, if there is one
267 if (idx, idx - 1) in edges or (idx, idx) in edges:
267 if (idx, idx - 1) in edges or (idx, idx) in edges:
268 # (idx, idx - 1) (idx, idx)
268 # (idx, idx - 1) (idx, idx)
269 # | | | | | | | |
269 # | | | | | | | |
270 # +---o | | o---+
270 # +---o | | o---+
271 # | | X | | X | |
271 # | | X | | X | |
272 # | |/ / | |/ /
272 # | |/ / | |/ /
273 # | | | | | |
273 # | | | | | |
274 line.extend(echars[idx * 2:(idx + 1) * 2])
274 line.extend(echars[idx * 2:(idx + 1) * 2])
275 else:
275 else:
276 line.extend([' ', ' '])
276 line.extend([' ', ' '])
277 # all edges to the right of the current node
277 # all edges to the right of the current node
278 remainder = ncols - idx - 1
278 remainder = ncols - idx - 1
279 if remainder > 0:
279 if remainder > 0:
280 line.extend(echars[-(remainder * 2):])
280 line.extend(echars[-(remainder * 2):])
281 return line
281 return line
282
282
283 def _drawendinglines(lines, extra, edgemap, seen):
283 def _drawendinglines(lines, extra, edgemap, seen):
284 """Draw ending lines for missing parent edges
284 """Draw ending lines for missing parent edges
285
285
286 None indicates an edge that ends at between this node and the next
286 None indicates an edge that ends at between this node and the next
287 Replace with a short line ending in ~ and add / lines to any edges to
287 Replace with a short line ending in ~ and add / lines to any edges to
288 the right.
288 the right.
289
289
290 """
290 """
291 if None not in edgemap.values():
291 if None not in edgemap.values():
292 return
292 return
293
293
294 # Check for more edges to the right of our ending edges.
294 # Check for more edges to the right of our ending edges.
295 # We need enough space to draw adjustment lines for these.
295 # We need enough space to draw adjustment lines for these.
296 edgechars = extra[::2]
296 edgechars = extra[::2]
297 while edgechars and edgechars[-1] is None:
297 while edgechars and edgechars[-1] is None:
298 edgechars.pop()
298 edgechars.pop()
299 shift_size = max((edgechars.count(None) * 2) - 1, 0)
299 shift_size = max((edgechars.count(None) * 2) - 1, 0)
300 while len(lines) < 3 + shift_size:
300 while len(lines) < 3 + shift_size:
301 lines.append(extra[:])
301 lines.append(extra[:])
302
302
303 if shift_size:
303 if shift_size:
304 empties = []
304 empties = []
305 toshift = []
305 toshift = []
306 first_empty = extra.index(None)
306 first_empty = extra.index(None)
307 for i, c in enumerate(extra[first_empty::2], first_empty // 2):
307 for i, c in enumerate(extra[first_empty::2], first_empty // 2):
308 if c is None:
308 if c is None:
309 empties.append(i * 2)
309 empties.append(i * 2)
310 else:
310 else:
311 toshift.append(i * 2)
311 toshift.append(i * 2)
312 targets = list(range(first_empty, first_empty + len(toshift) * 2, 2))
312 targets = list(range(first_empty, first_empty + len(toshift) * 2, 2))
313 positions = toshift[:]
313 positions = toshift[:]
314 for line in lines[-shift_size:]:
314 for line in lines[-shift_size:]:
315 line[first_empty:] = [' '] * (len(line) - first_empty)
315 line[first_empty:] = [' '] * (len(line) - first_empty)
316 for i in range(len(positions)):
316 for i in range(len(positions)):
317 pos = positions[i] - 1
317 pos = positions[i] - 1
318 positions[i] = max(pos, targets[i])
318 positions[i] = max(pos, targets[i])
319 line[pos] = '/' if pos > targets[i] else extra[toshift[i]]
319 line[pos] = '/' if pos > targets[i] else extra[toshift[i]]
320
320
321 map = {1: '|', 2: '~'}
321 map = {1: '|', 2: '~'}
322 for i, line in enumerate(lines):
322 for i, line in enumerate(lines):
323 if None not in line:
323 if None not in line:
324 continue
324 continue
325 line[:] = [c or map.get(i, ' ') for c in line]
325 line[:] = [c or map.get(i, ' ') for c in line]
326
326
327 # remove edges that ended
327 # remove edges that ended
328 remove = [p for p, c in edgemap.items() if c is None]
328 remove = [p for p, c in edgemap.items() if c is None]
329 for parent in remove:
329 for parent in remove:
330 del edgemap[parent]
330 del edgemap[parent]
331 seen.remove(parent)
331 seen.remove(parent)
332
332
333 def asciistate():
333 def asciistate():
334 """returns the initial value for the "state" argument to ascii()"""
334 """returns the initial value for the "state" argument to ascii()"""
335 return {
335 return {
336 'seen': [],
336 'seen': [],
337 'edges': {},
337 'edges': {},
338 'lastcoldiff': 0,
338 'lastcoldiff': 0,
339 'lastindex': 0,
339 'lastindex': 0,
340 'styles': EDGES.copy(),
340 'styles': EDGES.copy(),
341 'graphshorten': False,
341 'graphshorten': False,
342 }
342 }
343
343
344 def ascii(ui, state, type, char, text, coldata):
344 def ascii(ui, state, type, char, text, coldata):
345 """prints an ASCII graph of the DAG
345 """prints an ASCII graph of the DAG
346
346
347 takes the following arguments (one call per node in the graph):
347 takes the following arguments (one call per node in the graph):
348
348
349 - ui to write to
349 - ui to write to
350 - Somewhere to keep the needed state in (init to asciistate())
350 - Somewhere to keep the needed state in (init to asciistate())
351 - Column of the current node in the set of ongoing edges.
351 - Column of the current node in the set of ongoing edges.
352 - Type indicator of node data, usually 'C' for changesets.
352 - Type indicator of node data, usually 'C' for changesets.
353 - Payload: (char, lines):
353 - Payload: (char, lines):
354 - Character to use as node's symbol.
354 - Character to use as node's symbol.
355 - List of lines to display as the node's text.
355 - List of lines to display as the node's text.
356 - Edges; a list of (col, next_col) indicating the edges between
356 - Edges; a list of (col, next_col) indicating the edges between
357 the current node and its parents.
357 the current node and its parents.
358 - Number of columns (ongoing edges) in the current revision.
358 - Number of columns (ongoing edges) in the current revision.
359 - The difference between the number of columns (ongoing edges)
359 - The difference between the number of columns (ongoing edges)
360 in the next revision and the number of columns (ongoing edges)
360 in the next revision and the number of columns (ongoing edges)
361 in the current revision. That is: -1 means one column removed;
361 in the current revision. That is: -1 means one column removed;
362 0 means no columns added or removed; 1 means one column added.
362 0 means no columns added or removed; 1 means one column added.
363 """
363 """
364 idx, edges, ncols, coldiff = coldata
364 idx, edges, ncols, coldiff = coldata
365 assert -2 < coldiff < 2
365 assert -2 < coldiff < 2
366
366
367 edgemap, seen = state['edges'], state['seen']
367 edgemap, seen = state['edges'], state['seen']
368 # Be tolerant of history issues; make sure we have at least ncols + coldiff
368 # Be tolerant of history issues; make sure we have at least ncols + coldiff
369 # elements to work with. See test-glog.t for broken history test cases.
369 # elements to work with. See test-glog.t for broken history test cases.
370 echars = [c for p in seen for c in (edgemap.get(p, '|'), ' ')]
370 echars = [c for p in seen for c in (edgemap.get(p, '|'), ' ')]
371 echars.extend(('|', ' ') * max(ncols + coldiff - len(seen), 0))
371 echars.extend(('|', ' ') * max(ncols + coldiff - len(seen), 0))
372
372
373 if coldiff == -1:
373 if coldiff == -1:
374 # Transform
374 # Transform
375 #
375 #
376 # | | | | | |
376 # | | | | | |
377 # o | | into o---+
377 # o | | into o---+
378 # |X / |/ /
378 # |X / |/ /
379 # | | | |
379 # | | | |
380 _fixlongrightedges(edges)
380 _fixlongrightedges(edges)
381
381
382 # add_padding_line says whether to rewrite
382 # add_padding_line says whether to rewrite
383 #
383 #
384 # | | | | | | | |
384 # | | | | | | | |
385 # | o---+ into | o---+
385 # | o---+ into | o---+
386 # | / / | | | # <--- padding line
386 # | / / | | | # <--- padding line
387 # o | | | / /
387 # o | | | / /
388 # o | |
388 # o | |
389 add_padding_line = (len(text) > 2 and coldiff == -1 and
389 add_padding_line = (len(text) > 2 and coldiff == -1 and
390 [x for (x, y) in edges if x + 1 < y])
390 [x for (x, y) in edges if x + 1 < y])
391
391
392 # fix_nodeline_tail says whether to rewrite
392 # fix_nodeline_tail says whether to rewrite
393 #
393 #
394 # | | o | | | | o | |
394 # | | o | | | | o | |
395 # | | |/ / | | |/ /
395 # | | |/ / | | |/ /
396 # | o | | into | o / / # <--- fixed nodeline tail
396 # | o | | into | o / / # <--- fixed nodeline tail
397 # | |/ / | |/ /
397 # | |/ / | |/ /
398 # o | | o | |
398 # o | | o | |
399 fix_nodeline_tail = len(text) <= 2 and not add_padding_line
399 fix_nodeline_tail = len(text) <= 2 and not add_padding_line
400
400
401 # nodeline is the line containing the node character (typically o)
401 # nodeline is the line containing the node character (typically o)
402 nodeline = echars[:idx * 2]
402 nodeline = echars[:idx * 2]
403 nodeline.extend([char, " "])
403 nodeline.extend([char, " "])
404
404
405 nodeline.extend(
405 nodeline.extend(
406 _getnodelineedgestail(
406 _getnodelineedgestail(
407 echars, idx, state['lastindex'], ncols, coldiff,
407 echars, idx, state['lastindex'], ncols, coldiff,
408 state['lastcoldiff'], fix_nodeline_tail))
408 state['lastcoldiff'], fix_nodeline_tail))
409
409
410 # shift_interline is the line containing the non-vertical
410 # shift_interline is the line containing the non-vertical
411 # edges between this entry and the next
411 # edges between this entry and the next
412 shift_interline = echars[:idx * 2]
412 shift_interline = echars[:idx * 2]
413 for i in xrange(2 + coldiff):
413 for i in xrange(2 + coldiff):
414 shift_interline.append(' ')
414 shift_interline.append(' ')
415 count = ncols - idx - 1
415 count = ncols - idx - 1
416 if coldiff == -1:
416 if coldiff == -1:
417 for i in xrange(count):
417 for i in xrange(count):
418 shift_interline.extend(['/', ' '])
418 shift_interline.extend(['/', ' '])
419 elif coldiff == 0:
419 elif coldiff == 0:
420 shift_interline.extend(echars[(idx + 1) * 2:ncols * 2])
420 shift_interline.extend(echars[(idx + 1) * 2:ncols * 2])
421 else:
421 else:
422 for i in xrange(count):
422 for i in xrange(count):
423 shift_interline.extend(['\\', ' '])
423 shift_interline.extend(['\\', ' '])
424
424
425 # draw edges from the current node to its parents
425 # draw edges from the current node to its parents
426 _drawedges(echars, edges, nodeline, shift_interline)
426 _drawedges(echars, edges, nodeline, shift_interline)
427
427
428 # lines is the list of all graph lines to print
428 # lines is the list of all graph lines to print
429 lines = [nodeline]
429 lines = [nodeline]
430 if add_padding_line:
430 if add_padding_line:
431 lines.append(_getpaddingline(echars, idx, ncols, edges))
431 lines.append(_getpaddingline(echars, idx, ncols, edges))
432
432
433 # If 'graphshorten' config, only draw shift_interline
433 # If 'graphshorten' config, only draw shift_interline
434 # when there is any non vertical flow in graph.
434 # when there is any non vertical flow in graph.
435 if state['graphshorten']:
435 if state['graphshorten']:
436 if any(c in '\/' for c in shift_interline if c):
436 if any(c in '\/' for c in shift_interline if c):
437 lines.append(shift_interline)
437 lines.append(shift_interline)
438 # Else, no 'graphshorten' config so draw shift_interline.
438 # Else, no 'graphshorten' config so draw shift_interline.
439 else:
439 else:
440 lines.append(shift_interline)
440 lines.append(shift_interline)
441
441
442 # make sure that there are as many graph lines as there are
442 # make sure that there are as many graph lines as there are
443 # log strings
443 # log strings
444 extra_interline = echars[:(ncols + coldiff) * 2]
444 extra_interline = echars[:(ncols + coldiff) * 2]
445 if len(lines) < len(text):
445 if len(lines) < len(text):
446 while len(lines) < len(text):
446 while len(lines) < len(text):
447 lines.append(extra_interline[:])
447 lines.append(extra_interline[:])
448
448
449 _drawendinglines(lines, extra_interline, edgemap, seen)
449 _drawendinglines(lines, extra_interline, edgemap, seen)
450
450
451 while len(text) < len(lines):
451 while len(text) < len(lines):
452 text.append("")
452 text.append("")
453
453
454 if any(len(char) > 1 for char in edgemap.values()):
454 if any(len(char) > 1 for char in edgemap.values()):
455 # limit drawing an edge to the first or last N lines of the current
455 # limit drawing an edge to the first or last N lines of the current
456 # section the rest of the edge is drawn like a parent line.
456 # section the rest of the edge is drawn like a parent line.
457 parent = state['styles'][PARENT][-1]
457 parent = state['styles'][PARENT][-1:]
458 def _drawgp(char, i):
458 def _drawgp(char, i):
459 # should a grandparent character be drawn for this line?
459 # should a grandparent character be drawn for this line?
460 if len(char) < 2:
460 if len(char) < 2:
461 return True
461 return True
462 num = int(char[:-1])
462 num = int(char[:-1])
463 # either skip first num lines or take last num lines, based on sign
463 # either skip first num lines or take last num lines, based on sign
464 return -num <= i if num < 0 else (len(lines) - i) <= num
464 return -num <= i if num < 0 else (len(lines) - i) <= num
465 for i, line in enumerate(lines):
465 for i, line in enumerate(lines):
466 line[:] = [c[-1] if _drawgp(c, i) else parent for c in line]
466 line[:] = [c[-1:] if _drawgp(c, i) else parent for c in line]
467 edgemap.update(
467 edgemap.update(
468 (e, (c if len(c) < 2 else parent)) for e, c in edgemap.items())
468 (e, (c if len(c) < 2 else parent)) for e, c in edgemap.items())
469
469
470 # print lines
470 # print lines
471 indentation_level = max(ncols, ncols + coldiff)
471 indentation_level = max(ncols, ncols + coldiff)
472 for (line, logstr) in zip(lines, text):
472 for (line, logstr) in zip(lines, text):
473 ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr)
473 ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr)
474 ui.write(ln.rstrip() + '\n')
474 ui.write(ln.rstrip() + '\n')
475
475
476 # ... and start over
476 # ... and start over
477 state['lastcoldiff'] = coldiff
477 state['lastcoldiff'] = coldiff
478 state['lastindex'] = idx
478 state['lastindex'] = idx
@@ -1,654 +1,654 b''
1 # hgweb/webutil.py - utility library for the web interface.
1 # hgweb/webutil.py - utility library for the web interface.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import copy
11 import copy
12 import difflib
12 import difflib
13 import os
13 import os
14 import re
14 import re
15
15
16 from ..i18n import _
16 from ..i18n import _
17 from ..node import hex, nullid, short
17 from ..node import hex, nullid, short
18
18
19 from .common import (
19 from .common import (
20 ErrorResponse,
20 ErrorResponse,
21 HTTP_BAD_REQUEST,
21 HTTP_BAD_REQUEST,
22 HTTP_NOT_FOUND,
22 HTTP_NOT_FOUND,
23 paritygen,
23 paritygen,
24 )
24 )
25
25
26 from .. import (
26 from .. import (
27 context,
27 context,
28 error,
28 error,
29 match,
29 match,
30 mdiff,
30 mdiff,
31 patch,
31 patch,
32 pathutil,
32 pathutil,
33 pycompat,
33 pycompat,
34 templatefilters,
34 templatefilters,
35 templatekw,
35 templatekw,
36 ui as uimod,
36 ui as uimod,
37 util,
37 util,
38 )
38 )
39
39
40 def up(p):
40 def up(p):
41 if p[0] != "/":
41 if p[0] != "/":
42 p = "/" + p
42 p = "/" + p
43 if p[-1] == "/":
43 if p[-1] == "/":
44 p = p[:-1]
44 p = p[:-1]
45 up = os.path.dirname(p)
45 up = os.path.dirname(p)
46 if up == "/":
46 if up == "/":
47 return "/"
47 return "/"
48 return up + "/"
48 return up + "/"
49
49
50 def _navseq(step, firststep=None):
50 def _navseq(step, firststep=None):
51 if firststep:
51 if firststep:
52 yield firststep
52 yield firststep
53 if firststep >= 20 and firststep <= 40:
53 if firststep >= 20 and firststep <= 40:
54 firststep = 50
54 firststep = 50
55 yield firststep
55 yield firststep
56 assert step > 0
56 assert step > 0
57 assert firststep > 0
57 assert firststep > 0
58 while step <= firststep:
58 while step <= firststep:
59 step *= 10
59 step *= 10
60 while True:
60 while True:
61 yield 1 * step
61 yield 1 * step
62 yield 3 * step
62 yield 3 * step
63 step *= 10
63 step *= 10
64
64
65 class revnav(object):
65 class revnav(object):
66
66
67 def __init__(self, repo):
67 def __init__(self, repo):
68 """Navigation generation object
68 """Navigation generation object
69
69
70 :repo: repo object we generate nav for
70 :repo: repo object we generate nav for
71 """
71 """
72 # used for hex generation
72 # used for hex generation
73 self._revlog = repo.changelog
73 self._revlog = repo.changelog
74
74
75 def __nonzero__(self):
75 def __nonzero__(self):
76 """return True if any revision to navigate over"""
76 """return True if any revision to navigate over"""
77 return self._first() is not None
77 return self._first() is not None
78
78
79 __bool__ = __nonzero__
79 __bool__ = __nonzero__
80
80
81 def _first(self):
81 def _first(self):
82 """return the minimum non-filtered changeset or None"""
82 """return the minimum non-filtered changeset or None"""
83 try:
83 try:
84 return next(iter(self._revlog))
84 return next(iter(self._revlog))
85 except StopIteration:
85 except StopIteration:
86 return None
86 return None
87
87
88 def hex(self, rev):
88 def hex(self, rev):
89 return hex(self._revlog.node(rev))
89 return hex(self._revlog.node(rev))
90
90
91 def gen(self, pos, pagelen, limit):
91 def gen(self, pos, pagelen, limit):
92 """computes label and revision id for navigation link
92 """computes label and revision id for navigation link
93
93
94 :pos: is the revision relative to which we generate navigation.
94 :pos: is the revision relative to which we generate navigation.
95 :pagelen: the size of each navigation page
95 :pagelen: the size of each navigation page
96 :limit: how far shall we link
96 :limit: how far shall we link
97
97
98 The return is:
98 The return is:
99 - a single element tuple
99 - a single element tuple
100 - containing a dictionary with a `before` and `after` key
100 - containing a dictionary with a `before` and `after` key
101 - values are generator functions taking arbitrary number of kwargs
101 - values are generator functions taking arbitrary number of kwargs
102 - yield items are dictionaries with `label` and `node` keys
102 - yield items are dictionaries with `label` and `node` keys
103 """
103 """
104 if not self:
104 if not self:
105 # empty repo
105 # empty repo
106 return ({'before': (), 'after': ()},)
106 return ({'before': (), 'after': ()},)
107
107
108 targets = []
108 targets = []
109 for f in _navseq(1, pagelen):
109 for f in _navseq(1, pagelen):
110 if f > limit:
110 if f > limit:
111 break
111 break
112 targets.append(pos + f)
112 targets.append(pos + f)
113 targets.append(pos - f)
113 targets.append(pos - f)
114 targets.sort()
114 targets.sort()
115
115
116 first = self._first()
116 first = self._first()
117 navbefore = [("(%i)" % first, self.hex(first))]
117 navbefore = [("(%i)" % first, self.hex(first))]
118 navafter = []
118 navafter = []
119 for rev in targets:
119 for rev in targets:
120 if rev not in self._revlog:
120 if rev not in self._revlog:
121 continue
121 continue
122 if pos < rev < limit:
122 if pos < rev < limit:
123 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
123 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
124 if 0 < rev < pos:
124 if 0 < rev < pos:
125 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
125 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
126
126
127
127
128 navafter.append(("tip", "tip"))
128 navafter.append(("tip", "tip"))
129
129
130 data = lambda i: {"label": i[0], "node": i[1]}
130 data = lambda i: {"label": i[0], "node": i[1]}
131 return ({'before': lambda **map: (data(i) for i in navbefore),
131 return ({'before': lambda **map: (data(i) for i in navbefore),
132 'after': lambda **map: (data(i) for i in navafter)},)
132 'after': lambda **map: (data(i) for i in navafter)},)
133
133
134 class filerevnav(revnav):
134 class filerevnav(revnav):
135
135
136 def __init__(self, repo, path):
136 def __init__(self, repo, path):
137 """Navigation generation object
137 """Navigation generation object
138
138
139 :repo: repo object we generate nav for
139 :repo: repo object we generate nav for
140 :path: path of the file we generate nav for
140 :path: path of the file we generate nav for
141 """
141 """
142 # used for iteration
142 # used for iteration
143 self._changelog = repo.unfiltered().changelog
143 self._changelog = repo.unfiltered().changelog
144 # used for hex generation
144 # used for hex generation
145 self._revlog = repo.file(path)
145 self._revlog = repo.file(path)
146
146
147 def hex(self, rev):
147 def hex(self, rev):
148 return hex(self._changelog.node(self._revlog.linkrev(rev)))
148 return hex(self._changelog.node(self._revlog.linkrev(rev)))
149
149
150 class _siblings(object):
150 class _siblings(object):
151 def __init__(self, siblings=None, hiderev=None):
151 def __init__(self, siblings=None, hiderev=None):
152 if siblings is None:
152 if siblings is None:
153 siblings = []
153 siblings = []
154 self.siblings = [s for s in siblings if s.node() != nullid]
154 self.siblings = [s for s in siblings if s.node() != nullid]
155 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
155 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
156 self.siblings = []
156 self.siblings = []
157
157
158 def __iter__(self):
158 def __iter__(self):
159 for s in self.siblings:
159 for s in self.siblings:
160 d = {
160 d = {
161 'node': s.hex(),
161 'node': s.hex(),
162 'rev': s.rev(),
162 'rev': s.rev(),
163 'user': s.user(),
163 'user': s.user(),
164 'date': s.date(),
164 'date': s.date(),
165 'description': s.description(),
165 'description': s.description(),
166 'branch': s.branch(),
166 'branch': s.branch(),
167 }
167 }
168 if util.safehasattr(s, 'path'):
168 if util.safehasattr(s, 'path'):
169 d['file'] = s.path()
169 d['file'] = s.path()
170 yield d
170 yield d
171
171
172 def __len__(self):
172 def __len__(self):
173 return len(self.siblings)
173 return len(self.siblings)
174
174
175 def difffeatureopts(req, ui, section):
175 def difffeatureopts(req, ui, section):
176 diffopts = patch.difffeatureopts(ui, untrusted=True,
176 diffopts = patch.difffeatureopts(ui, untrusted=True,
177 section=section, whitespace=True)
177 section=section, whitespace=True)
178
178
179 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
179 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
180 v = req.form.get(k, [None])[0]
180 v = req.form.get(k, [None])[0]
181 if v is not None:
181 if v is not None:
182 v = util.parsebool(v)
182 v = util.parsebool(v)
183 setattr(diffopts, k, v if v is not None else True)
183 setattr(diffopts, k, v if v is not None else True)
184
184
185 return diffopts
185 return diffopts
186
186
187 def annotate(req, fctx, ui):
187 def annotate(req, fctx, ui):
188 diffopts = difffeatureopts(req, ui, 'annotate')
188 diffopts = difffeatureopts(req, ui, 'annotate')
189 return fctx.annotate(follow=True, linenumber=True, diffopts=diffopts)
189 return fctx.annotate(follow=True, linenumber=True, diffopts=diffopts)
190
190
191 def parents(ctx, hide=None):
191 def parents(ctx, hide=None):
192 if isinstance(ctx, context.basefilectx):
192 if isinstance(ctx, context.basefilectx):
193 introrev = ctx.introrev()
193 introrev = ctx.introrev()
194 if ctx.changectx().rev() != introrev:
194 if ctx.changectx().rev() != introrev:
195 return _siblings([ctx.repo()[introrev]], hide)
195 return _siblings([ctx.repo()[introrev]], hide)
196 return _siblings(ctx.parents(), hide)
196 return _siblings(ctx.parents(), hide)
197
197
198 def children(ctx, hide=None):
198 def children(ctx, hide=None):
199 return _siblings(ctx.children(), hide)
199 return _siblings(ctx.children(), hide)
200
200
201 def renamelink(fctx):
201 def renamelink(fctx):
202 r = fctx.renamed()
202 r = fctx.renamed()
203 if r:
203 if r:
204 return [{'file': r[0], 'node': hex(r[1])}]
204 return [{'file': r[0], 'node': hex(r[1])}]
205 return []
205 return []
206
206
207 def nodetagsdict(repo, node):
207 def nodetagsdict(repo, node):
208 return [{"name": i} for i in repo.nodetags(node)]
208 return [{"name": i} for i in repo.nodetags(node)]
209
209
210 def nodebookmarksdict(repo, node):
210 def nodebookmarksdict(repo, node):
211 return [{"name": i} for i in repo.nodebookmarks(node)]
211 return [{"name": i} for i in repo.nodebookmarks(node)]
212
212
213 def nodebranchdict(repo, ctx):
213 def nodebranchdict(repo, ctx):
214 branches = []
214 branches = []
215 branch = ctx.branch()
215 branch = ctx.branch()
216 # If this is an empty repo, ctx.node() == nullid,
216 # If this is an empty repo, ctx.node() == nullid,
217 # ctx.branch() == 'default'.
217 # ctx.branch() == 'default'.
218 try:
218 try:
219 branchnode = repo.branchtip(branch)
219 branchnode = repo.branchtip(branch)
220 except error.RepoLookupError:
220 except error.RepoLookupError:
221 branchnode = None
221 branchnode = None
222 if branchnode == ctx.node():
222 if branchnode == ctx.node():
223 branches.append({"name": branch})
223 branches.append({"name": branch})
224 return branches
224 return branches
225
225
226 def nodeinbranch(repo, ctx):
226 def nodeinbranch(repo, ctx):
227 branches = []
227 branches = []
228 branch = ctx.branch()
228 branch = ctx.branch()
229 try:
229 try:
230 branchnode = repo.branchtip(branch)
230 branchnode = repo.branchtip(branch)
231 except error.RepoLookupError:
231 except error.RepoLookupError:
232 branchnode = None
232 branchnode = None
233 if branch != 'default' and branchnode != ctx.node():
233 if branch != 'default' and branchnode != ctx.node():
234 branches.append({"name": branch})
234 branches.append({"name": branch})
235 return branches
235 return branches
236
236
237 def nodebranchnodefault(ctx):
237 def nodebranchnodefault(ctx):
238 branches = []
238 branches = []
239 branch = ctx.branch()
239 branch = ctx.branch()
240 if branch != 'default':
240 if branch != 'default':
241 branches.append({"name": branch})
241 branches.append({"name": branch})
242 return branches
242 return branches
243
243
244 def showtag(repo, tmpl, t1, node=nullid, **args):
244 def showtag(repo, tmpl, t1, node=nullid, **args):
245 for t in repo.nodetags(node):
245 for t in repo.nodetags(node):
246 yield tmpl(t1, tag=t, **args)
246 yield tmpl(t1, tag=t, **args)
247
247
248 def showbookmark(repo, tmpl, t1, node=nullid, **args):
248 def showbookmark(repo, tmpl, t1, node=nullid, **args):
249 for t in repo.nodebookmarks(node):
249 for t in repo.nodebookmarks(node):
250 yield tmpl(t1, bookmark=t, **args)
250 yield tmpl(t1, bookmark=t, **args)
251
251
252 def branchentries(repo, stripecount, limit=0):
252 def branchentries(repo, stripecount, limit=0):
253 tips = []
253 tips = []
254 heads = repo.heads()
254 heads = repo.heads()
255 parity = paritygen(stripecount)
255 parity = paritygen(stripecount)
256 sortkey = lambda item: (not item[1], item[0].rev())
256 sortkey = lambda item: (not item[1], item[0].rev())
257
257
258 def entries(**map):
258 def entries(**map):
259 count = 0
259 count = 0
260 if not tips:
260 if not tips:
261 for tag, hs, tip, closed in repo.branchmap().iterbranches():
261 for tag, hs, tip, closed in repo.branchmap().iterbranches():
262 tips.append((repo[tip], closed))
262 tips.append((repo[tip], closed))
263 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
263 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
264 if limit > 0 and count >= limit:
264 if limit > 0 and count >= limit:
265 return
265 return
266 count += 1
266 count += 1
267 if closed:
267 if closed:
268 status = 'closed'
268 status = 'closed'
269 elif ctx.node() not in heads:
269 elif ctx.node() not in heads:
270 status = 'inactive'
270 status = 'inactive'
271 else:
271 else:
272 status = 'open'
272 status = 'open'
273 yield {
273 yield {
274 'parity': next(parity),
274 'parity': next(parity),
275 'branch': ctx.branch(),
275 'branch': ctx.branch(),
276 'status': status,
276 'status': status,
277 'node': ctx.hex(),
277 'node': ctx.hex(),
278 'date': ctx.date()
278 'date': ctx.date()
279 }
279 }
280
280
281 return entries
281 return entries
282
282
283 def cleanpath(repo, path):
283 def cleanpath(repo, path):
284 path = path.lstrip('/')
284 path = path.lstrip('/')
285 return pathutil.canonpath(repo.root, '', path)
285 return pathutil.canonpath(repo.root, '', path)
286
286
287 def changeidctx(repo, changeid):
287 def changeidctx(repo, changeid):
288 try:
288 try:
289 ctx = repo[changeid]
289 ctx = repo[changeid]
290 except error.RepoError:
290 except error.RepoError:
291 man = repo.manifestlog._revlog
291 man = repo.manifestlog._revlog
292 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
292 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
293
293
294 return ctx
294 return ctx
295
295
296 def changectx(repo, req):
296 def changectx(repo, req):
297 changeid = "tip"
297 changeid = "tip"
298 if 'node' in req.form:
298 if 'node' in req.form:
299 changeid = req.form['node'][0]
299 changeid = req.form['node'][0]
300 ipos = changeid.find(':')
300 ipos = changeid.find(':')
301 if ipos != -1:
301 if ipos != -1:
302 changeid = changeid[(ipos + 1):]
302 changeid = changeid[(ipos + 1):]
303 elif 'manifest' in req.form:
303 elif 'manifest' in req.form:
304 changeid = req.form['manifest'][0]
304 changeid = req.form['manifest'][0]
305
305
306 return changeidctx(repo, changeid)
306 return changeidctx(repo, changeid)
307
307
308 def basechangectx(repo, req):
308 def basechangectx(repo, req):
309 if 'node' in req.form:
309 if 'node' in req.form:
310 changeid = req.form['node'][0]
310 changeid = req.form['node'][0]
311 ipos = changeid.find(':')
311 ipos = changeid.find(':')
312 if ipos != -1:
312 if ipos != -1:
313 changeid = changeid[:ipos]
313 changeid = changeid[:ipos]
314 return changeidctx(repo, changeid)
314 return changeidctx(repo, changeid)
315
315
316 return None
316 return None
317
317
318 def filectx(repo, req):
318 def filectx(repo, req):
319 if 'file' not in req.form:
319 if 'file' not in req.form:
320 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
320 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
321 path = cleanpath(repo, req.form['file'][0])
321 path = cleanpath(repo, req.form['file'][0])
322 if 'node' in req.form:
322 if 'node' in req.form:
323 changeid = req.form['node'][0]
323 changeid = req.form['node'][0]
324 elif 'filenode' in req.form:
324 elif 'filenode' in req.form:
325 changeid = req.form['filenode'][0]
325 changeid = req.form['filenode'][0]
326 else:
326 else:
327 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
327 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
328 try:
328 try:
329 fctx = repo[changeid][path]
329 fctx = repo[changeid][path]
330 except error.RepoError:
330 except error.RepoError:
331 fctx = repo.filectx(path, fileid=changeid)
331 fctx = repo.filectx(path, fileid=changeid)
332
332
333 return fctx
333 return fctx
334
334
335 def linerange(req):
335 def linerange(req):
336 linerange = req.form.get('linerange')
336 linerange = req.form.get('linerange')
337 if linerange is None:
337 if linerange is None:
338 return None
338 return None
339 if len(linerange) > 1:
339 if len(linerange) > 1:
340 raise ErrorResponse(HTTP_BAD_REQUEST,
340 raise ErrorResponse(HTTP_BAD_REQUEST,
341 'redundant linerange parameter')
341 'redundant linerange parameter')
342 try:
342 try:
343 fromline, toline = map(int, linerange[0].split(':', 1))
343 fromline, toline = map(int, linerange[0].split(':', 1))
344 except ValueError:
344 except ValueError:
345 raise ErrorResponse(HTTP_BAD_REQUEST,
345 raise ErrorResponse(HTTP_BAD_REQUEST,
346 'invalid linerange parameter')
346 'invalid linerange parameter')
347 try:
347 try:
348 return util.processlinerange(fromline, toline)
348 return util.processlinerange(fromline, toline)
349 except error.ParseError as exc:
349 except error.ParseError as exc:
350 raise ErrorResponse(HTTP_BAD_REQUEST, str(exc))
350 raise ErrorResponse(HTTP_BAD_REQUEST, str(exc))
351
351
352 def formatlinerange(fromline, toline):
352 def formatlinerange(fromline, toline):
353 return '%d:%d' % (fromline + 1, toline)
353 return '%d:%d' % (fromline + 1, toline)
354
354
355 def succsandmarkers(repo, ctx):
355 def succsandmarkers(repo, ctx):
356 for item in templatekw.showsuccsandmarkers(repo, ctx):
356 for item in templatekw.showsuccsandmarkers(repo, ctx):
357 item['successors'] = _siblings(repo[successor]
357 item['successors'] = _siblings(repo[successor]
358 for successor in item['successors'])
358 for successor in item['successors'])
359 yield item
359 yield item
360
360
361 def commonentry(repo, ctx):
361 def commonentry(repo, ctx):
362 node = ctx.node()
362 node = ctx.node()
363 return {
363 return {
364 'rev': ctx.rev(),
364 'rev': ctx.rev(),
365 'node': hex(node),
365 'node': hex(node),
366 'author': ctx.user(),
366 'author': ctx.user(),
367 'desc': ctx.description(),
367 'desc': ctx.description(),
368 'date': ctx.date(),
368 'date': ctx.date(),
369 'extra': ctx.extra(),
369 'extra': ctx.extra(),
370 'phase': ctx.phasestr(),
370 'phase': ctx.phasestr(),
371 'obsolete': ctx.obsolete(),
371 'obsolete': ctx.obsolete(),
372 'succsandmarkers': lambda **x: succsandmarkers(repo, ctx),
372 'succsandmarkers': lambda **x: succsandmarkers(repo, ctx),
373 'instabilities': [{"instability": i} for i in ctx.instabilities()],
373 'instabilities': [{"instability": i} for i in ctx.instabilities()],
374 'branch': nodebranchnodefault(ctx),
374 'branch': nodebranchnodefault(ctx),
375 'inbranch': nodeinbranch(repo, ctx),
375 'inbranch': nodeinbranch(repo, ctx),
376 'branches': nodebranchdict(repo, ctx),
376 'branches': nodebranchdict(repo, ctx),
377 'tags': nodetagsdict(repo, node),
377 'tags': nodetagsdict(repo, node),
378 'bookmarks': nodebookmarksdict(repo, node),
378 'bookmarks': nodebookmarksdict(repo, node),
379 'parent': lambda **x: parents(ctx),
379 'parent': lambda **x: parents(ctx),
380 'child': lambda **x: children(ctx),
380 'child': lambda **x: children(ctx),
381 }
381 }
382
382
383 def changelistentry(web, ctx, tmpl):
383 def changelistentry(web, ctx, tmpl):
384 '''Obtain a dictionary to be used for entries in a changelist.
384 '''Obtain a dictionary to be used for entries in a changelist.
385
385
386 This function is called when producing items for the "entries" list passed
386 This function is called when producing items for the "entries" list passed
387 to the "shortlog" and "changelog" templates.
387 to the "shortlog" and "changelog" templates.
388 '''
388 '''
389 repo = web.repo
389 repo = web.repo
390 rev = ctx.rev()
390 rev = ctx.rev()
391 n = ctx.node()
391 n = ctx.node()
392 showtags = showtag(repo, tmpl, 'changelogtag', n)
392 showtags = showtag(repo, tmpl, 'changelogtag', n)
393 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
393 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
394
394
395 entry = commonentry(repo, ctx)
395 entry = commonentry(repo, ctx)
396 entry.update(
396 entry.update(
397 allparents=lambda **x: parents(ctx),
397 allparents=lambda **x: parents(ctx),
398 parent=lambda **x: parents(ctx, rev - 1),
398 parent=lambda **x: parents(ctx, rev - 1),
399 child=lambda **x: children(ctx, rev + 1),
399 child=lambda **x: children(ctx, rev + 1),
400 changelogtag=showtags,
400 changelogtag=showtags,
401 files=files,
401 files=files,
402 )
402 )
403 return entry
403 return entry
404
404
405 def symrevorshortnode(req, ctx):
405 def symrevorshortnode(req, ctx):
406 if 'node' in req.form:
406 if 'node' in req.form:
407 return templatefilters.revescape(req.form['node'][0])
407 return templatefilters.revescape(req.form['node'][0])
408 else:
408 else:
409 return short(ctx.node())
409 return short(ctx.node())
410
410
411 def changesetentry(web, req, tmpl, ctx):
411 def changesetentry(web, req, tmpl, ctx):
412 '''Obtain a dictionary to be used to render the "changeset" template.'''
412 '''Obtain a dictionary to be used to render the "changeset" template.'''
413
413
414 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
414 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
415 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
415 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
416 ctx.node())
416 ctx.node())
417 showbranch = nodebranchnodefault(ctx)
417 showbranch = nodebranchnodefault(ctx)
418
418
419 files = []
419 files = []
420 parity = paritygen(web.stripecount)
420 parity = paritygen(web.stripecount)
421 for blockno, f in enumerate(ctx.files()):
421 for blockno, f in enumerate(ctx.files()):
422 template = 'filenodelink' if f in ctx else 'filenolink'
422 template = 'filenodelink' if f in ctx else 'filenolink'
423 files.append(tmpl(template,
423 files.append(tmpl(template,
424 node=ctx.hex(), file=f, blockno=blockno + 1,
424 node=ctx.hex(), file=f, blockno=blockno + 1,
425 parity=next(parity)))
425 parity=next(parity)))
426
426
427 basectx = basechangectx(web.repo, req)
427 basectx = basechangectx(web.repo, req)
428 if basectx is None:
428 if basectx is None:
429 basectx = ctx.p1()
429 basectx = ctx.p1()
430
430
431 style = web.config('web', 'style')
431 style = web.config('web', 'style')
432 if 'style' in req.form:
432 if 'style' in req.form:
433 style = req.form['style'][0]
433 style = req.form['style'][0]
434
434
435 diff = diffs(web, tmpl, ctx, basectx, None, style)
435 diff = diffs(web, tmpl, ctx, basectx, None, style)
436
436
437 parity = paritygen(web.stripecount)
437 parity = paritygen(web.stripecount)
438 diffstatsgen = diffstatgen(ctx, basectx)
438 diffstatsgen = diffstatgen(ctx, basectx)
439 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
439 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
440
440
441 return dict(
441 return dict(
442 diff=diff,
442 diff=diff,
443 symrev=symrevorshortnode(req, ctx),
443 symrev=symrevorshortnode(req, ctx),
444 basenode=basectx.hex(),
444 basenode=basectx.hex(),
445 changesettag=showtags,
445 changesettag=showtags,
446 changesetbookmark=showbookmarks,
446 changesetbookmark=showbookmarks,
447 changesetbranch=showbranch,
447 changesetbranch=showbranch,
448 files=files,
448 files=files,
449 diffsummary=lambda **x: diffsummary(diffstatsgen),
449 diffsummary=lambda **x: diffsummary(diffstatsgen),
450 diffstat=diffstats,
450 diffstat=diffstats,
451 archives=web.archivelist(ctx.hex()),
451 archives=web.archivelist(ctx.hex()),
452 **commonentry(web.repo, ctx))
452 **commonentry(web.repo, ctx))
453
453
454 def listfilediffs(tmpl, files, node, max):
454 def listfilediffs(tmpl, files, node, max):
455 for f in files[:max]:
455 for f in files[:max]:
456 yield tmpl('filedifflink', node=hex(node), file=f)
456 yield tmpl('filedifflink', node=hex(node), file=f)
457 if len(files) > max:
457 if len(files) > max:
458 yield tmpl('fileellipses')
458 yield tmpl('fileellipses')
459
459
460 def diffs(web, tmpl, ctx, basectx, files, style, linerange=None,
460 def diffs(web, tmpl, ctx, basectx, files, style, linerange=None,
461 lineidprefix=''):
461 lineidprefix=''):
462
462
463 def prettyprintlines(lines, blockno):
463 def prettyprintlines(lines, blockno):
464 for lineno, l in enumerate(lines, 1):
464 for lineno, l in enumerate(lines, 1):
465 difflineno = "%d.%d" % (blockno, lineno)
465 difflineno = "%d.%d" % (blockno, lineno)
466 if l.startswith('+'):
466 if l.startswith('+'):
467 ltype = "difflineplus"
467 ltype = "difflineplus"
468 elif l.startswith('-'):
468 elif l.startswith('-'):
469 ltype = "difflineminus"
469 ltype = "difflineminus"
470 elif l.startswith('@'):
470 elif l.startswith('@'):
471 ltype = "difflineat"
471 ltype = "difflineat"
472 else:
472 else:
473 ltype = "diffline"
473 ltype = "diffline"
474 yield tmpl(ltype,
474 yield tmpl(ltype,
475 line=l,
475 line=l,
476 lineno=lineno,
476 lineno=lineno,
477 lineid=lineidprefix + "l%s" % difflineno,
477 lineid=lineidprefix + "l%s" % difflineno,
478 linenumber="% 8s" % difflineno)
478 linenumber="% 8s" % difflineno)
479
479
480 repo = web.repo
480 repo = web.repo
481 if files:
481 if files:
482 m = match.exact(repo.root, repo.getcwd(), files)
482 m = match.exact(repo.root, repo.getcwd(), files)
483 else:
483 else:
484 m = match.always(repo.root, repo.getcwd())
484 m = match.always(repo.root, repo.getcwd())
485
485
486 diffopts = patch.diffopts(repo.ui, untrusted=True)
486 diffopts = patch.diffopts(repo.ui, untrusted=True)
487 node1 = basectx.node()
487 node1 = basectx.node()
488 node2 = ctx.node()
488 node2 = ctx.node()
489 parity = paritygen(web.stripecount)
489 parity = paritygen(web.stripecount)
490
490
491 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
491 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
492 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
492 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
493 if style != 'raw':
493 if style != 'raw':
494 header = header[1:]
494 header = header[1:]
495 lines = [h + '\n' for h in header]
495 lines = [h + '\n' for h in header]
496 for hunkrange, hunklines in hunks:
496 for hunkrange, hunklines in hunks:
497 if linerange is not None and hunkrange is not None:
497 if linerange is not None and hunkrange is not None:
498 s1, l1, s2, l2 = hunkrange
498 s1, l1, s2, l2 = hunkrange
499 if not mdiff.hunkinrange((s2, l2), linerange):
499 if not mdiff.hunkinrange((s2, l2), linerange):
500 continue
500 continue
501 lines.extend(hunklines)
501 lines.extend(hunklines)
502 if lines:
502 if lines:
503 yield tmpl('diffblock', parity=next(parity), blockno=blockno,
503 yield tmpl('diffblock', parity=next(parity), blockno=blockno,
504 lines=prettyprintlines(lines, blockno))
504 lines=prettyprintlines(lines, blockno))
505
505
506 def compare(tmpl, context, leftlines, rightlines):
506 def compare(tmpl, context, leftlines, rightlines):
507 '''Generator function that provides side-by-side comparison data.'''
507 '''Generator function that provides side-by-side comparison data.'''
508
508
509 def compline(type, leftlineno, leftline, rightlineno, rightline):
509 def compline(type, leftlineno, leftline, rightlineno, rightline):
510 lineid = leftlineno and ("l%s" % leftlineno) or ''
510 lineid = leftlineno and ("l%s" % leftlineno) or ''
511 lineid += rightlineno and ("r%s" % rightlineno) or ''
511 lineid += rightlineno and ("r%s" % rightlineno) or ''
512 return tmpl('comparisonline',
512 return tmpl('comparisonline',
513 type=type,
513 type=type,
514 lineid=lineid,
514 lineid=lineid,
515 leftlineno=leftlineno,
515 leftlineno=leftlineno,
516 leftlinenumber="% 6s" % (leftlineno or ''),
516 leftlinenumber="% 6s" % (leftlineno or ''),
517 leftline=leftline or '',
517 leftline=leftline or '',
518 rightlineno=rightlineno,
518 rightlineno=rightlineno,
519 rightlinenumber="% 6s" % (rightlineno or ''),
519 rightlinenumber="% 6s" % (rightlineno or ''),
520 rightline=rightline or '')
520 rightline=rightline or '')
521
521
522 def getblock(opcodes):
522 def getblock(opcodes):
523 for type, llo, lhi, rlo, rhi in opcodes:
523 for type, llo, lhi, rlo, rhi in opcodes:
524 len1 = lhi - llo
524 len1 = lhi - llo
525 len2 = rhi - rlo
525 len2 = rhi - rlo
526 count = min(len1, len2)
526 count = min(len1, len2)
527 for i in xrange(count):
527 for i in xrange(count):
528 yield compline(type=type,
528 yield compline(type=type,
529 leftlineno=llo + i + 1,
529 leftlineno=llo + i + 1,
530 leftline=leftlines[llo + i],
530 leftline=leftlines[llo + i],
531 rightlineno=rlo + i + 1,
531 rightlineno=rlo + i + 1,
532 rightline=rightlines[rlo + i])
532 rightline=rightlines[rlo + i])
533 if len1 > len2:
533 if len1 > len2:
534 for i in xrange(llo + count, lhi):
534 for i in xrange(llo + count, lhi):
535 yield compline(type=type,
535 yield compline(type=type,
536 leftlineno=i + 1,
536 leftlineno=i + 1,
537 leftline=leftlines[i],
537 leftline=leftlines[i],
538 rightlineno=None,
538 rightlineno=None,
539 rightline=None)
539 rightline=None)
540 elif len2 > len1:
540 elif len2 > len1:
541 for i in xrange(rlo + count, rhi):
541 for i in xrange(rlo + count, rhi):
542 yield compline(type=type,
542 yield compline(type=type,
543 leftlineno=None,
543 leftlineno=None,
544 leftline=None,
544 leftline=None,
545 rightlineno=i + 1,
545 rightlineno=i + 1,
546 rightline=rightlines[i])
546 rightline=rightlines[i])
547
547
548 s = difflib.SequenceMatcher(None, leftlines, rightlines)
548 s = difflib.SequenceMatcher(None, leftlines, rightlines)
549 if context < 0:
549 if context < 0:
550 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
550 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
551 else:
551 else:
552 for oc in s.get_grouped_opcodes(n=context):
552 for oc in s.get_grouped_opcodes(n=context):
553 yield tmpl('comparisonblock', lines=getblock(oc))
553 yield tmpl('comparisonblock', lines=getblock(oc))
554
554
555 def diffstatgen(ctx, basectx):
555 def diffstatgen(ctx, basectx):
556 '''Generator function that provides the diffstat data.'''
556 '''Generator function that provides the diffstat data.'''
557
557
558 stats = patch.diffstatdata(
558 stats = patch.diffstatdata(
559 util.iterlines(ctx.diff(basectx, noprefix=False)))
559 util.iterlines(ctx.diff(basectx, noprefix=False)))
560 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
560 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
561 while True:
561 while True:
562 yield stats, maxname, maxtotal, addtotal, removetotal, binary
562 yield stats, maxname, maxtotal, addtotal, removetotal, binary
563
563
564 def diffsummary(statgen):
564 def diffsummary(statgen):
565 '''Return a short summary of the diff.'''
565 '''Return a short summary of the diff.'''
566
566
567 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
567 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
568 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
568 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
569 len(stats), addtotal, removetotal)
569 len(stats), addtotal, removetotal)
570
570
571 def diffstat(tmpl, ctx, statgen, parity):
571 def diffstat(tmpl, ctx, statgen, parity):
572 '''Return a diffstat template for each file in the diff.'''
572 '''Return a diffstat template for each file in the diff.'''
573
573
574 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
574 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
575 files = ctx.files()
575 files = ctx.files()
576
576
577 def pct(i):
577 def pct(i):
578 if maxtotal == 0:
578 if maxtotal == 0:
579 return 0
579 return 0
580 return (float(i) / maxtotal) * 100
580 return (float(i) / maxtotal) * 100
581
581
582 fileno = 0
582 fileno = 0
583 for filename, adds, removes, isbinary in stats:
583 for filename, adds, removes, isbinary in stats:
584 template = 'diffstatlink' if filename in files else 'diffstatnolink'
584 template = 'diffstatlink' if filename in files else 'diffstatnolink'
585 total = adds + removes
585 total = adds + removes
586 fileno += 1
586 fileno += 1
587 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
587 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
588 total=total, addpct=pct(adds), removepct=pct(removes),
588 total=total, addpct=pct(adds), removepct=pct(removes),
589 parity=next(parity))
589 parity=next(parity))
590
590
591 class sessionvars(object):
591 class sessionvars(object):
592 def __init__(self, vars, start='?'):
592 def __init__(self, vars, start='?'):
593 self.start = start
593 self.start = start
594 self.vars = vars
594 self.vars = vars
595 def __getitem__(self, key):
595 def __getitem__(self, key):
596 return self.vars[key]
596 return self.vars[key]
597 def __setitem__(self, key, value):
597 def __setitem__(self, key, value):
598 self.vars[key] = value
598 self.vars[key] = value
599 def __copy__(self):
599 def __copy__(self):
600 return sessionvars(copy.copy(self.vars), self.start)
600 return sessionvars(copy.copy(self.vars), self.start)
601 def __iter__(self):
601 def __iter__(self):
602 separator = self.start
602 separator = self.start
603 for key, value in sorted(self.vars.iteritems()):
603 for key, value in sorted(self.vars.iteritems()):
604 yield {'name': key,
604 yield {'name': key,
605 'value': pycompat.bytestr(value),
605 'value': pycompat.bytestr(value),
606 'separator': separator,
606 'separator': separator,
607 }
607 }
608 separator = '&'
608 separator = '&'
609
609
610 class wsgiui(uimod.ui):
610 class wsgiui(uimod.ui):
611 # default termwidth breaks under mod_wsgi
611 # default termwidth breaks under mod_wsgi
612 def termwidth(self):
612 def termwidth(self):
613 return 80
613 return 80
614
614
615 def getwebsubs(repo):
615 def getwebsubs(repo):
616 websubtable = []
616 websubtable = []
617 websubdefs = repo.ui.configitems('websub')
617 websubdefs = repo.ui.configitems('websub')
618 # we must maintain interhg backwards compatibility
618 # we must maintain interhg backwards compatibility
619 websubdefs += repo.ui.configitems('interhg')
619 websubdefs += repo.ui.configitems('interhg')
620 for key, pattern in websubdefs:
620 for key, pattern in websubdefs:
621 # grab the delimiter from the character after the "s"
621 # grab the delimiter from the character after the "s"
622 unesc = pattern[1]
622 unesc = pattern[1:2]
623 delim = re.escape(unesc)
623 delim = re.escape(unesc)
624
624
625 # identify portions of the pattern, taking care to avoid escaped
625 # identify portions of the pattern, taking care to avoid escaped
626 # delimiters. the replace format and flags are optional, but
626 # delimiters. the replace format and flags are optional, but
627 # delimiters are required.
627 # delimiters are required.
628 match = re.match(
628 match = re.match(
629 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
629 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
630 % (delim, delim, delim), pattern)
630 % (delim, delim, delim), pattern)
631 if not match:
631 if not match:
632 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
632 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
633 % (key, pattern))
633 % (key, pattern))
634 continue
634 continue
635
635
636 # we need to unescape the delimiter for regexp and format
636 # we need to unescape the delimiter for regexp and format
637 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
637 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
638 regexp = delim_re.sub(unesc, match.group(1))
638 regexp = delim_re.sub(unesc, match.group(1))
639 format = delim_re.sub(unesc, match.group(2))
639 format = delim_re.sub(unesc, match.group(2))
640
640
641 # the pattern allows for 6 regexp flags, so set them if necessary
641 # the pattern allows for 6 regexp flags, so set them if necessary
642 flagin = match.group(3)
642 flagin = match.group(3)
643 flags = 0
643 flags = 0
644 if flagin:
644 if flagin:
645 for flag in flagin.upper():
645 for flag in flagin.upper():
646 flags |= re.__dict__[flag]
646 flags |= re.__dict__[flag]
647
647
648 try:
648 try:
649 regexp = re.compile(regexp, flags)
649 regexp = re.compile(regexp, flags)
650 websubtable.append((regexp, format))
650 websubtable.append((regexp, format))
651 except re.error:
651 except re.error:
652 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
652 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
653 % (key, regexp))
653 % (key, regexp))
654 return websubtable
654 return websubtable
General Comments 0
You need to be logged in to leave comments. Login now