-
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
496 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
Oops, something went wrong.