123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622 |
- // Package revision extracts git revision from string
- // More information 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
- }
|