arche / internal/revset/revset.go

commit 154431fd
  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}