Skip to content

Commit

Permalink
Merge pull request #161 from askgitdev/sourcegraph-search
Browse files Browse the repository at this point in the history
Sourcegraph search table `sourcegraph_search`
  • Loading branch information
patrickdevivo authored Aug 21, 2021
2 parents 8ffad44 + 34a5ccd commit a1a19fc
Show file tree
Hide file tree
Showing 12 changed files with 1,120 additions and 4 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,36 @@ SELECT github_stargazer_count('askgitdev', 'askgit', 'README.md');
SELECT github_stargazer_count('askgitdev/askgit', 'README.md'); -- both are equivalent
```

#### Sourcegraph API (`experimental`!)

You can use `askgit` to query the [Sourcegraph API](https://sourcegraph.com/api/console).

##### Authenticating

You must provide an authentication token in order to use the Sourcegraph API tables.
You can create a personal access token [following these instructions](https://docs.sourcegraph.com/cli/how-tos/creating_an_access_token).
`askgit` will look for a `SOURCEGRAPH_TOKEN` environment variable when executing, to use for authentication.
This is also true if running as a runtime loadable extension.

##### `sourcegraph_search`

Table-valued-function that returns results from a Sourcegraph search.

| Column | Type |
|----------------------|------|
| __typename | TEXT |
| results | TEXT |

`__typename` will be one of `Repository`, `CommitSearchResult`, or `FileMatch`.
`results` will be the JSON value of a search result (will match what's returned from the API)

Params:
1. `query` - a sourcegraph search query ([docs](https://docs.sourcegraph.com/))

```sql
SELECT sourcegraph_search('askgit');
```

### Example Queries

This will return all commits in the history of the currently checked out branch/commit of the repo.
Expand Down
9 changes: 5 additions & 4 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import (
"github.com/spf13/cobra"
)

var format string // output format flag
var presetQuery string // named / preset query flag
var repo string // path to repo on disk
var githubToken = os.Getenv("GITHUB_TOKEN") // GitHub auth token for GitHub tables
var format string // output format flag
var presetQuery string // named / preset query flag
var repo string // path to repo on disk
var githubToken = os.Getenv("GITHUB_TOKEN") // GitHub auth token for GitHub tables
var sourcegraphToken = os.Getenv("SOURCEGRAPH_TOKEN") // Sourcegraph auth token for Sourcegraph queries

func init() {
// local (root command only) flags
Expand Down
2 changes: 2 additions & 0 deletions cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ func registerExt() {
options.WithContextValue("githubToken", githubToken),
options.WithContextValue("githubPerPage", os.Getenv("GITHUB_PER_PAGE")),
options.WithContextValue("githubRateLimit", os.Getenv("GITHUB_RATE_LIMIT")),
options.WithSourcegraph(),
options.WithContextValue("sourcegraphToken", sourcegraphToken),
),
)
}
7 changes: 7 additions & 0 deletions extensions/extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/askgitdev/askgit/extensions/internal/github"
"github.com/askgitdev/askgit/extensions/internal/golang"
"github.com/askgitdev/askgit/extensions/internal/helpers"
"github.com/askgitdev/askgit/extensions/internal/sourcegraph"
"github.com/askgitdev/askgit/extensions/options"
"go.riyazali.net/sqlite"
)
Expand Down Expand Up @@ -39,6 +40,7 @@ func RegisterFn(fns ...options.OptionFn) func(ext *sqlite.ExtensionApi) (_ sqlit
if sqliteErr, err := golang.Register(ext, opt); err != nil {
return sqliteErr, err
}

}

// conditionally register the GitHub functionality
Expand All @@ -47,6 +49,11 @@ func RegisterFn(fns ...options.OptionFn) func(ext *sqlite.ExtensionApi) (_ sqlit
return sqliteErr, err
}
}
if opt.Sourcegraph{
if sqliteErr, err := sourcegraph.Register(ext, opt); err != nil {
return sqliteErr, err
}
}

return sqlite.SQLITE_OK, nil
}
Expand Down
645 changes: 645 additions & 0 deletions extensions/internal/sourcegraph/fixtures/TestSearch.yaml

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions extensions/internal/sourcegraph/sourcegraph.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package sourcegraph

import (
"context"

"github.com/askgitdev/askgit/extensions/options"
"github.com/pkg/errors"
"github.com/shurcooL/graphql"
"go.riyazali.net/sqlite"
"golang.org/x/oauth2"
)

var sourcegraphUrl string = "https://sourcegraph.com/.api/graphql"

// Register registers GitHub related functionality as a SQLite extension
func Register(ext *sqlite.ExtensionApi, opt *options.Options) (_ sqlite.ErrorCode, err error) {
sourcegraphOpts := &Options{
Client: func() *graphql.Client {
httpClient := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: GetSourcegraphTokenFromCtx(opt.Context)},
))
client := graphql.NewClient(sourcegraphUrl, httpClient)
return client
},
}

if opt.SourcegraphClientGetter != nil {
sourcegraphOpts.Client = opt.SourcegraphClientGetter
}

var modules = map[string]sqlite.Module{
"sourcegraph_search": NewSourcegraphSearchModule(sourcegraphOpts),
}

// register Sourcegraph tables
for name, mod := range modules {
if err = ext.CreateModule(name, mod); err != nil {
return sqlite.SQLITE_ERROR, errors.Wrapf(err, "failed to register Sourcegraph %q module", name)
}
}

return sqlite.SQLITE_OK, nil
}
239 changes: 239 additions & 0 deletions extensions/internal/sourcegraph/sourcegraph_search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
package sourcegraph

import (
"context"
"encoding/json"
"io"

"github.com/augmentable-dev/vtab"
"github.com/shurcooL/graphql"
"go.riyazali.net/sqlite"
)

type searchResults struct {
Results []struct {
Typename graphql.String `graphql:"__typename"`
FileMatchFields fileMatch `graphql:"... on FileMatch"`
CommitSearchResultFields commitSearchResults `graphql:"... on CommitSearchResult"`
RepositoryFields repositoryFields `graphql:"... on Repository"`
}
LimitHit graphql.Boolean
Cloning []struct {
Name graphql.String
}
Missing []struct {
Name graphql.String
}
Timedout []struct {
Name graphql.String
}
MatchCount graphql.Int
ElapsedMilliseconds graphql.Int
}

type fileMatch struct {
Repository struct {
Name graphql.String `json:"name"`
Url graphql.String `json:"url"`
} `json:"repository"`
File struct {
Name graphql.String `json:"name"`
Path graphql.String `json:"path"`
Url graphql.String `json:"url"`
Content graphql.String `json:"content"`
Commit struct {
Oid graphql.String `json:"oid"`
} `json:"commit"`
}
LineMatches []struct {
Preview graphql.String `json:"preview"`
LineNumber graphql.Int `json:"lineNumber"`
OffsetAndLengths [][]graphql.Int `json:"offsetAndLengths"`
} `json:"lineMatches"`
}

type preview struct {
Value graphql.String `json:"value"`
Highlights highlight `json:"highlights"`
}

type highlight struct {
Line graphql.String `json:"line"`
Character graphql.String `json:"character"`
Length graphql.Int `json:"length"`
}

type commitSearchResults struct {
MessagePreview preview `json:"messagePreview"`
DiffPreview preview `json:"diffPreview"`
Label struct {
Html graphql.String `json:"html"`
} `json:"label"`
Url graphql.String `json:"url"`
Matches struct {
Url graphql.String `json:"url"`
Body struct {
Html graphql.String `json:"html"`
Text graphql.String `json:"text"`
} `json:"body"`
Highlights []highlight `json:"highlights"`
}
Commit struct {
Repository struct {
Name graphql.String `json:"name"`
Url graphql.String `json:"url"`
} `json:"repository"`
Oid graphql.String `json:"oid"`
Url graphql.String `json:"url"`
Subject graphql.String `json:"subject"`
Author struct {
Date graphql.String `json:"date"`
Person struct {
DisplayName graphql.String `json:"displayName"`
} `json:"person"`
} `json:"author"`
}
}

type repositoryFields struct {
Name graphql.String `json:"name"`
Url graphql.String `json:"url"`
ExternalURLs []struct {
ServiceKind graphql.String `json:"serviceKind"`
Url graphql.String `json:"url"`
} `graphql:"externalURLs" json:"externalURLs"`
Label struct {
Html graphql.String `json:"html"`
} `json:"label"`
}

type fetchSourcegraphOptions struct {
Client *graphql.Client
Query string
}

func fetchSearch(ctx context.Context, input *fetchSourcegraphOptions) (*searchResults, error) {
var sourcegraphQuery struct {
Search struct {
Results searchResults
} `graphql:"search(query: $query, version: V2)"`
}

variables := map[string]interface{}{
"query": graphql.String(input.Query),
}

err := input.Client.Query(ctx, &sourcegraphQuery, variables)
if err != nil {
return nil, err
}

return &sourcegraphQuery.Search.Results, nil
}

type iterResults struct {
query string
client *graphql.Client
current int
results *searchResults
}

func (i *iterResults) Column(ctx *sqlite.Context, c int) error {
current := i.results.Results[i.current]
col := searchCols[c]
switch col.Name {
case "__typename":
ctx.ResultText(string(current.Typename))
case "results":
switch current.Typename {
case "Repository":
res, err := json.Marshal(current.RepositoryFields)
if err != nil {
return err
}
ctx.ResultText(string(res))
case "CommitSearchResult":
res, err := json.Marshal(current.CommitSearchResultFields)
if err != nil {
return err
}
ctx.ResultText(string(res))
case "FileMatch":
res, err := json.Marshal(current.FileMatchFields)
if err != nil {
return err
}
ctx.ResultText(string(res))
}
case "cloning":
res, err := json.Marshal(i.results.Cloning)
if err != nil {
return err
}
ctx.ResultText(string(res))
case "missing":
res, err := json.Marshal(i.results.Missing)
if err != nil {
return err
}
ctx.ResultText(string(res))
case "timed_out":
res, err := json.Marshal(i.results.Timedout)
if err != nil {
return err
}
ctx.ResultText(string(res))
case "match_count":
ctx.ResultInt(int(i.results.MatchCount))
case "elapsed_milliseconds":
ctx.ResultInt(int(i.results.ElapsedMilliseconds))
}

return nil
}

func (i *iterResults) Next() (vtab.Row, error) {
var err error
if i.current == -1 {
i.results, err = fetchSearch(context.Background(), &fetchSourcegraphOptions{i.client, i.query})
if err != nil {
return nil, err
}
}

i.current += 1
length := len(i.results.Results)

if i.results == nil || i.current >= length {
return nil, io.EOF
}

return i, nil
}

var searchCols = []vtab.Column{
{Name: "query", Type: "TEXT", NotNull: true, Hidden: true, Filters: []*vtab.ColumnFilter{{Op: sqlite.INDEX_CONSTRAINT_EQ, Required: true, OmitCheck: true}}},
{Name: "__typename", Type: "TEXT"},
{Name: "cloning", Type: "TEXT", Hidden: true},
{Name: "elapsed_milliseconds", Type: "INT", Hidden: true},
{Name: "match_count", Type: "INT", Hidden: true},
{Name: "missing", Type: "INT", Hidden: true},
{Name: "timed_out", Type: "TEXT", Hidden: true},
{Name: "results", Type: "TEXT"},
}

func NewSourcegraphSearchModule(opts *Options) sqlite.Module {
return vtab.NewTableFunc("sourcegraph_search", searchCols, func(constraints []*vtab.Constraint, orders []*sqlite.OrderBy) (vtab.Iterator, error) {
var query string
for _, constraint := range constraints {
if constraint.Op == sqlite.INDEX_CONSTRAINT_EQ {
switch constraint.ColIndex {
case 0:
query = constraint.Value.Text()
}
}
}

return &iterResults{query, opts.Client(), -1, nil}, nil
})
}
Loading

0 comments on commit a1a19fc

Please sign in to comment.