aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--internal/revision/parser.go622
-rw-r--r--internal/revision/parser_test.go395
-rw-r--r--internal/revision/scanner.go117
-rw-r--r--internal/revision/scanner_test.go194
-rw-r--r--internal/revision/token.go28
-rw-r--r--plumbing/revision.go11
-rw-r--r--repository.go127
-rw-r--r--repository_test.go52
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)