|
- // Package revision extracts git revision from string
- // More informations about revision : https://www.kernel.org/pub/software/scm/git/docs/gitrevisions.html
- package revision
-
- import (
- "bytes"
- "fmt"
- "io"
- "regexp"
- "strconv"
- "time"
- )
-
- // ErrInvalidRevision is emitted if string doesn't match valid revision
- type ErrInvalidRevision struct {
- s string
- }
-
- func (e *ErrInvalidRevision) Error() string {
- return "Revision invalid : " + e.s
- }
-
- // Revisioner represents a revision component.
- // A revision is made of multiple revision components
- // obtained after parsing a revision string,
- // for instance revision "master~" will be converted in
- // two revision components Ref and TildePath
- type Revisioner interface {
- }
-
- // Ref represents a reference name : HEAD, master
- type Ref string
-
- // TildePath represents ~, ~{n}
- type TildePath struct {
- Depth int
- }
-
- // CaretPath represents ^, ^{n}
- type CaretPath struct {
- Depth int
- }
-
- // CaretReg represents ^{/foo bar}
- type CaretReg struct {
- Regexp *regexp.Regexp
- Negate bool
- }
-
- // CaretType represents ^{commit}
- type CaretType struct {
- ObjectType string
- }
-
- // AtReflog represents @{n}
- type AtReflog struct {
- Depth int
- }
-
- // AtCheckout represents @{-n}
- type AtCheckout struct {
- Depth int
- }
-
- // AtUpstream represents @{upstream}, @{u}
- type AtUpstream struct {
- BranchName string
- }
-
- // AtPush represents @{push}
- type AtPush struct {
- BranchName string
- }
-
- // AtDate represents @{"2006-01-02T15:04:05Z"}
- type AtDate struct {
- Date time.Time
- }
-
- // ColonReg represents :/foo bar
- type ColonReg struct {
- Regexp *regexp.Regexp
- Negate bool
- }
-
- // ColonPath represents :./<path> :<path>
- type ColonPath struct {
- Path string
- }
-
- // ColonStagePath represents :<n>:/<path>
- type ColonStagePath struct {
- Path string
- Stage int
- }
-
- // Parser represents a parser
- // use to tokenize and transform to revisioner chunks
- // a given string
- type Parser struct {
- s *scanner
- currentParsedChar struct {
- tok token
- lit string
- }
- unreadLastChar bool
- }
-
- // NewParserFromString returns a new instance of parser from a string.
- func NewParserFromString(s string) *Parser {
- return NewParser(bytes.NewBufferString(s))
- }
-
- // NewParser returns a new instance of parser.
- func NewParser(r io.Reader) *Parser {
- return &Parser{s: newScanner(r)}
- }
-
- // scan returns the next token from the underlying scanner
- // or the last scanned token if an unscan was requested
- func (p *Parser) scan() (token, string, error) {
- if p.unreadLastChar {
- p.unreadLastChar = false
- return p.currentParsedChar.tok, p.currentParsedChar.lit, nil
- }
-
- tok, lit, err := p.s.scan()
-
- p.currentParsedChar.tok, p.currentParsedChar.lit = tok, lit
-
- return tok, lit, err
- }
-
- // unscan pushes the previously read token back onto the buffer.
- func (p *Parser) unscan() { p.unreadLastChar = true }
-
- // Parse explode a revision string into revisioner chunks
- func (p *Parser) Parse() ([]Revisioner, error) {
- var rev Revisioner
- var revs []Revisioner
- var tok token
- var err error
-
- for {
- tok, _, err = p.scan()
-
- if err != nil {
- return nil, err
- }
-
- switch tok {
- case at:
- rev, err = p.parseAt()
- case tilde:
- rev, err = p.parseTilde()
- case caret:
- rev, err = p.parseCaret()
- case colon:
- rev, err = p.parseColon()
- case eof:
- err = p.validateFullRevision(&revs)
-
- if err != nil {
- return []Revisioner{}, err
- }
-
- return revs, nil
- default:
- p.unscan()
- rev, err = p.parseRef()
- }
-
- if err != nil {
- return []Revisioner{}, err
- }
-
- revs = append(revs, rev)
- }
- }
-
- // validateFullRevision ensures all revisioner chunks make a valid revision
- func (p *Parser) validateFullRevision(chunks *[]Revisioner) error {
- var hasReference bool
-
- for i, chunk := range *chunks {
- switch chunk.(type) {
- case Ref:
- if i == 0 {
- hasReference = true
- } else {
- return &ErrInvalidRevision{`reference must be defined once at the beginning`}
- }
- case AtDate:
- if len(*chunks) == 1 || hasReference && len(*chunks) == 2 {
- return nil
- }
-
- return &ErrInvalidRevision{`"@" statement is not valid, could be : <refname>@{<ISO-8601 date>}, @{<ISO-8601 date>}`}
- case AtReflog:
- if len(*chunks) == 1 || hasReference && len(*chunks) == 2 {
- return nil
- }
-
- return &ErrInvalidRevision{`"@" statement is not valid, could be : <refname>@{<n>}, @{<n>}`}
- case AtCheckout:
- if len(*chunks) == 1 {
- return nil
- }
-
- return &ErrInvalidRevision{`"@" statement is not valid, could be : @{-<n>}`}
- case AtUpstream:
- if len(*chunks) == 1 || hasReference && len(*chunks) == 2 {
- return nil
- }
-
- return &ErrInvalidRevision{`"@" statement is not valid, could be : <refname>@{upstream}, @{upstream}, <refname>@{u}, @{u}`}
- case AtPush:
- if len(*chunks) == 1 || hasReference && len(*chunks) == 2 {
- return nil
- }
-
- return &ErrInvalidRevision{`"@" statement is not valid, could be : <refname>@{push}, @{push}`}
- case TildePath, CaretPath, CaretReg:
- if !hasReference {
- return &ErrInvalidRevision{`"~" or "^" statement must have a reference defined at the beginning`}
- }
- case ColonReg:
- if len(*chunks) == 1 {
- return nil
- }
-
- return &ErrInvalidRevision{`":" statement is not valid, could be : :/<regexp>`}
- case ColonPath:
- if i == len(*chunks)-1 && hasReference || len(*chunks) == 1 {
- return nil
- }
-
- return &ErrInvalidRevision{`":" statement is not valid, could be : <revision>:<path>`}
- case ColonStagePath:
- if len(*chunks) == 1 {
- return nil
- }
-
- return &ErrInvalidRevision{`":" statement is not valid, could be : :<n>:<path>`}
- }
- }
-
- return nil
- }
-
- // parseAt extract @ statements
- func (p *Parser) parseAt() (Revisioner, error) {
- var tok, nextTok token
- var lit, nextLit string
- var err error
-
- tok, _, err = p.scan()
-
- if err != nil {
- return nil, err
- }
-
- if tok != obrace {
- p.unscan()
-
- return Ref("HEAD"), nil
- }
-
- tok, lit, err = p.scan()
-
- if err != nil {
- return nil, err
- }
-
- nextTok, nextLit, err = p.scan()
-
- if err != nil {
- return nil, err
- }
-
- switch {
- case tok == word && (lit == "u" || lit == "upstream") && nextTok == cbrace:
- return AtUpstream{}, nil
- case tok == word && lit == "push" && nextTok == cbrace:
- return AtPush{}, nil
- case tok == number && nextTok == cbrace:
- n, _ := strconv.Atoi(lit)
-
- return AtReflog{n}, nil
- case tok == minus && nextTok == number:
- n, _ := strconv.Atoi(nextLit)
-
- t, _, err := p.scan()
-
- if err != nil {
- return nil, err
- }
-
- if t != cbrace {
- return nil, &ErrInvalidRevision{fmt.Sprintf(`missing "}" in @{-n} structure`)}
- }
-
- return AtCheckout{n}, nil
- default:
- p.unscan()
-
- date := lit
-
- for {
- tok, lit, err = p.scan()
-
- if err != nil {
- return nil, err
- }
-
- switch {
- case tok == cbrace:
- t, err := time.Parse("2006-01-02T15:04:05Z", date)
-
- if err != nil {
- return nil, &ErrInvalidRevision{fmt.Sprintf(`wrong date "%s" must fit ISO-8601 format : 2006-01-02T15:04:05Z`, date)}
- }
-
- return AtDate{t}, nil
- default:
- date += lit
- }
- }
- }
- }
-
- // parseTilde extract ~ statements
- func (p *Parser) parseTilde() (Revisioner, error) {
- var tok token
- var lit string
- var err error
-
- tok, lit, err = p.scan()
-
- if err != nil {
- return nil, err
- }
-
- switch {
- case tok == number:
- n, _ := strconv.Atoi(lit)
-
- return TildePath{n}, nil
- default:
- p.unscan()
- return TildePath{1}, nil
- }
- }
-
- // parseCaret extract ^ statements
- func (p *Parser) parseCaret() (Revisioner, error) {
- var tok token
- var lit string
- var err error
-
- tok, lit, err = p.scan()
-
- if err != nil {
- return nil, err
- }
-
- switch {
- case tok == obrace:
- r, err := p.parseCaretBraces()
-
- if err != nil {
- return nil, err
- }
-
- return r, nil
- case tok == number:
- n, _ := strconv.Atoi(lit)
-
- if n > 2 {
- return nil, &ErrInvalidRevision{fmt.Sprintf(`"%s" found must be 0, 1 or 2 after "^"`, lit)}
- }
-
- return CaretPath{n}, nil
- default:
- p.unscan()
- return CaretPath{1}, nil
- }
- }
-
- // parseCaretBraces extract ^{<data>} statements
- func (p *Parser) parseCaretBraces() (Revisioner, error) {
- var tok, nextTok token
- var lit, _ string
- start := true
- var re string
- var negate bool
- var err error
-
- for {
- tok, lit, err = p.scan()
-
- if err != nil {
- return nil, err
- }
-
- nextTok, _, err = p.scan()
-
- if err != nil {
- return nil, err
- }
-
- switch {
- case tok == word && nextTok == cbrace && (lit == "commit" || lit == "tree" || lit == "blob" || lit == "tag" || lit == "object"):
- return CaretType{lit}, nil
- case re == "" && tok == cbrace:
- return CaretType{"tag"}, nil
- case re == "" && tok == emark && nextTok == emark:
- re += lit
- case re == "" && tok == emark && nextTok == minus:
- negate = true
- case re == "" && tok == emark:
- return nil, &ErrInvalidRevision{fmt.Sprintf(`revision suffix brace component sequences starting with "/!" others than those defined are reserved`)}
- case re == "" && tok == slash:
- p.unscan()
- case tok != slash && start:
- return nil, &ErrInvalidRevision{fmt.Sprintf(`"%s" is not a valid revision suffix brace component`, lit)}
- case tok != cbrace:
- p.unscan()
- re += lit
- case tok == cbrace:
- p.unscan()
-
- reg, err := regexp.Compile(re)
-
- if err != nil {
- return CaretReg{}, &ErrInvalidRevision{fmt.Sprintf(`revision suffix brace component, %s`, err.Error())}
- }
-
- return CaretReg{reg, negate}, nil
- }
-
- start = false
- }
- }
-
- // parseColon extract : statements
- func (p *Parser) parseColon() (Revisioner, error) {
- var tok token
- var err error
-
- tok, _, err = p.scan()
-
- if err != nil {
- return nil, err
- }
-
- switch tok {
- case slash:
- return p.parseColonSlash()
- default:
- p.unscan()
- return p.parseColonDefault()
- }
- }
-
- // parseColonSlash extract :/<data> statements
- func (p *Parser) parseColonSlash() (Revisioner, error) {
- var tok, nextTok token
- var lit string
- var re string
- var negate bool
- var err error
-
- for {
- tok, lit, err = p.scan()
-
- if err != nil {
- return nil, err
- }
-
- nextTok, _, err = p.scan()
-
- if err != nil {
- return nil, err
- }
-
- switch {
- case tok == emark && nextTok == emark:
- re += lit
- case re == "" && tok == emark && nextTok == minus:
- negate = true
- case re == "" && tok == emark:
- return nil, &ErrInvalidRevision{fmt.Sprintf(`revision suffix brace component sequences starting with "/!" others than those defined are reserved`)}
- case tok == eof:
- p.unscan()
- reg, err := regexp.Compile(re)
-
- if err != nil {
- return ColonReg{}, &ErrInvalidRevision{fmt.Sprintf(`revision suffix brace component, %s`, err.Error())}
- }
-
- return ColonReg{reg, negate}, nil
- default:
- p.unscan()
- re += lit
- }
- }
- }
-
- // parseColonDefault extract :<data> statements
- func (p *Parser) parseColonDefault() (Revisioner, error) {
- var tok token
- var lit string
- var path string
- var stage int
- var err error
- var n = -1
-
- tok, lit, err = p.scan()
-
- if err != nil {
- return nil, err
- }
-
- nextTok, _, err := p.scan()
-
- if err != nil {
- return nil, err
- }
-
- if tok == number && nextTok == colon {
- n, _ = strconv.Atoi(lit)
- }
-
- switch n {
- case 0, 1, 2, 3:
- stage = n
- default:
- path += lit
- p.unscan()
- }
-
- for {
- tok, lit, err = p.scan()
-
- if err != nil {
- return nil, err
- }
-
- switch {
- case tok == eof && n == -1:
- return ColonPath{path}, nil
- case tok == eof:
- return ColonStagePath{path, stage}, nil
- default:
- path += lit
- }
- }
- }
-
- // parseRef extract reference name
- func (p *Parser) parseRef() (Revisioner, error) {
- var tok, prevTok token
- var lit, buf string
- var endOfRef bool
- var err error
-
- for {
- tok, lit, err = p.scan()
-
- if err != nil {
- return nil, err
- }
-
- switch tok {
- case eof, at, colon, tilde, caret:
- endOfRef = true
- }
-
- err := p.checkRefFormat(tok, lit, prevTok, buf, endOfRef)
-
- if err != nil {
- return "", err
- }
-
- if endOfRef {
- p.unscan()
- return Ref(buf), nil
- }
-
- buf += lit
- prevTok = tok
- }
- }
-
- // checkRefFormat ensure reference name follow rules defined here :
- // https://git-scm.com/docs/git-check-ref-format
- func (p *Parser) checkRefFormat(token token, literal string, previousToken token, buffer string, endOfRef bool) error {
- switch token {
- case aslash, space, control, qmark, asterisk, obracket:
- return &ErrInvalidRevision{fmt.Sprintf(`must not contains "%s"`, literal)}
- }
-
- switch {
- case (token == dot || token == slash) && buffer == "":
- return &ErrInvalidRevision{fmt.Sprintf(`must not start with "%s"`, literal)}
- case previousToken == slash && endOfRef:
- return &ErrInvalidRevision{`must not end with "/"`}
- case previousToken == dot && endOfRef:
- return &ErrInvalidRevision{`must not end with "."`}
- case token == dot && previousToken == slash:
- return &ErrInvalidRevision{`must not contains "/."`}
- case previousToken == dot && token == dot:
- return &ErrInvalidRevision{`must not contains ".."`}
- case previousToken == slash && token == slash:
- return &ErrInvalidRevision{`must not contains consecutively "/"`}
- case (token == slash || endOfRef) && len(buffer) > 4 && buffer[len(buffer)-5:] == ".lock":
- return &ErrInvalidRevision{"cannot end with .lock"}
- }
-
- return nil
- }
|