diff --git a/.gitignore b/.gitignore index 48ed38d..17138c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ -./gitignore dist/** bin/** -gitstrap _old/ +/gitstrap diff --git a/cmd/gitstrap/cmd_apply.go b/cmd/gitstrap/cmd_apply.go new file mode 100644 index 0000000..c9ee9bf --- /dev/null +++ b/cmd/gitstrap/cmd_apply.go @@ -0,0 +1,52 @@ +package main + +import ( + "os" + + "github.com/g4s8/gitstrap/internal/gitstrap" + "github.com/g4s8/gitstrap/internal/spec" + "github.com/g4s8/gitstrap/internal/view" + "github.com/urfave/cli/v2" +) + +var applyCommand = &cli.Command{ + Name: "apply", + Usage: "Apply new specficiation", + Action: cmdApply, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Usage: "Resource specification file", + }, + &cli.BoolFlag{ + Name: "force", + Usage: "Force create, replace existing resource if exists", + }, + }, +} + +func cmdApply(c *cli.Context) error { + token, err := resolveToken(c) + if err != nil { + return err + } + + model := new(spec.Model) + if err := model.FromFile(c.String("file")); err != nil { + return err + } + debug := os.Getenv("DEBUG") != "" + g, err := gitstrap.New(c.Context, token, debug) + if err != nil { + return err + } + if c.Bool("force") { + model.Metadata.Annotations["force"] = "true" + } + rs, errs := g.Apply(model) + if err := view.RenderOn(view.Console, rs, errs); err != nil { + fatal(err) + } + return nil +} diff --git a/cmd/gitstrap/cmd_create.go b/cmd/gitstrap/cmd_create.go new file mode 100644 index 0000000..1f3cdc7 --- /dev/null +++ b/cmd/gitstrap/cmd_create.go @@ -0,0 +1,53 @@ +package main + +import ( + "os" + + "github.com/g4s8/gitstrap/internal/gitstrap" + "github.com/g4s8/gitstrap/internal/spec" + "github.com/g4s8/gitstrap/internal/view" + "github.com/urfave/cli/v2" +) + +var createCommand = &cli.Command{ + Name: "create", + Aliases: []string{"c"}, + Usage: "Create new resource", + Action: cmdCreate, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Usage: "Resource specification file", + }, + &cli.BoolFlag{ + Name: "force", + Usage: "Force create, replace existing resource if exists", + }, + }, +} + +func cmdCreate(c *cli.Context) error { + token, err := resolveToken(c) + if err != nil { + return err + } + + model := new(spec.Model) + if err := model.FromFile(c.String("file")); err != nil { + return err + } + debug := os.Getenv("DEBUG") != "" + g, err := gitstrap.New(c.Context, token, debug) + if err != nil { + return err + } + if c.Bool("force") { + model.Metadata.Annotations["force"] = "true" + } + rs, errs := g.Create(model) + if err := view.RenderOn(view.Console, rs, errs); err != nil { + fatal(err) + } + return nil +} diff --git a/cmd/gitstrap/cmd_delete.go b/cmd/gitstrap/cmd_delete.go new file mode 100644 index 0000000..d0f604a --- /dev/null +++ b/cmd/gitstrap/cmd_delete.go @@ -0,0 +1,54 @@ +package main + +import ( + "os" + + "github.com/g4s8/gitstrap/internal/gitstrap" + "github.com/g4s8/gitstrap/internal/spec" + "github.com/g4s8/gitstrap/internal/view" + "github.com/urfave/cli/v2" + "gopkg.in/yaml.v3" + "io/ioutil" + "path/filepath" +) + +var deleteCommand = &cli.Command{ + Name: "delete", + Aliases: []string{"remove", "del", "rm"}, + Usage: "Delete resource", + Action: cmdDelete, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Usage: "Resource specification file", + }, + }, +} + +func cmdDelete(c *cli.Context) error { + token, err := resolveToken(c) + if err != nil { + return err + } + + fn, _ := filepath.Abs(c.String("file")) + data, err := ioutil.ReadFile(fn) + if err != nil { + return err + } + model := new(spec.Model) + if err := yaml.Unmarshal(data, model); err != nil { + return err + } + debug := os.Getenv("DEBUG") != "" + g, err := gitstrap.New(c.Context, token, debug) + if err != nil { + return err + } + rs, errs := g.Delete(model) + if err := view.RenderOn(view.Console, rs, errs); err != nil { + fatal(err) + } + return nil +} diff --git a/cmd/gitstrap/cmd_get.go b/cmd/gitstrap/cmd_get.go new file mode 100644 index 0000000..6eb78ed --- /dev/null +++ b/cmd/gitstrap/cmd_get.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + "os" + + "github.com/g4s8/gitstrap/internal/gitstrap" + "github.com/g4s8/gitstrap/internal/spec" + "github.com/g4s8/gitstrap/internal/view" + "github.com/urfave/cli/v2" +) + +var getCommand = &cli.Command{ + Name: "get", + Aliases: []string{"g"}, + Usage: "Get resource", + Subcommands: []*cli.Command{ + { + Name: "repo", + Usage: "Get repository", + Action: cmdGetRepo, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "owner", + Usage: "Get repositories of another user or organization", + }, + }, + }, + }, +} + +func cmdGetRepo(c *cli.Context) error { + token, err := resolveToken(c) + if err != nil { + return err + } + name := c.Args().First() + if name == "" { + return fmt.Errorf("Requires repository name argument") + } + format := spec.MfYaml + owner := c.String("owner") + debug := os.Getenv("DEBUG") != "" + g, err := gitstrap.New(c.Context, token, debug) + if err != nil { + return err + } + repo, errs := g.Get(name, owner, format) + if err := view.RenderOn(view.Console, repo, errs); err != nil { + fatal(err) + } + return nil +} diff --git a/cmd/gitstrap/cmd_list.go b/cmd/gitstrap/cmd_list.go new file mode 100644 index 0000000..fb3d2a7 --- /dev/null +++ b/cmd/gitstrap/cmd_list.go @@ -0,0 +1,75 @@ +package main + +import ( + "os" + + "github.com/g4s8/gitstrap/internal/gitstrap" + "github.com/g4s8/gitstrap/internal/view" + "github.com/urfave/cli/v2" +) + +var listCommand = &cli.Command{ + Name: "list", + Aliases: []string{"l", "ls", "lst"}, + Usage: "List resources", + Action: cmdListRepo, + Subcommands: []*cli.Command{ + { + Name: "repo", + Usage: "List repositories", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "owner", + Usage: "List repositories of another user or organization", + }, + &cli.BoolFlag{ + Name: "forks", + Usage: "Filter only fork repositories", + }, + &cli.BoolFlag{ + Name: "no-forks", + Usage: "Filter out fork repositories", + }, + &cli.IntFlag{ + Name: "stars-gt", + Usage: "Filter by stars greater than value", + }, + &cli.IntFlag{ + Name: "stars-lt", + Usage: "Filter by stars less than value", + }, + }, + }, + }, +} + +func cmdListRepo(c *cli.Context) error { + token, err := resolveToken(c) + if err != nil { + return err + } + owner := c.String("owner") + debug := os.Getenv("DEBUG") != "" + g, err := gitstrap.New(c.Context, token, debug) + if err != nil { + return err + } + filter := gitstrap.LfNop + if c.Bool("forks") { + filter = gitstrap.LfForks(filter, true) + } + if c.Bool("no-forks") { + filter = gitstrap.LfForks(filter, false) + } + if gt := c.Int("stars-gt"); gt > 0 { + filter = gitstrap.LfStars(filter, gitstrap.LfStarsGt(gt)) + } + if lt := c.Int("stars-lt"); lt > 0 { + filter = gitstrap.LfStars(filter, gitstrap.LfStarsLt(lt)) + } + lst, errs := g.List(filter, owner) + if err := view.RenderOn(view.Console, lst, errs); err != nil { + fatal(err) + } + return nil +} diff --git a/internal/gitstrap/apply.go b/internal/gitstrap/apply.go new file mode 100644 index 0000000..6576e59 --- /dev/null +++ b/internal/gitstrap/apply.go @@ -0,0 +1,68 @@ +package gitstrap + +import ( + "context" + "fmt" + + "github.com/g4s8/gitstrap/internal/github" + "github.com/g4s8/gitstrap/internal/spec" + "github.com/g4s8/gitstrap/internal/view" + gh "github.com/google/go-github/v33/github" +) + +func (g *Gitstrap) Apply(m *spec.Model) (<-chan view.Printable, <-chan error) { + res := make(chan view.Printable) + errs := make(chan error) + ctx, cancel := g.newContext() + go func() { + defer close(res) + defer close(errs) + defer cancel() + var r view.Printable + var err error + switch m.Kind { + case spec.KindRepo: + repo := m.Spec.(*spec.Repo) + r, err = g.applyRepo(ctx, repo, m.Metadata) + } + if err != nil { + errs <- err + } else { + res <- r + } + }() + return res, errs +} + +type resRepoApply struct { + *gh.Repository +} + +func (r *resRepoApply) PrintOn(p view.Printer) { + p.Print(fmt.Sprintf("Repository %s updated", r.GetFullName())) +} + +func (g *Gitstrap) applyRepo(ctx context.Context, repo *spec.Repo, meta *spec.Metadata) (view.Printable, error) { + owner := meta.Owner + if owner == "" { + owner = g.me + } + name := meta.Name + exist, err := github.RepoExist(g.gh, ctx, owner, name) + if err != nil { + return nil, err + } + if !exist { + return g.createRepo(ctx, repo, meta) + } + gr := new(gh.Repository) + if err := repo.ToGithub(gr); err != nil { + return nil, err + } + gr.ID = meta.ID + gr, _, err = g.gh.Repositories.Edit(ctx, owner, name, gr) + if err != nil { + return nil, err + } + return &resRepoApply{gr}, nil +} diff --git a/internal/gitstrap/create.go b/internal/gitstrap/create.go new file mode 100644 index 0000000..e95acf0 --- /dev/null +++ b/internal/gitstrap/create.go @@ -0,0 +1,143 @@ +package gitstrap + +import ( + "context" + "fmt" + + "github.com/g4s8/gitstrap/internal/spec" + "github.com/g4s8/gitstrap/internal/view" + "github.com/google/go-github/v33/github" +) + +type errUnsupportModelKind struct { + kind string +} + +func (e *errUnsupportModelKind) Error() string { + return fmt.Sprintf("Unsupported model kind: `%s`", e.kind) +} + +func (g *Gitstrap) Create(m *spec.Model) (<-chan view.Printable, <-chan error) { + res := make(chan view.Printable) + errs := make(chan error) + ctx, cancel := g.newContext() + go func() { + defer close(res) + defer close(errs) + defer cancel() + switch m.Kind { + case spec.KindRepo: + spec := m.Spec.(*spec.Repo) + r, err := g.createRepo(ctx, spec, m.Metadata) + if err != nil { + errs <- err + } else { + res <- r + } + case spec.KindReadme: + spec := m.Spec.(*spec.Readme) + r, err := g.createReadme(ctx, spec, m.Metadata) + if err != nil { + errs <- err + } else { + res <- r + } + default: + errs <- &errUnsupportModelKind{m.Kind} + } + }() + return res, errs +} + +type createRepoResult struct { + repo *github.Repository +} + +func (cr *createRepoResult) PrintOn(p view.Printer) { + p.Print(fmt.Sprintf("Repository [%d] %s created", cr.repo.GetID(), cr.repo.GetFullName())) +} + +func (g *Gitstrap) createRepo(ctx context.Context, repo *spec.Repo, meta *spec.Metadata) (view.Printable, error) { + grepo := new(github.Repository) + if err := repo.ToGithub(grepo); err != nil { + return nil, err + } + grepo.Name = &meta.Name + fn := fmt.Sprintf("%s/%s", meta.Owner, meta.Name) + grepo.FullName = &fn + owner := meta.Owner + if owner == "" || owner == g.me { + owner = "" + } + r, _, err := g.gh.Repositories.Create(ctx, owner, grepo) + if err != nil { + return nil, err + } + return &createRepoResult{r}, nil +} + +type createReadmeResult struct { + *github.RepositoryContentResponse +} + +func (r *createReadmeResult) PrintOn(p view.Printer) { + p.Print(fmt.Sprintf("README created with %s", r.GetSHA())) +} + +type errReadmeExists struct { + owner, repo string +} + +func (e *errReadmeExists) Error() string { + return fmt.Sprintf("README.md already exists in %s/%s (try --force for replacing it)", e.owner, e.repo) +} + +type errReadmeNotFile struct { + rtype string +} + +func (e *errReadmeNotFile) Error() string { + return fmt.Sprintf("README is no a file: `%s`", e.rtype) +} + +func (g *Gitstrap) createReadme(ctx context.Context, spec *spec.Readme, meta *spec.Metadata) (view.Printable, error) { + owner := spec.Selector.Owner + if owner == "" { + owner = g.me + } + repo, _, err := g.gh.Repositories.Get(ctx, owner, spec.Selector.Repository) + if err != nil { + return nil, err + } + msg := "Updated README.md" + if cm, ok := meta.Annotations["commitMessage"]; ok { + msg = cm + } + opts := &github.RepositoryContentFileOptions{ + Content: []byte(spec.String()), + Message: &msg, + } + if meta.Annotations["force"] == "true" { + getopts := &github.RepositoryContentGetOptions{} + cnt, _, rsp, err := g.gh.Repositories.GetContents(ctx, owner, repo.GetName(), "README.md", getopts) + if rsp.StatusCode == 404 { + goto SKIP_GET + } + if err != nil { + return nil, err + } + if *cnt.Type != "file" { + return nil, &errReadmeNotFile{*cnt.Type} + } + opts.SHA = cnt.SHA + SKIP_GET: + } + rs, rsp, err := g.gh.Repositories.UpdateFile(ctx, owner, repo.GetName(), "README.md", opts) + if err != nil { + if rsp.StatusCode == 422 && opts.SHA == nil { + return nil, &errReadmeExists{owner, repo.GetName()} + } + return nil, err + } + return &createReadmeResult{rs}, nil +} diff --git a/internal/gitstrap/debug_transport.go b/internal/gitstrap/debug_transport.go new file mode 100644 index 0000000..82daacb --- /dev/null +++ b/internal/gitstrap/debug_transport.go @@ -0,0 +1,38 @@ +package gitstrap + +import ( + "bytes" + "io/ioutil" + "log" + "net/http" +) + +type logTransport struct { + origin http.RoundTripper + tag string +} + +func (t *logTransport) RoundTrip(req *http.Request) (*http.Response, error) { + log.Printf("[%s] >>> %s %s", t.tag, req.Method, req.URL) + if req.Body != nil { + defer req.Body.Close() + if data, err := ioutil.ReadAll(req.Body); err == nil { + req.Body = ioutil.NopCloser(bytes.NewBuffer(data)) + log.Print(string(data)) + } + } + rsp, err := t.origin.RoundTrip(req) + if err != nil { + log.Printf("[%s] %s ERR: %s", t.tag, req.URL, err) + } else { + log.Printf("[%s] %s <<< %d", t.tag, req.URL, rsp.StatusCode) + if rsp.Body != nil { + defer rsp.Body.Close() + if data, err := ioutil.ReadAll(rsp.Body); err == nil { + rsp.Body = ioutil.NopCloser(bytes.NewBuffer(data)) + log.Print(string(data)) + } + } + } + return rsp, err +} diff --git a/internal/gitstrap/delete.go b/internal/gitstrap/delete.go new file mode 100644 index 0000000..c055732 --- /dev/null +++ b/internal/gitstrap/delete.go @@ -0,0 +1,102 @@ +package gitstrap + +import ( + "context" + "fmt" + + "github.com/g4s8/gitstrap/internal/spec" + "github.com/g4s8/gitstrap/internal/view" + "github.com/google/go-github/v33/github" +) + +func (g *Gitstrap) Delete(m *spec.Model) (<-chan view.Printable, <-chan error) { + res := make(chan view.Printable) + errs := make(chan error) + ctx, cancel := g.newContext() + go func() { + defer close(res) + defer close(errs) + defer cancel() + var rs view.Printable + var err error + switch m.Kind { + case spec.KindRepo: + rs, err = g.deleteRepo(ctx, m.Metadata) + case spec.KindReadme: + rspec := m.Spec.(*spec.Readme) + rs, err = g.deleteReadme(ctx, rspec, m.Metadata) + default: + errs <- &errUnsupportModelKind{m.Kind} + return + } + if err != nil { + errs <- err + } else { + res <- rs + } + }() + return res, errs +} + +type repoDeleteResult struct { + owner string + name string +} + +func (r *repoDeleteResult) PrintOn(p view.Printer) { + p.Print(fmt.Sprintf("Repository %s/%s deleted successfully", r.owner, r.name)) +} + +func (g *Gitstrap) deleteRepo(ctx context.Context, meta *spec.Metadata) (view.Printable, error) { + owner := meta.Owner + if owner == "" { + owner = g.me + } + _, err := g.gh.Repositories.Delete(ctx, owner, meta.Name) + if err != nil { + return nil, err + } + return &repoDeleteResult{meta.Owner, meta.Name}, nil +} + +type errReadmeNotExists struct { + owner, repo string +} + +func (e *errReadmeNotExists) Error() string { + return fmt.Sprintf("README `%s/%s` doesn't exist", e.owner, e.repo) +} + +func (g *Gitstrap) deleteReadme(ctx context.Context, spec *spec.Readme, meta *spec.Metadata) (view.Printable, error) { + owner := spec.Selector.Owner + if owner == "" { + owner = g.me + } + repo, _, err := g.gh.Repositories.Get(ctx, owner, spec.Selector.Repository) + if err != nil { + return nil, err + } + msg := "README.md removed" + if cm, ok := meta.Annotations["commitMessage"]; ok { + msg = cm + } + opts := &github.RepositoryContentFileOptions{ + Message: &msg, + } + getopts := new(github.RepositoryContentGetOptions) + cnt, _, rsp, err := g.gh.Repositories.GetContents(ctx, owner, repo.GetName(), "README.md", getopts) + if rsp.StatusCode == 404 { + return nil, &errReadmeNotExists{owner, repo.GetName()} + } + if err != nil { + return nil, err + } + if *cnt.Type != "file" { + return nil, &errReadmeNotFile{*cnt.Type} + } + opts.SHA = cnt.SHA + if _, _, err := g.gh.Repositories.DeleteFile(ctx, owner, repo.GetName(), "README.md", opts); err != nil { + return nil, err + } + return &repoDeleteResult{owner, repo.GetName()}, nil +} diff --git a/internal/gitstrap/get.go b/internal/gitstrap/get.go new file mode 100644 index 0000000..845c13c --- /dev/null +++ b/internal/gitstrap/get.go @@ -0,0 +1,57 @@ +package gitstrap + +import ( + "github.com/g4s8/gitstrap/internal/spec" + "github.com/g4s8/gitstrap/internal/view" + "github.com/google/go-github/v33/github" +) + +// Get resource +func (g *Gitstrap) Get(name string, owner string, format spec.ModelFormat) (<-chan view.Printable, <-chan error) { + res := make(chan view.Printable) + errs := make(chan error) + ctx, cancel := g.newContext() + if owner == "" { + owner = g.me + } + go func() { + defer close(res) + defer close(errs) + defer cancel() + r, _, err := g.gh.Repositories.Get(ctx, owner, name) + if err != nil { + errs <- err + return + } + cls := make([]*github.User, 0) + copts := &github.ListCollaboratorsOptions{Affiliation: "direct"} + PAGINATION: + batch, rsp, err := g.gh.Repositories.ListCollaborators(ctx, owner, name, copts) + cls = append(cls, batch...) + if rsp.NextPage < rsp.LastPage { + copts.Page = rsp.NextPage + goto PAGINATION + } + model := new(spec.Model) + model.Kind = spec.KindRepo + model.Version = spec.Version + model.Metadata = new(spec.Metadata) + model.Metadata.Name = name + model.Metadata.ID = r.ID + if owner != "" { + model.Metadata.Owner = owner + } else { + model.Metadata.Owner = g.me + } + repo := new(spec.Repo) + repo.FromGithub(r) + repo.Collaborators.FromUsers(cls) + model.Spec = repo + if p, err := format.ToView(model); err != nil { + errs <- err + } else { + res <- p + } + }() + return res, errs +} diff --git a/internal/gitstrap/gitstrap.go b/internal/gitstrap/gitstrap.go new file mode 100644 index 0000000..4ae3430 --- /dev/null +++ b/internal/gitstrap/gitstrap.go @@ -0,0 +1,53 @@ +package gitstrap + +import ( + "context" + "github.com/google/go-github/v33/github" + "golang.org/x/oauth2" + "log" + "net/http" + "time" +) + +// Gitstrap - main context +type Gitstrap struct { + ctx context.Context + gh *github.Client + debug bool + me string +} + +// New gitstrap context +func New(ctx context.Context, token string, debug bool) (*Gitstrap, error) { + g := new(Gitstrap) + g.debug = debug + g.ctx = ctx + if debug { + // print first chars of token if debug + log.Printf("Debug mode enabled: token='%s***%s'", token[:3], token[len(token)-2:]) + } + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + tc := oauth2.NewClient(ctx, ts) + if debug { + // attach logging HTTP transport on debug + tr := new(logTransport) + tr.tag = "GH" + if tc.Transport != nil { + tr.origin = tc.Transport + } else { + tr.origin = http.DefaultTransport + } + tc.Transport = tr + } + g.gh = github.NewClient(tc) + me, _, err := g.gh.Users.Get(g.ctx, "") + if err != nil { + return nil, err + } + g.me = me.GetLogin() + return g, nil +} + +func (g *Gitstrap) newContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(g.ctx, 25*time.Second) +} diff --git a/internal/gitstrap/list.go b/internal/gitstrap/list.go new file mode 100644 index 0000000..501e766 --- /dev/null +++ b/internal/gitstrap/list.go @@ -0,0 +1,101 @@ +package gitstrap + +import ( + "fmt" + "math" + "strconv" + + "github.com/g4s8/gitstrap/internal/view" + "github.com/google/go-github/v33/github" +) + +type listResult struct { + name string + public bool + fork bool + stars int + forks int +} + +func (r *listResult) isFork() (s string) { + if r.fork { + s = "fork" + } + return +} + +func (r *listResult) visibility() (s string) { + if r.public { + s = "public" + } else { + s = "private" + } + return +} + +func (r *listResult) starsStr() string { + if r.stars < 1000 { + return strconv.Itoa(r.stars) + } + val := float64(r.stars) / 1000 + if val < 10 { + return fmt.Sprintf("%.1fK", val) + } + return fmt.Sprintf("%dK", int(math.Floor(val))) +} + +func (r *listResult) forksStr() string { + if r.forks < 1000 { + return strconv.Itoa(r.stars) + } + val := float64(r.forks) / 1000 + if val < 10 { + return fmt.Sprintf("%.1fK", val) + } + return fmt.Sprintf("%dK", int(math.Floor(val))) +} + +func (r *listResult) PrintOn(p view.Printer) { + p.Print(fmt.Sprintf("| %40s | %4s | %5s | %8s ★ | %8s ⎇ |", + r.name, r.isFork(), r.visibility(), + r.starsStr(), r.forksStr())) +} + +// List of repositories +func (g *Gitstrap) List(filter ListFilter, owner string) (<-chan view.Printable, <-chan error) { + if filter == nil { + filter = LfNop + } + res := make(chan view.Printable) + errs := make(chan error) + ctx, cancel := g.newContext() + go func() { + defer close(res) + defer close(errs) + defer cancel() + opts := new(github.RepositoryListOptions) + PAGINATION: + list, rsp, err := g.gh.Repositories.List(ctx, owner, opts) + if err != nil { + errs <- err + return + } + for _, item := range list { + entry := new(listResult) + entry.name = item.GetFullName() + entry.public = !item.GetPrivate() + entry.fork = item.GetFork() + entry.stars = item.GetStargazersCount() + entry.forks = item.GetForksCount() + if filter.check(entry) { + res <- entry + } + } + if rsp.NextPage < rsp.LastPage { + opts.Page = rsp.NextPage + goto PAGINATION + } + + }() + return res, errs +} diff --git a/internal/gitstrap/list_filters.go b/internal/gitstrap/list_filters.go new file mode 100644 index 0000000..c9ac88c --- /dev/null +++ b/internal/gitstrap/list_filters.go @@ -0,0 +1,60 @@ +package gitstrap + +// ListFilter for list results +type ListFilter interface { + check(*listResult) bool +} + +// LfNop - list filter does nothing +var LfNop ListFilter = &lfNop{} + +type lfNop struct{} + +func (f *lfNop) check(r *listResult) bool { + return true +} + +// LfForks - list filter by fork criteria +func LfForks(origin ListFilter, fork bool) ListFilter { + return &lfFork{origin, fork} +} + +type lfFork struct { + origin ListFilter + fork bool +} + +func (f *lfFork) check(r *listResult) bool { + return f.origin.check(r) && r.fork == f.fork +} + +// LfStarsCriteria - criteria of repository stars for filtering +type LfStarsCriteria func(int) bool + +// LfStarsGt - list filter stars criteria: greater than `val` +func LfStarsGt(val int) LfStarsCriteria { + return func(x int) bool { + return x > val + } +} + +// LfStarsLt - list filter stars criteria: less than `val` +func LfStarsLt(val int) LfStarsCriteria { + return func(x int) bool { + return x < val + } +} + +// LfStars - list filter by stars count +func LfStars(origin ListFilter, criteria LfStarsCriteria) ListFilter { + return &lfStars{origin, criteria} +} + +type lfStars struct { + origin ListFilter + criteria LfStarsCriteria +} + +func (f *lfStars) check(i *listResult) bool { + return f.origin.check(i) && f.criteria(i.stars) +}