diff --git a/cmd/gitql/main.go b/cmd/gitql/main.go index b0a0c0deb..c1028ebf8 100644 --- a/cmd/gitql/main.go +++ b/cmd/gitql/main.go @@ -10,6 +10,7 @@ import ( func main() { parser := flags.NewNamedParser("gitql", flags.Default) parser.AddCommand("query", "Execute a SQL query a repository.", "", &CmdQuery{}) + parser.AddCommand("shell", "Start an interactive session.", "", &CmdShell{}) parser.AddCommand("version", "Show the version information.", "", &CmdVersion{}) _, err := parser.Parse() diff --git a/cmd/gitql/query.go b/cmd/gitql/query.go index a55e74a2d..d3dc9d343 100644 --- a/cmd/gitql/query.go +++ b/cmd/gitql/query.go @@ -1,31 +1,12 @@ package main -import ( - "errors" - "fmt" - "io" - "os" - "path/filepath" - - "github.com/gitql/gitql" - gitqlgit "github.com/gitql/gitql/git" - "github.com/gitql/gitql/internal/format" - "github.com/gitql/gitql/sql" - - "gopkg.in/src-d/go-git.v4" -) - type CmdQuery struct { - cmd + cmdQueryBase - Path string `short:"p" long:"path" description:"Path where the git repository is located"` Format string `short:"f" long:"format" default:"pretty" description:"Ouptut format. Formats supported: pretty, csv, json."` Args struct { SQL string `positional-arg-name:"sql" required:"true" description:"SQL query to execute"` } `positional-args:"yes"` - - r *git.Repository - db sql.Database } func (c *CmdQuery) Execute(args []string) error { @@ -37,119 +18,10 @@ func (c *CmdQuery) Execute(args []string) error { return err } - return c.executeQuery() -} - -func (c *CmdQuery) validate() error { - var err error - c.Path, err = findDotGitFolder(c.Path) - return err -} - -func (c *CmdQuery) buildDatabase() error { - c.print("opening %q repository...\n", c.Path) - - var err error - c.r, err = git.NewFilesystemRepository(c.Path) - if err != nil { - return err - } - - empty, err := c.r.IsEmpty() - if err != nil { - return err - } - - if empty { - return errors.New("error: the repository is empty") - } - - head, err := c.r.Head() - if err != nil { - return err - } - - c.print("current HEAD %q\n", head.Hash()) - - name := filepath.Base(filepath.Join(c.Path, "..")) - c.db = gitqlgit.NewDatabase(name, c.r) - return nil -} - -func (c *CmdQuery) executeQuery() error { - c.print("executing %q at %q\n", c.Args.SQL, c.db.Name()) - - fmt.Println(c.Args.SQL) - e := gitql.New() - e.AddDatabase(c.db) - schema, iter, err := e.Query(c.Args.SQL) - if err != nil { - return err - } - - return c.printQuery(schema, iter) -} - -func (c *CmdQuery) printQuery(schema sql.Schema, iter sql.RowIter) error { - f, err := format.NewFormat(c.Format, os.Stdout) + schema, rowIter, err := c.executeQuery(c.Args.SQL) if err != nil { return err } - headers := []string{} - for _, f := range schema { - headers = append(headers, f.Name) - } - - if err := f.WriteHeader(headers); err != nil { - return err - } - - for { - row, err := iter.Next() - if err == io.EOF { - break - } - if err != nil { - return err - } - - record := make([]interface{}, len(row)) - for i := 0; i < len(row); i++ { - record[i] = row[i] - } - - if err := f.Write(record); err != nil { - return err - } - } - - return f.Close() -} - -func findDotGitFolder(path string) (string, error) { - if path == "" { - var err error - path, err = os.Getwd() - if err != nil { - return "", err - } - } - - git := filepath.Join(path, ".git") - _, err := os.Stat(git) - if err == nil { - return git, nil - } - - if !os.IsNotExist(err) { - return "", err - } - - next := filepath.Join(path, "..") - if next == path { - return "", errors.New("unable to find a git repository") - } - - return findDotGitFolder(next) + return c.printQuery(schema, rowIter, c.Format) } diff --git a/cmd/gitql/query_base.go b/cmd/gitql/query_base.go new file mode 100644 index 000000000..92a5053fb --- /dev/null +++ b/cmd/gitql/query_base.go @@ -0,0 +1,134 @@ +package main + +import ( + "errors" + "io" + "os" + "path/filepath" + + "github.com/gitql/gitql" + gitqlgit "github.com/gitql/gitql/git" + "github.com/gitql/gitql/internal/format" + "github.com/gitql/gitql/sql" + + "gopkg.in/src-d/go-git.v4" +) + +type cmdQueryBase struct { + cmd + + Path string `short:"p" long:"path" description:"Path where the git repository is located"` + + db sql.Database + e *gitql.Engine +} + +func (c *cmdQueryBase) validate() error { + var err error + c.Path, err = findDotGitFolder(c.Path) + return err +} + +func (c *cmdQueryBase) buildDatabase() error { + c.print("opening %q repository...\n", c.Path) + + var err error + r, err := git.NewFilesystemRepository(c.Path) + if err != nil { + return err + } + + empty, err := r.IsEmpty() + if err != nil { + return err + } + + if empty { + return errors.New("error: the repository is empty") + } + + head, err := r.Head() + if err != nil { + return err + } + + c.print("current HEAD %q\n", head.Hash()) + + name := filepath.Base(filepath.Join(c.Path, "..")) + + c.db = gitqlgit.NewDatabase(name, r) + c.e = gitql.New() + c.e.AddDatabase(c.db) + + return nil +} + +func (c *cmdQueryBase) executeQuery(sql string) (sql.Schema, sql.RowIter, error) { + c.print("executing %q at %q\n", sql, c.db.Name()) + + return c.e.Query(sql) +} + +func (c *cmdQueryBase) printQuery(schema sql.Schema, iter sql.RowIter, formatId string) error { + f, err := format.NewFormat(formatId, os.Stdout) + if err != nil { + return err + } + + headers := []string{} + for _, f := range schema { + headers = append(headers, f.Name) + } + + if err := f.WriteHeader(headers); err != nil { + return err + } + + for { + row, err := iter.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + dataRow := make([]interface{}, len(row)) + for i := range row { + dataRow[i] = interface{}(row[i]) + } + + if err := f.Write(dataRow); err != nil { + return err + } + } + + return f.Close() +} + +func findDotGitFolder(path string) (string, error) { + if path == "" { + var err error + path, err = os.Getwd() + if err != nil { + return "", err + } + } + + git := filepath.Join(path, ".git") + _, err := os.Stat(git) + if err == nil { + return git, nil + } + + if !os.IsNotExist(err) { + return "", err + } + + next := filepath.Join(path, "..") + if next == path { + return "", errors.New("unable to find a git repository") + } + + return findDotGitFolder(next) +} diff --git a/cmd/gitql/shell.go b/cmd/gitql/shell.go new file mode 100644 index 000000000..8a1fbf8e8 --- /dev/null +++ b/cmd/gitql/shell.go @@ -0,0 +1,89 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "os" + "strings" +) + +type CmdShell struct { + cmdQueryBase +} + +func (c *CmdShell) Execute(args []string) error { + if err := c.validate(); err != nil { + return err + } + + if err := c.buildDatabase(); err != nil { + return err + } + + fmt.Print(` + gitQL SHELL + ----------- +You must end your queries with ';' + +`) + + s := bufio.NewScanner(os.Stdin) + + s.Split(scanQueries) + + for { + fmt.Print("!> ") + + if !s.Scan() { + break + } + + query := s.Text() + + query = strings.Replace(query, "\n", " ", -1) + query = strings.TrimSpace(query) + + fmt.Printf("\n--> Executing query: %s\n\n", query) + + schema, rowIter, err := c.executeQuery(query) + if err != nil { + c.printError(err) + continue + } + + if err := c.printQuery(schema, rowIter, "pretty"); err != nil { + c.printError(err) + } + } + + return s.Err() +} + +func (c *CmdShell) printError(err error) { + fmt.Printf("ERROR: %v\n\n", err) +} + +func scanQueries(data []byte, atEOF bool) (int, []byte, error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := bytes.IndexByte(data, ';'); i >= 0 { + // We have a full newline-terminated line. + return i + 1, dropCR(data[0:i]), nil + } + // If we're at EOF, we have a final, non-terminated line. Return it. + if atEOF { + return len(data), dropCR(data), nil + } + // Request more data. + return 0, nil, nil +} + +// dropCR drops a terminal \r from the data. +func dropCR(data []byte) []byte { + if len(data) > 0 && data[len(data)-1] == '\r' { + return data[0 : len(data)-1] + } + return data +}