Skip to content

Commit

Permalink
create pizza show
Browse files Browse the repository at this point in the history
  • Loading branch information
k1nho committed Aug 21, 2023
1 parent ec2b357 commit 9170b6c
Show file tree
Hide file tree
Showing 7 changed files with 496 additions and 0 deletions.
2 changes: 2 additions & 0 deletions cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/open-sauced/pizza-cli/cmd/auth"
"github.com/open-sauced/pizza-cli/cmd/bake"
repoquery "github.com/open-sauced/pizza-cli/cmd/repo-query"
"github.com/open-sauced/pizza-cli/cmd/show"
"github.com/open-sauced/pizza-cli/pkg/api"
)

Expand All @@ -27,6 +28,7 @@ func NewRootCommand() (*cobra.Command, error) {
cmd.AddCommand(bake.NewBakeCommand())
cmd.AddCommand(repoquery.NewRepoQueryCommand())
cmd.AddCommand(auth.NewLoginCommand())
cmd.AddCommand(show.NewShowCommand())

return cmd, nil
}
Expand Down
278 changes: 278 additions & 0 deletions cmd/show/show.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
// Package show contains the bootstrapping and tooling for the pizza show
// cobra command
package show

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"

"github.com/charmbracelet/bubbles/table"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
)

// Options are the options for the pizza show command including user
// defined configurations
type Options struct {
// RepoName is the git repo name to get statistics
RepoName string

// Page is the page to be requested
Page int

// Limit is the number of records to be retrieved
Limit int

// Range is the number of days to take into account when retrieving statistics
Range int
}

const showLongDesc string = `WARNING: Proof of concept feature.
The show command accepts the name of a git repository in the format 'owner/name' and uses OpenSauced api
to retrieve metrics of the repository to be displayed as a TUI.`

// NewBakeCommand returns a new cobra command for 'pizza bake'
func NewShowCommand() *cobra.Command {
opts := &Options{}

cmd := &cobra.Command{
Use: "show repository-name [flags]",
Short: "Get visual metrics of a repository",
Long: showLongDesc,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("must specify the URL of a git repository to analyze")
}
opts.RepoName = args[0]
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return run(opts)
},
}

cmd.Flags().IntVarP(&opts.Range, "range", "r", 30, "The last N number of days to consider")
cmd.Flags().IntVarP(&opts.Limit, "limit", "l", 10, "The number of records to retrieve")
cmd.Flags().IntVarP(&opts.Page, "page", "p", 1, "The page number to retrieve")

return cmd
}

type Author struct {
Name string `json:"author_login"`
}

type Metadata struct {
Page int `json:"page"`
Limit int `json:"limit"`
ItemCount int `json:"itemCount"`
PageCount int `json:"pageCount"`
HasPreviousPage bool `json:"hasPreviousPage"`
HasNextPage bool `json:"hasNextPage"`
}

type showRespose struct {
Contributors []Author `json:"data"`
Metadata Metadata `json:"meta"`
}

type showResponseResult struct {
ShowRespose showRespose
Error error
}

type reposResponse struct {
ID int `json:"id"`
Size int `json:"size"`
Issues int `json:"issues"`
Stars int `json:"stars"`
Forks int `json:"forks"`
Watchers int `json:"watchers"`
Subscribers int `json:"subscribers"`
Network int `json:"network"`
IsFork bool `json:"is_fork"`
IsPrivate bool `json:"is_private"`
IsTemplate bool `json:"is_template"`
IsArchived bool `json:"is_archived"`
IsDisabled bool `json:"is_disabled"`
HasIssues bool `json:"has_issues"`
HasProjects bool `json:"has_projects"`
HasDownloads bool `json:"has_downloads"`
HasWiki bool `json:"has_wiki"`
HasPages bool `json:"has_pages"`
HasDisscussions bool `json:"has_discussions"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
PushedAt time.Time `json:"pushed_at"`
DefaultBranch string `json:"default_branch"`
NodeID string `json:"node_id"`
GitURL string `json:"git_url"`
SSHURL string `json:"ssh_url"`
CloneURL string `json:"clone_url"`
SVNURL string `json:"svn_url"`
MirrorURL string `json:"mirror_url"`
Name string `json:"name"`
FullName string `json:"full_name"`
Description string `json:"description"`
Language string `json:"language"`
License string `json:"license"`
URL string `json:"url"`
Homepage string `json:"homepage"`
Topics []string `json:"topics"`
ContributionCount int `json:"contributionsCount"`
VotesCount int `json:"votesCount"`
SubmissionCount int `json:"submissionsCount"`
StargazersCount int `json:"stargazersCount"`
StarsCount int `json:"starsCount"`
}

func run(opts *Options) error {

err := validateShowQuery(opts)
if err != nil {
return err
}

var client = &http.Client{Timeout: 10 * time.Second}

// retrieve repository info
resp, err := client.Get(fmt.Sprintf("https://api.opensauced.pizza/v1/repos/%s", opts.RepoName))
if err != nil {
return err
}
defer resp.Body.Close()

var repoInfo reposResponse
err = json.NewDecoder(resp.Body).Decode(&repoInfo)
if err != nil {
return err
}

if repoInfo.ID == 0 {
return errors.New("could not find repository")
}

newContributors, err := FetchNewContributors(opts, repoInfo.ID)
if err != nil {
return err
}

churnContributors, err := FetchChurnedContributors(opts, repoInfo.ID)
if err != nil {
return err
}

// create model for the TUI
model := Model{
RepositoryInfo: repoInfo,
newContributorsTable: setupTable(newContributors.Contributors),
churnContributorsTable: setupTable(churnContributors.Contributors),
curView: 0,
}

// Load the pizza TUI
pizzaTUI(model)

return nil
}

func validateShowQuery(opts *Options) error {
if opts.RepoName == "" {
return errors.New("no repository was provided")
}

if len(strings.Split(opts.RepoName, "/")) != 2 {
return errors.New("wrong repository name format. Must be in the form owner/name, i.e(open-sauced/pizza)")
}
if opts.Limit < 1 {
return errors.New("--limit flag must be a positive integer value")
}
if opts.Range < 1 {
return errors.New("--range flag must be a positive integer value")
}

if opts.Page < 1 {
return errors.New("--page flag must be a positive integer value")
}

return nil
}

// FetchNewContributors: Returns all the new contributors
func FetchNewContributors(opts *Options, repoID int) (showRespose, error) {
var client = &http.Client{Timeout: 10 * time.Second}
var newContributors showRespose

resp, err := client.Get(fmt.Sprintf("https://api.opensauced.pizza/v1/contributors/insights/new?page=%d&limit=%d&range=%d&repoIds=%d",
opts.Page, opts.Limit, opts.Range, repoID))
if err != nil {
return newContributors, err
}
defer resp.Body.Close()

err = json.NewDecoder(resp.Body).Decode(&newContributors)
if err != nil {
return newContributors, err
}
return newContributors, nil
}

// FetchChurnedContributors: Returns all churned contributors
func FetchChurnedContributors(opts *Options, repoID int) (showRespose, error) {
// retrieve churn contributors info
var client = &http.Client{Timeout: 10 * time.Second}
var churnContributors showRespose

resp, err := client.Get(fmt.Sprintf("https://api.opensauced.pizza/v1/contributors/insights/churn?page=%d&limit=%d&range=%d&repoIds=%d",
opts.Page, opts.Limit, opts.Range, repoID))
if err != nil {
return churnContributors, err
}
defer resp.Body.Close()

err = json.NewDecoder(resp.Body).Decode(&churnContributors)
if err != nil {
return churnContributors, err
}

return churnContributors, nil
}

func setupTable(contributors []Author) table.Model {
columns := []table.Column{
{Title: "ID", Width: 5},
{Title: "Name", Width: 20},
}

rows := []table.Row{}

for i, contributor := range contributors {
rows = append(rows, table.Row{strconv.Itoa(i), contributor.Name})
}

contributorTable := table.New(
table.WithColumns(columns),
table.WithRows(rows),
table.WithFocused(true),
table.WithHeight(15),
)

s := table.DefaultStyles()
s.Header = s.Header.
BorderStyle(lipgloss.NormalBorder()).
BorderBottom(true).
Bold(true)

s.Selected = s.Selected.
Foreground(lipgloss.Color("#FFFFFF")).
Background(lipgloss.Color("#FF4500"))

contributorTable.SetStyles(s)
return contributorTable
}
25 changes: 25 additions & 0 deletions cmd/show/styles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package show

import "github.com/charmbracelet/lipgloss"

// Container: container styling (width: 80, py: 0, px: 5)
var container = lipgloss.NewStyle().Width(80).Padding(0, 5)

// WidgetContainer: container for tables, and graphs (py:2, px:2)
var widgetContainer = lipgloss.NewStyle().Padding(2, 2)

// TableTitle: The style for table titles (width:25, align-horizontal:center, bold:true)
var tableTitle = lipgloss.NewStyle().Width(25).AlignHorizontal(lipgloss.Center).Bold(true)

// Color: the color palette (Light: #000000, Dark: #FF4500)
var color = lipgloss.AdaptiveColor{Light: "#000000", Dark: "#FF4500"}

// ActiveStyle: table when selected (border:normal, border-foreground:#FF4500)
var activeStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("#FF4500"))

// InactiveStyle: table when unselected (border: normal, border-foreground:#FFFFFF)
var inactiveStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("#FFFFFF"))
76 changes: 76 additions & 0 deletions cmd/show/tui.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package show

import (
"fmt"
"os"

"github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)

// ██████╗ ██╗███████╗███████╗ █████╗
// ██╔══██╗██║╚══███╔╝╚══███╔╝██╔══██╗
// ██████╔╝██║ ███╔╝ ███╔╝ ███████║
// ██╔═══╝ ██║ ███╔╝ ███╔╝ ██╔══██║
// ██║ ██║███████╗███████╗██║ ██║
// ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝

const (
newContributorsView = iota
churnContributorsView
)

type Model struct {
newContributorsTable table.Model
churnContributorsTable table.Model
RepositoryInfo reposResponse
curView int
}

// View: the view of the TUI
func (m Model) View() string {
titleView, repoInfoView, metricsView := m.drawTitle(), m.drawRepositoryInfo(), m.drawMetrics()
mainView := lipgloss.JoinVertical(lipgloss.Center, titleView, repoInfoView, metricsView)

return mainView
}

// Init: initial IO before program start
func (m Model) Init() tea.Cmd { return nil }

// Update: Handle IO and Commands
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "right", "l":
m.curView = (m.curView + 1) % 2
case "left", "h":
if m.curView-1 <= 0 {
m.curView = 0
} else {
m.curView--
}
case "q", "ctrl+c":
return m, tea.Quit
}

switch m.curView {
case newContributorsView:
m.newContributorsTable, cmd = m.newContributorsTable.Update(msg)
case churnContributorsView:
m.churnContributorsTable, cmd = m.churnContributorsTable.Update(msg)
}
}

return m, cmd
}

func pizzaTUI(model Model) {
if _, err := tea.NewProgram(model, tea.WithAltScreen()).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
Loading

0 comments on commit 9170b6c

Please sign in to comment.