##// END OF EJS Templates
merge with stable
Martin Geisler -
r12142:a55e3c50 merge default
parent child Browse files
Show More
@@ -1,474 +1,474 b''
1 # dagparser.py - parser and generator for concise description of DAGs
1 # dagparser.py - parser and generator for concise description of DAGs
2 #
2 #
3 # Copyright 2010 Peter Arrenbrecht <peter@arrenbrecht.ch>
3 # Copyright 2010 Peter Arrenbrecht <peter@arrenbrecht.ch>
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 import re, string
8 import re, string
9 import util
9 import util
10 from i18n import _
10 from i18n import _
11
11
12 def parsedag(desc):
12 def parsedag(desc):
13 '''parses a DAG from a concise textual description; generates events
13 '''parses a DAG from a concise textual description; generates events
14
14
15 "+n" is a linear run of n nodes based on the current default parent
15 "+n" is a linear run of n nodes based on the current default parent
16 "." is a single node based on the current default parent
16 "." is a single node based on the current default parent
17 "$" resets the default parent to -1 (implied at the start);
17 "$" resets the default parent to -1 (implied at the start);
18 otherwise the default parent is always the last node created
18 otherwise the default parent is always the last node created
19 "<p" sets the default parent to the backref p
19 "<p" sets the default parent to the backref p
20 "*p" is a fork at parent p, where p is a backref
20 "*p" is a fork at parent p, where p is a backref
21 "*p1/p2/.../pn" is a merge of parents p1..pn, where the pi are backrefs
21 "*p1/p2/.../pn" is a merge of parents p1..pn, where the pi are backrefs
22 "/p2/.../pn" is a merge of the preceding node and p2..pn
22 "/p2/.../pn" is a merge of the preceding node and p2..pn
23 ":name" defines a label for the preceding node; labels can be redefined
23 ":name" defines a label for the preceding node; labels can be redefined
24 "@text" emits an annotation event for text
24 "@text" emits an annotation event for text
25 "!command" emits an action event for the current node
25 "!command" emits an action event for the current node
26 "!!my command\n" is like "!", but to the end of the line
26 "!!my command\n" is like "!", but to the end of the line
27 "#...\n" is a comment up to the end of the line
27 "#...\n" is a comment up to the end of the line
28
28
29 Whitespace between the above elements is ignored.
29 Whitespace between the above elements is ignored.
30
30
31 A backref is either
31 A backref is either
32 * a number n, which references the node curr-n, where curr is the current
32 * a number n, which references the node curr-n, where curr is the current
33 node, or
33 node, or
34 * the name of a label you placed earlier using ":name", or
34 * the name of a label you placed earlier using ":name", or
35 * empty to denote the default parent.
35 * empty to denote the default parent.
36
36
37 All string valued-elements are either strictly alphanumeric, or must
37 All string valued-elements are either strictly alphanumeric, or must
38 be enclosed in double quotes ("..."), with "\" as escape character.
38 be enclosed in double quotes ("..."), with "\" as escape character.
39
39
40 Generates sequence of
40 Generates sequence of
41
41
42 ('n', (id, [parentids])) for node creation
42 ('n', (id, [parentids])) for node creation
43 ('l', (id, labelname)) for labels on nodes
43 ('l', (id, labelname)) for labels on nodes
44 ('a', text) for annotations
44 ('a', text) for annotations
45 ('c', command) for actions (!)
45 ('c', command) for actions (!)
46 ('C', command) for line actions (!!)
46 ('C', command) for line actions (!!)
47
47
48 Examples
48 Examples
49 --------
49 --------
50
50
51 Example of a complex graph (output not shown for brevity):
51 Example of a complex graph (output not shown for brevity):
52
52
53 >>> len(list(parsedag("""
53 >>> len(list(parsedag("""
54 ...
54 ...
55 ... +3 # 3 nodes in linear run
55 ... +3 # 3 nodes in linear run
56 ... :forkhere # a label for the last of the 3 nodes from above
56 ... :forkhere # a label for the last of the 3 nodes from above
57 ... +5 # 5 more nodes on one branch
57 ... +5 # 5 more nodes on one branch
58 ... :mergethis # label again
58 ... :mergethis # label again
59 ... <forkhere # set default parent to labelled fork node
59 ... <forkhere # set default parent to labelled fork node
60 ... +10 # 10 more nodes on a parallel branch
60 ... +10 # 10 more nodes on a parallel branch
61 ... @stable # following nodes will be annotated as "stable"
61 ... @stable # following nodes will be annotated as "stable"
62 ... +5 # 5 nodes in stable
62 ... +5 # 5 nodes in stable
63 ... !addfile # custom command; could trigger new file in next node
63 ... !addfile # custom command; could trigger new file in next node
64 ... +2 # two more nodes
64 ... +2 # two more nodes
65 ... /mergethis # merge last node with labelled node
65 ... /mergethis # merge last node with labelled node
66 ... +4 # 4 more nodes descending from merge node
66 ... +4 # 4 more nodes descending from merge node
67 ...
67 ...
68 ... """)))
68 ... """)))
69 34
69 34
70
70
71 Empty list:
71 Empty list:
72
72
73 >>> list(parsedag(""))
73 >>> list(parsedag(""))
74 []
74 []
75
75
76 A simple linear run:
76 A simple linear run:
77
77
78 >>> list(parsedag("+3"))
78 >>> list(parsedag("+3"))
79 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
79 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
80
80
81 Some non-standard ways to define such runs:
81 Some non-standard ways to define such runs:
82
82
83 >>> list(parsedag("+1+2"))
83 >>> list(parsedag("+1+2"))
84 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
84 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
85
85
86 >>> list(parsedag("+1*1*"))
86 >>> list(parsedag("+1*1*"))
87 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
87 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
88
88
89 >>> list(parsedag("*"))
89 >>> list(parsedag("*"))
90 [('n', (0, [-1]))]
90 [('n', (0, [-1]))]
91
91
92 >>> list(parsedag("..."))
92 >>> list(parsedag("..."))
93 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
93 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [1]))]
94
94
95 A fork and a join, using numeric back references:
95 A fork and a join, using numeric back references:
96
96
97 >>> list(parsedag("+2*2*/2"))
97 >>> list(parsedag("+2*2*/2"))
98 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])), ('n', (3, [2, 1]))]
98 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])), ('n', (3, [2, 1]))]
99
99
100 >>> list(parsedag("+2<2+1/2"))
100 >>> list(parsedag("+2<2+1/2"))
101 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])), ('n', (3, [2, 1]))]
101 [('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])), ('n', (3, [2, 1]))]
102
102
103 Placing a label:
103 Placing a label:
104
104
105 >>> list(parsedag("+1 :mylabel +1"))
105 >>> list(parsedag("+1 :mylabel +1"))
106 [('n', (0, [-1])), ('l', (0, 'mylabel')), ('n', (1, [0]))]
106 [('n', (0, [-1])), ('l', (0, 'mylabel')), ('n', (1, [0]))]
107
107
108 An empty label (silly, really):
108 An empty label (silly, really):
109
109
110 >>> list(parsedag("+1:+1"))
110 >>> list(parsedag("+1:+1"))
111 [('n', (0, [-1])), ('l', (0, '')), ('n', (1, [0]))]
111 [('n', (0, [-1])), ('l', (0, '')), ('n', (1, [0]))]
112
112
113 Fork and join, but with labels instead of numeric back references:
113 Fork and join, but with labels instead of numeric back references:
114
114
115 >>> list(parsedag("+1:f +1:p2 *f */p2"))
115 >>> list(parsedag("+1:f +1:p2 *f */p2"))
116 [('n', (0, [-1])), ('l', (0, 'f')), ('n', (1, [0])), ('l', (1, 'p2')),
116 [('n', (0, [-1])), ('l', (0, 'f')), ('n', (1, [0])), ('l', (1, 'p2')),
117 ('n', (2, [0])), ('n', (3, [2, 1]))]
117 ('n', (2, [0])), ('n', (3, [2, 1]))]
118
118
119 >>> list(parsedag("+1:f +1:p2 <f +1 /p2"))
119 >>> list(parsedag("+1:f +1:p2 <f +1 /p2"))
120 [('n', (0, [-1])), ('l', (0, 'f')), ('n', (1, [0])), ('l', (1, 'p2')),
120 [('n', (0, [-1])), ('l', (0, 'f')), ('n', (1, [0])), ('l', (1, 'p2')),
121 ('n', (2, [0])), ('n', (3, [2, 1]))]
121 ('n', (2, [0])), ('n', (3, [2, 1]))]
122
122
123 Restarting from the root:
123 Restarting from the root:
124
124
125 >>> list(parsedag("+1 $ +1"))
125 >>> list(parsedag("+1 $ +1"))
126 [('n', (0, [-1])), ('n', (1, [-1]))]
126 [('n', (0, [-1])), ('n', (1, [-1]))]
127
127
128 Annotations, which are meant to introduce sticky state for subsequent nodes:
128 Annotations, which are meant to introduce sticky state for subsequent nodes:
129
129
130 >>> list(parsedag("+1 @ann +1"))
130 >>> list(parsedag("+1 @ann +1"))
131 [('n', (0, [-1])), ('a', 'ann'), ('n', (1, [0]))]
131 [('n', (0, [-1])), ('a', 'ann'), ('n', (1, [0]))]
132
132
133 >>> list(parsedag('+1 @"my annotation" +1'))
133 >>> list(parsedag('+1 @"my annotation" +1'))
134 [('n', (0, [-1])), ('a', 'my annotation'), ('n', (1, [0]))]
134 [('n', (0, [-1])), ('a', 'my annotation'), ('n', (1, [0]))]
135
135
136 Commands, which are meant to operate on the most recently created node:
136 Commands, which are meant to operate on the most recently created node:
137
137
138 >>> list(parsedag("+1 !cmd +1"))
138 >>> list(parsedag("+1 !cmd +1"))
139 [('n', (0, [-1])), ('c', 'cmd'), ('n', (1, [0]))]
139 [('n', (0, [-1])), ('c', 'cmd'), ('n', (1, [0]))]
140
140
141 >>> list(parsedag('+1 !"my command" +1'))
141 >>> list(parsedag('+1 !"my command" +1'))
142 [('n', (0, [-1])), ('c', 'my command'), ('n', (1, [0]))]
142 [('n', (0, [-1])), ('c', 'my command'), ('n', (1, [0]))]
143
143
144 >>> list(parsedag('+1 !!my command line\\n +1'))
144 >>> list(parsedag('+1 !!my command line\\n +1'))
145 [('n', (0, [-1])), ('C', 'my command line'), ('n', (1, [0]))]
145 [('n', (0, [-1])), ('C', 'my command line'), ('n', (1, [0]))]
146
146
147 Comments, which extend to the end of the line:
147 Comments, which extend to the end of the line:
148
148
149 >>> list(parsedag('+1 # comment\\n+1'))
149 >>> list(parsedag('+1 # comment\\n+1'))
150 [('n', (0, [-1])), ('n', (1, [0]))]
150 [('n', (0, [-1])), ('n', (1, [0]))]
151
151
152 Error:
152 Error:
153
153
154 >>> try: list(parsedag('+1 bad'))
154 >>> try: list(parsedag('+1 bad'))
155 ... except Exception, e: print e
155 ... except Exception, e: print e
156 invalid character in dag description: bad...
156 invalid character in dag description: bad...
157
157
158 '''
158 '''
159 if not desc:
159 if not desc:
160 return
160 return
161
161
162 wordchars = string.ascii_letters + string.digits
162 wordchars = string.ascii_letters + string.digits
163
163
164 labels = {}
164 labels = {}
165 p1 = -1
165 p1 = -1
166 r = 0
166 r = 0
167
167
168 def resolve(ref):
168 def resolve(ref):
169 if not ref:
169 if not ref:
170 return p1
170 return p1
171 elif ref[0] in string.digits:
171 elif ref[0] in string.digits:
172 return r - int(ref)
172 return r - int(ref)
173 else:
173 else:
174 return labels[ref]
174 return labels[ref]
175
175
176 chiter = (c for c in desc)
176 chiter = (c for c in desc)
177
177
178 def nextch():
178 def nextch():
179 try:
179 try:
180 return chiter.next()
180 return chiter.next()
181 except StopIteration:
181 except StopIteration:
182 return '\0'
182 return '\0'
183
183
184 def nextrun(c, allow):
184 def nextrun(c, allow):
185 s = ''
185 s = ''
186 while c in allow:
186 while c in allow:
187 s += c
187 s += c
188 c = nextch()
188 c = nextch()
189 return c, s
189 return c, s
190
190
191 def nextdelimited(c, limit, escape):
191 def nextdelimited(c, limit, escape):
192 s = ''
192 s = ''
193 while c != limit:
193 while c != limit:
194 if c == escape:
194 if c == escape:
195 c = nextch()
195 c = nextch()
196 s += c
196 s += c
197 c = nextch()
197 c = nextch()
198 return nextch(), s
198 return nextch(), s
199
199
200 def nextstring(c):
200 def nextstring(c):
201 if c == '"':
201 if c == '"':
202 return nextdelimited(nextch(), '"', '\\')
202 return nextdelimited(nextch(), '"', '\\')
203 else:
203 else:
204 return nextrun(c, wordchars)
204 return nextrun(c, wordchars)
205
205
206 c = nextch()
206 c = nextch()
207 while c != '\0':
207 while c != '\0':
208 while c in string.whitespace:
208 while c in string.whitespace:
209 c = nextch()
209 c = nextch()
210 if c == '.':
210 if c == '.':
211 yield 'n', (r, [p1])
211 yield 'n', (r, [p1])
212 p1 = r
212 p1 = r
213 r += 1
213 r += 1
214 c = nextch()
214 c = nextch()
215 elif c == '+':
215 elif c == '+':
216 c, digs = nextrun(nextch(), string.digits)
216 c, digs = nextrun(nextch(), string.digits)
217 n = int(digs)
217 n = int(digs)
218 for i in xrange(0, n):
218 for i in xrange(0, n):
219 yield 'n', (r, [p1])
219 yield 'n', (r, [p1])
220 p1 = r
220 p1 = r
221 r += 1
221 r += 1
222 elif c == '*' or c == '/':
222 elif c == '*' or c == '/':
223 if c == '*':
223 if c == '*':
224 c = nextch()
224 c = nextch()
225 c, pref = nextstring(c)
225 c, pref = nextstring(c)
226 prefs = [pref]
226 prefs = [pref]
227 while c == '/':
227 while c == '/':
228 c, pref = nextstring(nextch())
228 c, pref = nextstring(nextch())
229 prefs.append(pref)
229 prefs.append(pref)
230 ps = [resolve(ref) for ref in prefs]
230 ps = [resolve(ref) for ref in prefs]
231 yield 'n', (r, ps)
231 yield 'n', (r, ps)
232 p1 = r
232 p1 = r
233 r += 1
233 r += 1
234 elif c == '<':
234 elif c == '<':
235 c, ref = nextstring(nextch())
235 c, ref = nextstring(nextch())
236 p1 = resolve(ref)
236 p1 = resolve(ref)
237 elif c == ':':
237 elif c == ':':
238 c, name = nextstring(nextch())
238 c, name = nextstring(nextch())
239 labels[name] = p1
239 labels[name] = p1
240 yield 'l', (p1, name)
240 yield 'l', (p1, name)
241 elif c == '@':
241 elif c == '@':
242 c, text = nextstring(nextch())
242 c, text = nextstring(nextch())
243 yield 'a', text
243 yield 'a', text
244 elif c == '!':
244 elif c == '!':
245 c = nextch()
245 c = nextch()
246 if c == '!':
246 if c == '!':
247 cmd = ''
247 cmd = ''
248 c = nextch()
248 c = nextch()
249 while c not in '\n\r\0':
249 while c not in '\n\r\0':
250 cmd += c
250 cmd += c
251 c = nextch()
251 c = nextch()
252 yield 'C', cmd
252 yield 'C', cmd
253 else:
253 else:
254 c, cmd = nextstring(c)
254 c, cmd = nextstring(c)
255 yield 'c', cmd
255 yield 'c', cmd
256 elif c == '#':
256 elif c == '#':
257 while c not in '\n\r\0':
257 while c not in '\n\r\0':
258 c = nextch()
258 c = nextch()
259 elif c == '$':
259 elif c == '$':
260 p1 = -1
260 p1 = -1
261 c = nextch()
261 c = nextch()
262 elif c == '\0':
262 elif c == '\0':
263 return # in case it was preceded by whitespace
263 return # in case it was preceded by whitespace
264 else:
264 else:
265 s = ''
265 s = ''
266 i = 0
266 i = 0
267 while c != '\0' and i < 10:
267 while c != '\0' and i < 10:
268 s += c
268 s += c
269 i += 1
269 i += 1
270 c = nextch()
270 c = nextch()
271 raise util.Abort("invalid character in dag description: %s..." % s)
271 raise util.Abort(_("invalid character in dag description: %s...") % s)
272
272
273 def dagtextlines(events,
273 def dagtextlines(events,
274 addspaces=True,
274 addspaces=True,
275 wraplabels=False,
275 wraplabels=False,
276 wrapannotations=False,
276 wrapannotations=False,
277 wrapcommands=False,
277 wrapcommands=False,
278 wrapnonlinear=False,
278 wrapnonlinear=False,
279 usedots=False,
279 usedots=False,
280 maxlinewidth=70):
280 maxlinewidth=70):
281 '''generates single lines for dagtext()'''
281 '''generates single lines for dagtext()'''
282
282
283 def wrapstring(text):
283 def wrapstring(text):
284 if re.match("^[0-9a-z]*$", text):
284 if re.match("^[0-9a-z]*$", text):
285 return text
285 return text
286 return '"' + text.replace('\\', '\\\\').replace('"', '\"') + '"'
286 return '"' + text.replace('\\', '\\\\').replace('"', '\"') + '"'
287
287
288 def gen():
288 def gen():
289 labels = {}
289 labels = {}
290 run = 0
290 run = 0
291 wantr = 0
291 wantr = 0
292 needroot = False
292 needroot = False
293 for kind, data in events:
293 for kind, data in events:
294 if kind == 'n':
294 if kind == 'n':
295 r, ps = data
295 r, ps = data
296
296
297 # sanity check
297 # sanity check
298 if r != wantr:
298 if r != wantr:
299 raise util.Abort("Expected id %i, got %i" % (wantr, r))
299 raise util.Abort(_("expected id %i, got %i") % (wantr, r))
300 if not ps:
300 if not ps:
301 ps = [-1]
301 ps = [-1]
302 else:
302 else:
303 for p in ps:
303 for p in ps:
304 if p >= r:
304 if p >= r:
305 raise util.Abort("Parent id %i is larger than "
305 raise util.Abort(_("parent id %i is larger than "
306 "current id %i" % (p, r))
306 "current id %i") % (p, r))
307 wantr += 1
307 wantr += 1
308
308
309 # new root?
309 # new root?
310 p1 = r - 1
310 p1 = r - 1
311 if len(ps) == 1 and ps[0] == -1:
311 if len(ps) == 1 and ps[0] == -1:
312 if needroot:
312 if needroot:
313 if run:
313 if run:
314 yield '+' + str(run)
314 yield '+' + str(run)
315 run = 0
315 run = 0
316 if wrapnonlinear:
316 if wrapnonlinear:
317 yield '\n'
317 yield '\n'
318 yield '$'
318 yield '$'
319 p1 = -1
319 p1 = -1
320 else:
320 else:
321 needroot = True
321 needroot = True
322 if len(ps) == 1 and ps[0] == p1:
322 if len(ps) == 1 and ps[0] == p1:
323 if usedots:
323 if usedots:
324 yield "."
324 yield "."
325 else:
325 else:
326 run += 1
326 run += 1
327 else:
327 else:
328 if run:
328 if run:
329 yield '+' + str(run)
329 yield '+' + str(run)
330 run = 0
330 run = 0
331 if wrapnonlinear:
331 if wrapnonlinear:
332 yield '\n'
332 yield '\n'
333 prefs = []
333 prefs = []
334 for p in ps:
334 for p in ps:
335 if p == p1:
335 if p == p1:
336 prefs.append('')
336 prefs.append('')
337 elif p in labels:
337 elif p in labels:
338 prefs.append(labels[p])
338 prefs.append(labels[p])
339 else:
339 else:
340 prefs.append(str(r - p))
340 prefs.append(str(r - p))
341 yield '*' + '/'.join(prefs)
341 yield '*' + '/'.join(prefs)
342 else:
342 else:
343 if run:
343 if run:
344 yield '+' + str(run)
344 yield '+' + str(run)
345 run = 0
345 run = 0
346 if kind == 'l':
346 if kind == 'l':
347 rid, name = data
347 rid, name = data
348 labels[rid] = name
348 labels[rid] = name
349 yield ':' + name
349 yield ':' + name
350 if wraplabels:
350 if wraplabels:
351 yield '\n'
351 yield '\n'
352 elif kind == 'c':
352 elif kind == 'c':
353 yield '!' + wrapstring(data)
353 yield '!' + wrapstring(data)
354 if wrapcommands:
354 if wrapcommands:
355 yield '\n'
355 yield '\n'
356 elif kind == 'C':
356 elif kind == 'C':
357 yield '!!' + data
357 yield '!!' + data
358 yield '\n'
358 yield '\n'
359 elif kind == 'a':
359 elif kind == 'a':
360 if wrapannotations:
360 if wrapannotations:
361 yield '\n'
361 yield '\n'
362 yield '@' + wrapstring(data)
362 yield '@' + wrapstring(data)
363 elif kind == '#':
363 elif kind == '#':
364 yield '#' + data
364 yield '#' + data
365 yield '\n'
365 yield '\n'
366 else:
366 else:
367 raise util.Abort(_("invalid event type in dag: %s")
367 raise util.Abort(_("invalid event type in dag: %s")
368 % str((type, data)))
368 % str((type, data)))
369 if run:
369 if run:
370 yield '+' + str(run)
370 yield '+' + str(run)
371
371
372 line = ''
372 line = ''
373 for part in gen():
373 for part in gen():
374 if part == '\n':
374 if part == '\n':
375 if line:
375 if line:
376 yield line
376 yield line
377 line = ''
377 line = ''
378 else:
378 else:
379 if len(line) + len(part) >= maxlinewidth:
379 if len(line) + len(part) >= maxlinewidth:
380 yield line
380 yield line
381 line = ''
381 line = ''
382 elif addspaces and line and part != '.':
382 elif addspaces and line and part != '.':
383 line += ' '
383 line += ' '
384 line += part
384 line += part
385 if line:
385 if line:
386 yield line
386 yield line
387
387
388 def dagtext(dag,
388 def dagtext(dag,
389 addspaces=True,
389 addspaces=True,
390 wraplabels=False,
390 wraplabels=False,
391 wrapannotations=False,
391 wrapannotations=False,
392 wrapcommands=False,
392 wrapcommands=False,
393 wrapnonlinear=False,
393 wrapnonlinear=False,
394 usedots=False,
394 usedots=False,
395 maxlinewidth=70):
395 maxlinewidth=70):
396 '''generates lines of a textual representation for a dag event stream
396 '''generates lines of a textual representation for a dag event stream
397
397
398 events should generate what parsedag() does, so:
398 events should generate what parsedag() does, so:
399
399
400 ('n', (id, [parentids])) for node creation
400 ('n', (id, [parentids])) for node creation
401 ('l', (id, labelname)) for labels on nodes
401 ('l', (id, labelname)) for labels on nodes
402 ('a', text) for annotations
402 ('a', text) for annotations
403 ('c', text) for commands
403 ('c', text) for commands
404 ('C', text) for line commands ('!!')
404 ('C', text) for line commands ('!!')
405 ('#', text) for comment lines
405 ('#', text) for comment lines
406
406
407 Parent nodes must come before child nodes.
407 Parent nodes must come before child nodes.
408
408
409 Examples
409 Examples
410 --------
410 --------
411
411
412 Linear run:
412 Linear run:
413
413
414 >>> dagtext([('n', (0, [-1])), ('n', (1, [0]))])
414 >>> dagtext([('n', (0, [-1])), ('n', (1, [0]))])
415 '+2'
415 '+2'
416
416
417 Two roots:
417 Two roots:
418
418
419 >>> dagtext([('n', (0, [-1])), ('n', (1, [-1]))])
419 >>> dagtext([('n', (0, [-1])), ('n', (1, [-1]))])
420 '+1 $ +1'
420 '+1 $ +1'
421
421
422 Fork and join:
422 Fork and join:
423
423
424 >>> dagtext([('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])),
424 >>> dagtext([('n', (0, [-1])), ('n', (1, [0])), ('n', (2, [0])),
425 ... ('n', (3, [2, 1]))])
425 ... ('n', (3, [2, 1]))])
426 '+2 *2 */2'
426 '+2 *2 */2'
427
427
428 Fork and join with labels:
428 Fork and join with labels:
429
429
430 >>> dagtext([('n', (0, [-1])), ('l', (0, 'f')), ('n', (1, [0])),
430 >>> dagtext([('n', (0, [-1])), ('l', (0, 'f')), ('n', (1, [0])),
431 ... ('l', (1, 'p2')), ('n', (2, [0])), ('n', (3, [2, 1]))])
431 ... ('l', (1, 'p2')), ('n', (2, [0])), ('n', (3, [2, 1]))])
432 '+1 :f +1 :p2 *f */p2'
432 '+1 :f +1 :p2 *f */p2'
433
433
434 Annotations:
434 Annotations:
435
435
436 >>> dagtext([('n', (0, [-1])), ('a', 'ann'), ('n', (1, [0]))])
436 >>> dagtext([('n', (0, [-1])), ('a', 'ann'), ('n', (1, [0]))])
437 '+1 @ann +1'
437 '+1 @ann +1'
438
438
439 >>> dagtext([('n', (0, [-1])), ('a', 'my annotation'), ('n', (1, [0]))])
439 >>> dagtext([('n', (0, [-1])), ('a', 'my annotation'), ('n', (1, [0]))])
440 '+1 @"my annotation" +1'
440 '+1 @"my annotation" +1'
441
441
442 Commands:
442 Commands:
443
443
444 >>> dagtext([('n', (0, [-1])), ('c', 'cmd'), ('n', (1, [0]))])
444 >>> dagtext([('n', (0, [-1])), ('c', 'cmd'), ('n', (1, [0]))])
445 '+1 !cmd +1'
445 '+1 !cmd +1'
446
446
447 >>> dagtext([('n', (0, [-1])), ('c', 'my command'), ('n', (1, [0]))])
447 >>> dagtext([('n', (0, [-1])), ('c', 'my command'), ('n', (1, [0]))])
448 '+1 !"my command" +1'
448 '+1 !"my command" +1'
449
449
450 >>> dagtext([('n', (0, [-1])), ('C', 'my command line'), ('n', (1, [0]))])
450 >>> dagtext([('n', (0, [-1])), ('C', 'my command line'), ('n', (1, [0]))])
451 '+1 !!my command line\\n+1'
451 '+1 !!my command line\\n+1'
452
452
453 Comments:
453 Comments:
454
454
455 >>> dagtext([('n', (0, [-1])), ('#', ' comment'), ('n', (1, [0]))])
455 >>> dagtext([('n', (0, [-1])), ('#', ' comment'), ('n', (1, [0]))])
456 '+1 # comment\\n+1'
456 '+1 # comment\\n+1'
457
457
458 >>> dagtext([])
458 >>> dagtext([])
459 ''
459 ''
460
460
461 Combining parsedag and dagtext:
461 Combining parsedag and dagtext:
462
462
463 >>> dagtext(parsedag('+1 :f +1 :p2 *f */p2'))
463 >>> dagtext(parsedag('+1 :f +1 :p2 *f */p2'))
464 '+1 :f +1 :p2 *f */p2'
464 '+1 :f +1 :p2 *f */p2'
465
465
466 '''
466 '''
467 return "\n".join(dagtextlines(dag,
467 return "\n".join(dagtextlines(dag,
468 addspaces,
468 addspaces,
469 wraplabels,
469 wraplabels,
470 wrapannotations,
470 wrapannotations,
471 wrapcommands,
471 wrapcommands,
472 wrapnonlinear,
472 wrapnonlinear,
473 usedots,
473 usedots,
474 maxlinewidth))
474 maxlinewidth))
@@ -1,250 +1,251 b''
1 # match.py - filename matching
1 # match.py - filename matching
2 #
2 #
3 # Copyright 2008, 2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2008, 2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import re
8 import re
9 import util
9 import util
10 from i18n import _
10
11
11 class match(object):
12 class match(object):
12 def __init__(self, root, cwd, patterns, include=[], exclude=[],
13 def __init__(self, root, cwd, patterns, include=[], exclude=[],
13 default='glob', exact=False):
14 default='glob', exact=False):
14 """build an object to match a set of file patterns
15 """build an object to match a set of file patterns
15
16
16 arguments:
17 arguments:
17 root - the canonical root of the tree you're matching against
18 root - the canonical root of the tree you're matching against
18 cwd - the current working directory, if relevant
19 cwd - the current working directory, if relevant
19 patterns - patterns to find
20 patterns - patterns to find
20 include - patterns to include
21 include - patterns to include
21 exclude - patterns to exclude
22 exclude - patterns to exclude
22 default - if a pattern in names has no explicit type, assume this one
23 default - if a pattern in names has no explicit type, assume this one
23 exact - patterns are actually literals
24 exact - patterns are actually literals
24
25
25 a pattern is one of:
26 a pattern is one of:
26 'glob:<glob>' - a glob relative to cwd
27 'glob:<glob>' - a glob relative to cwd
27 're:<regexp>' - a regular expression
28 're:<regexp>' - a regular expression
28 'path:<path>' - a path relative to canonroot
29 'path:<path>' - a path relative to canonroot
29 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
30 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
30 'relpath:<path>' - a path relative to cwd
31 'relpath:<path>' - a path relative to cwd
31 'relre:<regexp>' - a regexp that needn't match the start of a name
32 'relre:<regexp>' - a regexp that needn't match the start of a name
32 '<something>' - a pattern of the specified default type
33 '<something>' - a pattern of the specified default type
33 """
34 """
34
35
35 self._root = root
36 self._root = root
36 self._cwd = cwd
37 self._cwd = cwd
37 self._files = []
38 self._files = []
38 self._anypats = bool(include or exclude)
39 self._anypats = bool(include or exclude)
39
40
40 if include:
41 if include:
41 im = _buildmatch(_normalize(include, 'glob', root, cwd), '(?:/|$)')
42 im = _buildmatch(_normalize(include, 'glob', root, cwd), '(?:/|$)')
42 if exclude:
43 if exclude:
43 em = _buildmatch(_normalize(exclude, 'glob', root, cwd), '(?:/|$)')
44 em = _buildmatch(_normalize(exclude, 'glob', root, cwd), '(?:/|$)')
44 if exact:
45 if exact:
45 self._files = patterns
46 self._files = patterns
46 pm = self.exact
47 pm = self.exact
47 elif patterns:
48 elif patterns:
48 pats = _normalize(patterns, default, root, cwd)
49 pats = _normalize(patterns, default, root, cwd)
49 self._files = _roots(pats)
50 self._files = _roots(pats)
50 self._anypats = self._anypats or _anypats(pats)
51 self._anypats = self._anypats or _anypats(pats)
51 pm = _buildmatch(pats, '$')
52 pm = _buildmatch(pats, '$')
52
53
53 if patterns or exact:
54 if patterns or exact:
54 if include:
55 if include:
55 if exclude:
56 if exclude:
56 m = lambda f: im(f) and not em(f) and pm(f)
57 m = lambda f: im(f) and not em(f) and pm(f)
57 else:
58 else:
58 m = lambda f: im(f) and pm(f)
59 m = lambda f: im(f) and pm(f)
59 else:
60 else:
60 if exclude:
61 if exclude:
61 m = lambda f: not em(f) and pm(f)
62 m = lambda f: not em(f) and pm(f)
62 else:
63 else:
63 m = pm
64 m = pm
64 else:
65 else:
65 if include:
66 if include:
66 if exclude:
67 if exclude:
67 m = lambda f: im(f) and not em(f)
68 m = lambda f: im(f) and not em(f)
68 else:
69 else:
69 m = im
70 m = im
70 else:
71 else:
71 if exclude:
72 if exclude:
72 m = lambda f: not em(f)
73 m = lambda f: not em(f)
73 else:
74 else:
74 m = lambda f: True
75 m = lambda f: True
75
76
76 self.matchfn = m
77 self.matchfn = m
77 self._fmap = set(self._files)
78 self._fmap = set(self._files)
78
79
79 def __call__(self, fn):
80 def __call__(self, fn):
80 return self.matchfn(fn)
81 return self.matchfn(fn)
81 def __iter__(self):
82 def __iter__(self):
82 for f in self._files:
83 for f in self._files:
83 yield f
84 yield f
84 def bad(self, f, msg):
85 def bad(self, f, msg):
85 '''callback for each explicit file that can't be
86 '''callback for each explicit file that can't be
86 found/accessed, with an error message
87 found/accessed, with an error message
87 '''
88 '''
88 pass
89 pass
89 def dir(self, f):
90 def dir(self, f):
90 pass
91 pass
91 def missing(self, f):
92 def missing(self, f):
92 pass
93 pass
93 def exact(self, f):
94 def exact(self, f):
94 return f in self._fmap
95 return f in self._fmap
95 def rel(self, f):
96 def rel(self, f):
96 return util.pathto(self._root, self._cwd, f)
97 return util.pathto(self._root, self._cwd, f)
97 def files(self):
98 def files(self):
98 return self._files
99 return self._files
99 def anypats(self):
100 def anypats(self):
100 return self._anypats
101 return self._anypats
101
102
102 class exact(match):
103 class exact(match):
103 def __init__(self, root, cwd, files):
104 def __init__(self, root, cwd, files):
104 match.__init__(self, root, cwd, files, exact = True)
105 match.__init__(self, root, cwd, files, exact = True)
105
106
106 class always(match):
107 class always(match):
107 def __init__(self, root, cwd):
108 def __init__(self, root, cwd):
108 match.__init__(self, root, cwd, [])
109 match.__init__(self, root, cwd, [])
109
110
110 def patkind(pat):
111 def patkind(pat):
111 return _patsplit(pat, None)[0]
112 return _patsplit(pat, None)[0]
112
113
113 def _patsplit(pat, default):
114 def _patsplit(pat, default):
114 """Split a string into an optional pattern kind prefix and the
115 """Split a string into an optional pattern kind prefix and the
115 actual pattern."""
116 actual pattern."""
116 if ':' in pat:
117 if ':' in pat:
117 kind, val = pat.split(':', 1)
118 kind, val = pat.split(':', 1)
118 if kind in ('re', 'glob', 'path', 'relglob', 'relpath', 'relre'):
119 if kind in ('re', 'glob', 'path', 'relglob', 'relpath', 'relre'):
119 return kind, val
120 return kind, val
120 return default, pat
121 return default, pat
121
122
122 def _globre(pat):
123 def _globre(pat):
123 "convert a glob pattern into a regexp"
124 "convert a glob pattern into a regexp"
124 i, n = 0, len(pat)
125 i, n = 0, len(pat)
125 res = ''
126 res = ''
126 group = 0
127 group = 0
127 escape = re.escape
128 escape = re.escape
128 def peek():
129 def peek():
129 return i < n and pat[i]
130 return i < n and pat[i]
130 while i < n:
131 while i < n:
131 c = pat[i]
132 c = pat[i]
132 i += 1
133 i += 1
133 if c not in '*?[{},\\':
134 if c not in '*?[{},\\':
134 res += escape(c)
135 res += escape(c)
135 elif c == '*':
136 elif c == '*':
136 if peek() == '*':
137 if peek() == '*':
137 i += 1
138 i += 1
138 res += '.*'
139 res += '.*'
139 else:
140 else:
140 res += '[^/]*'
141 res += '[^/]*'
141 elif c == '?':
142 elif c == '?':
142 res += '.'
143 res += '.'
143 elif c == '[':
144 elif c == '[':
144 j = i
145 j = i
145 if j < n and pat[j] in '!]':
146 if j < n and pat[j] in '!]':
146 j += 1
147 j += 1
147 while j < n and pat[j] != ']':
148 while j < n and pat[j] != ']':
148 j += 1
149 j += 1
149 if j >= n:
150 if j >= n:
150 res += '\\['
151 res += '\\['
151 else:
152 else:
152 stuff = pat[i:j].replace('\\','\\\\')
153 stuff = pat[i:j].replace('\\','\\\\')
153 i = j + 1
154 i = j + 1
154 if stuff[0] == '!':
155 if stuff[0] == '!':
155 stuff = '^' + stuff[1:]
156 stuff = '^' + stuff[1:]
156 elif stuff[0] == '^':
157 elif stuff[0] == '^':
157 stuff = '\\' + stuff
158 stuff = '\\' + stuff
158 res = '%s[%s]' % (res, stuff)
159 res = '%s[%s]' % (res, stuff)
159 elif c == '{':
160 elif c == '{':
160 group += 1
161 group += 1
161 res += '(?:'
162 res += '(?:'
162 elif c == '}' and group:
163 elif c == '}' and group:
163 res += ')'
164 res += ')'
164 group -= 1
165 group -= 1
165 elif c == ',' and group:
166 elif c == ',' and group:
166 res += '|'
167 res += '|'
167 elif c == '\\':
168 elif c == '\\':
168 p = peek()
169 p = peek()
169 if p:
170 if p:
170 i += 1
171 i += 1
171 res += escape(p)
172 res += escape(p)
172 else:
173 else:
173 res += escape(c)
174 res += escape(c)
174 else:
175 else:
175 res += escape(c)
176 res += escape(c)
176 return res
177 return res
177
178
178 def _regex(kind, name, tail):
179 def _regex(kind, name, tail):
179 '''convert a pattern into a regular expression'''
180 '''convert a pattern into a regular expression'''
180 if not name:
181 if not name:
181 return ''
182 return ''
182 if kind == 're':
183 if kind == 're':
183 return name
184 return name
184 elif kind == 'path':
185 elif kind == 'path':
185 return '^' + re.escape(name) + '(?:/|$)'
186 return '^' + re.escape(name) + '(?:/|$)'
186 elif kind == 'relglob':
187 elif kind == 'relglob':
187 return '(?:|.*/)' + _globre(name) + tail
188 return '(?:|.*/)' + _globre(name) + tail
188 elif kind == 'relpath':
189 elif kind == 'relpath':
189 return re.escape(name) + '(?:/|$)'
190 return re.escape(name) + '(?:/|$)'
190 elif kind == 'relre':
191 elif kind == 'relre':
191 if name.startswith('^'):
192 if name.startswith('^'):
192 return name
193 return name
193 return '.*' + name
194 return '.*' + name
194 return _globre(name) + tail
195 return _globre(name) + tail
195
196
196 def _buildmatch(pats, tail):
197 def _buildmatch(pats, tail):
197 """build a matching function from a set of patterns"""
198 """build a matching function from a set of patterns"""
198 try:
199 try:
199 pat = '(?:%s)' % '|'.join([_regex(k, p, tail) for (k, p) in pats])
200 pat = '(?:%s)' % '|'.join([_regex(k, p, tail) for (k, p) in pats])
200 if len(pat) > 20000:
201 if len(pat) > 20000:
201 raise OverflowError()
202 raise OverflowError()
202 return re.compile(pat).match
203 return re.compile(pat).match
203 except OverflowError:
204 except OverflowError:
204 # We're using a Python with a tiny regex engine and we
205 # We're using a Python with a tiny regex engine and we
205 # made it explode, so we'll divide the pattern list in two
206 # made it explode, so we'll divide the pattern list in two
206 # until it works
207 # until it works
207 l = len(pats)
208 l = len(pats)
208 if l < 2:
209 if l < 2:
209 raise
210 raise
210 a, b = _buildmatch(pats[:l//2], tail), _buildmatch(pats[l//2:], tail)
211 a, b = _buildmatch(pats[:l//2], tail), _buildmatch(pats[l//2:], tail)
211 return lambda s: a(s) or b(s)
212 return lambda s: a(s) or b(s)
212 except re.error:
213 except re.error:
213 for k, p in pats:
214 for k, p in pats:
214 try:
215 try:
215 re.compile('(?:%s)' % _regex(k, p, tail))
216 re.compile('(?:%s)' % _regex(k, p, tail))
216 except re.error:
217 except re.error:
217 raise util.Abort("invalid pattern (%s): %s" % (k, p))
218 raise util.Abort(_("invalid pattern (%s): %s") % (k, p))
218 raise util.Abort("invalid pattern")
219 raise util.Abort(_("invalid pattern"))
219
220
220 def _normalize(names, default, root, cwd):
221 def _normalize(names, default, root, cwd):
221 pats = []
222 pats = []
222 for kind, name in [_patsplit(p, default) for p in names]:
223 for kind, name in [_patsplit(p, default) for p in names]:
223 if kind in ('glob', 'relpath'):
224 if kind in ('glob', 'relpath'):
224 name = util.canonpath(root, cwd, name)
225 name = util.canonpath(root, cwd, name)
225 elif kind in ('relglob', 'path'):
226 elif kind in ('relglob', 'path'):
226 name = util.normpath(name)
227 name = util.normpath(name)
227
228
228 pats.append((kind, name))
229 pats.append((kind, name))
229 return pats
230 return pats
230
231
231 def _roots(patterns):
232 def _roots(patterns):
232 r = []
233 r = []
233 for kind, name in patterns:
234 for kind, name in patterns:
234 if kind == 'glob': # find the non-glob prefix
235 if kind == 'glob': # find the non-glob prefix
235 root = []
236 root = []
236 for p in name.split('/'):
237 for p in name.split('/'):
237 if '[' in p or '{' in p or '*' in p or '?' in p:
238 if '[' in p or '{' in p or '*' in p or '?' in p:
238 break
239 break
239 root.append(p)
240 root.append(p)
240 r.append('/'.join(root) or '.')
241 r.append('/'.join(root) or '.')
241 elif kind in ('relpath', 'path'):
242 elif kind in ('relpath', 'path'):
242 r.append(name or '.')
243 r.append(name or '.')
243 elif kind == 'relglob':
244 elif kind == 'relglob':
244 r.append('.')
245 r.append('.')
245 return r
246 return r
246
247
247 def _anypats(patterns):
248 def _anypats(patterns):
248 for kind, name in patterns:
249 for kind, name in patterns:
249 if kind in ('glob', 're', 'relglob', 'relre'):
250 if kind in ('glob', 're', 'relglob', 'relre'):
250 return True
251 return True
General Comments 0
You need to be logged in to leave comments. Login now