Skip to content

Commit

Permalink
Refactor: (#10)
Browse files Browse the repository at this point in the history
- extract common code for listing repos
- extend and unify ways to infer/set the access token
- unify common flags
  • Loading branch information
pmatseykanets authored Jan 1, 2021
1 parent 51e8c1a commit 9e64774
Show file tree
Hide file tree
Showing 13 changed files with 414 additions and 387 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,18 @@ GitHub Tools
- [gh-purge-artifacts](cmd/gh-purge-artifacts) Purge GitHub Actions artifacts across GitHub repositories
- [gh-go-rdeps](cmd/gh-go-rdeps) Find reverse Go dependencies across GitHub repositories
- [gh-find](cmd/gh-find) Walk file hierarchies across GitHub repositories

## Authentication

All tools require a GitHub access token in order to authenticate API requests and use following methods, in the order of precedence, to infer/set the token:

- If `-token` flag is used a user will be asked to enter the token interactively
- `GHTOOLS_TOKEN` environment variable
- `GITHUB_TOKEN` environment variable
- `~/.config/gh-tools/auth.yml` file, containing the token

```yaml
oauth_token: <token>
```
- `gh cli`'s configuration file
85 changes: 85 additions & 0 deletions auth/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package auth

import (
"os"

"gopkg.in/yaml.v2"
)

// GetToken tries to infer the access token
// from environment variables and config files.
func GetToken() string {
var token string

// gh-tools specific env variable.
if token = os.Getenv("GHTOOLS_TOKEN"); token != "" {
return token
}
// Generic env variable.
if token = os.Getenv("GITHUB_TOKEN"); token != "" {
return token
}
// Read the token from gh-tools auth file ~/.config/gh-tools/auth.yml
if token = fromAuthFile(); token != "" {
return token
}
// Try to read the token from gh cli's config file ~/.config/gh/hosts.yml
if token = fromGhCliConfig(); token != "" {
return token
}

return ""
}

func fromAuthFile() string {
path := "/.config/gh-tools/auth.yml"

home, err := os.UserHomeDir()
if err != nil {
return ""
}
path = home + path

file, err := os.Open(path)
if err != nil {
return ""
}

auth := struct {
OauthToken string `yaml:"oauth_token"`
}{}
err = yaml.NewDecoder(file).Decode(auth)
if err != nil {
return ""
}

return auth.OauthToken
}

func fromGhCliConfig() string {
path := "/.config/gh/hosts.yml"

home, err := os.UserHomeDir()
if err != nil {
return ""
}
path = home + path

file, err := os.Open(path)
if err != nil {
return ""
}

hosts := map[string]struct {
OauthToken string `yaml:"oauth_token"`
User string `yaml:"user"`
}{}
err = yaml.NewDecoder(file).Decode(hosts)
if err != nil {
return ""
}

auth := hosts["github.com"]

return auth.OauthToken
}
2 changes: 1 addition & 1 deletion cmd/gh-find/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Flags:

## Environment variables

`GITHUB_TOKEN` shoud be set and contain GitHub personal access token
`GHTOOLS_TOKEN` and `GITHUB_TOKEN` in the order of precedence can be used to set a GitHub access token.

### Examples

Expand Down
151 changes: 27 additions & 124 deletions cmd/gh-find/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import (
"strings"

"github.com/google/go-github/v32/github"
"github.com/pmatseykanets/gh-tools/auth"
gh "github.com/pmatseykanets/gh-tools/github"
"github.com/pmatseykanets/gh-tools/terminal"
"github.com/pmatseykanets/gh-tools/version"
"golang.org/x/oauth2"
)
Expand All @@ -34,11 +37,9 @@ Flags:
-path The pattern to match the pathname
-no-path The pattern to reject the pathname
-repo The pattern to match repository names
-token Prompt for an Access Token
-type File type f - file, d - directory
-version Print the version and exit
Environment variables:
GITHUB_TOKEN an authentication token for github.com API requests
`
fmt.Printf("gh-find version %s\n", version.Version)
fmt.Println(usage)
Expand Down Expand Up @@ -69,6 +70,7 @@ type config struct {
pathRegexp []*regexp.Regexp // The pattern to match the pathname.
noPathRegexp []*regexp.Regexp // The pattern to reject the pathname.
grepRegexp *regexp.Regexp // The pattern to match the contents of matching files.
token bool // Propmt for an access token.
}

type finder struct {
Expand Down Expand Up @@ -106,8 +108,8 @@ func readConfig() (config, error) {
name, path, noName, noPath stringList
err error
)
flag.BoolVar(&showHelp, "help", showHelp, "Print this information and exit")
flag.StringVar(&config.branch, "branch", "", "Repository branch name if different from the default")
flag.BoolVar(&showHelp, "help", showHelp, "Print this information and exit")
flag.StringVar(&grep, "grep", "", "The pattern to match the file contents")
flag.IntVar(&config.maxDepth, "maxdepth", 0, "Descend at most n directory levels")
flag.IntVar(&config.minDepth, "mindepth", 0, "Descend at least n directory levels")
Expand All @@ -116,6 +118,7 @@ func readConfig() (config, error) {
flag.Var(&path, "path", "The pattern to match the pathname")
flag.Var(&noPath, "no-path", "The pattern to reject the pathname")
flag.StringVar(&repo, "repo", "", "The pattern to match repository names")
flag.BoolVar(&config.token, "token", config.token, "Prompt for Access Token")
flag.StringVar(&config.ftype, "type", "", "File type f - file, d - directory")
flag.BoolVar(&showVersion, "version", showVersion, "Print version and exit")
flag.Usage = usage
Expand Down Expand Up @@ -217,22 +220,31 @@ func run(ctx context.Context) error {
return err
}

ghToken := os.Getenv("GITHUB_TOKEN")
if ghToken == "" {
return fmt.Errorf("GITHUB_TOKEN env variable should be set")
var token string
if finder.config.token {
token, _ = terminal.PasswordPrompt("Access Token: ")
} else {
token = auth.GetToken()
}
if token == "" {
return fmt.Errorf("access token is required")
}

finder.gh = github.NewClient(
oauth2.NewClient(ctx, oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: ghToken}),
),
)
finder.gh = github.NewClient(oauth2.NewClient(ctx, oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)))

return finder.find(ctx)
}

func (f *finder) find(ctx context.Context) error {
repos, err := f.getRepos(ctx)
repoFinder := gh.RepoFinder{
Client: f.gh,
Owner: f.config.owner,
Repo: f.config.repo,
RepoRegexp: f.config.repoRegexp,
}
repos, err := repoFinder.Find(ctx)
if err != nil {
return err
}
Expand All @@ -241,15 +253,12 @@ func (f *finder) find(ctx context.Context) error {
branch, entryPath, basename string
level int
)
// REPOS:
for _, repo := range repos {
branch = f.config.branch
if branch == "" {
branch = repo.GetDefaultBranch()
}

// fmt.Println(repo.GetFullName(), branch)

tree, resp, err := f.gh.Git.GetTree(ctx, f.config.owner, *repo.Name, branch, true)
if err != nil {
if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusConflict {
Expand Down Expand Up @@ -306,12 +315,12 @@ func (f *finder) find(ctx context.Context) error {
return err
}
for _, match := range results.matches {
fmt.Println(repo.GetFullName(), entry.GetPath(), match.lineno, match.line)
fmt.Fprintln(f.stdout, repo.GetFullName(), entry.GetPath(), match.lineno, match.line)
}
continue nextEntry
}

fmt.Println(repo.GetFullName(), entry.GetPath())
fmt.Fprintln(f.stdout, repo.GetFullName(), entry.GetPath())
}
}

Expand All @@ -333,112 +342,6 @@ func (f *finder) grepContents(ctx context.Context, repo *github.Repository, bran
return grep(contents, f.config.grepRegexp)
}

func (f *finder) getSingleRepo(ctx context.Context) (*github.Repository, error) {
repo, _, err := f.gh.Repositories.Get(ctx, f.config.owner, f.config.repo)
if err != nil {
return nil, fmt.Errorf("can't read repository: %s", err)
}

return repo, nil
}

func (f *finder) getUserRepos(ctx context.Context) ([]*github.Repository, error) {
opt := &github.RepositoryListOptions{
ListOptions: github.ListOptions{PerPage: 30},
Affiliation: "owner",
}
var (
list, repos []*github.Repository
resp *github.Response
err error
)
for {
repos, resp, err = f.gh.Repositories.List(ctx, f.config.owner, opt)
if err != nil {
return nil, fmt.Errorf("can't read repositories: %s", err)
}

if f.config.repoRegexp == nil {
list = append(list, repos...)
} else {
for _, repo := range repos {
if f.config.repoRegexp.MatchString(repo.GetName()) {
list = append(list, repo)
}
}
}

if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage
}

return list, nil
}

func (f *finder) getOrgRepos(ctx context.Context) ([]*github.Repository, error) {
opt := &github.RepositoryListByOrgOptions{
ListOptions: github.ListOptions{PerPage: 30},
}
var (
list, repos []*github.Repository
resp *github.Response
err error
)
for {
repos, resp, err = f.gh.Repositories.ListByOrg(ctx, f.config.owner, opt)
if err != nil {
return nil, fmt.Errorf("can't read repositories: %s", err)
}

if f.config.repoRegexp == nil {
list = append(list, repos...)
} else {
for _, repo := range repos {
if f.config.repoRegexp.Match([]byte(repo.GetName())) {
list = append(list, repo)
}
}
}

if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage
}

return list, nil
}

func (f *finder) getRepos(ctx context.Context) ([]*github.Repository, error) {
owner, _, err := f.gh.Users.Get(ctx, f.config.owner)
if err != nil {
return nil, fmt.Errorf("can't read owner information: %s", err)
}

// A single repository.
if f.config.repo != "" {
repo, err := f.getSingleRepo(ctx)
if err != nil {
return nil, err
}
return []*github.Repository{repo}, nil
}

var repos []*github.Repository
switch t := owner.GetType(); t {
case "User":
repos, err = f.getUserRepos(ctx)
case "Organization":
repos, err = f.getOrgRepos(ctx)
default:
err = fmt.Errorf("unknown owner type %s", t)
}

return repos, err
}

func levels(path string) int {
return len(path) - len(strings.ReplaceAll(path, "/", "")) + 1
}
Expand Down
8 changes: 4 additions & 4 deletions cmd/gh-go-rdeps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ Usage: gh-go-rdeps [flags] <owner> <path>
Flags:
-help Print this information and exit
-progress Show the progress
-regexp= Regexp to match repository names
-repo The pattern to match repository names
-token Prompt for an Access Token
-version Print the version and exit
```

## Environment variables

`GITHUB_TOKEN` shoud be set and contain GitHub personal access token
`GHTOOLS_TOKEN` and `GITHUB_TOKEN` in the order of precedence can be used to set a GitHub access token.

### Examples

Expand All @@ -37,5 +37,5 @@ gh-go-rdeps owner golang.org/x/sync
Find all Go repositories that start with `api` and depend on `github.com/owner/library`

```sh
gh-go-rdeps -regexp '^api' owner github.com/owner/library
gh-go-rdeps -repo '^api' owner github.com/owner/library
```
Loading

0 comments on commit 9e64774

Please sign in to comment.