From db3556e3f2fba9c974c780c370ae915194c5fd64 Mon Sep 17 00:00:00 2001 From: adam Date: Wed, 21 Dec 2022 16:42:00 +0100 Subject: [PATCH] =?UTF-8?q?Add=20missing=20=F0=9F=93=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/authenticationErrorMsg.go | 5 + messages/authenticationMsg.go | 7 ++ messages/errMsg.go | 3 + messages/orgListMsg.go | 7 ++ messages/repositoryListMsg.go | 7 ++ models/mainModel.go | 16 +++ models/organisationModel.go | 160 +++++++++++++++++++++++++++++ models/organisationModel_test.go | 35 +++++++ models/userModel.go | 110 ++++++++++++++++++++ models/userModel_test.go | 39 +++++++ queries/organizationQuery.go | 55 ++++++++++ structs/organisation.go | 7 ++ structs/repository.go | 18 ++++ structs/user.go | 6 ++ utils/baseStyle.go | 7 ++ utils/yesNo.go | 8 ++ utils/yesNo_test.go | 27 +++++ 17 files changed, 517 insertions(+) create mode 100644 messages/authenticationErrorMsg.go create mode 100644 messages/authenticationMsg.go create mode 100644 messages/errMsg.go create mode 100644 messages/orgListMsg.go create mode 100644 messages/repositoryListMsg.go create mode 100644 models/mainModel.go create mode 100644 models/organisationModel.go create mode 100644 models/organisationModel_test.go create mode 100644 models/userModel.go create mode 100644 models/userModel_test.go create mode 100644 queries/organizationQuery.go create mode 100644 structs/organisation.go create mode 100644 structs/repository.go create mode 100644 structs/user.go create mode 100644 utils/baseStyle.go create mode 100644 utils/yesNo.go create mode 100644 utils/yesNo_test.go diff --git a/messages/authenticationErrorMsg.go b/messages/authenticationErrorMsg.go new file mode 100644 index 0000000..351eafc --- /dev/null +++ b/messages/authenticationErrorMsg.go @@ -0,0 +1,5 @@ +package messages + +type AuthenticationErrorMsg struct { + Err error +} diff --git a/messages/authenticationMsg.go b/messages/authenticationMsg.go new file mode 100644 index 0000000..8d00635 --- /dev/null +++ b/messages/authenticationMsg.go @@ -0,0 +1,7 @@ +package messages + +import "github.com/admcpr/hub-bub/structs" + +type AuthenticationMsg struct { + User structs.User +} diff --git a/messages/errMsg.go b/messages/errMsg.go new file mode 100644 index 0000000..56e2b21 --- /dev/null +++ b/messages/errMsg.go @@ -0,0 +1,3 @@ +package messages + +type ErrMsg struct{ Err error } diff --git a/messages/orgListMsg.go b/messages/orgListMsg.go new file mode 100644 index 0000000..3447f07 --- /dev/null +++ b/messages/orgListMsg.go @@ -0,0 +1,7 @@ +package messages + +import "github.com/admcpr/hub-bub/structs" + +type OrgListMsg struct { + Organisations []structs.Organisation +} diff --git a/messages/repositoryListMsg.go b/messages/repositoryListMsg.go new file mode 100644 index 0000000..db7d31b --- /dev/null +++ b/messages/repositoryListMsg.go @@ -0,0 +1,7 @@ +package messages + +import "github.com/admcpr/hub-bub/queries" + +type RepositoryListMsg struct { + OrganizationQuery queries.OrganizationQuery +} diff --git a/models/mainModel.go b/models/mainModel.go new file mode 100644 index 0000000..2dac01f --- /dev/null +++ b/models/mainModel.go @@ -0,0 +1,16 @@ +package models + +import tea "github.com/charmbracelet/bubbletea" + +/* +Model management +Need to replace this with the a MainModel, see nested models youtube +*/ +type modelName int + +var MainModel []tea.Model + +const ( + UserModelName modelName = iota + OrganisationModelName +) diff --git a/models/organisationModel.go b/models/organisationModel.go new file mode 100644 index 0000000..8374944 --- /dev/null +++ b/models/organisationModel.go @@ -0,0 +1,160 @@ +package models + +import ( + "log" + + "github.com/admcpr/hub-bub/messages" + "github.com/admcpr/hub-bub/queries" + "github.com/admcpr/hub-bub/structs" + "github.com/admcpr/hub-bub/utils" + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/cli/go-gh" + graphql "github.com/cli/shurcooL-graphql" +) + +/* Repository model */ +type OrganisationModel struct { + Title string + Url string + RepositoryTable table.Model +} + +func (m OrganisationModel) Init() tea.Cmd { + return nil +} + +func (m OrganisationModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + + case messages.RepositoryListMsg: + // m.RepositoryTable = buildRepositoryTable(msg.Repositories) + m.RepositoryTable = buildRepositoryTable(msg.OrganizationQuery) + return m, nil + + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + case "enter", " ": + return m, tea.Batch( + tea.Printf("Let's go to %s!", m.RepositoryTable.SelectedRow()[1]), + ) + case "esc": + return MainModel[UserModelName], nil + } + } + + m.RepositoryTable, cmd = m.RepositoryTable.Update(msg) + + return m, cmd +} + +// View implements tea.Model +func (m OrganisationModel) View() string { + return utils.BaseStyle.Render(m.RepositoryTable.View()) + "\n" +} + +func (m OrganisationModel) GetRepositories() tea.Msg { + client, err := gh.GQLClient(nil) + if err != nil { + return messages.AuthenticationErrorMsg{Err: err} + } + + var organizationQuery = queries.OrganizationQuery{} + + variables := map[string]interface{}{ + "login": graphql.String(m.Title), + "first": graphql.Int(30), + } + err = client.Query("OrganizationRepositories", &organizationQuery, variables) + if err != nil { + log.Fatal(err) + } + + return messages.RepositoryListMsg{OrganizationQuery: organizationQuery} +} + +func buildOrganisationTable(organisations []structs.Organisation) table.Model { + columns := []table.Column{ + {Title: "Login", Width: 20}, + {Title: "Url", Width: 80}, + } + + rows := make([]table.Row, len(organisations)) + for i, org := range organisations { + rows[i] = table.Row{org.Login, org.Url} + } + + t := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(len(organisations)), + ) + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")). + BorderBottom(true). + Bold(false) + s.Selected = s.Selected. + Foreground(lipgloss.Color("229")). + Background(lipgloss.Color("57")). + Bold(false) + t.SetStyles(s) + + return t +} + +func buildRepositoryTable(organizationQuery queries.OrganizationQuery) table.Model { + columns := []table.Column{ + {Title: "Name", Width: 20}, + {Title: "Issues", Width: 10}, + {Title: "Wiki", Width: 10}, + {Title: "Projects", Width: 10}, + {Title: "Rebase Merge", Width: 10}, + {Title: "Auto Merge", Width: 10}, + {Title: "Delete Branch On Merge", Width: 10}, + } + + edges := organizationQuery.Organization.Repositories.Edges + + rows := make([]table.Row, len(edges)) + for i, repo := range edges { + rows[i] = table.Row{ + repo.Node.Name, + utils.YesNo(repo.Node.HasIssuesEnabled), + utils.YesNo(repo.Node.HasWikiEnabled), + utils.YesNo(repo.Node.HasProjectsEnabled), + utils.YesNo(repo.Node.RebaseMergeAllowed), + utils.YesNo(repo.Node.AutoMergeAllowed), + utils.YesNo(repo.Node.DeleteBranchOnMerge), + } + } + + t := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(len(edges)), + ) + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")). + BorderBottom(true). + Bold(false) + s.Selected = s.Selected. + Foreground(lipgloss.Color("229")). + Background(lipgloss.Color("57")). + Bold(false) + t.SetStyles(s) + + return t +} diff --git a/models/organisationModel_test.go b/models/organisationModel_test.go new file mode 100644 index 0000000..4f538ef --- /dev/null +++ b/models/organisationModel_test.go @@ -0,0 +1,35 @@ +package models + +import ( + "reflect" + "testing" + + tea "github.com/charmbracelet/bubbletea" +) + +func TestOrganisationModel_Update(t *testing.T) { + type args struct { + msg tea.Msg + } + tests := []struct { + name string + m OrganisationModel + args args + wantModel tea.Model + wantCmd tea.Cmd + }{ + // TODO: Add more test cases. + {"Quit KeyMsg", OrganisationModel{}, args{tea.KeyMsg{Type: tea.KeyCtrlC}}, OrganisationModel{}, tea.Quit}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotModel, gotCmd := tt.m.Update(tt.args.msg) + if !reflect.DeepEqual(gotModel, tt.wantModel) { + t.Errorf("OrganisationModel.Update() gotModel = %v, want %v", gotModel, tt.wantModel) + } + if reflect.ValueOf(gotCmd) != reflect.ValueOf(tt.wantCmd) { + t.Errorf("OrganisationModel.Update() gotCmd = %v, want %v", gotCmd, tt.wantCmd) + } + }) + } +} diff --git a/models/userModel.go b/models/userModel.go new file mode 100644 index 0000000..35c0485 --- /dev/null +++ b/models/userModel.go @@ -0,0 +1,110 @@ +package models + +import ( + "fmt" + + "github.com/admcpr/hub-bub/messages" + "github.com/admcpr/hub-bub/structs" + "github.com/admcpr/hub-bub/utils" + "github.com/cli/go-gh" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" +) + +type UserModel struct { + Authenticated bool + User structs.User + SelectedOrgUrl string + OrganisationTable table.Model +} + +func (m UserModel) Init() tea.Cmd { + return checkLoginStatus +} + +func (m UserModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + + case messages.AuthenticationMsg: + m.Authenticated = true + m.User = msg.User + return m, getOrganisations + + case messages.AuthenticationErrorMsg: + m.Authenticated = false + return m, nil + + case messages.OrgListMsg: + m.OrganisationTable = buildOrganisationTable(msg.Organisations) + return m, nil + + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + case "enter", " ": + MainModel[UserModelName] = m + orgModel := &OrganisationModel{ + Title: m.OrganisationTable.SelectedRow()[0], + Url: m.OrganisationTable.SelectedRow()[1], + } + + MainModel[OrganisationModelName] = orgModel + + return orgModel, orgModel.GetRepositories + } + } + + m.OrganisationTable, cmd = m.OrganisationTable.Update(msg) + + return m, cmd +} + +func (m UserModel) View() string { + s := fmt.Sprintln("Press q to quit.") + + if !m.Authenticated { + return fmt.Sprintln("You are not authenticated try running `gh auth login`") + } + + s += fmt.Sprintf("Hello %s, press Enter to select an organisation.\n", m.User.Name) + s += utils.BaseStyle.Render(m.OrganisationTable.View()) + "\n" + + return s +} + +func checkLoginStatus() tea.Msg { + // Use an API helper to grab repository tags + client, err := gh.RESTClient(nil) + if err != nil { + return messages.AuthenticationErrorMsg{Err: err} + } + response := structs.User{} + + err = client.Get("user", &response) + if err != nil { + fmt.Println(err) + return messages.AuthenticationErrorMsg{Err: err} + } + + return messages.AuthenticationMsg{User: response} +} + +func getOrganisations() tea.Msg { + client, err := gh.RESTClient(nil) + if err != nil { + return messages.AuthenticationErrorMsg{Err: err} + } + response := []structs.Organisation{} + + err = client.Get("user/orgs", &response) + if err != nil { + fmt.Println(err) + return messages.ErrMsg{Err: err} + } + + return messages.OrgListMsg{Organisations: response} +} diff --git a/models/userModel_test.go b/models/userModel_test.go new file mode 100644 index 0000000..518ecd5 --- /dev/null +++ b/models/userModel_test.go @@ -0,0 +1,39 @@ +package models + +import ( + "reflect" + "testing" + + messages "github.com/admcpr/hub-bub/messages" + structs "github.com/admcpr/hub-bub/structs" + tea "github.com/charmbracelet/bubbletea" +) + +func TestUserModel_Update(t *testing.T) { + testUser := structs.User{Login: "test"} + type args struct { + msg tea.Msg + } + tests := []struct { + name string + m UserModel + args args + wantModel tea.Model + wantCmd tea.Cmd + }{ + // TODO: Add test cases. + {"Authentication Success", UserModel{}, args{messages.AuthenticationMsg{User: testUser}}, UserModel{Authenticated: true, User: testUser}, getOrganisations}, + {"Authentication Failure", UserModel{}, args{messages.AuthenticationErrorMsg{}}, UserModel{Authenticated: false}, nil}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotModel, gotCmd := tt.m.Update(tt.args.msg) + if !reflect.DeepEqual(gotModel, tt.wantModel) { + t.Errorf("UserModel.Update() gotModel = %v, want %v", gotModel, tt.wantModel) + } + if reflect.ValueOf(gotCmd) != reflect.ValueOf(tt.wantCmd) { + t.Errorf("UserModel.Update() gotCmd = %v, want %v", gotCmd, tt.wantCmd) + } + }) + } +} diff --git a/queries/organizationQuery.go b/queries/organizationQuery.go new file mode 100644 index 0000000..c0dbf08 --- /dev/null +++ b/queries/organizationQuery.go @@ -0,0 +1,55 @@ +package queries + +import ( + "time" +) + +type OrganizationQuery struct { + Organization struct { + Id string + Repositories struct { + Edges []struct { + Node struct { + Name string + Id string + AutoMergeAllowed bool + DeleteBranchOnMerge bool + RebaseMergeAllowed bool + HasDiscussionsEnabled bool + HasIssuesEnabled bool + HasWikiEnabled bool + HasProjectsEnabled bool + IsArchived bool + IsDisabled bool + IsFork bool + IsLocked bool + IsMirror bool + IsPrivate bool + IsTemplate bool + StargazerCount int + SquashMergeAllowed bool + UpdatedAt time.Time + DefaultBranchRef struct { + Name string + BranchProtectionRule struct { + AllowsDeletions bool + AllowsForcePushes bool + DismissesStaleReviews bool + IsAdminEnforced bool + RequiredApprovingReviewCount int + RequiresApprovingReviews bool + RequiresCodeOwnerReviews bool + RequiresCommitSignatures bool + RequiresConversationResolution bool + RequiresLinearHistory bool + RequiresStatusChecks bool + } `graphql:"branchProtectionRule"` + } `graphql:"defaultBranchRef"` + VulnerabilityAlerts struct { + TotalCount int + } `graphql:"vulnerabilityAlerts"` + } + } `graphql:"edges"` + } `graphql:"repositories(first: $first)"` + } `graphql:"organization(login: $login)"` +} diff --git a/structs/organisation.go b/structs/organisation.go new file mode 100644 index 0000000..c6ea8fa --- /dev/null +++ b/structs/organisation.go @@ -0,0 +1,7 @@ +package structs + +type Organisation struct { + Login string `json:"login"` + Url string `json:"url"` + Repositories []Repository +} diff --git a/structs/repository.go b/structs/repository.go new file mode 100644 index 0000000..8a85b9b --- /dev/null +++ b/structs/repository.go @@ -0,0 +1,18 @@ +package structs + +type Repository struct { + Name string `json:"name"` + DefaultBranch string `json:"default_branch"` + DeleteBranchOnMerge bool `json:"delete_branch_on_merge"` + HasPages bool `json:"has_pages"` + HasIssues bool `json:"has_issues"` + HasProjects bool `json:"has_projects"` + HasWiki bool `json:"has_wiki"` + IsPrivate bool `json:"private"` + IsTemplate bool `json:"is_template"` + IsArchived bool `json:"archived"` + AllowAutoMerge bool `json:"allow_auto_merge"` + AllowRebaseMerge bool `json:"allow_rebase_merge"` + AllowMergeCommit bool `json:"allow_merge_commit"` + Url string `json:"url"` +} diff --git a/structs/user.go b/structs/user.go new file mode 100644 index 0000000..c5618e2 --- /dev/null +++ b/structs/user.go @@ -0,0 +1,6 @@ +package structs + +type User struct { + Name string + Login string +} diff --git a/utils/baseStyle.go b/utils/baseStyle.go new file mode 100644 index 0000000..9924621 --- /dev/null +++ b/utils/baseStyle.go @@ -0,0 +1,7 @@ +package utils + +import "github.com/charmbracelet/lipgloss" + +var BaseStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")) diff --git a/utils/yesNo.go b/utils/yesNo.go new file mode 100644 index 0000000..fd5fa8d --- /dev/null +++ b/utils/yesNo.go @@ -0,0 +1,8 @@ +package utils + +func YesNo(b bool) string { + if b { + return "Yes" + } + return "No" +} diff --git a/utils/yesNo_test.go b/utils/yesNo_test.go new file mode 100644 index 0000000..67d915a --- /dev/null +++ b/utils/yesNo_test.go @@ -0,0 +1,27 @@ +package utils + +import ( + "testing" +) + +func TestYesNo(t *testing.T) { + type args struct { + b bool + } + tests := []struct { + name string + args args + want string + }{ + // TODO: Add test cases. + {"Yes", args{true}, "Yes"}, + {"No", args{false}, "No"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := YesNo(tt.args.b); got != tt.want { + t.Errorf("YesNo() = %v, want %v", got, tt.want) + } + }) + } +}