1package revset
2
3import (
4 "fmt"
5 "strings"
6 "time"
7 "unicode"
8
9 "arche/internal/object"
10)
11
12type Func func(id [32]byte, c *object.Commit, phase object.Phase) bool
13
14func Parse(expr string) (Func, error) {
15 p := &parser{tokens: tokenize(expr)}
16 fn, err := p.parseOr()
17 if err != nil {
18 return nil, err
19 }
20 if !p.eof() {
21 return nil, fmt.Errorf("revset: unexpected token %q", p.peek())
22 }
23 return fn, nil
24}
25
26type tokKind int
27
28const (
29 tokWord tokKind = iota
30 tokStr
31 tokLParen
32 tokRParen
33 tokComma
34 tokDotDot
35 tokEOF
36)
37
38type token struct {
39 kind tokKind
40 val string
41}
42
43func tokenize(s string) []token {
44 var out []token
45 i := 0
46 for i < len(s) {
47 ch := rune(s[i])
48 if unicode.IsSpace(ch) {
49 i++
50 continue
51 }
52 switch {
53 case ch == '(' || ch == ')' || ch == ',':
54 k := tokLParen
55 switch ch {
56 case ')':
57 k = tokRParen
58 case ',':
59 k = tokComma
60 }
61 out = append(out, token{k, string(ch)})
62 i++
63 case i+1 < len(s) && s[i] == '.' && s[i+1] == '.':
64 out = append(out, token{tokDotDot, ".."})
65 i += 2
66 case ch == '"' || ch == '\'':
67 quote := s[i]
68 i++
69 start := i
70 for i < len(s) && s[i] != quote {
71 i++
72 }
73 out = append(out, token{tokStr, s[start:i]})
74 if i < len(s) {
75 i++
76 }
77 default:
78 start := i
79 for i < len(s) && !unicode.IsSpace(rune(s[i])) &&
80 s[i] != '(' && s[i] != ')' && s[i] != ',' {
81 i++
82 }
83 out = append(out, token{tokWord, s[start:i]})
84 }
85 }
86 out = append(out, token{tokEOF, ""})
87 return out
88}
89
90type parser struct {
91 tokens []token
92 pos int
93}
94
95func (p *parser) peek() string {
96 return p.tokens[p.pos].val
97}
98
99func (p *parser) peekKind() tokKind {
100 return p.tokens[p.pos].kind
101}
102
103func (p *parser) consume() token {
104 t := p.tokens[p.pos]
105 if t.kind != tokEOF {
106 p.pos++
107 }
108 return t
109}
110
111func (p *parser) eof() bool {
112 return p.tokens[p.pos].kind == tokEOF
113}
114
115func (p *parser) expect(kind tokKind) (string, error) {
116 t := p.consume()
117 if t.kind != kind {
118 return "", fmt.Errorf("revset: expected %v, got %q", kind, t.val)
119 }
120 return t.val, nil
121}
122
123func (p *parser) parseOr() (Func, error) {
124 left, err := p.parseAnd()
125 if err != nil {
126 return nil, err
127 }
128 for p.peekKind() == tokWord && strings.EqualFold(p.peek(), "or") {
129 p.consume()
130 right, err := p.parseAnd()
131 if err != nil {
132 return nil, err
133 }
134 l, r := left, right
135 left = func(id [32]byte, c *object.Commit, ph object.Phase) bool {
136 return l(id, c, ph) || r(id, c, ph)
137 }
138 }
139 return left, nil
140}
141
142func (p *parser) parseAnd() (Func, error) {
143 left, err := p.parseNot()
144 if err != nil {
145 return nil, err
146 }
147 for p.peekKind() == tokWord && strings.EqualFold(p.peek(), "and") {
148 p.consume()
149 right, err := p.parseNot()
150 if err != nil {
151 return nil, err
152 }
153 l, r := left, right
154 left = func(id [32]byte, c *object.Commit, ph object.Phase) bool {
155 return l(id, c, ph) && r(id, c, ph)
156 }
157 }
158 return left, nil
159}
160
161func (p *parser) parseNot() (Func, error) {
162 if p.peekKind() == tokWord && strings.EqualFold(p.peek(), "not") {
163 p.consume()
164 inner, err := p.parseNot()
165 if err != nil {
166 return nil, err
167 }
168 return func(id [32]byte, c *object.Commit, ph object.Phase) bool {
169 return !inner(id, c, ph)
170 }, nil
171 }
172 return p.parsePrimary()
173}
174
175func (p *parser) parsePrimary() (Func, error) {
176 if p.peekKind() == tokLParen {
177 p.consume()
178 fn, err := p.parseOr()
179 if err != nil {
180 return nil, err
181 }
182 if _, err := p.expect(tokRParen); err != nil {
183 return nil, err
184 }
185 return fn, nil
186 }
187
188 name, err := p.expect(tokWord)
189 if err != nil {
190 return nil, err
191 }
192
193 if p.peekKind() != tokLParen {
194 return nil, fmt.Errorf("revset: expected '(' after %q", name)
195 }
196 p.consume()
197
198 args, err := p.parseArgs()
199 if err != nil {
200 return nil, err
201 }
202
203 if _, err := p.expect(tokRParen); err != nil {
204 return nil, err
205 }
206
207 return buildPredicate(strings.ToLower(name), args)
208}
209
210func (p *parser) parseArgs() ([]string, error) {
211 var args []string
212 if p.peekKind() == tokRParen {
213 return args, nil
214 }
215 for {
216 switch p.peekKind() {
217 case tokWord, tokStr:
218 args = append(args, p.consume().val)
219 case tokDotDot:
220 args = append(args, p.consume().val)
221 default:
222 return nil, fmt.Errorf("revset: unexpected token in arg list: %q", p.peek())
223 }
224 if p.peekKind() != tokComma {
225 break
226 }
227 p.consume()
228 }
229 return args, nil
230}
231
232func buildPredicate(name string, args []string) (Func, error) {
233 switch name {
234 case "all":
235 if len(args) != 0 {
236 return nil, fmt.Errorf("revset: all() takes no arguments")
237 }
238 return func(_ [32]byte, _ *object.Commit, _ object.Phase) bool { return true }, nil
239
240 case "none":
241 if len(args) != 0 {
242 return nil, fmt.Errorf("revset: none() takes no arguments")
243 }
244 return func(_ [32]byte, _ *object.Commit, _ object.Phase) bool { return false }, nil
245
246 case "draft":
247 if len(args) != 0 {
248 return nil, fmt.Errorf("revset: draft() takes no arguments")
249 }
250 return func(_ [32]byte, _ *object.Commit, ph object.Phase) bool {
251 return ph == object.PhaseDraft
252 }, nil
253
254 case "public":
255 if len(args) != 0 {
256 return nil, fmt.Errorf("revset: public() takes no arguments")
257 }
258 return func(_ [32]byte, _ *object.Commit, ph object.Phase) bool {
259 return ph == object.PhasePublic
260 }, nil
261
262 case "secret":
263 if len(args) != 0 {
264 return nil, fmt.Errorf("revset: secret() takes no arguments")
265 }
266 return func(_ [32]byte, _ *object.Commit, ph object.Phase) bool {
267 return ph == object.PhaseSecret
268 }, nil
269
270 case "phase":
271 if len(args) != 1 {
272 return nil, fmt.Errorf("revset: phase() requires exactly one argument")
273 }
274 var want object.Phase
275 switch strings.ToLower(args[0]) {
276 case "draft":
277 want = object.PhaseDraft
278 case "public":
279 want = object.PhasePublic
280 case "secret":
281 want = object.PhaseSecret
282 default:
283 return nil, fmt.Errorf("revset: unknown phase %q (want draft|public|secret)", args[0])
284 }
285 return func(_ [32]byte, _ *object.Commit, ph object.Phase) bool {
286 return ph == want
287 }, nil
288
289 case "author":
290 if len(args) != 1 {
291 return nil, fmt.Errorf("revset: author() requires exactly one argument")
292 }
293 pat := strings.ToLower(args[0])
294 return func(_ [32]byte, c *object.Commit, _ object.Phase) bool {
295 return strings.Contains(strings.ToLower(c.Author.Name), pat) ||
296 strings.Contains(strings.ToLower(c.Author.Email), pat)
297 }, nil
298
299 case "message":
300 if len(args) != 1 {
301 return nil, fmt.Errorf("revset: message() requires exactly one argument")
302 }
303 pat := strings.ToLower(args[0])
304 return func(_ [32]byte, c *object.Commit, _ object.Phase) bool {
305 return strings.Contains(strings.ToLower(c.Message), pat)
306 }, nil
307
308 case "date":
309 return buildDatePredicate(args)
310
311 default:
312 return nil, fmt.Errorf("revset: unknown predicate %q", name)
313 }
314}
315
316func buildDatePredicate(args []string) (Func, error) {
317 raw := strings.Join(args, "")
318 if raw == "" {
319 return nil, fmt.Errorf("revset: date() requires an argument")
320 }
321
322 const layout = "2006-01-02"
323 var from, to time.Time
324
325 if idx := strings.Index(raw, ".."); idx >= 0 {
326 fromStr := raw[:idx]
327 toStr := raw[idx+2:]
328 if fromStr != "" {
329 t, err := time.ParseInLocation(layout, fromStr, time.Local)
330 if err != nil {
331 return nil, fmt.Errorf("revset: date: invalid from date %q", fromStr)
332 }
333 from = t
334 }
335 if toStr != "" {
336 t, err := time.ParseInLocation(layout, toStr, time.Local)
337 if err != nil {
338 return nil, fmt.Errorf("revset: date: invalid to date %q", toStr)
339 }
340 to = t.Add(24 * time.Hour)
341 }
342 } else {
343 t, err := time.ParseInLocation(layout, raw, time.Local)
344 if err != nil {
345 return nil, fmt.Errorf("revset: date: invalid date %q", raw)
346 }
347 from = t
348 to = t.Add(24 * time.Hour)
349 }
350
351 return func(_ [32]byte, c *object.Commit, _ object.Phase) bool {
352 ts := c.Author.Timestamp
353 if !from.IsZero() && ts.Before(from) {
354 return false
355 }
356 if !to.IsZero() && !ts.Before(to) {
357 return false
358 }
359 return true
360 }, nil
361}