diff options
-rw-r--r-- | internal/revision/parser.go | 622 | ||||
-rw-r--r-- | internal/revision/parser_test.go | 395 | ||||
-rw-r--r-- | internal/revision/scanner.go | 117 | ||||
-rw-r--r-- | internal/revision/scanner_test.go | 194 | ||||
-rw-r--r-- | internal/revision/token.go | 28 | ||||
-rw-r--r-- | plumbing/revision.go | 11 | ||||
-rw-r--r-- | repository.go | 127 | ||||
-rw-r--r-- | repository_test.go | 52 |
8 files changed, 1546 insertions, 0 deletions
diff --git a/internal/revision/parser.go b/internal/revision/parser.go new file mode 100644 index 0000000..b45a6d8 --- /dev/null +++ b/internal/revision/parser.go @@ -0,0 +1,622 @@ +// 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, lit, 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 +} diff --git a/internal/revision/parser_test.go b/internal/revision/parser_test.go new file mode 100644 index 0000000..a58f0ad --- /dev/null +++ b/internal/revision/parser_test.go @@ -0,0 +1,395 @@ +package revision + +import ( + "bytes" + "regexp" + "time" + + . "gopkg.in/check.v1" +) + +type ParserSuite struct{} + +var _ = Suite(&ParserSuite{}) + +func (s *ParserSuite) TestErrInvalidRevision(c *C) { + e := ErrInvalidRevision{"test"} + + c.Assert(e.Error(), Equals, "Revision invalid : test") +} + +func (s *ParserSuite) TestNewParserFromString(c *C) { + p := NewParserFromString("test") + + c.Assert(p, FitsTypeOf, &Parser{}) +} + +func (s *ParserSuite) TestScan(c *C) { + parser := NewParser(bytes.NewBufferString("Hello world !")) + + expected := []struct { + t token + s string + }{ + { + word, + "Hello", + }, + { + space, + " ", + }, + { + word, + "world", + }, + { + space, + " ", + }, + { + emark, + "!", + }, + } + + for i := 0; ; { + tok, str, err := parser.scan() + + if tok == eof { + return + } + + c.Assert(err, Equals, nil) + c.Assert(str, Equals, expected[i].s) + c.Assert(tok, Equals, expected[i].t) + + i++ + } +} + +func (s *ParserSuite) TestUnscan(c *C) { + parser := NewParser(bytes.NewBufferString("Hello world !")) + + tok, str, err := parser.scan() + + c.Assert(err, Equals, nil) + c.Assert(str, Equals, "Hello") + c.Assert(tok, Equals, word) + + parser.unscan() + + tok, str, err = parser.scan() + + c.Assert(err, Equals, nil) + c.Assert(str, Equals, "Hello") + c.Assert(tok, Equals, word) +} + +func (s *ParserSuite) TestParseWithValidExpression(c *C) { + tim, _ := time.Parse("2006-01-02T15:04:05Z", "2016-12-16T21:42:47Z") + + datas := map[string]Revisioner{ + "@": []Revisioner{Ref("HEAD")}, + "@~3": []Revisioner{ + Ref("HEAD"), + TildePath{3}, + }, + "@{2016-12-16T21:42:47Z}": []Revisioner{AtDate{tim}}, + "@{1}": []Revisioner{AtReflog{1}}, + "@{-1}": []Revisioner{AtCheckout{1}}, + "master@{upstream}": []Revisioner{ + Ref("master"), + AtUpstream{}, + }, + "@{upstream}": []Revisioner{ + AtUpstream{}, + }, + "@{u}": []Revisioner{ + AtUpstream{}, + }, + "master@{push}": []Revisioner{ + Ref("master"), + AtPush{}, + }, + "master@{2016-12-16T21:42:47Z}": []Revisioner{ + Ref("master"), + AtDate{tim}, + }, + "HEAD^": []Revisioner{ + Ref("HEAD"), + CaretPath{1}, + }, + "master~3": []Revisioner{ + Ref("master"), + TildePath{3}, + }, + "v0.99.8^{commit}": []Revisioner{ + Ref("v0.99.8"), + CaretType{"commit"}, + }, + "v0.99.8^{}": []Revisioner{ + Ref("v0.99.8"), + CaretType{"tag"}, + }, + "HEAD^{/fix nasty bug}": []Revisioner{ + Ref("HEAD"), + CaretReg{regexp.MustCompile("fix nasty bug"), false}, + }, + ":/fix nasty bug": []Revisioner{ + ColonReg{regexp.MustCompile("fix nasty bug"), false}, + }, + "HEAD:README": []Revisioner{ + Ref("HEAD"), + ColonPath{"README"}, + }, + ":README": []Revisioner{ + ColonPath{"README"}, + }, + "master:./README": []Revisioner{ + Ref("master"), + ColonPath{"./README"}, + }, + "master^1~:./README": []Revisioner{ + Ref("master"), + CaretPath{1}, + TildePath{1}, + ColonPath{"./README"}, + }, + ":0:README": []Revisioner{ + ColonStagePath{"README", 0}, + }, + ":3:README": []Revisioner{ + ColonStagePath{"README", 3}, + }, + "master~1^{/update}~5~^^1": []Revisioner{ + Ref("master"), + TildePath{1}, + CaretReg{regexp.MustCompile("update"), false}, + TildePath{5}, + TildePath{1}, + CaretPath{1}, + CaretPath{1}, + }, + } + + for d, expected := range datas { + parser := NewParser(bytes.NewBufferString(d)) + + result, err := parser.Parse() + + c.Assert(err, Equals, nil) + c.Assert(result, DeepEquals, expected) + } +} + +func (s *ParserSuite) TestParseWithUnValidExpression(c *C) { + datas := map[string]error{ + "..": &ErrInvalidRevision{`must not start with "."`}, + "master^1master": &ErrInvalidRevision{`reference must be defined once at the beginning`}, + "master^1@{2016-12-16T21:42:47Z}": &ErrInvalidRevision{`"@" statement is not valid, could be : <refname>@{<ISO-8601 date>}, @{<ISO-8601 date>}`}, + "master^1@{1}": &ErrInvalidRevision{`"@" statement is not valid, could be : <refname>@{<n>}, @{<n>}`}, + "master@{-1}": &ErrInvalidRevision{`"@" statement is not valid, could be : @{-<n>}`}, + "master^1@{upstream}": &ErrInvalidRevision{`"@" statement is not valid, could be : <refname>@{upstream}, @{upstream}, <refname>@{u}, @{u}`}, + "master^1@{u}": &ErrInvalidRevision{`"@" statement is not valid, could be : <refname>@{upstream}, @{upstream}, <refname>@{u}, @{u}`}, + "master^1@{push}": &ErrInvalidRevision{`"@" statement is not valid, could be : <refname>@{push}, @{push}`}, + "^1": &ErrInvalidRevision{`"~" or "^" statement must have a reference defined at the beginning`}, + "^{/test}": &ErrInvalidRevision{`"~" or "^" statement must have a reference defined at the beginning`}, + "~1": &ErrInvalidRevision{`"~" or "^" statement must have a reference defined at the beginning`}, + "master:/test": &ErrInvalidRevision{`":" statement is not valid, could be : :/<regexp>`}, + "master:0:README": &ErrInvalidRevision{`":" statement is not valid, could be : :<n>:<path>`}, + } + + for s, e := range datas { + parser := NewParser(bytes.NewBufferString(s)) + _, err := parser.Parse() + c.Assert(err, DeepEquals, e) + } +} + +func (s *ParserSuite) TestParseAtWithValidExpression(c *C) { + tim, _ := time.Parse("2006-01-02T15:04:05Z", "2016-12-16T21:42:47Z") + + datas := map[string]Revisioner{ + "": Ref("HEAD"), + "{1}": AtReflog{1}, + "{-1}": AtCheckout{1}, + "{push}": AtPush{}, + "{upstream}": AtUpstream{}, + "{u}": AtUpstream{}, + "{2016-12-16T21:42:47Z}": AtDate{tim}, + } + + for d, expected := range datas { + parser := NewParser(bytes.NewBufferString(d)) + + result, err := parser.parseAt() + + c.Assert(err, Equals, nil) + c.Assert(result, DeepEquals, expected) + } +} + +func (s *ParserSuite) TestParseAtWithUnValidExpression(c *C) { + datas := map[string]error{ + "{test}": &ErrInvalidRevision{`wrong date "test" must fit ISO-8601 format : 2006-01-02T15:04:05Z`}, + "{-1": &ErrInvalidRevision{`missing "}" in @{-n} structure`}, + } + + for s, e := range datas { + parser := NewParser(bytes.NewBufferString(s)) + + _, err := parser.parseAt() + + c.Assert(err, DeepEquals, e) + } +} + +func (s *ParserSuite) TestParseCaretWithValidExpression(c *C) { + datas := map[string]Revisioner{ + "": CaretPath{1}, + "2": CaretPath{2}, + "{}": CaretType{"tag"}, + "{commit}": CaretType{"commit"}, + "{tree}": CaretType{"tree"}, + "{blob}": CaretType{"blob"}, + "{tag}": CaretType{"tag"}, + "{object}": CaretType{"object"}, + "{/hello world !}": CaretReg{regexp.MustCompile("hello world !"), false}, + "{/!-hello world !}": CaretReg{regexp.MustCompile("hello world !"), true}, + "{/!! hello world !}": CaretReg{regexp.MustCompile("! hello world !"), false}, + } + + for d, expected := range datas { + parser := NewParser(bytes.NewBufferString(d)) + + result, err := parser.parseCaret() + + c.Assert(err, Equals, nil) + c.Assert(result, DeepEquals, expected) + } +} + +func (s *ParserSuite) TestParseCaretWithUnValidExpression(c *C) { + datas := map[string]error{ + "3": &ErrInvalidRevision{`"3" found must be 0, 1 or 2 after "^"`}, + "{test}": &ErrInvalidRevision{`"test" is not a valid revision suffix brace component`}, + "{/!test}": &ErrInvalidRevision{`revision suffix brace component sequences starting with "/!" others than those defined are reserved`}, + "{/test**}": &ErrInvalidRevision{"revision suffix brace component, error parsing regexp: invalid nested repetition operator: `**`"}, + } + + for s, e := range datas { + parser := NewParser(bytes.NewBufferString(s)) + + _, err := parser.parseCaret() + + c.Assert(err, DeepEquals, e) + } +} + +func (s *ParserSuite) TestParseTildeWithValidExpression(c *C) { + datas := map[string]Revisioner{ + "3": TildePath{3}, + "1": TildePath{1}, + "": TildePath{1}, + } + + for d, expected := range datas { + parser := NewParser(bytes.NewBufferString(d)) + + result, err := parser.parseTilde() + + c.Assert(err, Equals, nil) + c.Assert(result, DeepEquals, expected) + } +} + +func (s *ParserSuite) TestParseColonWithValidExpression(c *C) { + datas := map[string]Revisioner{ + "/hello world !": ColonReg{regexp.MustCompile("hello world !"), false}, + "/!-hello world !": ColonReg{regexp.MustCompile("hello world !"), true}, + "/!! hello world !": ColonReg{regexp.MustCompile("! hello world !"), false}, + "../parser.go": ColonPath{"../parser.go"}, + "./parser.go": ColonPath{"./parser.go"}, + "parser.go": ColonPath{"parser.go"}, + "0:parser.go": ColonStagePath{"parser.go", 0}, + "1:parser.go": ColonStagePath{"parser.go", 1}, + "2:parser.go": ColonStagePath{"parser.go", 2}, + "3:parser.go": ColonStagePath{"parser.go", 3}, + } + + for d, expected := range datas { + parser := NewParser(bytes.NewBufferString(d)) + + result, err := parser.parseColon() + + c.Assert(err, Equals, nil) + c.Assert(result, DeepEquals, expected) + } +} + +func (s *ParserSuite) TestParseColonWithUnValidExpression(c *C) { + datas := map[string]error{ + "/!test": &ErrInvalidRevision{`revision suffix brace component sequences starting with "/!" others than those defined are reserved`}, + "/*": &ErrInvalidRevision{"revision suffix brace component, error parsing regexp: missing argument to repetition operator: `*`"}, + } + + for s, e := range datas { + parser := NewParser(bytes.NewBufferString(s)) + + _, err := parser.parseColon() + + c.Assert(err, DeepEquals, e) + } +} + +func (s *ParserSuite) TestParseRefWithValidName(c *C) { + datas := []string{ + "lock", + "master", + "v1.0.0", + "refs/stash", + "refs/tags/v1.0.0", + "refs/heads/master", + "refs/remotes/test", + "refs/remotes/origin/HEAD", + "refs/remotes/origin/master", + } + + for _, d := range datas { + parser := NewParser(bytes.NewBufferString(d)) + + result, err := parser.parseRef() + + c.Assert(err, Equals, nil) + c.Assert(result, Equals, Ref(d)) + } +} + +func (s *ParserSuite) TestParseRefWithUnvalidName(c *C) { + datas := map[string]error{ + ".master": &ErrInvalidRevision{`must not start with "."`}, + "/master": &ErrInvalidRevision{`must not start with "/"`}, + "master/": &ErrInvalidRevision{`must not end with "/"`}, + "master.": &ErrInvalidRevision{`must not end with "."`}, + "refs/remotes/.origin/HEAD": &ErrInvalidRevision{`must not contains "/."`}, + "test..test": &ErrInvalidRevision{`must not contains ".."`}, + "test..": &ErrInvalidRevision{`must not contains ".."`}, + "test test": &ErrInvalidRevision{`must not contains " "`}, + "test*test": &ErrInvalidRevision{`must not contains "*"`}, + "test?test": &ErrInvalidRevision{`must not contains "?"`}, + "test\\test": &ErrInvalidRevision{`must not contains "\"`}, + "test[test": &ErrInvalidRevision{`must not contains "["`}, + "te//st": &ErrInvalidRevision{`must not contains consecutively "/"`}, + "refs/remotes/test.lock/HEAD": &ErrInvalidRevision{`cannot end with .lock`}, + "test.lock": &ErrInvalidRevision{`cannot end with .lock`}, + } + + for s, e := range datas { + parser := NewParser(bytes.NewBufferString(s)) + + _, err := parser.parseRef() + + c.Assert(err, DeepEquals, e) + } +} diff --git a/internal/revision/scanner.go b/internal/revision/scanner.go new file mode 100644 index 0000000..fb5f333 --- /dev/null +++ b/internal/revision/scanner.go @@ -0,0 +1,117 @@ +package revision + +import ( + "bufio" + "io" + "unicode" +) + +// runeCategoryValidator takes a rune as input and +// validates it belongs to a rune category +type runeCategoryValidator func(r rune) bool + +// tokenizeExpression aggegates a series of runes matching check predicate into a single +// string and provides given tokenType as token type +func tokenizeExpression(ch rune, tokenType token, check runeCategoryValidator, r *bufio.Reader) (token, string, error) { + var data []rune + data = append(data, ch) + + for { + c, _, err := r.ReadRune() + + if c == zeroRune { + break + } + + if err != nil { + return tokenError, "", err + } + + if check(c) { + data = append(data, c) + } else { + err := r.UnreadRune() + + if err != nil { + return tokenError, "", err + } + + return tokenType, string(data), nil + } + } + + return tokenType, string(data), nil +} + +var zeroRune = rune(0) + +// scanner represents a lexical scanner. +type scanner struct { + r *bufio.Reader +} + +// newScanner returns a new instance of scanner. +func newScanner(r io.Reader) *scanner { + return &scanner{r: bufio.NewReader(r)} +} + +// Scan extracts tokens and their strings counterpart +// from the reader +func (s *scanner) scan() (token, string, error) { + ch, _, err := s.r.ReadRune() + + if err != nil && err != io.EOF { + return tokenError, "", err + } + + switch ch { + case zeroRune: + return eof, "", nil + case ':': + return colon, string(ch), nil + case '~': + return tilde, string(ch), nil + case '^': + return caret, string(ch), nil + case '.': + return dot, string(ch), nil + case '/': + return slash, string(ch), nil + case '{': + return obrace, string(ch), nil + case '}': + return cbrace, string(ch), nil + case '-': + return minus, string(ch), nil + case '@': + return at, string(ch), nil + case '\\': + return aslash, string(ch), nil + case '?': + return qmark, string(ch), nil + case '*': + return asterisk, string(ch), nil + case '[': + return obracket, string(ch), nil + case '!': + return emark, string(ch), nil + } + + if unicode.IsSpace(ch) { + return space, string(ch), nil + } + + if unicode.IsControl(ch) { + return control, string(ch), nil + } + + if unicode.IsLetter(ch) { + return tokenizeExpression(ch, word, unicode.IsLetter, s.r) + } + + if unicode.IsNumber(ch) { + return tokenizeExpression(ch, number, unicode.IsNumber, s.r) + } + + return tokenError, string(ch), nil +} diff --git a/internal/revision/scanner_test.go b/internal/revision/scanner_test.go new file mode 100644 index 0000000..d27ccb1 --- /dev/null +++ b/internal/revision/scanner_test.go @@ -0,0 +1,194 @@ +package revision + +import ( + "bytes" + "testing" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type ScannerSuite struct{} + +var _ = Suite(&ScannerSuite{}) + +func (s *ScannerSuite) TestReadColon(c *C) { + scanner := newScanner(bytes.NewBufferString(":")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, ":") + c.Assert(tok, Equals, colon) +} + +func (s *ScannerSuite) TestReadTilde(c *C) { + scanner := newScanner(bytes.NewBufferString("~")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, "~") + c.Assert(tok, Equals, tilde) +} + +func (s *ScannerSuite) TestReadCaret(c *C) { + scanner := newScanner(bytes.NewBufferString("^")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, "^") + c.Assert(tok, Equals, caret) +} + +func (s *ScannerSuite) TestReadDot(c *C) { + scanner := newScanner(bytes.NewBufferString(".")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, ".") + c.Assert(tok, Equals, dot) +} + +func (s *ScannerSuite) TestReadSlash(c *C) { + scanner := newScanner(bytes.NewBufferString("/")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, "/") + c.Assert(tok, Equals, slash) +} + +func (s *ScannerSuite) TestReadEOF(c *C) { + scanner := newScanner(bytes.NewBufferString(string(rune(0)))) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, "") + c.Assert(tok, Equals, eof) +} + +func (s *ScannerSuite) TestReadNumber(c *C) { + scanner := newScanner(bytes.NewBufferString("1234")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, "1234") + c.Assert(tok, Equals, number) +} + +func (s *ScannerSuite) TestReadSpace(c *C) { + scanner := newScanner(bytes.NewBufferString(" ")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, " ") + c.Assert(tok, Equals, space) +} + +func (s *ScannerSuite) TestReadControl(c *C) { + scanner := newScanner(bytes.NewBufferString("")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, "\x01") + c.Assert(tok, Equals, control) +} + +func (s *ScannerSuite) TestReadOpenBrace(c *C) { + scanner := newScanner(bytes.NewBufferString("{")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, "{") + c.Assert(tok, Equals, obrace) +} + +func (s *ScannerSuite) TestReadCloseBrace(c *C) { + scanner := newScanner(bytes.NewBufferString("}")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, "}") + c.Assert(tok, Equals, cbrace) +} + +func (s *ScannerSuite) TestReadMinus(c *C) { + scanner := newScanner(bytes.NewBufferString("-")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, "-") + c.Assert(tok, Equals, minus) +} + +func (s *ScannerSuite) TestReadAt(c *C) { + scanner := newScanner(bytes.NewBufferString("@")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, "@") + c.Assert(tok, Equals, at) +} + +func (s *ScannerSuite) TestReadAntislash(c *C) { + scanner := newScanner(bytes.NewBufferString("\\")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, "\\") + c.Assert(tok, Equals, aslash) +} + +func (s *ScannerSuite) TestReadQuestionMark(c *C) { + scanner := newScanner(bytes.NewBufferString("?")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, "?") + c.Assert(tok, Equals, qmark) +} + +func (s *ScannerSuite) TestReadAsterisk(c *C) { + scanner := newScanner(bytes.NewBufferString("*")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, "*") + c.Assert(tok, Equals, asterisk) +} + +func (s *ScannerSuite) TestReadOpenBracket(c *C) { + scanner := newScanner(bytes.NewBufferString("[")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, "[") + c.Assert(tok, Equals, obracket) +} + +func (s *ScannerSuite) TestReadExclamationMark(c *C) { + scanner := newScanner(bytes.NewBufferString("!")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, "!") + c.Assert(tok, Equals, emark) +} + +func (s *ScannerSuite) TestReadWord(c *C) { + scanner := newScanner(bytes.NewBufferString("abcde")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, "abcde") + c.Assert(tok, Equals, word) +} + +func (s *ScannerSuite) TestReadTokenError(c *C) { + scanner := newScanner(bytes.NewBufferString("`")) + tok, data, err := scanner.scan() + + c.Assert(err, Equals, nil) + c.Assert(data, Equals, "`") + c.Assert(tok, Equals, tokenError) +} diff --git a/internal/revision/token.go b/internal/revision/token.go new file mode 100644 index 0000000..abc4048 --- /dev/null +++ b/internal/revision/token.go @@ -0,0 +1,28 @@ +package revision + +// token represents a entity extracted from string parsing +type token int + +const ( + eof token = iota + + aslash + asterisk + at + caret + cbrace + colon + control + dot + emark + minus + number + obrace + obracket + qmark + slash + space + tilde + tokenError + word +) diff --git a/plumbing/revision.go b/plumbing/revision.go new file mode 100644 index 0000000..5f053b2 --- /dev/null +++ b/plumbing/revision.go @@ -0,0 +1,11 @@ +package plumbing + +// Revision represents a git revision +// to get more details about git revisions +// please check git manual page : +// https://www.kernel.org/pub/software/scm/git/docs/gitrevisions.html +type Revision string + +func (r Revision) String() string { + return string(r) +} diff --git a/repository.go b/repository.go index 4a65f46..b11e8b6 100644 --- a/repository.go +++ b/repository.go @@ -6,6 +6,7 @@ import ( "os" "srcd.works/go-git.v4/config" + "srcd.works/go-git.v4/internal/revision" "srcd.works/go-git.v4/plumbing" "srcd.works/go-git.v4/plumbing/object" "srcd.works/go-git.v4/plumbing/storer" @@ -632,3 +633,129 @@ func (r *Repository) Worktree() (*Worktree, error) { return &Worktree{r: r, fs: r.wt}, nil } + +// ResolveRevision resolves revision to corresponding hash +func (r *Repository) ResolveRevision(rev plumbing.Revision) (*plumbing.Hash, error) { + p := revision.NewParserFromString(string(rev)) + + items, err := p.Parse() + + if err != nil { + return nil, err + } + + var commit *object.Commit + + for _, item := range items { + switch item.(type) { + case revision.Ref: + ref, err := storer.ResolveReference(r.s, plumbing.ReferenceName(item.(revision.Ref))) + + if err != nil { + return &plumbing.ZeroHash, err + } + + h := ref.Hash() + + commit, err = r.Commit(h) + + if err != nil { + return &plumbing.ZeroHash, err + } + case revision.CaretPath: + depth := item.(revision.CaretPath).Depth + + if depth == 0 { + break + } + + iter := commit.Parents() + + c, err := iter.Next() + + if err != nil { + return &plumbing.ZeroHash, err + } + + if depth == 1 { + commit = c + + break + } + + c, err = iter.Next() + + if err != nil { + return &plumbing.ZeroHash, err + } + + commit = c + case revision.TildePath: + for i := 0; i < item.(revision.TildePath).Depth; i++ { + c, err := commit.Parents().Next() + + if err != nil { + return &plumbing.ZeroHash, err + } + + commit = c + } + case revision.CaretReg: + history, err := commit.History() + + if err != nil { + return &plumbing.ZeroHash, err + } + + re := item.(revision.CaretReg).Regexp + negate := item.(revision.CaretReg).Negate + + var c *object.Commit + + for i := 0; i < len(history); i++ { + if !negate && re.MatchString(history[i].Message) { + c = history[i] + + break + } + + if negate && !re.MatchString(history[i].Message) { + c = history[i] + + break + } + } + + if c == nil { + return &plumbing.ZeroHash, fmt.Errorf(`No commit message match regexp : "%s"`, re.String()) + } + + commit = c + case revision.AtDate: + history, err := commit.History() + + if err != nil { + return &plumbing.ZeroHash, err + } + + date := item.(revision.AtDate).Date + var c *object.Commit + + for i := 0; i < len(history); i++ { + if date.Equal(history[i].Committer.When.UTC()) || history[i].Committer.When.UTC().Before(date) { + c = history[i] + + break + } + } + + if c == nil { + return &plumbing.ZeroHash, fmt.Errorf(`No commit exists prior to date "%s"`, date.String()) + } + + commit = c + } + } + + return &commit.Hash, nil +} diff --git a/repository_test.go b/repository_test.go index 76ba9f3..2d29cd3 100644 --- a/repository_test.go +++ b/repository_test.go @@ -780,6 +780,58 @@ func (s *RepositorySuite) TestWorktreeBare(c *C) { c.Assert(w, IsNil) } +func (s *RepositorySuite) TestResolveRevision(c *C) { + url := s.GetLocalRepositoryURL( + fixtures.ByURL("https://github.com/git-fixtures/basic.git").One(), + ) + + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(&CloneOptions{URL: url}) + c.Assert(err, IsNil) + + datas := map[string]string{ + "HEAD": "6ecf0ef2c2dffb796033e5a02219af86ec6584e5", + "refs/heads/master~2^^~": "b029517f6300c2da0f4b651b8642506cd6aaf45d", + "HEAD~2^^~": "b029517f6300c2da0f4b651b8642506cd6aaf45d", + "HEAD~3^2": "a5b8b09e2f8fcb0bb99d3ccb0958157b40890d69", + "HEAD~3^2^0": "a5b8b09e2f8fcb0bb99d3ccb0958157b40890d69", + "HEAD~2^{/binary file}": "35e85108805c84807bc66a02d91535e1e24b38b9", + "HEAD~^{!-some}": "1669dce138d9b841a518c64b10914d88f5e488ea", + "HEAD@{2015-03-31T11:56:18Z}": "918c48b83bd081e863dbe1b80f8998f058cd8294", + "HEAD@{2015-03-31T11:49:00Z}": "1669dce138d9b841a518c64b10914d88f5e488ea", + } + + for rev, hash := range datas { + h, err := r.ResolveRevision(plumbing.Revision(rev)) + + c.Assert(err, IsNil) + c.Assert(h.String(), Equals, hash) + } +} + +func (s *RepositorySuite) TestResolveRevisionWithErrors(c *C) { + url := s.GetLocalRepositoryURL( + fixtures.ByURL("https://github.com/git-fixtures/basic.git").One(), + ) + + r, _ := Init(memory.NewStorage(), nil) + err := r.clone(&CloneOptions{URL: url}) + c.Assert(err, IsNil) + + datas := map[string]string{ + "efs/heads/master~": "reference not found", + "HEAD^3": `Revision invalid : "3" found must be 0, 1 or 2 after "^"`, + "HEAD^{/whatever}": `No commit message match regexp : "whatever"`, + "HEAD@{2015-03-31T09:49:00Z}": `No commit exists prior to date "2015-03-31 09:49:00 +0000 UTC"`, + } + + for rev, rerr := range datas { + _, err := r.ResolveRevision(plumbing.Revision(rev)) + + c.Assert(err.Error(), Equals, rerr) + } +} + func ExecuteOnPath(c *C, path string, cmds ...string) error { for _, cmd := range cmds { err := executeOnPath(path, cmd) |