From aa9ca4311560e121fd922c736470d59c2067845c Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Thu, 23 Jan 2025 21:02:25 +0000 Subject: [PATCH 01/12] refactor: move several packages to internal folder --- {ascii => internal/ascii}/block.go | 0 {ascii => internal/ascii}/block_test.go | 0 {ascii => internal/ascii}/generator.go | 2 +- {ascii => internal/ascii}/generator_test.go | 2 +- {ascii => internal/ascii}/text.go | 0 {ascii => internal/ascii}/text_test.go | 0 {errors => internal/errors}/errors.go | 0 {errors => internal/errors}/errors_test.go | 0 {github => internal/github}/client.go | 4 ++-- {github => internal/github}/client_test.go | 6 +++--- {logger => internal/logger}/logger.go | 0 {logger => internal/logger}/logger_test.go | 0 {stl => internal/stl}/generator.go | 8 ++++---- {stl => internal/stl}/generator_test.go | 2 +- {stl => internal/stl}/geometry/assets.go | 2 +- .../stl}/geometry/assets/invertocat.png | Bin .../stl}/geometry/assets/monasans-medium.ttf | Bin .../stl}/geometry/assets/monasans-regular.ttf | Bin {stl => internal/stl}/geometry/assets_test.go | 0 {stl => internal/stl}/geometry/geometry.go | 2 +- {stl => internal/stl}/geometry/geometry_test.go | 2 +- {stl => internal/stl}/geometry/shapes.go | 4 ++-- {stl => internal/stl}/geometry/shapes_test.go | 2 +- {stl => internal/stl}/geometry/text.go | 4 ++-- {stl => internal/stl}/geometry/text_test.go | 0 {stl => internal/stl}/geometry/vector.go | 4 ++-- {stl => internal/stl}/geometry/vector_test.go | 2 +- {stl => internal/stl}/stl.go | 6 +++--- {stl => internal/stl}/stl_test.go | 2 +- {testutil => internal/testutil}/fixtures/github.go | 2 +- {testutil => internal/testutil}/mocks/github.go | 4 ++-- {types => internal/types}/types.go | 0 {types => internal/types}/types_test.go | 0 main.go | 12 ++++++------ main_test.go | 6 +++--- 35 files changed, 39 insertions(+), 39 deletions(-) rename {ascii => internal/ascii}/block.go (100%) rename {ascii => internal/ascii}/block_test.go (100%) rename {ascii => internal/ascii}/generator.go (98%) rename {ascii => internal/ascii}/generator_test.go (99%) rename {ascii => internal/ascii}/text.go (100%) rename {ascii => internal/ascii}/text_test.go (100%) rename {errors => internal/errors}/errors.go (100%) rename {errors => internal/errors}/errors_test.go (100%) rename {github => internal/github}/client.go (97%) rename {github => internal/github}/client_test.go (97%) rename {logger => internal/logger}/logger.go (100%) rename {logger => internal/logger}/logger_test.go (100%) rename {stl => internal/stl}/generator.go (98%) rename {stl => internal/stl}/generator_test.go (99%) rename {stl => internal/stl}/geometry/assets.go (98%) rename {stl => internal/stl}/geometry/assets/invertocat.png (100%) rename {stl => internal/stl}/geometry/assets/monasans-medium.ttf (100%) rename {stl => internal/stl}/geometry/assets/monasans-regular.ttf (100%) rename {stl => internal/stl}/geometry/assets_test.go (100%) rename {stl => internal/stl}/geometry/geometry.go (98%) rename {stl => internal/stl}/geometry/geometry_test.go (98%) rename {stl => internal/stl}/geometry/shapes.go (97%) rename {stl => internal/stl}/geometry/shapes_test.go (99%) rename {stl => internal/stl}/geometry/text.go (98%) rename {stl => internal/stl}/geometry/text_test.go (100%) rename {stl => internal/stl}/geometry/vector.go (95%) rename {stl => internal/stl}/geometry/vector_test.go (98%) rename {stl => internal/stl}/stl.go (97%) rename {stl => internal/stl}/stl_test.go (98%) rename {testutil => internal/testutil}/fixtures/github.go (96%) rename {testutil => internal/testutil}/mocks/github.go (94%) rename {types => internal/types}/types.go (100%) rename {types => internal/types}/types_test.go (100%) diff --git a/ascii/block.go b/internal/ascii/block.go similarity index 100% rename from ascii/block.go rename to internal/ascii/block.go diff --git a/ascii/block_test.go b/internal/ascii/block_test.go similarity index 100% rename from ascii/block_test.go rename to internal/ascii/block_test.go diff --git a/ascii/generator.go b/internal/ascii/generator.go similarity index 98% rename from ascii/generator.go rename to internal/ascii/generator.go index 87954dc..1044619 100644 --- a/ascii/generator.go +++ b/internal/ascii/generator.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/internal/types" ) // ErrInvalidGrid is returned when the contribution grid is invalid diff --git a/ascii/generator_test.go b/internal/ascii/generator_test.go similarity index 99% rename from ascii/generator_test.go rename to internal/ascii/generator_test.go index 4ee56c9..7cdaf8c 100644 --- a/ascii/generator_test.go +++ b/internal/ascii/generator_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/internal/types" ) func TestGenerateASCII(t *testing.T) { diff --git a/ascii/text.go b/internal/ascii/text.go similarity index 100% rename from ascii/text.go rename to internal/ascii/text.go diff --git a/ascii/text_test.go b/internal/ascii/text_test.go similarity index 100% rename from ascii/text_test.go rename to internal/ascii/text_test.go diff --git a/errors/errors.go b/internal/errors/errors.go similarity index 100% rename from errors/errors.go rename to internal/errors/errors.go diff --git a/errors/errors_test.go b/internal/errors/errors_test.go similarity index 100% rename from errors/errors_test.go rename to internal/errors/errors_test.go diff --git a/github/client.go b/internal/github/client.go similarity index 97% rename from github/client.go rename to internal/github/client.go index 54e641b..8694f44 100644 --- a/github/client.go +++ b/internal/github/client.go @@ -6,8 +6,8 @@ import ( "fmt" "time" - "github.com/github/gh-skyline/errors" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/internal/errors" + "github.com/github/gh-skyline/internal/types" ) // APIClient interface defines the methods we need from the client diff --git a/github/client_test.go b/internal/github/client_test.go similarity index 97% rename from github/client_test.go rename to internal/github/client_test.go index 1d822ad..c83ede9 100644 --- a/github/client_test.go +++ b/internal/github/client_test.go @@ -4,9 +4,9 @@ import ( "testing" "time" - "github.com/github/gh-skyline/errors" - "github.com/github/gh-skyline/testutil/mocks" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/internal/errors" + "github.com/github/gh-skyline/internal/testutil/mocks" + "github.com/github/gh-skyline/internal/types" ) func TestGetAuthenticatedUser(t *testing.T) { diff --git a/logger/logger.go b/internal/logger/logger.go similarity index 100% rename from logger/logger.go rename to internal/logger/logger.go diff --git a/logger/logger_test.go b/internal/logger/logger_test.go similarity index 100% rename from logger/logger_test.go rename to internal/logger/logger_test.go diff --git a/stl/generator.go b/internal/stl/generator.go similarity index 98% rename from stl/generator.go rename to internal/stl/generator.go index 98cde8b..8dfb5bb 100644 --- a/stl/generator.go +++ b/internal/stl/generator.go @@ -4,10 +4,10 @@ import ( "fmt" "sync" - "github.com/github/gh-skyline/errors" - "github.com/github/gh-skyline/logger" - "github.com/github/gh-skyline/stl/geometry" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/internal/errors" + "github.com/github/gh-skyline/internal/logger" + "github.com/github/gh-skyline/internal/stl/geometry" + "github.com/github/gh-skyline/internal/types" ) // GenerateSTL creates a 3D model from GitHub contribution data and writes it to an STL file. diff --git a/stl/generator_test.go b/internal/stl/generator_test.go similarity index 99% rename from stl/generator_test.go rename to internal/stl/generator_test.go index 3ad8b17..eea9421 100644 --- a/stl/generator_test.go +++ b/internal/stl/generator_test.go @@ -7,7 +7,7 @@ import ( "sync" "testing" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/internal/types" ) // Test data setup diff --git a/stl/geometry/assets.go b/internal/stl/geometry/assets.go similarity index 98% rename from stl/geometry/assets.go rename to internal/stl/geometry/assets.go index 6f8023e..0f09b77 100644 --- a/stl/geometry/assets.go +++ b/internal/stl/geometry/assets.go @@ -5,7 +5,7 @@ import ( "fmt" "os" - "github.com/github/gh-skyline/errors" + "github.com/github/gh-skyline/internal/errors" ) //go:embed assets/* diff --git a/stl/geometry/assets/invertocat.png b/internal/stl/geometry/assets/invertocat.png similarity index 100% rename from stl/geometry/assets/invertocat.png rename to internal/stl/geometry/assets/invertocat.png diff --git a/stl/geometry/assets/monasans-medium.ttf b/internal/stl/geometry/assets/monasans-medium.ttf similarity index 100% rename from stl/geometry/assets/monasans-medium.ttf rename to internal/stl/geometry/assets/monasans-medium.ttf diff --git a/stl/geometry/assets/monasans-regular.ttf b/internal/stl/geometry/assets/monasans-regular.ttf similarity index 100% rename from stl/geometry/assets/monasans-regular.ttf rename to internal/stl/geometry/assets/monasans-regular.ttf diff --git a/stl/geometry/assets_test.go b/internal/stl/geometry/assets_test.go similarity index 100% rename from stl/geometry/assets_test.go rename to internal/stl/geometry/assets_test.go diff --git a/stl/geometry/geometry.go b/internal/stl/geometry/geometry.go similarity index 98% rename from stl/geometry/geometry.go rename to internal/stl/geometry/geometry.go index e804771..89f3583 100644 --- a/stl/geometry/geometry.go +++ b/internal/stl/geometry/geometry.go @@ -3,7 +3,7 @@ package geometry import ( "math" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/internal/types" ) // Model dimension constants define the basic measurements for the 3D model. diff --git a/stl/geometry/geometry_test.go b/internal/stl/geometry/geometry_test.go similarity index 98% rename from stl/geometry/geometry_test.go rename to internal/stl/geometry/geometry_test.go index fe9906a..63879eb 100644 --- a/stl/geometry/geometry_test.go +++ b/internal/stl/geometry/geometry_test.go @@ -4,7 +4,7 @@ import ( "math" "testing" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/internal/types" ) const ( diff --git a/stl/geometry/shapes.go b/internal/stl/geometry/shapes.go similarity index 97% rename from stl/geometry/shapes.go rename to internal/stl/geometry/shapes.go index cbe5958..5f731db 100644 --- a/stl/geometry/shapes.go +++ b/internal/stl/geometry/shapes.go @@ -2,8 +2,8 @@ package geometry import ( - "github.com/github/gh-skyline/errors" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/internal/errors" + "github.com/github/gh-skyline/internal/types" ) // CreateQuad creates two triangles forming a quadrilateral from four vertices. diff --git a/stl/geometry/shapes_test.go b/internal/stl/geometry/shapes_test.go similarity index 99% rename from stl/geometry/shapes_test.go rename to internal/stl/geometry/shapes_test.go index 31e7b4d..7b9b785 100644 --- a/stl/geometry/shapes_test.go +++ b/internal/stl/geometry/shapes_test.go @@ -4,7 +4,7 @@ import ( "math" "testing" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/internal/types" ) // TestCreateCuboidBase verifies cuboid base generation functionality. diff --git a/stl/geometry/text.go b/internal/stl/geometry/text.go similarity index 98% rename from stl/geometry/text.go rename to internal/stl/geometry/text.go index bee5245..e29127a 100644 --- a/stl/geometry/text.go +++ b/internal/stl/geometry/text.go @@ -6,8 +6,8 @@ import ( "os" "github.com/fogleman/gg" - "github.com/github/gh-skyline/errors" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/internal/errors" + "github.com/github/gh-skyline/internal/types" ) const ( diff --git a/stl/geometry/text_test.go b/internal/stl/geometry/text_test.go similarity index 100% rename from stl/geometry/text_test.go rename to internal/stl/geometry/text_test.go diff --git a/stl/geometry/vector.go b/internal/stl/geometry/vector.go similarity index 95% rename from stl/geometry/vector.go rename to internal/stl/geometry/vector.go index 5c6b897..cdbd9d5 100644 --- a/stl/geometry/vector.go +++ b/internal/stl/geometry/vector.go @@ -4,8 +4,8 @@ package geometry import ( "math" - "github.com/github/gh-skyline/errors" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/internal/errors" + "github.com/github/gh-skyline/internal/types" ) // validateVector checks if a vector's components are valid numbers diff --git a/stl/geometry/vector_test.go b/internal/stl/geometry/vector_test.go similarity index 98% rename from stl/geometry/vector_test.go rename to internal/stl/geometry/vector_test.go index e836ebd..c0759af 100644 --- a/stl/geometry/vector_test.go +++ b/internal/stl/geometry/vector_test.go @@ -4,7 +4,7 @@ import ( "math" "testing" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/internal/types" ) // Epsilon is declared in geometry_test.go diff --git a/stl/stl.go b/internal/stl/stl.go similarity index 97% rename from stl/stl.go rename to internal/stl/stl.go index 8d6c03f..a4fb5e8 100644 --- a/stl/stl.go +++ b/internal/stl/stl.go @@ -20,9 +20,9 @@ import ( "math" "os" - "github.com/github/gh-skyline/errors" - "github.com/github/gh-skyline/logger" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/internal/errors" + "github.com/github/gh-skyline/internal/logger" + "github.com/github/gh-skyline/internal/types" ) const ( diff --git a/stl/stl_test.go b/internal/stl/stl_test.go similarity index 98% rename from stl/stl_test.go rename to internal/stl/stl_test.go index cee1cdc..8229cae 100644 --- a/stl/stl_test.go +++ b/internal/stl/stl_test.go @@ -6,7 +6,7 @@ import ( "path/filepath" "testing" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/internal/types" ) // Helper function to verify STL file header diff --git a/testutil/fixtures/github.go b/internal/testutil/fixtures/github.go similarity index 96% rename from testutil/fixtures/github.go rename to internal/testutil/fixtures/github.go index 491baba..5498f9c 100644 --- a/testutil/fixtures/github.go +++ b/internal/testutil/fixtures/github.go @@ -5,7 +5,7 @@ package fixtures import ( "time" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/internal/types" ) // GenerateContributionsResponse creates a mock contributions response diff --git a/testutil/mocks/github.go b/internal/testutil/mocks/github.go similarity index 94% rename from testutil/mocks/github.go rename to internal/testutil/mocks/github.go index b03409e..8d6f906 100644 --- a/testutil/mocks/github.go +++ b/internal/testutil/mocks/github.go @@ -5,8 +5,8 @@ import ( "fmt" "time" - "github.com/github/gh-skyline/testutil/fixtures" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/internal/testutil/fixtures" + "github.com/github/gh-skyline/internal/types" ) // MockGitHubClient implements both GitHubClientInterface and APIClient interfaces diff --git a/types/types.go b/internal/types/types.go similarity index 100% rename from types/types.go rename to internal/types/types.go diff --git a/types/types_test.go b/internal/types/types_test.go similarity index 100% rename from types/types_test.go rename to internal/types/types_test.go diff --git a/main.go b/main.go index a5a0cdc..d8b8179 100644 --- a/main.go +++ b/main.go @@ -11,12 +11,12 @@ import ( "github.com/cli/go-gh/v2/pkg/api" "github.com/cli/go-gh/v2/pkg/browser" - "github.com/github/gh-skyline/ascii" - "github.com/github/gh-skyline/errors" - "github.com/github/gh-skyline/github" - "github.com/github/gh-skyline/logger" - "github.com/github/gh-skyline/stl" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/internal/ascii" + "github.com/github/gh-skyline/internal/errors" + "github.com/github/gh-skyline/internal/github" + "github.com/github/gh-skyline/internal/logger" + "github.com/github/gh-skyline/internal/stl" + "github.com/github/gh-skyline/internal/types" "github.com/spf13/cobra" ) diff --git a/main_test.go b/main_test.go index 242d6ef..3bc8530 100644 --- a/main_test.go +++ b/main_test.go @@ -5,9 +5,9 @@ import ( "fmt" - "github.com/github/gh-skyline/github" - "github.com/github/gh-skyline/testutil/fixtures" - "github.com/github/gh-skyline/testutil/mocks" + "github.com/github/gh-skyline/internal/github" + "github.com/github/gh-skyline/internal/testutil/fixtures" + "github.com/github/gh-skyline/internal/testutil/mocks" ) // MockBrowser implements the Browser interface From 85822ea6f305a505274945c53c5ebc243016eab8 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Thu, 23 Jan 2025 23:29:02 +0000 Subject: [PATCH 02/12] Refactor main.go into cmd, utils packages and the github client. --- cmd/root.go | 122 ++++++++++++++ cmd/root_test.go | 98 +++++++++++ cmd/skyline/skyline.go | 122 ++++++++++++++ cmd/skyline/skyline_test.go | 81 +++++++++ internal/github/init.go | 20 +++ internal/utils/utils.go | 76 +++++++++ internal/utils/utils_test.go | 175 ++++++++++++++++++++ main.go | 301 ++------------------------------- main_test.go | 311 ----------------------------------- 9 files changed, 708 insertions(+), 598 deletions(-) create mode 100644 cmd/root.go create mode 100644 cmd/root_test.go create mode 100644 cmd/skyline/skyline.go create mode 100644 cmd/skyline/skyline_test.go create mode 100644 internal/github/init.go create mode 100644 internal/utils/utils.go create mode 100644 internal/utils/utils_test.go delete mode 100644 main_test.go diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..fcda959 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,122 @@ +// Package cmd is a package that contains the root command (entrypoint) for the GitHub Skyline CLI tool. +package cmd + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/cli/go-gh/v2/pkg/browser" + "github.com/github/gh-skyline/cmd/skyline" + "github.com/github/gh-skyline/internal/errors" + "github.com/github/gh-skyline/internal/github" + "github.com/github/gh-skyline/internal/logger" + "github.com/github/gh-skyline/internal/utils" + "github.com/spf13/cobra" +) + +// Command line variables and root command configuration +var ( + yearRange string + user string + full bool + debug bool + web bool + artOnly bool + output string // new output path flag + + rootCmd = &cobra.Command{ + Use: "skyline", + Short: "Generate a 3D model of a user's GitHub contribution history", + Long: `GitHub Skyline creates 3D printable STL files from GitHub contribution data. +It can generate models for specific years or year ranges for the authenticated user or an optional specified user. + +While the STL file is being generated, an ASCII preview will be displayed in the terminal. + +ASCII Preview Legend: + ' ' Empty/Sky - No contributions + '.' Future dates - What contributions could you make? + '░' Low level - Light contribution activity + '▒' Medium level - Moderate contribution activity + '▓' High level - Heavy contribution activity + '╻┃╽' Top level - Last block with contributions in the week (Low, Medium, High) + +Layout: +Each column represents one week. Days within each week are reordered vertically +to create a "building" effect, with empty spaces (no contributions) at the top.`, + RunE: func(_ *cobra.Command, _ []string) error { + log := logger.GetLogger() + if debug { + log.SetLevel(logger.DEBUG) + if err := log.Debug("Debug logging enabled"); err != nil { + return err + } + } + + client, err := github.InitializeGitHubClient() + if err != nil { + return errors.New(errors.NetworkError, "failed to initialize GitHub client", err) + } + + if web { + b := browser.New("", os.Stdout, os.Stderr) + if err := openGitHubProfile(user, client, b); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + return nil + } + + startYear, endYear, err := utils.ParseYearRange(yearRange) + if err != nil { + return fmt.Errorf("invalid year range: %v", err) + } + + return skyline.GenerateSkyline(startYear, endYear, user, full, output, artOnly) + }, + } +) + +// init sets up command line flags for the skyline CLI tool +func initFlags() { + flags := rootCmd.Flags() + flags.StringVarP(&yearRange, "year", "y", fmt.Sprintf("%d", time.Now().Year()), "Year or year range (e.g., 2024 or 2014-2024)") + flags.StringVarP(&user, "user", "u", "", "GitHub username (optional, defaults to authenticated user)") + flags.BoolVarP(&full, "full", "f", false, "Generate contribution graph from join year to current year") + flags.BoolVarP(&debug, "debug", "d", false, "Enable debug logging") + flags.BoolVarP(&web, "web", "w", false, "Open GitHub profile (authenticated or specified user).") + flags.BoolVarP(&artOnly, "art-only", "a", false, "Generate only ASCII preview") + flags.StringVarP(&output, "output", "o", "", "Output file path (optional)") +} + +func init() { + initFlags() +} + +// Execute initializes and executes the root command for the GitHub Skyline CLI +func Execute(context context.Context) error { + if err := rootCmd.Execute(); err != nil { + return err + } + return nil +} + +// Browser interface matches browser.Browser functionality +type Browser interface { + Browse(url string) error +} + +// openGitHubProfile opens the GitHub profile page for the specified user or authenticated user +func openGitHubProfile(targetUser string, client skyline.GitHubClientInterface, b Browser) error { + if targetUser == "" { + username, err := client.GetAuthenticatedUser() + if err != nil { + return errors.New(errors.NetworkError, "failed to get authenticated user", err) + } + targetUser = username + } + + profileURL := fmt.Sprintf("https://github.com/%s", targetUser) + return b.Browse(profileURL) +} diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 0000000..c6bd877 --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,98 @@ +package cmd + +import ( + "fmt" + "testing" + + "github.com/github/gh-skyline/internal/testutil/mocks" +) + +// MockBrowser implements the Browser interface +type MockBrowser struct { + LastURL string + Err error +} + +// Browse implements the Browser interface +func (m *MockBrowser) Browse(url string) error { + m.LastURL = url + return m.Err +} + +func TestRootCmd(t *testing.T) { + cmd := rootCmd + if cmd.Use != "skyline" { + t.Errorf("expected command use to be 'skyline', got %s", cmd.Use) + } + if cmd.Short != "Generate a 3D model of a user's GitHub contribution history" { + t.Errorf("expected command short description to be 'Generate a 3D model of a user's GitHub contribution history', got %s", cmd.Short) + } + if cmd.Long == "" { + t.Error("expected command long description to be non-empty") + } +} + +func TestInit(t *testing.T) { + flags := rootCmd.Flags() + expectedFlags := []string{"year", "user", "full", "debug", "web", "art-only", "output"} + for _, flag := range expectedFlags { + if flags.Lookup(flag) == nil { + t.Errorf("expected flag %s to be initialized", flag) + } + } +} + +// TestOpenGitHubProfile tests the openGitHubProfile function +func TestOpenGitHubProfile(t *testing.T) { + tests := []struct { + name string + targetUser string + mockClient *mocks.MockGitHubClient + wantURL string + wantErr bool + }{ + { + name: "specific user", + targetUser: "testuser", + mockClient: &mocks.MockGitHubClient{}, + wantURL: "https://github.com/testuser", + wantErr: false, + }, + { + name: "authenticated user", + targetUser: "", + mockClient: &mocks.MockGitHubClient{ + Username: "authuser", + }, + wantURL: "https://github.com/authuser", + wantErr: false, + }, + { + name: "client error", + targetUser: "", + mockClient: &mocks.MockGitHubClient{ + Err: fmt.Errorf("mock error"), + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockBrowser := &MockBrowser{} + if tt.wantErr { + mockBrowser.Err = fmt.Errorf("mock error") + } + err := openGitHubProfile(tt.targetUser, tt.mockClient, mockBrowser) + + if (err != nil) != tt.wantErr { + t.Errorf("openGitHubProfile() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr && mockBrowser.LastURL != tt.wantURL { + t.Errorf("openGitHubProfile() URL = %v, want %v", mockBrowser.LastURL, tt.wantURL) + } + }) + } +} diff --git a/cmd/skyline/skyline.go b/cmd/skyline/skyline.go new file mode 100644 index 0000000..de62e5e --- /dev/null +++ b/cmd/skyline/skyline.go @@ -0,0 +1,122 @@ +// Package skyline provides the entry point for the GitHub Skyline Generator. +// It generates a 3D model of GitHub contributions in STL format. +package skyline + +import ( + "fmt" + "strings" + "time" + + "github.com/github/gh-skyline/internal/ascii" + "github.com/github/gh-skyline/internal/errors" + "github.com/github/gh-skyline/internal/github" + "github.com/github/gh-skyline/internal/logger" + "github.com/github/gh-skyline/internal/stl" + "github.com/github/gh-skyline/internal/types" + "github.com/github/gh-skyline/internal/utils" +) + +// GitHubClientInterface defines the methods for interacting with GitHub API +type GitHubClientInterface interface { + GetAuthenticatedUser() (string, error) + GetUserJoinYear(username string) (int, error) + FetchContributions(username string, year int) (*types.ContributionsResponse, error) +} + +// GenerateSkyline creates a 3D model with ASCII art preview of GitHub contributions for the specified year range, or "full lifetime" of the user +func GenerateSkyline(startYear, endYear int, targetUser string, full bool, output string, artOnly bool) error { + log := logger.GetLogger() + + client, err := github.InitializeGitHubClient() + if err != nil { + return errors.New(errors.NetworkError, "failed to initialize GitHub client", err) + } + + if targetUser == "" { + if err := log.Debug("No target user specified, using authenticated user"); err != nil { + return err + } + username, err := client.GetAuthenticatedUser() + if err != nil { + return errors.New(errors.NetworkError, "failed to get authenticated user", err) + } + targetUser = username + } + + if full { + joinYear, err := client.GetUserJoinYear(targetUser) + if err != nil { + return errors.New(errors.NetworkError, "failed to get user join year", err) + } + startYear = joinYear + endYear = time.Now().Year() + } + + var allContributions [][][]types.ContributionDay + for year := startYear; year <= endYear; year++ { + contributions, err := fetchContributionData(client, targetUser, year) + if err != nil { + return err + } + allContributions = append(allContributions, contributions) + + // Generate ASCII art for each year + asciiArt, err := ascii.GenerateASCII(contributions, targetUser, year, (year == startYear) && !artOnly, !artOnly) + if err != nil { + if warnErr := log.Warning("Failed to generate ASCII preview: %v", err); warnErr != nil { + return warnErr + } + } else { + if year == startYear { + // For first year, show full ASCII art including header + fmt.Println(asciiArt) + } else { + // For subsequent years, skip the header + lines := strings.Split(asciiArt, "\n") + gridStart := 0 + for i, line := range lines { + containsEmptyBlock := strings.Contains(line, string(ascii.EmptyBlock)) + containsFoundationLow := strings.Contains(line, string(ascii.FoundationLow)) + isNotOnlyEmptyBlocks := strings.Trim(line, string(ascii.EmptyBlock)) != "" + + if (containsEmptyBlock || containsFoundationLow) && isNotOnlyEmptyBlocks { + gridStart = i + break + } + } + // Print just the grid and user info + fmt.Println(strings.Join(lines[gridStart:], "\n")) + } + } + } + + if !artOnly { + // Generate filename + outputPath := utils.GenerateOutputFilename(targetUser, startYear, endYear, output) + + // Generate the STL file + if len(allContributions) == 1 { + return stl.GenerateSTL(allContributions[0], outputPath, targetUser, startYear) + } + return stl.GenerateSTLRange(allContributions, outputPath, targetUser, startYear, endYear) + } + + return nil +} + +// fetchContributionData retrieves and formats the contribution data for the specified year. +func fetchContributionData(client *github.Client, username string, year int) ([][]types.ContributionDay, error) { + response, err := client.FetchContributions(username, year) + if err != nil { + return nil, fmt.Errorf("failed to fetch contributions: %w", err) + } + + // Convert weeks data to 2D array for STL generation + weeks := response.User.ContributionsCollection.ContributionCalendar.Weeks + contributionGrid := make([][]types.ContributionDay, len(weeks)) + for i, week := range weeks { + contributionGrid[i] = week.ContributionDays + } + + return contributionGrid, nil +} diff --git a/cmd/skyline/skyline_test.go b/cmd/skyline/skyline_test.go new file mode 100644 index 0000000..05970a5 --- /dev/null +++ b/cmd/skyline/skyline_test.go @@ -0,0 +1,81 @@ +package skyline + +import ( + "testing" + + "github.com/github/gh-skyline/internal/github" + "github.com/github/gh-skyline/internal/testutil/fixtures" + "github.com/github/gh-skyline/internal/testutil/mocks" +) + +func TestGenerateSkyline(t *testing.T) { + // Save original initializer + originalInit := github.InitializeGitHubClient + defer func() { + github.InitializeGitHubClient = originalInit + }() + + tests := []struct { + name string + startYear int + endYear int + targetUser string + full bool + mockClient *mocks.MockGitHubClient + wantErr bool + }{ + { + name: "single year", + startYear: 2024, + endYear: 2024, + targetUser: "testuser", + full: false, + mockClient: &mocks.MockGitHubClient{ + Username: "testuser", + JoinYear: 2020, + MockData: fixtures.GenerateContributionsResponse("testuser", 2024), + }, + wantErr: false, + }, + { + name: "year range", + startYear: 2020, + endYear: 2024, + targetUser: "testuser", + full: false, + mockClient: &mocks.MockGitHubClient{ + Username: "testuser", + JoinYear: 2020, + MockData: fixtures.GenerateContributionsResponse("testuser", 2024), + }, + wantErr: false, + }, + { + name: "full range", + startYear: 2008, + endYear: 2024, + targetUser: "testuser", + full: true, + mockClient: &mocks.MockGitHubClient{ + Username: "testuser", + JoinYear: 2008, + MockData: fixtures.GenerateContributionsResponse("testuser", 2024), + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a closure that returns our mock client + github.InitializeGitHubClient = func() (*github.Client, error) { + return github.NewClient(tt.mockClient), nil + } + + err := GenerateSkyline(tt.startYear, tt.endYear, tt.targetUser, tt.full, "", false) + if (err != nil) != tt.wantErr { + t.Errorf("GenerateSkyline() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/github/init.go b/internal/github/init.go new file mode 100644 index 0000000..c12eeed --- /dev/null +++ b/internal/github/init.go @@ -0,0 +1,20 @@ +// Package github provides a function to initialize the GitHub client. +package github + +import ( + "fmt" + + "github.com/cli/go-gh/v2/pkg/api" +) + +// ClientInitializer is a function type for initializing GitHub clients +type ClientInitializer func() (*Client, error) + +// InitializeGitHubClient is the default client initializer +var InitializeGitHubClient ClientInitializer = func() (*Client, error) { + apiClient, err := api.DefaultGraphQLClient() + if err != nil { + return nil, fmt.Errorf("failed to create GraphQL client: %w", err) + } + return NewClient(apiClient), nil +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 0000000..17f5f5c --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,76 @@ +// package utils are utility functions for the GitHub Skyline project +package utils + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +// Constants for GitHub launch year and default output file format +const ( + githubLaunchYear = 2008 + outputFileFormat = "%s-%s-github-skyline.stl" +) + +// Parse year range string (e.g., "2024" or "2014-2024") +func ParseYearRange(yearRange string) (startYear, endYear int, err error) { + if strings.Contains(yearRange, "-") { + parts := strings.Split(yearRange, "-") + if len(parts) != 2 { + return 0, 0, fmt.Errorf("invalid year range format") + } + startYear, err = strconv.Atoi(parts[0]) + if err != nil { + return 0, 0, err + } + endYear, err = strconv.Atoi(parts[1]) + if err != nil { + return 0, 0, err + } + } else { + year, err := strconv.Atoi(yearRange) + if err != nil { + return 0, 0, err + } + startYear, endYear = year, year + } + return startYear, endYear, validateYearRange(startYear, endYear) +} + +// validateYearRange checks if the years are within the range +// of GitHub's launch year to the current year and if +// the start year is not greater than the end year. +func validateYearRange(startYear, endYear int) error { + currentYear := time.Now().Year() + if startYear < githubLaunchYear || endYear > currentYear { + return fmt.Errorf("years must be between %d and %d", githubLaunchYear, currentYear) + } + if startYear > endYear { + return fmt.Errorf("start year cannot be after end year") + } + return nil +} + +// FormatYearRange returns a formatted string representation of the year range +func FormatYearRange(startYear, endYear int) string { + if startYear == endYear { + return fmt.Sprintf("%d", startYear) + } + // Use YYYY-YY format for multi-year ranges + return fmt.Sprintf("%04d-%02d", startYear, endYear%100) +} + +// GenerateOutputFilename creates a consistent filename for the STL output +func GenerateOutputFilename(user string, startYear, endYear int, output string) string { + if output != "" { + // Ensure the filename ends with .stl + if !strings.HasSuffix(strings.ToLower(output), ".stl") { + return output + ".stl" + } + return output + } + yearStr := FormatYearRange(startYear, endYear) + return fmt.Sprintf(outputFileFormat, user, yearStr) +} diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go new file mode 100644 index 0000000..8253ed1 --- /dev/null +++ b/internal/utils/utils_test.go @@ -0,0 +1,175 @@ +package utils + +import "testing" + +func TestParseYearRange(t *testing.T) { + tests := []struct { + name string + yearRange string + wantStart int + wantEnd int + wantErr bool + wantErrString string + }{ + { + name: "single year", + yearRange: "2024", + wantStart: 2024, + wantEnd: 2024, + wantErr: false, + }, + { + name: "year range", + yearRange: "2020-2024", + wantStart: 2020, + wantEnd: 2024, + wantErr: false, + }, + { + name: "invalid format", + yearRange: "2020-2024-2025", + wantErr: true, + wantErrString: "invalid year range format", + }, + { + name: "invalid number", + yearRange: "abc-2024", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + start, end, err := ParseYearRange(tt.yearRange) + if (err != nil) != tt.wantErr { + t.Errorf("parseYearRange() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErr && tt.wantErrString != "" && err.Error() != tt.wantErrString { + t.Errorf("parseYearRange() error = %v, wantErrString %v", err, tt.wantErrString) + return + } + if !tt.wantErr { + if start != tt.wantStart { + t.Errorf("parseYearRange() start = %v, want %v", start, tt.wantStart) + } + if end != tt.wantEnd { + t.Errorf("parseYearRange() end = %v, want %v", end, tt.wantEnd) + } + } + }) + } +} + +func TestValidateYearRange(t *testing.T) { + tests := []struct { + name string + startYear int + endYear int + wantErr bool + }{ + { + name: "valid range", + startYear: 2020, + endYear: 2024, + wantErr: false, + }, + { + name: "invalid start year", + startYear: 2007, + endYear: 2024, + wantErr: true, + }, + { + name: "start after end", + startYear: 2024, + endYear: 2020, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateYearRange(tt.startYear, tt.endYear) + if (err != nil) != tt.wantErr { + t.Errorf("validateYearRange() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestFormatYearRange(t *testing.T) { + tests := []struct { + name string + startYear int + endYear int + want string + }{ + { + name: "same year", + startYear: 2024, + endYear: 2024, + want: "2024", + }, + { + name: "different years", + startYear: 2020, + endYear: 2024, + want: "2020-24", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := FormatYearRange(tt.startYear, tt.endYear) + if got != tt.want { + t.Errorf("formatYearRange() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGenerateOutputFilename(t *testing.T) { + tests := []struct { + name string + user string + startYear int + endYear int + output string + want string + }{ + { + name: "single year", + user: "testuser", + startYear: 2024, + endYear: 2024, + output: "", + want: "testuser-2024-github-skyline.stl", + }, + { + name: "year range", + user: "testuser", + startYear: 2020, + endYear: 2024, + output: "", + want: "testuser-2020-24-github-skyline.stl", + }, + { + name: "override", + user: "testuser", + startYear: 2020, + endYear: 2024, + output: "myoutput.stl", + want: "myoutput.stl", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GenerateOutputFilename(tt.user, tt.startYear, tt.endYear, tt.output) + if got != tt.want { + t.Errorf("generateOutputFilename() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/main.go b/main.go index d8b8179..0fc58be 100644 --- a/main.go +++ b/main.go @@ -1,305 +1,32 @@ -// Package main provides the entry point for the GitHub Skyline Generator. -// It generates a 3D model of GitHub contributions in STL format. +// Package main provides the entry point for the GitHub CLI gh-skyline extension. package main import ( - "fmt" + "context" "os" - "strconv" - "strings" - "time" - "github.com/cli/go-gh/v2/pkg/api" - "github.com/cli/go-gh/v2/pkg/browser" - "github.com/github/gh-skyline/internal/ascii" - "github.com/github/gh-skyline/internal/errors" - "github.com/github/gh-skyline/internal/github" - "github.com/github/gh-skyline/internal/logger" - "github.com/github/gh-skyline/internal/stl" - "github.com/github/gh-skyline/internal/types" - "github.com/spf13/cobra" + "github.com/github/gh-skyline/cmd" ) -// Browser interface matches browser.Browser functionality -type Browser interface { - Browse(url string) error -} - -// GitHubClientInterface defines the methods for interacting with GitHub API -type GitHubClientInterface interface { - GetAuthenticatedUser() (string, error) - GetUserJoinYear(username string) (int, error) - FetchContributions(username string, year int) (*types.ContributionsResponse, error) -} +type exitCode int -// Constants for GitHub launch year and default output file format const ( - githubLaunchYear = 2008 - outputFileFormat = "%s-%s-github-skyline.stl" -) - -// Command line variables and root command configuration -var ( - yearRange string - user string - full bool - debug bool - web bool - artOnly bool - output string // new output path flag - - rootCmd = &cobra.Command{ - Use: "skyline", - Short: "Generate a 3D model of a user's GitHub contribution history", - Long: `GitHub Skyline creates 3D printable STL files from GitHub contribution data. -It can generate models for specific years or year ranges for the authenticated user or an optional specified user. - -While the STL file is being generated, an ASCII preview will be displayed in the terminal. - -ASCII Preview Legend: - ' ' Empty/Sky - No contributions - '.' Future dates - What contributions could you make? - '░' Low level - Light contribution activity - '▒' Medium level - Moderate contribution activity - '▓' High level - Heavy contribution activity - '╻┃╽' Top level - Last block with contributions in the week (Low, Medium, High) - -Layout: -Each column represents one week. Days within each week are reordered vertically -to create a "building" effect, with empty spaces (no contributions) at the top.`, - RunE: func(_ *cobra.Command, _ []string) error { - log := logger.GetLogger() - if debug { - log.SetLevel(logger.DEBUG) - if err := log.Debug("Debug logging enabled"); err != nil { - return err - } - } - - client, err := initializeGitHubClient() - if err != nil { - return errors.New(errors.NetworkError, "failed to initialize GitHub client", err) - } - - if web { - b := browser.New("", os.Stdout, os.Stderr) - if err := openGitHubProfile(user, client, b); err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } - return nil - } - - startYear, endYear, err := parseYearRange(yearRange) - if err != nil { - return fmt.Errorf("invalid year range: %v", err) - } - - return generateSkyline(startYear, endYear, user, full) - }, - } + exitOK exitCode = 0 + exitError exitCode = 1 ) -// init sets up command line flags for the skyline CLI tool -func init() { - rootCmd.Flags().StringVarP(&yearRange, "year", "y", fmt.Sprintf("%d", time.Now().Year()), "Year or year range (e.g., 2024 or 2014-2024)") - rootCmd.Flags().StringVarP(&user, "user", "u", "", "GitHub username (optional, defaults to authenticated user)") - rootCmd.Flags().BoolVarP(&full, "full", "f", false, "Generate contribution graph from join year to current year") - rootCmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable debug logging") - rootCmd.Flags().BoolVarP(&web, "web", "w", false, "Open GitHub profile (authenticated or specified user).") - rootCmd.Flags().BoolVarP(&artOnly, "art-only", "a", false, "Generate only ASCII preview") - rootCmd.Flags().StringVarP(&output, "output", "o", "", "Output file path (optional)") -} - -// main initializes and executes the root command for the GitHub Skyline CLI func main() { - if err := rootCmd.Execute(); err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } -} - -// formatYearRange returns a formatted string representation of the year range -func formatYearRange(startYear, endYear int) string { - if startYear == endYear { - return fmt.Sprintf("%d", startYear) - } - // Use YYYY-YY format for multi-year ranges - return fmt.Sprintf("%04d-%02d", startYear, endYear%100) -} - -// generateOutputFilename creates a consistent filename for the STL output -func generateOutputFilename(user string, startYear, endYear int) string { - if output != "" { - // Ensure the filename ends with .stl - if !strings.HasSuffix(strings.ToLower(output), ".stl") { - return output + ".stl" - } - return output - } - yearStr := formatYearRange(startYear, endYear) - return fmt.Sprintf(outputFileFormat, user, yearStr) + code := mainRun() + os.Exit(int(code)) } -// generateSkyline creates a 3D model with ASCII art preview of GitHub contributions for the specified year range, or "full lifetime" of the user -func generateSkyline(startYear, endYear int, targetUser string, full bool) error { - log := logger.GetLogger() - - client, err := initializeGitHubClient() - if err != nil { - return errors.New(errors.NetworkError, "failed to initialize GitHub client", err) - } - - if targetUser == "" { - if err := log.Debug("No target user specified, using authenticated user"); err != nil { - return err - } - username, err := client.GetAuthenticatedUser() - if err != nil { - return errors.New(errors.NetworkError, "failed to get authenticated user", err) - } - targetUser = username - } - - if full { - joinYear, err := client.GetUserJoinYear(targetUser) - if err != nil { - return errors.New(errors.NetworkError, "failed to get user join year", err) - } - startYear = joinYear - endYear = time.Now().Year() - } - - var allContributions [][][]types.ContributionDay - for year := startYear; year <= endYear; year++ { - contributions, err := fetchContributionData(client, targetUser, year) - if err != nil { - return err - } - allContributions = append(allContributions, contributions) - - // Generate ASCII art for each year - asciiArt, err := ascii.GenerateASCII(contributions, targetUser, year, (year == startYear) && !artOnly, !artOnly) - if err != nil { - if warnErr := log.Warning("Failed to generate ASCII preview: %v", err); warnErr != nil { - return warnErr - } - } else { - if year == startYear { - // For first year, show full ASCII art including header - fmt.Println(asciiArt) - } else { - // For subsequent years, skip the header - lines := strings.Split(asciiArt, "\n") - gridStart := 0 - for i, line := range lines { - containsEmptyBlock := strings.Contains(line, string(ascii.EmptyBlock)) - containsFoundationLow := strings.Contains(line, string(ascii.FoundationLow)) - isNotOnlyEmptyBlocks := strings.Trim(line, string(ascii.EmptyBlock)) != "" - - if (containsEmptyBlock || containsFoundationLow) && isNotOnlyEmptyBlocks { - gridStart = i - break - } - } - // Print just the grid and user info - fmt.Println(strings.Join(lines[gridStart:], "\n")) - } - } - } - - if !artOnly { - // Generate filename - outputPath := generateOutputFilename(targetUser, startYear, endYear) - - // Generate the STL file - if len(allContributions) == 1 { - return stl.GenerateSTL(allContributions[0], outputPath, targetUser, startYear) - } - return stl.GenerateSTLRange(allContributions, outputPath, targetUser, startYear, endYear) - } - - return nil -} - -// Variable for client initialization - allows for testing -var initializeGitHubClient = defaultGitHubClient - -// defaultGitHubClient is the default implementation of client initialization -func defaultGitHubClient() (*github.Client, error) { - apiClient, err := api.DefaultGraphQLClient() - if err != nil { - return nil, fmt.Errorf("failed to create GraphQL client: %w", err) - } - return github.NewClient(apiClient), nil -} - -// fetchContributionData retrieves and formats the contribution data for the specified year. -func fetchContributionData(client *github.Client, username string, year int) ([][]types.ContributionDay, error) { - response, err := client.FetchContributions(username, year) - if err != nil { - return nil, fmt.Errorf("failed to fetch contributions: %w", err) - } - - // Convert weeks data to 2D array for STL generation - weeks := response.User.ContributionsCollection.ContributionCalendar.Weeks - contributionGrid := make([][]types.ContributionDay, len(weeks)) - for i, week := range weeks { - contributionGrid[i] = week.ContributionDays - } - - return contributionGrid, nil -} - -// Parse year range string (e.g., "2024" or "2014-2024") -func parseYearRange(yearRange string) (startYear, endYear int, err error) { - if strings.Contains(yearRange, "-") { - parts := strings.Split(yearRange, "-") - if len(parts) != 2 { - return 0, 0, fmt.Errorf("invalid year range format") - } - startYear, err = strconv.Atoi(parts[0]) - if err != nil { - return 0, 0, err - } - endYear, err = strconv.Atoi(parts[1]) - if err != nil { - return 0, 0, err - } - } else { - year, err := strconv.Atoi(yearRange) - if err != nil { - return 0, 0, err - } - startYear, endYear = year, year - } - return startYear, endYear, validateYearRange(startYear, endYear) -} - -// validateYearRange checks if the years are within the range -// of GitHub's launch year to the current year and if -// the start year is not greater than the end year. -func validateYearRange(startYear, endYear int) error { - currentYear := time.Now().Year() - if startYear < githubLaunchYear || endYear > currentYear { - return fmt.Errorf("years must be between %d and %d", githubLaunchYear, currentYear) - } - if startYear > endYear { - return fmt.Errorf("start year cannot be after end year") - } - return nil -} +func mainRun() exitCode { + exitCode := exitOK + ctx := context.Background() -// openGitHubProfile opens the GitHub profile page for the specified user or authenticated user -func openGitHubProfile(targetUser string, client GitHubClientInterface, b Browser) error { - if targetUser == "" { - username, err := client.GetAuthenticatedUser() - if err != nil { - return errors.New(errors.NetworkError, "failed to get authenticated user", err) - } - targetUser = username + if err := cmd.Execute(ctx); err != nil { + exitCode = exitError } - profileURL := fmt.Sprintf("https://github.com/%s", targetUser) - return b.Browse(profileURL) + return exitCode } diff --git a/main_test.go b/main_test.go deleted file mode 100644 index 3bc8530..0000000 --- a/main_test.go +++ /dev/null @@ -1,311 +0,0 @@ -package main - -import ( - "testing" - - "fmt" - - "github.com/github/gh-skyline/internal/github" - "github.com/github/gh-skyline/internal/testutil/fixtures" - "github.com/github/gh-skyline/internal/testutil/mocks" -) - -// MockBrowser implements the Browser interface -type MockBrowser struct { - LastURL string - Err error -} - -// Browse implements the Browser interface -func (m *MockBrowser) Browse(url string) error { - m.LastURL = url - return m.Err -} - -func TestFormatYearRange(t *testing.T) { - tests := []struct { - name string - startYear int - endYear int - want string - }{ - { - name: "same year", - startYear: 2024, - endYear: 2024, - want: "2024", - }, - { - name: "different years", - startYear: 2020, - endYear: 2024, - want: "2020-24", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := formatYearRange(tt.startYear, tt.endYear) - if got != tt.want { - t.Errorf("formatYearRange() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestGenerateOutputFilename(t *testing.T) { - tests := []struct { - name string - user string - startYear int - endYear int - want string - }{ - { - name: "single year", - user: "testuser", - startYear: 2024, - endYear: 2024, - want: "testuser-2024-github-skyline.stl", - }, - { - name: "year range", - user: "testuser", - startYear: 2020, - endYear: 2024, - want: "testuser-2020-24-github-skyline.stl", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := generateOutputFilename(tt.user, tt.startYear, tt.endYear) - if got != tt.want { - t.Errorf("generateOutputFilename() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestParseYearRange(t *testing.T) { - tests := []struct { - name string - yearRange string - wantStart int - wantEnd int - wantErr bool - wantErrString string - }{ - { - name: "single year", - yearRange: "2024", - wantStart: 2024, - wantEnd: 2024, - wantErr: false, - }, - { - name: "year range", - yearRange: "2020-2024", - wantStart: 2020, - wantEnd: 2024, - wantErr: false, - }, - { - name: "invalid format", - yearRange: "2020-2024-2025", - wantErr: true, - wantErrString: "invalid year range format", - }, - { - name: "invalid number", - yearRange: "abc-2024", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - start, end, err := parseYearRange(tt.yearRange) - if (err != nil) != tt.wantErr { - t.Errorf("parseYearRange() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.wantErr && tt.wantErrString != "" && err.Error() != tt.wantErrString { - t.Errorf("parseYearRange() error = %v, wantErrString %v", err, tt.wantErrString) - return - } - if !tt.wantErr { - if start != tt.wantStart { - t.Errorf("parseYearRange() start = %v, want %v", start, tt.wantStart) - } - if end != tt.wantEnd { - t.Errorf("parseYearRange() end = %v, want %v", end, tt.wantEnd) - } - } - }) - } -} - -func TestValidateYearRange(t *testing.T) { - tests := []struct { - name string - startYear int - endYear int - wantErr bool - }{ - { - name: "valid range", - startYear: 2020, - endYear: 2024, - wantErr: false, - }, - { - name: "invalid start year", - startYear: 2007, - endYear: 2024, - wantErr: true, - }, - { - name: "start after end", - startYear: 2024, - endYear: 2020, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := validateYearRange(tt.startYear, tt.endYear) - if (err != nil) != tt.wantErr { - t.Errorf("validateYearRange() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestGenerateSkyline(t *testing.T) { - // Save original client creation function - originalInitFn := initializeGitHubClient - defer func() { - initializeGitHubClient = originalInitFn - }() - - tests := []struct { - name string - startYear int - endYear int - targetUser string - full bool - mockClient *mocks.MockGitHubClient - wantErr bool - }{ - { - name: "single year", - startYear: 2024, - endYear: 2024, - targetUser: "testuser", - full: false, - mockClient: &mocks.MockGitHubClient{ - Username: "testuser", - JoinYear: 2020, - MockData: fixtures.GenerateContributionsResponse("testuser", 2024), - }, - wantErr: false, - }, - { - name: "year range", - startYear: 2020, - endYear: 2024, - targetUser: "testuser", - full: false, - mockClient: &mocks.MockGitHubClient{ - Username: "testuser", - JoinYear: 2020, - MockData: fixtures.GenerateContributionsResponse("testuser", 2024), - }, - wantErr: false, - }, - { - name: "full range", - startYear: 2008, - endYear: 2024, - targetUser: "testuser", - full: true, - mockClient: &mocks.MockGitHubClient{ - Username: "testuser", - JoinYear: 2008, - MockData: fixtures.GenerateContributionsResponse("testuser", 2024), - }, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Override the client initialization for testing - initializeGitHubClient = func() (*github.Client, error) { - return github.NewClient(tt.mockClient), nil - } - - err := generateSkyline(tt.startYear, tt.endYear, tt.targetUser, tt.full) - if (err != nil) != tt.wantErr { - t.Errorf("generateSkyline() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -// TestOpenGitHubProfile tests the openGitHubProfile function -func TestOpenGitHubProfile(t *testing.T) { - tests := []struct { - name string - targetUser string - mockClient *mocks.MockGitHubClient - wantURL string - wantErr bool - }{ - { - name: "specific user", - targetUser: "testuser", - mockClient: &mocks.MockGitHubClient{}, - wantURL: "https://github.com/testuser", - wantErr: false, - }, - { - name: "authenticated user", - targetUser: "", - mockClient: &mocks.MockGitHubClient{ - Username: "authuser", - }, - wantURL: "https://github.com/authuser", - wantErr: false, - }, - { - name: "client error", - targetUser: "", - mockClient: &mocks.MockGitHubClient{ - Err: fmt.Errorf("mock error"), - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockBrowser := &MockBrowser{} - if tt.wantErr { - mockBrowser.Err = fmt.Errorf("mock error") - } - err := openGitHubProfile(tt.targetUser, tt.mockClient, mockBrowser) - - if (err != nil) != tt.wantErr { - t.Errorf("openGitHubProfile() error = %v, wantErr %v", err, tt.wantErr) - return - } - - if !tt.wantErr && mockBrowser.LastURL != tt.wantURL { - t.Errorf("openGitHubProfile() URL = %v, want %v", mockBrowser.LastURL, tt.wantURL) - } - }) - } -} From 95b3bae548778452742c5337168715c8bd1915ce Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Sun, 26 Jan 2025 22:55:48 +0000 Subject: [PATCH 03/12] fix: update GitHub profile URL to use default hostname --- cmd/root.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index fcda959..1bc1ae0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,6 +7,7 @@ import ( "os" "time" + "github.com/cli/go-gh/v2/pkg/auth" "github.com/cli/go-gh/v2/pkg/browser" "github.com/github/gh-skyline/cmd/skyline" "github.com/github/gh-skyline/internal/errors" @@ -117,6 +118,7 @@ func openGitHubProfile(targetUser string, client skyline.GitHubClientInterface, targetUser = username } - profileURL := fmt.Sprintf("https://github.com/%s", targetUser) + hostname, _ := auth.DefaultHost() + profileURL := fmt.Sprintf("https://%s/%s", hostname, targetUser) return b.Browse(profileURL) } From b88e36aa554cbf065ed420a5ebe569aab1980843 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:20:45 +0000 Subject: [PATCH 04/12] refactor: restructure root command and improve command handling in CLI tool --- cmd/root.go | 97 ++++++++++++++++++++++++++++------------------------- main.go | 4 +-- 2 files changed, 53 insertions(+), 48 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 1bc1ae0..38d2b97 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -26,11 +26,13 @@ var ( web bool artOnly bool output string // new output path flag +) - rootCmd = &cobra.Command{ - Use: "skyline", - Short: "Generate a 3D model of a user's GitHub contribution history", - Long: `GitHub Skyline creates 3D printable STL files from GitHub contribution data. +// rootCmd is the root command for the GitHub Skyline CLI tool. +var rootCmd = &cobra.Command{ + Use: "skyline", + Short: "Generate a 3D model of a user's GitHub contribution history", + Long: `GitHub Skyline creates 3D printable STL files from GitHub contribution data. It can generate models for specific years or year ranges for the authenticated user or an optional specified user. While the STL file is being generated, an ASCII preview will be displayed in the terminal. @@ -46,40 +48,23 @@ ASCII Preview Legend: Layout: Each column represents one week. Days within each week are reordered vertically to create a "building" effect, with empty spaces (no contributions) at the top.`, - RunE: func(_ *cobra.Command, _ []string) error { - log := logger.GetLogger() - if debug { - log.SetLevel(logger.DEBUG) - if err := log.Debug("Debug logging enabled"); err != nil { - return err - } - } - - client, err := github.InitializeGitHubClient() - if err != nil { - return errors.New(errors.NetworkError, "failed to initialize GitHub client", err) - } - - if web { - b := browser.New("", os.Stdout, os.Stderr) - if err := openGitHubProfile(user, client, b); err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } - return nil - } - - startYear, endYear, err := utils.ParseYearRange(yearRange) - if err != nil { - return fmt.Errorf("invalid year range: %v", err) - } - - return skyline.GenerateSkyline(startYear, endYear, user, full, output, artOnly) - }, + RunE: handleSkylineCommand, +} + +// init initializes command line flags for the skyline CLI tool. +func init() { + initFlags() +} + +// Execute initializes and executes the root command for the GitHub Skyline CLI. +func Execute(_ context.Context) error { + if err := rootCmd.Execute(); err != nil { + return err } -) + return nil +} -// init sets up command line flags for the skyline CLI tool +// initFlags sets up command line flags for the skyline CLI tool. func initFlags() { flags := rootCmd.Flags() flags.StringVarP(&yearRange, "year", "y", fmt.Sprintf("%d", time.Now().Year()), "Year or year range (e.g., 2024 or 2014-2024)") @@ -91,24 +76,44 @@ func initFlags() { flags.StringVarP(&output, "output", "o", "", "Output file path (optional)") } -func init() { - initFlags() -} +// executeRootCmd is the main execution function for the root command. +func handleSkylineCommand(_ *cobra.Command, _ []string) error { + log := logger.GetLogger() + if debug { + log.SetLevel(logger.DEBUG) + if err := log.Debug("Debug logging enabled"); err != nil { + return err + } + } -// Execute initializes and executes the root command for the GitHub Skyline CLI -func Execute(context context.Context) error { - if err := rootCmd.Execute(); err != nil { - return err + client, err := github.InitializeGitHubClient() + if err != nil { + return errors.New(errors.NetworkError, "failed to initialize GitHub client", err) } - return nil + + if web { + b := browser.New("", os.Stdout, os.Stderr) + if err := openGitHubProfile(user, client, b); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + return nil + } + + startYear, endYear, err := utils.ParseYearRange(yearRange) + if err != nil { + return fmt.Errorf("invalid year range: %v", err) + } + + return skyline.GenerateSkyline(startYear, endYear, user, full, output, artOnly) } -// Browser interface matches browser.Browser functionality +// Browser interface matches browser.Browser functionality. type Browser interface { Browse(url string) error } -// openGitHubProfile opens the GitHub profile page for the specified user or authenticated user +// openGitHubProfile opens the GitHub profile page for the specified user or authenticated user. func openGitHubProfile(targetUser string, client skyline.GitHubClientInterface, b Browser) error { if targetUser == "" { username, err := client.GetAuthenticatedUser() diff --git a/main.go b/main.go index 0fc58be..837dc3f 100644 --- a/main.go +++ b/main.go @@ -16,11 +16,11 @@ const ( ) func main() { - code := mainRun() + code := Start() os.Exit(int(code)) } -func mainRun() exitCode { +func Start() exitCode { exitCode := exitOK ctx := context.Background() From c5f4e6fd9eafbabbd55509ea9e30b84ab6e5220e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 18:27:23 +0000 Subject: [PATCH 05/12] Bump github.com/spf13/pflag in the go-dependencies group Bumps the go-dependencies group with 1 update: [github.com/spf13/pflag](https://github.com/spf13/pflag). Updates `github.com/spf13/pflag` from 1.0.5 to 1.0.6 - [Release notes](https://github.com/spf13/pflag/releases) - [Commits](https://github.com/spf13/pflag/compare/v1.0.5...v1.0.6) --- updated-dependencies: - dependency-name: github.com/spf13/pflag dependency-type: indirect update-type: version-update:semver-patch dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 96e4328..261c0eb 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/mattn/go-runewidth v0.0.16 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/thlib/go-timezone-local v0.0.6 // indirect golang.org/x/image v0.23.0 // indirect golang.org/x/sys v0.29.0 // indirect diff --git a/go.sum b/go.sum index a8d94af..b9fb70f 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,9 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/thlib/go-timezone-local v0.0.6 h1:Ii3QJ4FhosL/+eCZl6Hsdr4DDU4tfevNoV83yAEo2tU= From 6f0782f881a3fd5a5a2c537ff14da2d3e21193c9 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:14:47 +0000 Subject: [PATCH 06/12] fix: correct comments and function naming conventions in utils and main packages --- internal/utils/utils.go | 4 ++-- main.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 17f5f5c..61b681b 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -1,4 +1,4 @@ -// package utils are utility functions for the GitHub Skyline project +// Package utils are utility functions for the GitHub Skyline project package utils import ( @@ -14,7 +14,7 @@ const ( outputFileFormat = "%s-%s-github-skyline.stl" ) -// Parse year range string (e.g., "2024" or "2014-2024") +// ParseYearRange parses whether a year is a single year or a range of years. func ParseYearRange(yearRange string) (startYear, endYear int, err error) { if strings.Contains(yearRange, "-") { parts := strings.Split(yearRange, "-") diff --git a/main.go b/main.go index 837dc3f..ec1d8f9 100644 --- a/main.go +++ b/main.go @@ -16,11 +16,11 @@ const ( ) func main() { - code := Start() + code := start() os.Exit(int(code)) } -func Start() exitCode { +func start() exitCode { exitCode := exitOK ctx := context.Background() From adc30e7cd891f8151dafcf1059595060bb2ce8a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 18:58:24 +0000 Subject: [PATCH 07/12] Bump the go-dependencies group with 5 updates Bumps the go-dependencies group with 5 updates: | Package | From | To | | --- | --- | --- | | [github.com/spf13/cobra](https://github.com/spf13/cobra) | `1.8.1` | `1.9.1` | | [golang.org/x/image](https://github.com/golang/image) | `0.23.0` | `0.24.0` | | [golang.org/x/sys](https://github.com/golang/sys) | `0.29.0` | `0.30.0` | | [golang.org/x/term](https://github.com/golang/term) | `0.28.0` | `0.29.0` | | [golang.org/x/text](https://github.com/golang/text) | `0.21.0` | `0.22.0` | Updates `github.com/spf13/cobra` from 1.8.1 to 1.9.1 - [Release notes](https://github.com/spf13/cobra/releases) - [Commits](https://github.com/spf13/cobra/compare/v1.8.1...v1.9.1) Updates `golang.org/x/image` from 0.23.0 to 0.24.0 - [Commits](https://github.com/golang/image/compare/v0.23.0...v0.24.0) Updates `golang.org/x/sys` from 0.29.0 to 0.30.0 - [Commits](https://github.com/golang/sys/compare/v0.29.0...v0.30.0) Updates `golang.org/x/term` from 0.28.0 to 0.29.0 - [Commits](https://github.com/golang/term/compare/v0.28.0...v0.29.0) Updates `golang.org/x/text` from 0.21.0 to 0.22.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.21.0...v0.22.0) --- updated-dependencies: - dependency-name: github.com/spf13/cobra dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/image dependency-type: indirect update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/sys dependency-type: indirect update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/term dependency-type: indirect update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/text dependency-type: indirect update-type: version-update:semver-minor dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] --- go.mod | 10 +++++----- go.sum | 23 +++++++++++------------ 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 261c0eb..7f88600 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.23.3 require ( github.com/cli/go-gh/v2 v2.11.2 github.com/fogleman/gg v1.3.0 - github.com/spf13/cobra v1.8.1 + github.com/spf13/cobra v1.9.1 ) require ( @@ -27,9 +27,9 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/thlib/go-timezone-local v0.0.6 // indirect - golang.org/x/image v0.23.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/term v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/image v0.24.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.29.0 // indirect + golang.org/x/text v0.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b9fb70f..0944f2e 100644 --- a/go.sum +++ b/go.sum @@ -8,7 +8,7 @@ github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= github.com/cli/shurcooL-graphql v0.0.4 h1:6MogPnQJLjKkaXPyGqPRXOI2qCsQdqNfUY1QSJu2GuY= github.com/cli/shurcooL-graphql v0.0.4/go.mod h1:3waN4u02FiZivIV+p1y4d0Jo1jc6BViMA73C+sZo2fk= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -42,25 +42,24 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/thlib/go-timezone-local v0.0.6 h1:Ii3QJ4FhosL/+eCZl6Hsdr4DDU4tfevNoV83yAEo2tU= github.com/thlib/go-timezone-local v0.0.6/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= -golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= -golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= +golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= +golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 043ab7d1c45100711c2045f897da6ef807da0936 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 19:40:00 +0000 Subject: [PATCH 08/12] Bump github.com/muesli/termenv in the go-dependencies group Bumps the go-dependencies group with 1 update: [github.com/muesli/termenv](https://github.com/muesli/termenv). Updates `github.com/muesli/termenv` from 0.15.2 to 0.16.0 - [Release notes](https://github.com/muesli/termenv/releases) - [Commits](https://github.com/muesli/termenv/compare/v0.15.2...v0.16.0) --- updated-dependencies: - dependency-name: github.com/muesli/termenv dependency-type: indirect update-type: version-update:semver-minor dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] --- go.mod | 3 +-- go.sum | 7 ++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 7f88600..6d23402 100644 --- a/go.mod +++ b/go.mod @@ -22,8 +22,7 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/muesli/termenv v0.15.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/thlib/go-timezone-local v0.0.6 // indirect diff --git a/go.sum b/go.sum index 0944f2e..b051311 100644 --- a/go.sum +++ b/go.sum @@ -32,13 +32,10 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= From 8444e6e5193881e241f4613dbd0d239fd163bb88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 18:50:34 +0000 Subject: [PATCH 09/12] Bump super-linter/super-linter in the github-actions group Bumps the github-actions group with 1 update: [super-linter/super-linter](https://github.com/super-linter/super-linter). Updates `super-linter/super-linter` from 7.2.1 to 7.3.0 - [Release notes](https://github.com/super-linter/super-linter/releases) - [Changelog](https://github.com/super-linter/super-linter/blob/main/CHANGELOG.md) - [Commits](https://github.com/super-linter/super-linter/compare/85f7611e0f7b53c8573cca84aa0ed4344f6f6a4d...4e8a7c2bf106c4c766c816b35ec612638dc9b6b2) --- updated-dependencies: - dependency-name: super-linter/super-linter dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index bea4066..3b139ba 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -29,7 +29,7 @@ jobs: go-version: "1.23" - name: Run Super-Linter - uses: super-linter/super-linter/slim@85f7611e0f7b53c8573cca84aa0ed4344f6f6a4d # v7.2.1 + uses: super-linter/super-linter/slim@4e8a7c2bf106c4c766c816b35ec612638dc9b6b2 # v7.3.0 env: VALIDATE_ALL_CODEBASE: true DEFAULT_BRANCH: "main" From 04247bad16f3cbf0c6b2060994779df8ef9fb8a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Mar 2025 10:00:31 +0000 Subject: [PATCH 10/12] Bump the go-dependencies group with 4 updates Bumps the go-dependencies group with 4 updates: [golang.org/x/image](https://github.com/golang/image), [golang.org/x/sys](https://github.com/golang/sys), [golang.org/x/term](https://github.com/golang/term) and [golang.org/x/text](https://github.com/golang/text). Updates `golang.org/x/image` from 0.24.0 to 0.25.0 - [Commits](https://github.com/golang/image/compare/v0.24.0...v0.25.0) Updates `golang.org/x/sys` from 0.30.0 to 0.31.0 - [Commits](https://github.com/golang/sys/compare/v0.30.0...v0.31.0) Updates `golang.org/x/term` from 0.29.0 to 0.30.0 - [Commits](https://github.com/golang/term/compare/v0.29.0...v0.30.0) Updates `golang.org/x/text` from 0.22.0 to 0.23.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.22.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/image dependency-type: indirect update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/sys dependency-type: indirect update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/term dependency-type: indirect update-type: version-update:semver-minor dependency-group: go-dependencies - dependency-name: golang.org/x/text dependency-type: indirect update-type: version-update:semver-minor dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] --- go.mod | 11 +++++------ go.sum | 16 ++++++++-------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 6d23402..0c44b53 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,7 @@ module github.com/github/gh-skyline go 1.22 - -toolchain go1.23.3 +toolchain go1.24.1 require ( github.com/cli/go-gh/v2 v2.11.2 @@ -26,9 +25,9 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/thlib/go-timezone-local v0.0.6 // indirect - golang.org/x/image v0.24.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/term v0.29.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/image v0.25.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b051311..58987b0 100644 --- a/go.sum +++ b/go.sum @@ -47,16 +47,16 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/thlib/go-timezone-local v0.0.6 h1:Ii3QJ4FhosL/+eCZl6Hsdr4DDU4tfevNoV83yAEo2tU= github.com/thlib/go-timezone-local v0.0.6/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= -golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= -golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From cb2c5932f1d185958d5177ac946b879347282732 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Tue, 25 Mar 2025 10:16:38 +0000 Subject: [PATCH 11/12] Update Go version to 1.24.1 in devcontainer and go.mod --- .devcontainer/devcontainer.json | 2 +- go.mod | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8883a33..ac35121 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,7 @@ { "name": "Go", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/go:1-1.23-bookworm", + "image": "mcr.microsoft.com/devcontainers/go:1-1.24-bookworm", "customizations": { "vscode": { "extensions": [ diff --git a/go.mod b/go.mod index 0c44b53..b9c022b 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,6 @@ module github.com/github/gh-skyline -go 1.22 -toolchain go1.24.1 +go 1.24.1 require ( github.com/cli/go-gh/v2 v2.11.2 From 81424bdc896589325fa5965fcdfa99f75e0f0078 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Tue, 25 Mar 2025 10:17:47 +0000 Subject: [PATCH 12/12] Update Go version setup in linter workflow to use go.mod file --- .github/workflows/linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 3b139ba..4ccaf2f 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -26,7 +26,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.23" + go-version-file: go.mod - name: Run Super-Linter uses: super-linter/super-linter/slim@4e8a7c2bf106c4c766c816b35ec612638dc9b6b2 # v7.3.0