diff --git a/go.work.sum b/go.work.sum index 15d70305..4a4de599 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1481,6 +1481,7 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= diff --git a/marketplace/cmd/info.go b/marketplace/cmd/info.go index 3d307330..1981e21e 100644 --- a/marketplace/cmd/info.go +++ b/marketplace/cmd/info.go @@ -1,45 +1,56 @@ package cmd import ( - "context" "fmt" - "strings" + "hash/fnv" + "path" + "strconv" "time" + "github.com/charmbracelet/lipgloss" "github.com/dustin/go-humanize" - "github.com/google/go-github/v56/github" - "github.com/gookit/color" + "github.com/ignite/apps/marketplace/pkg/apps" "github.com/ignite/apps/marketplace/pkg/xgithub" "github.com/ignite/cli/ignite/pkg/cliui" "github.com/spf13/cobra" ) -// NewInfo creates a new info command that shows the details of an ignite app. +const igniteCLIPackage = "github.com/ignite/cli" + +var ( + linkStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("10")). + Underline(true) + installaitonStyle = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("9")). + MarginLeft(7) + commandStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("2")). + Bold(true) +) + +// NewInfo creates a new info command that shows the details of an ignite application repository. func NewInfo() *cobra.Command { return &cobra.Command{ - Use: "info [app-name]", - Short: "Show the details of an ignite app", + Use: "info [package-url]", + Short: "Show the details of an ignite application repository", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - appName := args[0] - repoOwner, repoName, err := validateAppName(appName) - if err != nil { - return err - } + githubToken, _ := cmd.Flags().GetString(githubTokenFlag) session := cliui.New(cliui.StartSpinner()) defer session.End() - session.StartSpinner("Fetching app details from GitHub...") - repo, err := getRepo(cmd.Context(), repoOwner, repoName) + session.StartSpinner("šŸ”Ž Fetching repository details from GitHub...") + + client := xgithub.NewClient(githubToken) + repo, err := apps.GetRepositoryDetails(cmd.Context(), client, args[0]) if err != nil { return err } - session.StopSpinner() - if !isIgniteApp(repo) { - return fmt.Errorf("the repository is not an ignite app") - } + session.StopSpinner() printRepoDetails(session, repo) @@ -48,41 +59,41 @@ func NewInfo() *cobra.Command { } } -func validateAppName(appName string) (owner, name string, err error) { - appName = strings.TrimPrefix(appName, "github.com/") - appNameParts := strings.Split(appName, "/") - if len(appNameParts) != 2 { - return "", "", fmt.Errorf("invalid app name: %s", appName) +func printRepoDetails(sess *cliui.Session, repo *apps.AppRepositoryDetails) { + sess.Println("Description:", repo.Description) + sess.Print("Tags:") + for _, tag := range repo.Tags { + sess.Print(lipgloss.NewStyle().Background(colorFromText(tag)).Render(tag), " ") } - owner = appNameParts[0] - name = appNameParts[1] - - return owner, name, nil + sess.Println() + sess.Println("Stars ā­ļø:", repo.Stars) + sess.Println("License āš–ļø :", repo.License) + sess.Printf("Updated At šŸ•’: %s %s\n", repo.UpdatedAt.Format(time.DateTime), updatedAtStyle.Render("("+humanize.Time(repo.UpdatedAt)+")")) + sess.Println("URL šŸŒŽ: ", linkStyle.Render(repo.URL)) + sess.Println("Apps šŸ”„:") + printAppsTable(sess, repo) } -func getRepo(ctx context.Context, owner, name string) (*github.Repository, error) { - client := xgithub.NewClient(githubToken) - return client.GetRepository(ctx, owner, name) +func colorFromText(text string) lipgloss.Color { + h := fnv.New64a() + h.Write([]byte(text)) + return lipgloss.Color(strconv.FormatUint(h.Sum64()%16, 10)) } -func isIgniteApp(repo *github.Repository) bool { - for _, topic := range repo.Topics { - if topic == "ignite-app" { - return true +func printAppsTable(sess *cliui.Session, repo *apps.AppRepositoryDetails) { + for i, app := range repo.Apps { + sess.Println("\tName:", app.Name) + sess.Println("\tDescription:", app.Description) + sess.Println("\tPath:", app.Path) + sess.Println("\tGo Version:", app.GoVersion) + sess.Println("\tIgnite Version:", app.IgniteVersion) + sess.Println(installaitonStyle.Render(fmt.Sprintf( + "šŸš€ Install via: %s", + commandStyle.Render(fmt.Sprintf("ignite app -g install %s", path.Join(repo.PackageURL, app.Path))), + ))) + + if i < len(repo.Apps)-1 { + sess.Println() } } - - return false -} - -func printRepoDetails(sess *cliui.Session, repo *github.Repository) { - sess.Println("Name: ", repo.GetName()) - sess.Println("Owner: ", repo.GetOwner().GetLogin()) - sess.Println("Description: ", repo.GetDescription()) - sess.Println("Stars ā­ļø: ", repo.GetStargazersCount()) - sess.Println("License: ", repo.GetLicense().GetName()) - sess.Printf("Updated At: %s (%s)\n", repo.GetUpdatedAt().Format(time.DateTime), humanize.Time(repo.GetUpdatedAt().Time)) - sess.Println("URL: ", repo.GetHTMLURL()) - sess.Println() - sess.Printf("šŸš€ Install via: %s\n", color.Green.Sprintf("ignite app install github.com/%s", repo.GetFullName())) } diff --git a/marketplace/cmd/list.go b/marketplace/cmd/list.go index fdb8226b..a88fc193 100644 --- a/marketplace/cmd/list.go +++ b/marketplace/cmd/list.go @@ -1,10 +1,12 @@ package cmd import ( - "context" + "fmt" + "github.com/charmbracelet/lipgloss" "github.com/dustin/go-humanize" - "github.com/google/go-github/v56/github" + "github.com/ignite/apps/marketplace/pkg/apps" + "github.com/ignite/apps/marketplace/pkg/tree" "github.com/ignite/apps/marketplace/pkg/xgithub" "github.com/ignite/cli/ignite/pkg/cliui" "github.com/spf13/cobra" @@ -12,86 +14,86 @@ import ( const ( minStarsFlag = "min-stars" - igniteAppTopic = "ignite-cli-app" - descriptionLimit = 50 + queryFlag = "query" + descriptionLimit = 75 +) + +var ( + starsCountStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("3")) + updatedAtStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("12")) ) // NewList creates a new list command that searches all the ignite apps in GitHub. func NewList() *cobra.Command { c := &cobra.Command{ - Use: "list [query]", + Use: "list", Short: "List all the ignite apps", - Args: cobra.MaximumNArgs(1), + Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - query := "" - if len(args) > 0 { - query = args[0] - } + githubToken, _ := cmd.Flags().GetString(githubTokenFlag) + query, _ := cmd.Flags().GetString(queryFlag) minStars, _ := cmd.Flags().GetUint(minStarsFlag) session := cliui.New(cliui.StartSpinner()) defer session.End() session.StartSpinner("šŸ”Ž Searching for ignite apps on GitHub...") - repos, total, err := searchIgniteApps(cmd.Context(), query, minStars) + client := xgithub.NewClient(githubToken) + repos, err := apps.Search(cmd.Context(), client, query, minStars) if err != nil { return err } session.StopSpinner() - session.Printf("šŸŽ‰ Found %d results\n", total) - - if total > 0 { - session.Println() - printRepoList(session, repos) + if len(repos) < 1 { + session.Println("āŒ No ignite application were found") + return nil } + printRepoTree(session, repos) + return nil }, } + c.Flags().StringP(queryFlag, "q", "", "Query string to search for") c.Flags().Uint(minStarsFlag, 10, "Minimum number of stars to search for") return c } -func searchIgniteApps(ctx context.Context, query string, minStars uint) ([]*github.Repository, int, error) { - client := xgithub.NewClient(githubToken) - - opts := &github.SearchOptions{ - Sort: "stars", - Order: "desc", - } - repos, total, err := client.SearchRepositories(ctx, opts, - xgithub.StringQuery(query), - xgithub.LanguageQuery("go"), - xgithub.TopicQuery(igniteAppTopic), - xgithub.MinStarsQuery(int(minStars))) - if err != nil { - return nil, 0, err +func printRepoTree(sess *cliui.Session, repos []apps.AppRepository) { + for i, repo := range repos { + node := tree.NewNode(fmt.Sprintf( + "šŸ“¦ %-50s %s %s", + repo.PackageURL, + starsCountStyle.Render(humanizeInt(repo.Stars, "ā­ļø")), + updatedAtStyle.Render("("+humanize.Time(repo.UpdatedAt)+")"), + )) + node.AddChild(nil) + for _, app := range repo.Apps { + node.AddChild(tree.NewNode(fmt.Sprintf( + "šŸ”„ %s\t%s", + app.Name, + limitTextlength(app.Description, descriptionLimit), + ))) + } + sess.Print(node) + + if i < len(repos)-1 { + sess.Println() + } } - - return repos, total, nil } -func printRepoList(sess *cliui.Session, repos []*github.Repository) { - header := []string{"Name", "Description", "Stars ā­ļø", "Updated At"} - rows := make([][]string, 0, len(repos)) - for _, repo := range repos { - rows = append(rows, []string{ - repo.GetFullName(), - limitTextlength(repo.GetDescription(), descriptionLimit), - humanize.SIWithDigits(float64(repo.GetStargazersCount()), 1, ""), - humanize.Time(repo.GetPushedAt().Time), - }) - } - - sess.PrintTable(header, rows...) +func humanizeInt(n int, unit string) string { + value, suffix := humanize.ComputeSI(float64(n)) + return humanize.FtoaWithDigits(value, 1) + suffix + " " + unit } func limitTextlength(text string, limit int) string { if len(text) > limit { - return text[:limit] + "..." + return text[:limit-3] + "..." } return text diff --git a/marketplace/cmd/marketplace.go b/marketplace/cmd/marketplace.go index 5b77c450..ac9bf1b4 100644 --- a/marketplace/cmd/marketplace.go +++ b/marketplace/cmd/marketplace.go @@ -1,12 +1,12 @@ package cmd import ( - "os" - "github.com/spf13/cobra" ) -var githubToken = os.Getenv("GITHUB_TOKEN") +const ( + githubTokenFlag = "github-token" +) // NewMarketplace creates a new marketplace command that holds // some other sub commands related to running marketplace like @@ -31,5 +31,7 @@ func NewMarketplace() *cobra.Command { NewInfo(), ) + c.Flags().String(githubTokenFlag, "", "GitHub access token") + return c } diff --git a/marketplace/go.mod b/marketplace/go.mod index 44e3c7cd..8fe3fa3e 100644 --- a/marketplace/go.mod +++ b/marketplace/go.mod @@ -3,14 +3,18 @@ module github.com/ignite/apps/marketplace go 1.21.1 require ( + github.com/blang/semver/v4 v4.0.0 + github.com/charmbracelet/lipgloss v0.9.1 github.com/dustin/go-humanize v1.0.1 + github.com/goccy/go-yaml v1.9.7 github.com/golangci/golangci-lint v1.55.2 github.com/google/go-github/v56 v56.0.0 - github.com/gookit/color v1.5.4 github.com/hashicorp/go-plugin v1.6.0 github.com/ignite/cli v0.27.2-0.20231110144902-718f27cb581d + github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.8.4 + golang.org/x/mod v0.14.0 golang.org/x/tools v0.16.0 golang.org/x/vuln v1.0.1 mvdan.cc/gofumpt v0.5.0 @@ -55,7 +59,6 @@ require ( github.com/ccojocar/zxcvbn-go v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/charithe/durationcheck v0.0.10 // indirect - github.com/charmbracelet/lipgloss v0.6.0 // indirect github.com/chavacava/garif v0.1.0 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/cloudflare/circl v1.3.3 // indirect @@ -151,7 +154,7 @@ require ( github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mbilski/exhaustivestruct v1.2.0 // indirect github.com/mgechev/revive v1.3.4 // indirect @@ -162,7 +165,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moricho/tparallel v0.3.1 // indirect github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.15.1 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/nakabonne/nestif v0.3.1 // indirect github.com/nishanths/exhaustive v0.11.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect @@ -171,7 +174,6 @@ require ( github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/polyfloyd/go-errorlint v1.4.5 // indirect github.com/prometheus/client_golang v1.14.0 // indirect @@ -222,7 +224,6 @@ require ( github.com/uudashr/gocognit v1.1.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xen0n/gosmopolitan v1.2.2 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.2.0 // indirect github.com/ykadowak/zerologlint v0.1.3 // indirect @@ -237,12 +238,12 @@ require ( golang.org/x/crypto v0.16.0 // indirect golang.org/x/exp v0.0.0-20230711153332-06a737ee72cb // indirect golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833 // indirect - golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/sync v0.5.0 // indirect golang.org/x/sys v0.15.0 // indirect golang.org/x/term v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect google.golang.org/grpc v1.56.3 // indirect google.golang.org/protobuf v1.31.0 // indirect diff --git a/marketplace/go.sum b/marketplace/go.sum index 9c208078..56389e21 100644 --- a/marketplace/go.sum +++ b/marketplace/go.sum @@ -111,6 +111,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bkielbasa/cyclop v1.2.1 h1:AeF71HZDob1P2/pRm1so9cd1alZnrpyc4q2uP2l0gJY= github.com/bkielbasa/cyclop v1.2.1/go.mod h1:K/dT/M0FPAiYjBgQGau7tz+3TMh4FWAEqlMhzFWCrgM= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= github.com/bombsimon/wsl/v3 v3.4.0 h1:RkSxjT3tmlptwfgEgTgU+KYKLI35p/tviNXNXiL2aNU= @@ -139,8 +141,8 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= -github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY= -github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= +github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= +github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -187,6 +189,7 @@ github.com/esimonov/ifshort v1.0.4/go.mod h1:Pe8zjlRrJ80+q2CxHLfEOfTwxCZ4O+MuhcH github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw= github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= @@ -227,6 +230,14 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= @@ -270,6 +281,8 @@ github.com/gobuffalo/validate/v3 v3.3.3 h1:o7wkIGSvZBYBd6ChQoLxkz2y1pfmhbI4jNJYh github.com/gobuffalo/validate/v3 v3.3.3/go.mod h1:YC7FsbJ/9hW/VjQdmXPvFqvRis4vrRYFxr69WiNZw6g= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-yaml v1.9.7 h1:D/Vx+JITklB1ugSkncB4BNR67M3X6AKs9+rqVeo3ddw= +github.com/goccy/go-yaml v1.9.7/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -370,8 +383,6 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= -github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601 h1:mrEEilTAUmaAORhssPPkxj84TsHrPMLBGW2Z4SoTxm8= github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= @@ -473,6 +484,8 @@ github.com/ldez/gomoddirectives v0.2.3 h1:y7MBaisZVDYmKvt9/l1mjNCiSA1BVn34U0ObUc github.com/ldez/gomoddirectives v0.2.3/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0= github.com/ldez/tagliatelle v0.5.0 h1:epgfuYt9v0CG3fms0pEgIMNPuFf/LpPIfjk4kyqSioo= github.com/ldez/tagliatelle v0.5.0/go.mod h1:rj1HmWiL1MiKQuOONhd09iySTEkUuE/8+5jtPYz9xa4= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leonklingele/grouper v1.1.1 h1:suWXRU57D4/Enn6pXR0QVqqWWrnJ9Osrz+5rjt8ivzU= github.com/leonklingele/grouper v1.1.1/go.mod h1:uk3I3uDfi9B6PeUjsCKi6ndcf63Uy7snXgR4yDYQVDY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -496,20 +509,19 @@ github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -536,12 +548,10 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/moricho/tparallel v0.3.1 h1:fQKD4U1wRMAYNngDonW5XupoB/ZGJHdpzrWqgyg9krA= github.com/moricho/tparallel v0.3.1/go.mod h1:leENX2cUv7Sv2qDgdi0D0fCftN8fRC67Bcn8pqzeYNI= -github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= -github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= -github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= @@ -728,8 +738,6 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= github.com/yeya24/promlinter v0.2.0 h1:xFKDQ82orCU5jQujdaD8stOHiv8UN68BSdn2a8u8Y3o= @@ -929,6 +937,7 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -958,12 +967,12 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1085,6 +1094,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= diff --git a/marketplace/main.go b/marketplace/main.go index 9fa37954..f71278ef 100644 --- a/marketplace/main.go +++ b/marketplace/main.go @@ -13,7 +13,9 @@ import ( type app struct{} func (app) Manifest(context.Context) (*plugin.Manifest, error) { - m := &plugin.Manifest{Name: "marketplace"} + m := &plugin.Manifest{ + Name: "marketplace", + } m.ImportCobraCommand(cmd.NewMarketplace(), "ignite") return m, nil } diff --git a/marketplace/pkg/apps/app_yml.go b/marketplace/pkg/apps/app_yml.go new file mode 100644 index 00000000..a5a6e5e9 --- /dev/null +++ b/marketplace/pkg/apps/app_yml.go @@ -0,0 +1,18 @@ +package apps + +import ( + semver "github.com/blang/semver/v4" +) + +// AppYML is the structure of app.ignite.yml file. +type AppYML struct { + Version semver.Version `yaml:"version"` + Apps map[string]AppInfo `yaml:"apps"` +} + +// AppInfo is the structure of app info in app.ignite.yml file which only holds +// the description and the relative path of the app. +type AppInfo struct { + Description string `yaml:"description"` + Path string `yaml:"path"` +} diff --git a/marketplace/pkg/apps/details.go b/marketplace/pkg/apps/details.go new file mode 100644 index 00000000..7ca35ffa --- /dev/null +++ b/marketplace/pkg/apps/details.go @@ -0,0 +1,122 @@ +package apps + +import ( + "context" + "fmt" + "path" + "strings" + "time" + + "github.com/google/go-github/v56/github" + "github.com/ignite/apps/marketplace/pkg/xgithub" + "github.com/pkg/errors" + "golang.org/x/mod/modfile" +) + +const igniteCLIPackage = "github.com/ignite/cli" + +// AppRepositoryDetails represents the details of an Ignite app repository. +type AppRepositoryDetails struct { + PackageURL string + Name string + Owner string + Description string + Tags []string + Stars int + License string + UpdatedAt time.Time + URL string + Apps []AppDetails +} + +// AppDetails represents the details of an Ignite app. +type AppDetails struct { + Name string + Description string + Path string + GoVersion string + IgniteVersion string +} + +// GetRepositoryDetails returns the details of an Ignite app repository. +func GetRepositoryDetails(ctx context.Context, client *xgithub.Client, pkgURL string) (*AppRepositoryDetails, error) { + repoOwner, repoName, err := validatePackageURL(pkgURL) + if err != nil { + return nil, errors.Wrap(err, "invalid package URL") + } + + repo, err := client.GetRepository(ctx, repoOwner, repoName) + if err != nil { + return nil, err + } + + appYML, err := getAppYML(ctx, client, repo) + if err != nil { + return nil, err + } + + result := &AppRepositoryDetails{ + PackageURL: pkgURL, + Name: repo.GetName(), + Owner: repo.GetOwner().GetLogin(), + Description: repo.GetDescription(), + Tags: repo.Topics, + Stars: repo.GetStargazersCount(), + License: repo.GetLicense().GetName(), + UpdatedAt: repo.GetUpdatedAt().Time, + URL: repo.GetHTMLURL(), + Apps: make([]AppDetails, 0, len(appYML.Apps)), + } + for name, info := range appYML.Apps { + goMod, err := getGoMod(ctx, client, repo, path.Clean(info.Path)) + if err != nil { + return nil, errors.Wrapf(err, "failed to get go.mod for app %s", name) + } + + result.Apps = append(result.Apps, AppDetails{ + Name: name, + Description: info.Description, + Path: info.Path, + GoVersion: goMod.Go.Version, + IgniteVersion: findCLIVersion(goMod), + }) + } + + return result, nil +} + +func validatePackageURL(pkgURL string) (owner, name string, err error) { + parts := strings.Split(pkgURL, "/") + if len(parts) != 3 { + return "", "", fmt.Errorf("package URL must be in github.com/{owner}/{repo} format") + } + if parts[0] != "github.com" { + return "", "", fmt.Errorf("only GitHub packages are supported") + } + + return parts[1], parts[2], nil +} + +func getGoMod(ctx context.Context, client *xgithub.Client, repo *github.Repository, fpath string) (*modfile.File, error) { + contents, err := client.GetFileContent(ctx, repo.GetOwner().GetLogin(), repo.GetName(), path.Join(fpath, "go.mod")) + if err != nil { + return nil, errors.Wrap(err, "failed to get file content") + } + + mod, err := modfile.Parse(fmt.Sprintf("%s/%s", repo.GetFullName(), "go.mod"), contents, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to parse go.mod") + } + + return mod, nil +} + +func findCLIVersion(modFile *modfile.File) string { + for _, require := range modFile.Require { + if strings.HasPrefix(require.Mod.Path, igniteCLIPackage) { + return require.Mod.Version + } + } + + return "" +} diff --git a/marketplace/pkg/apps/search.go b/marketplace/pkg/apps/search.go new file mode 100644 index 00000000..48bddc8b --- /dev/null +++ b/marketplace/pkg/apps/search.go @@ -0,0 +1,102 @@ +package apps + +import ( + "context" + "fmt" + "time" + + "github.com/goccy/go-yaml" + "github.com/google/go-github/v56/github" + "github.com/ignite/apps/marketplace/pkg/xgithub" + "github.com/pkg/errors" +) + +const ( + igniteAppTopic = "ignite-cli-app" + appYMLFileName = "app.ignite.yml" +) + +// AppRepository represents a GitHub repository with Ignite apps. +type AppRepository struct { + PackageURL string + Name string + Owner string + Stars int + UpdatedAt time.Time + Apps []App +} + +// App represents an Ignite app inside the repository. +type App struct { + Name string + Description string +} + +// Search searches for repositories that have ignite app topic on GitHub given the query string and the minimum number of stars +// and then fetches the app.ignite.yml file from each repository and returns the list of repositories along with their apps. +func Search(ctx context.Context, client *xgithub.Client, query string, minStars uint) ([]AppRepository, error) { + opts := &github.SearchOptions{ + Sort: "stars", + Order: "desc", + } + repos, _, err := client.SearchRepositories(ctx, opts, + xgithub.StringQuery(query), + xgithub.LanguageQuery("go"), + xgithub.TopicQuery(igniteAppTopic), + xgithub.MinStarsQuery(int(minStars))) + if err != nil { + return nil, err + } + + result := make([]AppRepository, 0, len(repos)) + for _, repo := range repos { + apps, err := listApps(ctx, client, repo) + if err != nil && !errors.Is(err, &github.RateLimitError{}) { + // Ignore the repository since it doesn't have a valid app.ignite.yml file. + continue + } + + result = append(result, AppRepository{ + PackageURL: fmt.Sprintf("github.com/%s/%s", repo.GetOwner().GetLogin(), repo.GetName()), + Name: repo.GetName(), + Owner: repo.GetOwner().GetLogin(), + Stars: repo.GetStargazersCount(), + UpdatedAt: repo.GetPushedAt().Time, + Apps: apps, + }) + } + + return result, nil +} + +func listApps(ctx context.Context, client *xgithub.Client, repo *github.Repository) ([]App, error) { + appYML, err := getAppYML(ctx, client, repo) + if err != nil { + return nil, err + } + + var apps []App + for name, info := range appYML.Apps { + apps = append(apps, App{ + Name: name, + Description: info.Description, + }) + } + + return apps, nil +} + +func getAppYML(ctx context.Context, client *xgithub.Client, repo *github.Repository) (*AppYML, error) { + data, err := client.GetFileContent(ctx, repo.GetOwner().GetLogin(), repo.GetName(), appYMLFileName) + if err != nil { + return nil, errors.Wrapf(err, "failed to get %s file content", appYMLFileName) + } + + var appYML AppYML + yaml.UnmarshalContext(ctx, data, &appYML) + if err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal %s file", appYMLFileName) + } + + return &appYML, nil +} diff --git a/marketplace/pkg/tree/tree.go b/marketplace/pkg/tree/tree.go new file mode 100644 index 00000000..a59583ae --- /dev/null +++ b/marketplace/pkg/tree/tree.go @@ -0,0 +1,56 @@ +package tree + +import ( + "fmt" + "strings" +) + +// Node represents a node in a tree. +type Node struct { + Text string + Children []*Node +} + +// NewNode creates a new node with the given text. +func NewNode(text string) *Node { + return &Node{ + Text: text, + Children: []*Node{}, + } +} + +// AddChild adds a child node to the node. +func (n *Node) AddChild(child *Node) { + n.Children = append(n.Children, child) +} + +// Format implements fmt.Formatter. +func (n *Node) Format(f fmt.State, verb rune) { + fprintNode(f, verb, "", n) +} + +func fprintNode(f fmt.State, verb rune, prefix string, n *Node) { + fmt.Fprintln(f, n.Text) + + width := 2 + if w, ok := f.Width(); ok { + width = w + } + + for i, child := range n.Children { + if child == nil { + fmt.Fprint(f, prefix) + if i < len(n.Children)-1 { + fmt.Fprintln(f, "ā”‚") + } + continue + } + if i < len(n.Children)-1 { + fmt.Fprintf(f, "%sā”œ%s ", prefix, strings.Repeat("ā”€", width)) + fprintNode(f, verb, prefix+"ā”‚"+strings.Repeat(" ", width+1), child) + } else { + fmt.Fprintf(f, "%sā””%s ", prefix, strings.Repeat("ā”€", width)) + fprintNode(f, verb, prefix+strings.Repeat(" ", width+2), child) + } + } +} diff --git a/marketplace/pkg/xgithub/github.go b/marketplace/pkg/xgithub/github.go index fb1a4fd9..a12c4262 100644 --- a/marketplace/pkg/xgithub/github.go +++ b/marketplace/pkg/xgithub/github.go @@ -5,6 +5,7 @@ import ( "strconv" "github.com/google/go-github/v56/github" + "github.com/pkg/errors" ) // Client is a wrapper around the GitHub client so that it can be used as @@ -87,3 +88,18 @@ func (c *Client) GetRepository(ctx context.Context, owner, name string) (*github return repo, nil } + +// GetFileContent gets the content of the file from GitHub given the repository name and the file path. +func (c *Client) GetFileContent(ctx context.Context, owner, repo, path string) ([]byte, error) { + file, _, _, err := c.gc.Repositories.GetContents(ctx, owner, repo, path, nil) + if err != nil { + return nil, err + } + + s, err := file.GetContent() + if err != nil { + return nil, errors.Wrap(err, "failed to get file content") + } + + return []byte(s), nil +}