From 9dd38d5062aacf18244c3e03ac5ba266eb8250bc Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:46:46 +0000 Subject: [PATCH 01/29] Add Contributor Covenant Code of Conduct --- CODE_OF_CONDUCT.md | 74 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..a1f82f0 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at . All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ \ No newline at end of file From a19d74a339e8177ebf34e92940031601e247e93d Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:54:27 +0000 Subject: [PATCH 02/29] Linting fixes --- CODE_OF_CONDUCT.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index a1f82f0..bb6d2a4 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,21 +14,21 @@ orientation. Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities @@ -47,7 +47,7 @@ threatening, offensive, or harmful. This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail +representing a project or community include using an official project email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. @@ -71,4 +71,4 @@ This Code of Conduct is adapted from the [Contributor Covenant][homepage], versi available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ \ No newline at end of file +[version]: http://contributor-covenant.org/version/1/4/ From 746c17b66e0104d3c9527db74c33b2ba5005f69c Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:06:02 +0000 Subject: [PATCH 03/29] Fix broken links --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 85f38d8..0e5f65f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,8 @@ # Contributing -[fork]: https://github.com/github/REPO/fork -[pr]: https://github.com/github/REPO/compare -[style]: https://github.com/github/REPO/blob/main/.golangci.yaml +[fork]: https://github.com/github/gh-skyline/fork +[pr]: https://github.com/github/gh-skyline/compare +[style]: https://github.com/github/gh-skyline/blob/main/.github/linters/.golangci.yml Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. From 968aef5c35948c40ad08ad624feba3540e2874e6 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:38:18 +0000 Subject: [PATCH 04/29] Add a section on visualizing STL files --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 6416604..518bc6d 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,10 @@ The extension generates ASCII art in terminal while loading, a unique and fun wa - `'▓'` High level: Heavy contribution activity - `'╻┃╽'` Top level: Last block with contributions in the week (Low, Medium, High) +## Visualizing your Skyline + +Once you have generated your STL file, you can visualize it using 3D modeling or 3D printing software. But did you know that you can upload your STL file to a GitHub repository and view your Skyline there? For example, take a look at [@chrisreddington's GitHub Skyline from 2011 - 2024](https://github.com/chrisreddington/chrisreddington/blob/master/chrisreddington-11-24-github-skyline.stl). + ## Project Structure ```text From bf9f13979674427232a36b74c0c19461872bcb61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2024 19:01:05 +0000 Subject: [PATCH 05/29] Bump github.com/thlib/go-timezone-local in the go-dependencies group Bumps the go-dependencies group with 1 update: [github.com/thlib/go-timezone-local](https://github.com/thlib/go-timezone-local). Updates `github.com/thlib/go-timezone-local` from 0.0.3 to 0.0.4 - [Release notes](https://github.com/thlib/go-timezone-local/releases) - [Commits](https://github.com/thlib/go-timezone-local/compare/v0.0.3...v0.0.4) --- updated-dependencies: - dependency-name: github.com/thlib/go-timezone-local dependency-type: indirect update-type: version-update:semver-patch dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 50dfda7..8a2aa58 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( 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/thlib/go-timezone-local v0.0.3 // indirect + github.com/thlib/go-timezone-local v0.0.4 // indirect golang.org/x/image v0.23.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.27.0 // indirect diff --git a/go.sum b/go.sum index 35d856f..63ad29a 100644 --- a/go.sum +++ b/go.sum @@ -48,8 +48,8 @@ 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/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.3 h1:ie5XtZWG5lQ4+1MtC5KZ/FeWlOKzW2nPoUnXYUbV/1s= -github.com/thlib/go-timezone-local v0.0.3/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= +github.com/thlib/go-timezone-local v0.0.4 h1:9oqkZLirWUtrFmhF/7WxR8Y0TBAGaThe95w6K3pLKBk= +github.com/thlib/go-timezone-local v0.0.4/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/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 44fee7dc8cf4822c01ad2f4880b5461d39196cc5 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Wed, 11 Dec 2024 14:12:26 +0000 Subject: [PATCH 06/29] Refactor GitHub client to consitently use GraphQL API. Add mock data utilities to consolidate duplication in tests --- github/client.go | 97 +++++++-------- github/client_test.go | 228 +++++++++++++++++------------------- main.go | 8 +- main_test.go | 172 +++++---------------------- testutil/fixtures/github.go | 43 +++++++ testutil/mocks/github.go | 79 +++++++++++++ types/types.go | 30 ++--- types/types_test.go | 40 +++---- 8 files changed, 335 insertions(+), 362 deletions(-) create mode 100644 testutil/fixtures/github.go create mode 100644 testutil/mocks/github.go diff --git a/github/client.go b/github/client.go index ed11802..54e641b 100644 --- a/github/client.go +++ b/github/client.go @@ -3,10 +3,7 @@ package github import ( - "bytes" - "encoding/json" "fmt" - "io" "time" "github.com/github/gh-skyline/errors" @@ -15,8 +12,7 @@ import ( // APIClient interface defines the methods we need from the client type APIClient interface { - Get(path string, response interface{}) error - Post(path string, body io.Reader, response interface{}) error + Do(query string, variables map[string]interface{}, response interface{}) error } // Client holds the API client @@ -31,17 +27,31 @@ func NewClient(apiClient APIClient) *Client { // GetAuthenticatedUser fetches the authenticated user's login name from GitHub. func (c *Client) GetAuthenticatedUser() (string, error) { - response := struct{ Login string }{} - err := c.api.Get("user", &response) + // GraphQL query to fetch the authenticated user's login. + query := ` + query { + viewer { + login + } + }` + + var response struct { + Viewer struct { + Login string `json:"login"` + } `json:"viewer"` + } + + // Execute the GraphQL query. + err := c.api.Do(query, nil, &response) if err != nil { return "", errors.New(errors.NetworkError, "failed to fetch authenticated user", err) } - if response.Login == "" { + if response.Viewer.Login == "" { return "", errors.New(errors.ValidationError, "received empty username from GitHub API", nil) } - return response.Login, nil + return response.Viewer.Login, nil } // FetchContributions retrieves the contribution data for a given username and year from GitHub. @@ -54,9 +64,10 @@ func (c *Client) FetchContributions(username string, year int) (*types.Contribut return nil, errors.New(errors.ValidationError, "year cannot be before GitHub's launch (2008)", nil) } - startDate := fmt.Sprintf("%d-01-01", year) - endDate := fmt.Sprintf("%d-12-31", year) + startDate := fmt.Sprintf("%d-01-01T00:00:00Z", year) + endDate := fmt.Sprintf("%d-12-31T23:59:59Z", year) + // GraphQL query to fetch the user's contributions within the specified date range. query := ` query ContributionGraph($username: String!, $from: DateTime!, $to: DateTime!) { user(login: $username) { @@ -77,34 +88,23 @@ func (c *Client) FetchContributions(username string, year int) (*types.Contribut variables := map[string]interface{}{ "username": username, - "from": startDate + "T00:00:00Z", - "to": endDate + "T23:59:59Z", + "from": startDate, + "to": endDate, } - payload := struct { - Query string `json:"query"` - Variables map[string]interface{} `json:"variables"` - }{ - Query: query, - Variables: variables, - } + var response types.ContributionsResponse - body, err := json.Marshal(payload) + // Execute the GraphQL query. + err := c.api.Do(query, variables, &response) if err != nil { - return nil, err - } - - var resp types.ContributionsResponse - if err := c.api.Post("graphql", bytes.NewBuffer(body), &resp); err != nil { - return nil, errors.New(errors.GraphQLError, "failed to fetch contributions", err) + return nil, errors.New(errors.NetworkError, "failed to fetch contributions", err) } - // Validate response - if resp.Data.User.Login == "" { - return nil, errors.New(errors.GraphQLError, "user not found", nil) + if response.User.Login == "" { + return nil, errors.New(errors.ValidationError, "received empty username from GitHub API", nil) } - return &resp, nil + return &response, nil } // GetUserJoinYear fetches the year a user joined GitHub using the GitHub API. @@ -113,6 +113,7 @@ func (c *Client) GetUserJoinYear(username string) (int, error) { return 0, errors.New(errors.ValidationError, "username cannot be empty", nil) } + // GraphQL query to fetch the user's account creation date. query := ` query UserJoinDate($username: String!) { user(login: $username) { @@ -124,35 +125,23 @@ func (c *Client) GetUserJoinYear(username string) (int, error) { "username": username, } - payload := struct { - Query string `json:"query"` - Variables map[string]interface{} `json:"variables"` - }{ - Query: query, - Variables: variables, + var response struct { + User struct { + CreatedAt time.Time `json:"createdAt"` + } `json:"user"` } - body, err := json.Marshal(payload) + // Execute the GraphQL query. + err := c.api.Do(query, variables, &response) if err != nil { - return 0, err - } - - var resp struct { - Data struct { - User struct { - CreatedAt string `json:"createdAt"` - } `json:"user"` - } `json:"data"` - } - if err := c.api.Post("graphql", bytes.NewBuffer(body), &resp); err != nil { - return 0, errors.New(errors.GraphQLError, "failed to fetch user join date", err) + return 0, errors.New(errors.NetworkError, "failed to fetch user's join date", err) } // Parse the join date - joinDate, err := time.Parse(time.RFC3339, resp.Data.User.CreatedAt) - if err != nil { - return 0, errors.New(errors.ValidationError, "failed to parse join date", err) + joinYear := response.User.CreatedAt.Year() + if joinYear == 0 { + return 0, errors.New(errors.ValidationError, "invalid join date received from GitHub API", nil) } - return joinDate.Year(), nil + return joinYear, nil } diff --git a/github/client_test.go b/github/client_test.go index 5587783..1d822ad 100644 --- a/github/client_test.go +++ b/github/client_test.go @@ -1,98 +1,50 @@ package github import ( - "encoding/json" - "io" "testing" + "time" "github.com/github/gh-skyline/errors" + "github.com/github/gh-skyline/testutil/mocks" + "github.com/github/gh-skyline/types" ) -type MockAPIClient struct { - GetFunc func(path string, response interface{}) error - PostFunc func(path string, body io.Reader, response interface{}) error -} - -func (m *MockAPIClient) Get(path string, response interface{}) error { - return m.GetFunc(path, response) -} - -func (m *MockAPIClient) Post(path string, body io.Reader, response interface{}) error { - return m.PostFunc(path, body, response) -} - -// mockAPIClient implements APIClient for testing -type mockAPIClient struct { - getResponse string - postResponse string - shouldError bool -} - -func (m *mockAPIClient) Get(_ string, response interface{}) error { - if m.shouldError { - return errors.New(errors.NetworkError, "mock error", nil) - } - return json.Unmarshal([]byte(m.getResponse), response) -} - -func (m *mockAPIClient) Post(_ string, _ io.Reader, response interface{}) error { - if m.shouldError { - return errors.New(errors.NetworkError, "mock error", nil) - } - return json.Unmarshal([]byte(m.postResponse), response) -} - -func TestNewClient(t *testing.T) { - mock := &mockAPIClient{} - client := NewClient(mock) - if client == nil { - t.Fatal("NewClient returned nil") - } - if client.api != mock { - t.Error("NewClient did not set api client correctly") - } -} - func TestGetAuthenticatedUser(t *testing.T) { tests := []struct { name string - response string - shouldError bool + mockResponse string + mockError error expectedUser string expectedError bool }{ { name: "successful response", - response: `{"login": "testuser"}`, + mockResponse: "testuser", expectedUser: "testuser", expectedError: false, }, { name: "empty username", - response: `{"login": ""}`, + mockResponse: "", expectedError: true, }, { name: "network error", - shouldError: true, + mockError: errors.New(errors.NetworkError, "network error", nil), expectedError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mock := &mockAPIClient{ - getResponse: tt.response, - shouldError: tt.shouldError, - } - client := NewClient(mock) + client := NewClient(&mocks.MockGitHubClient{ + Username: tt.mockResponse, + Err: tt.mockError, + }) user, err := client.GetAuthenticatedUser() - if tt.expectedError && err == nil { - t.Error("expected error but got none") - } - if !tt.expectedError && err != nil { - t.Errorf("unexpected error: %v", err) + if (err != nil) != tt.expectedError { + t.Errorf("expected error: %v, got: %v", tt.expectedError, err) } if user != tt.expectedUser { t.Errorf("expected user %q, got %q", tt.expectedUser, user) @@ -101,123 +53,153 @@ func TestGetAuthenticatedUser(t *testing.T) { } } -func TestFetchContributions(t *testing.T) { +func TestGetUserJoinYear(t *testing.T) { tests := []struct { name string username string - year int - response string - shouldError bool + mockResponse time.Time + mockError error + expectedYear int expectedError bool }{ { - name: "successful response", - username: "testuser", - year: 2023, - response: `{"data":{"user":{"login":"testuser","contributionsCollection":{"contributionCalendar":{"totalContributions":100,"weeks":[]}}}}}`, + name: "successful response", + username: "testuser", + mockResponse: time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC), + expectedYear: 2015, + expectedError: false, }, { name: "empty username", username: "", - year: 2023, - expectedError: true, - }, - { - name: "invalid year", - username: "testuser", - year: 2007, expectedError: true, }, { name: "network error", username: "testuser", - year: 2023, - shouldError: true, - expectedError: true, - }, - { - name: "user not found", - username: "testuser", - year: 2023, - response: `{"data":{"user":{"login":""}}}`, + mockError: errors.New(errors.NetworkError, "network error", nil), expectedError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mock := &mockAPIClient{ - postResponse: tt.response, - shouldError: tt.shouldError, - } - client := NewClient(mock) - - resp, err := client.FetchContributions(tt.username, tt.year) - if tt.expectedError && err == nil { - t.Error("expected error but got none") + client := NewClient(&mocks.MockGitHubClient{ + JoinYear: tt.expectedYear, + Err: tt.mockError, + }) + + year, err := client.GetUserJoinYear(tt.username) + if (err != nil) != tt.expectedError { + t.Errorf("expected error: %v, got: %v", tt.expectedError, err) } - if !tt.expectedError && err != nil { - t.Errorf("unexpected error: %v", err) - } - if !tt.expectedError && resp == nil { - t.Error("expected response but got nil") + if !tt.expectedError && year != tt.expectedYear { + t.Errorf("expected year %d, got %d", tt.expectedYear, year) } }) } } -func TestGetUserJoinYear(t *testing.T) { +func TestFetchContributions(t *testing.T) { + mockContributions := &types.ContributionsResponse{ + User: struct { + Login string `json:"login"` + ContributionsCollection struct { + ContributionCalendar struct { + TotalContributions int `json:"totalContributions"` + Weeks []struct { + ContributionDays []types.ContributionDay `json:"contributionDays"` + } `json:"weeks"` + } `json:"contributionCalendar"` + } `json:"contributionsCollection"` + }{ + Login: "chrisreddington", + ContributionsCollection: struct { + ContributionCalendar struct { + TotalContributions int `json:"totalContributions"` + Weeks []struct { + ContributionDays []types.ContributionDay `json:"contributionDays"` + } `json:"weeks"` + } `json:"contributionCalendar"` + }{ + ContributionCalendar: struct { + TotalContributions int `json:"totalContributions"` + Weeks []struct { + ContributionDays []types.ContributionDay `json:"contributionDays"` + } `json:"weeks"` + }{ + TotalContributions: 100, + Weeks: []struct { + ContributionDays []types.ContributionDay `json:"contributionDays"` + }{ + { + ContributionDays: []types.ContributionDay{ + { + ContributionCount: 5, + Date: "2023-01-01", + }, + }, + }, + }, + }, + }, + }, + } + tests := []struct { name string username string - response string - shouldError bool - expectedYear int + year int + mockResponse *types.ContributionsResponse + mockError error expectedError bool }{ { name: "successful response", username: "testuser", - response: `{"data":{"user":{"createdAt":"2015-01-01T00:00:00Z"}}}`, - expectedYear: 2015, + year: 2023, + mockResponse: mockContributions, expectedError: false, }, { name: "empty username", username: "", + year: 2023, expectedError: true, }, { - name: "network error", + name: "invalid year", username: "testuser", - shouldError: true, + year: 2007, expectedError: true, }, { - name: "invalid date format", + name: "network error", username: "testuser", - response: `{"data":{"user":{"createdAt":"invalid-date"}}}`, + year: 2023, + mockError: errors.New(errors.NetworkError, "network error", nil), expectedError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mock := &mockAPIClient{ - postResponse: tt.response, - shouldError: tt.shouldError, - } - client := NewClient(mock) + client := NewClient(&mocks.MockGitHubClient{ + Username: tt.username, + MockData: tt.mockResponse, + Err: tt.mockError, + }) - joinYear, err := client.GetUserJoinYear(tt.username) - if tt.expectedError && err == nil { - t.Error("expected error but got none") - } - if !tt.expectedError && err != nil { - t.Errorf("unexpected error: %v", err) + resp, err := client.FetchContributions(tt.username, tt.year) + if (err != nil) != tt.expectedError { + t.Errorf("expected error: %v, got: %v", tt.expectedError, err) } - if joinYear != tt.expectedYear { - t.Errorf("expected year %d, got %d", tt.expectedYear, joinYear) + if !tt.expectedError { + if resp == nil { + t.Error("expected response but got nil") + } else if resp.User.Login != "testuser" { + t.Errorf("expected user testuser, got %s", resp.User.Login) + } } }) } diff --git a/main.go b/main.go index e815810..253bfdf 100644 --- a/main.go +++ b/main.go @@ -217,22 +217,22 @@ var initializeGitHubClient = defaultGitHubClient // defaultGitHubClient is the default implementation of client initialization func defaultGitHubClient() (*github.Client, error) { - apiClient, err := api.DefaultRESTClient() + apiClient, err := api.DefaultGraphQLClient() if err != nil { - return nil, fmt.Errorf("failed to create REST client: %w", err) + 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) { - resp, err := client.FetchContributions(username, year) + 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 := resp.Data.User.ContributionsCollection.ContributionCalendar.Weeks + weeks := response.User.ContributionsCollection.ContributionCalendar.Weeks contributionGrid := make([][]types.ContributionDay, len(weeks)) for i, week := range weeks { contributionGrid[i] = week.ContributionDays diff --git a/main_test.go b/main_test.go index ec9d22d..be9ae03 100644 --- a/main_test.go +++ b/main_test.go @@ -1,142 +1,25 @@ package main import ( - "io" "testing" - "time" - "encoding/json" "fmt" - "strings" "github.com/github/gh-skyline/github" - "github.com/github/gh-skyline/types" + "github.com/github/gh-skyline/testutil/fixtures" + "github.com/github/gh-skyline/testutil/mocks" ) -// MockGitHubClient implements the github.APIClient interface -type MockGitHubClient struct { - username string - joinYear int - shouldError bool // Add error flag -} - -// Get implements the APIClient interface -func (m *MockGitHubClient) Get(_ string, _ interface{}) error { - return nil -} - -// Post implements the APIClient interface -func (m *MockGitHubClient) Post(path string, body io.Reader, response interface{}) error { - if path == "graphql" { - // Read the request body to determine which GraphQL query is being made - bodyBytes, _ := io.ReadAll(body) - bodyStr := string(bodyBytes) - - if strings.Contains(bodyStr, "UserJoinDate") { - // Handle user join date query - resp := response.(*struct { - Data struct { - User struct { - CreatedAt string `json:"createdAt"` - } `json:"user"` - } `json:"data"` - }) - resp.Data.User.CreatedAt = time.Date(m.joinYear, 1, 1, 0, 0, 0, 0, time.UTC).Format(time.RFC3339) - return nil - } - - if strings.Contains(bodyStr, "ContributionGraph") { - // Handle contribution graph query (existing logic) - return json.Unmarshal(contributionResponse(m.username), response) - } - } - return nil -} - -// Helper function to generate mock contribution response -func contributionResponse(username string) []byte { - response := fmt.Sprintf(`{ - "data": { - "user": { - "login": "%s", - "contributionsCollection": { - "contributionCalendar": { - "totalContributions": 1, - "weeks": [ - { - "contributionDays": [ - { - "contributionCount": 1, - "date": "2024-01-01" - } - ] - } - ] - } - } - } - } - }`, username) - return []byte(response) -} - -// GetAuthenticatedUser returns the authenticated user's username or an error -// if the mock client is set to error or the username is not set. -func (m *MockGitHubClient) GetAuthenticatedUser() (string, error) { - // Return error if shouldError is true - if m.shouldError { - return "", fmt.Errorf("mock client error") - } - // Validate username is not empty - if m.username == "" { - return "", fmt.Errorf("mock username not set") - } - return m.username, nil -} - -// GetUserJoinYear implements the GitHubClientInterface. -// It returns the year the user joined GitHub. -func (m *MockGitHubClient) GetUserJoinYear(_ string) (int, error) { - return m.joinYear, nil -} - -// FetchContributions mocks fetching GitHub contributions for a user -// in a given year, returning minimal valid data. -func (m *MockGitHubClient) FetchContributions(username string, year int) (*types.ContributionsResponse, error) { - // Return minimal valid response - resp := &types.ContributionsResponse{} - resp.Data.User.Login = username - // Add a single week with a single day for minimal valid data - week := struct { - ContributionDays []types.ContributionDay `json:"contributionDays"` - }{ - ContributionDays: []types.ContributionDay{ - { - ContributionCount: 1, - Date: time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC).Format("2006-01-02"), - }, - }, - } - resp.Data.User.ContributionsCollection.ContributionCalendar.Weeks = []struct { - ContributionDays []types.ContributionDay `json:"contributionDays"` - }{week} - return resp, nil -} - // MockBrowser implements the Browser interface type MockBrowser struct { - LastURL string - ShouldError bool + LastURL string + Err error } // Browse implements the Browser interface -// Changed from pointer receiver to value receiver func (m *MockBrowser) Browse(url string) error { m.LastURL = url - if m.ShouldError { - return fmt.Errorf("mock browser error") - } - return nil + return m.Err } func TestFormatYearRange(t *testing.T) { @@ -313,7 +196,7 @@ func TestGenerateSkyline(t *testing.T) { endYear int targetUser string full bool - mockClient *MockGitHubClient + mockClient *mocks.MockGitHubClient wantErr bool }{ { @@ -322,9 +205,10 @@ func TestGenerateSkyline(t *testing.T) { endYear: 2024, targetUser: "testuser", full: false, - mockClient: &MockGitHubClient{ - username: "testuser", - joinYear: 2020, + mockClient: &mocks.MockGitHubClient{ + Username: "testuser", + JoinYear: 2020, + MockData: fixtures.GenerateContributionsResponse("testuser", 2024), }, wantErr: false, }, @@ -334,21 +218,23 @@ func TestGenerateSkyline(t *testing.T) { endYear: 2024, targetUser: "testuser", full: false, - mockClient: &MockGitHubClient{ - username: "testuser", - joinYear: 2020, + mockClient: &mocks.MockGitHubClient{ + Username: "testuser", + JoinYear: 2020, + MockData: fixtures.GenerateContributionsResponse("testuser", 2024), }, wantErr: false, }, { name: "full range", - startYear: 2020, + startYear: 2008, endYear: 2024, targetUser: "testuser", full: true, - mockClient: &MockGitHubClient{ - username: "testuser", - joinYear: 2020, + mockClient: &mocks.MockGitHubClient{ + Username: "testuser", + JoinYear: 2008, + MockData: fixtures.GenerateContributionsResponse("testuser", 2024), }, wantErr: false, }, @@ -374,23 +260,22 @@ func TestOpenGitHubProfile(t *testing.T) { tests := []struct { name string targetUser string - mockClient *MockGitHubClient + mockClient *mocks.MockGitHubClient wantURL string wantErr bool }{ { name: "specific user", targetUser: "testuser", - mockClient: &MockGitHubClient{}, + mockClient: &mocks.MockGitHubClient{}, wantURL: "https://github.com/testuser", wantErr: false, }, { name: "authenticated user", targetUser: "", - mockClient: &MockGitHubClient{ - username: "authuser", - shouldError: false, + mockClient: &mocks.MockGitHubClient{ + Username: "authuser", }, wantURL: "https://github.com/authuser", wantErr: false, @@ -398,9 +283,8 @@ func TestOpenGitHubProfile(t *testing.T) { { name: "client error", targetUser: "", - mockClient: &MockGitHubClient{ - username: "", - shouldError: true, + mockClient: &mocks.MockGitHubClient{ + Err: fmt.Errorf("mock error"), }, wantErr: true, }, @@ -408,8 +292,10 @@ func TestOpenGitHubProfile(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Create MockBrowser and call openGitHubProfile - mockBrowser := &MockBrowser{ShouldError: tt.wantErr} + mockBrowser := &MockBrowser{} + if tt.wantErr { + mockBrowser.Err = fmt.Errorf("mock error") + } err := openGitHubProfile(tt.targetUser, tt.mockClient, mockBrowser) if (err != nil) != tt.wantErr { diff --git a/testutil/fixtures/github.go b/testutil/fixtures/github.go new file mode 100644 index 0000000..491baba --- /dev/null +++ b/testutil/fixtures/github.go @@ -0,0 +1,43 @@ +// Package fixtures provides test utilities and mock data generators +// for testing the gh-skyline application. +package fixtures + +import ( + "time" + + "github.com/github/gh-skyline/types" +) + +// GenerateContributionsResponse creates a mock contributions response +func GenerateContributionsResponse(username string, year int) *types.ContributionsResponse { + response := &types.ContributionsResponse{} + response.User.Login = username + response.User.ContributionsCollection.ContributionCalendar.TotalContributions = 100 + + // Create sample weeks with contribution days + weeks := make([]struct { + ContributionDays []types.ContributionDay `json:"contributionDays"` + }, 52) + + for i := range weeks { + days := make([]types.ContributionDay, 7) + for j := range days { + days[j] = types.ContributionDay{ + ContributionCount: (i + j) % 10, + Date: time.Date(year, 1, 1+i*7+j, 0, 0, 0, 0, time.UTC).Format("2006-01-02"), + } + } + weeks[i].ContributionDays = days + } + + response.User.ContributionsCollection.ContributionCalendar.Weeks = weeks + return response +} + +// CreateMockContributionDay creates a mock contribution day +func CreateMockContributionDay(date time.Time, count int) types.ContributionDay { + return types.ContributionDay{ + ContributionCount: count, + Date: date.Format("2006-01-02"), + } +} diff --git a/testutil/mocks/github.go b/testutil/mocks/github.go new file mode 100644 index 0000000..b03409e --- /dev/null +++ b/testutil/mocks/github.go @@ -0,0 +1,79 @@ +// Package mocks provides mock implementations of interfaces used in testing +package mocks + +import ( + "fmt" + "time" + + "github.com/github/gh-skyline/testutil/fixtures" + "github.com/github/gh-skyline/types" +) + +// MockGitHubClient implements both GitHubClientInterface and APIClient interfaces +type MockGitHubClient struct { + Username string + JoinYear int + MockData *types.ContributionsResponse + Response interface{} // Generic response field for testing + Err error // Error to return if needed +} + +// GetAuthenticatedUser implements GitHubClientInterface +func (m *MockGitHubClient) GetAuthenticatedUser() (string, error) { + if m.Err != nil { + return "", m.Err + } + if m.Username == "" { + return "", fmt.Errorf("mock username not set") + } + return m.Username, nil +} + +// GetUserJoinYear implements GitHubClientInterface +func (m *MockGitHubClient) GetUserJoinYear(_ string) (int, error) { + if m.Err != nil { + return 0, m.Err + } + if m.JoinYear == 0 { + return 0, fmt.Errorf("mock join year not set") + } + return m.JoinYear, nil +} + +// FetchContributions implements GitHubClientInterface +func (m *MockGitHubClient) FetchContributions(username string, year int) (*types.ContributionsResponse, error) { + if m.Err != nil { + return nil, m.Err + } + // Always return generated mock data with valid contributions + return fixtures.GenerateContributionsResponse(username, year), nil +} + +// Do implements APIClient +func (m *MockGitHubClient) Do(_ string, _ map[string]interface{}, response interface{}) error { + if m.Err != nil { + return m.Err + } + + switch v := response.(type) { + case *struct { + Viewer struct { + Login string `json:"login"` + } `json:"viewer"` + }: + v.Viewer.Login = m.Username + case *struct { + User struct { + CreatedAt time.Time `json:"createdAt"` + } `json:"user"` + }: + if m.JoinYear > 0 { + v.User.CreatedAt = time.Date(m.JoinYear, 1, 1, 0, 0, 0, 0, time.UTC) + } + case *types.ContributionsResponse: + // Always use generated mock data instead of empty response + mockResp := fixtures.GenerateContributionsResponse(m.Username, time.Now().Year()) + *v = *mockResp + } + return nil +} diff --git a/types/types.go b/types/types.go index f70f84d..13f6dd6 100644 --- a/types/types.go +++ b/types/types.go @@ -9,9 +9,8 @@ import ( ) // ContributionDay represents a single day of GitHub contributions. -// It contains the number of contributions made on a specific date. type ContributionDay struct { - ContributionCount int + ContributionCount int `json:"contributionCount"` Date string `json:"date"` } @@ -37,22 +36,19 @@ func (c ContributionDay) Validate() error { return nil } -// ContributionsResponse represents the GitHub GraphQL API response structure -// for fetching user contributions data. +// ContributionsResponse represents the contribution data returned by the GitHub API. type ContributionsResponse struct { - Data struct { - User struct { - Login string - ContributionsCollection struct { - ContributionCalendar struct { - TotalContributions int `json:"totalContributions"` - Weeks []struct { - ContributionDays []ContributionDay `json:"contributionDays"` - } `json:"weeks"` - } `json:"contributionCalendar"` - } `json:"contributionsCollection"` - } `json:"user"` - } `json:"data"` + User struct { + Login string `json:"login"` + ContributionsCollection struct { + ContributionCalendar struct { + TotalContributions int `json:"totalContributions"` + Weeks []struct { + ContributionDays []ContributionDay `json:"contributionDays"` + } `json:"weeks"` + } `json:"contributionCalendar"` + } `json:"contributionsCollection"` + } `json:"user"` } // Point3D represents a point in 3D space using float64 for accuracy in calculations. diff --git a/types/types_test.go b/types/types_test.go index 29a9be2..ccf1798 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -44,23 +44,21 @@ func TestContributionDaySerialization(t *testing.T) { // is properly parsed with nested fields. func TestContributionsResponseParsing(t *testing.T) { sampleResponse := `{ - "data": { - "user": { - "login": "testuser", - "contributionsCollection": { - "contributionCalendar": { - "totalContributions": 100, - "weeks": [ - { - "contributionDays": [ - { - "contributionCount": 5, - "date": "2024-03-21" - } - ] - } - ] - } + "user": { + "login": "testuser", + "contributionsCollection": { + "contributionCalendar": { + "totalContributions": 100, + "weeks": [ + { + "contributionDays": [ + { + "contributionCount": 5, + "date": "2024-03-21" + } + ] + } + ] } } } @@ -75,12 +73,12 @@ func TestContributionsResponseParsing(t *testing.T) { expectedUsername := "testuser" expectedTotalContributions := 100 - if parsedResponse.Data.User.Login != expectedUsername { - t.Errorf("username mismatch: got %q, want %q", parsedResponse.Data.User.Login, expectedUsername) + if parsedResponse.User.Login != expectedUsername { + t.Errorf("username mismatch: got %q, want %q", parsedResponse.User.Login, expectedUsername) } - if parsedResponse.Data.User.ContributionsCollection.ContributionCalendar.TotalContributions != expectedTotalContributions { + if parsedResponse.User.ContributionsCollection.ContributionCalendar.TotalContributions != expectedTotalContributions { t.Errorf("total contributions mismatch: got %d, want %d", - parsedResponse.Data.User.ContributionsCollection.ContributionCalendar.TotalContributions, + parsedResponse.User.ContributionsCollection.ContributionCalendar.TotalContributions, expectedTotalContributions) } } From 0940e5c29112526e2ceb207c332fabe309d95993 Mon Sep 17 00:00:00 2001 From: Laotree Date: Thu, 12 Dec 2024 11:28:31 +0800 Subject: [PATCH 07/29] Update CONTRIBUTING.md Reading this document, I found 2 improvements could occur. - a typo - a link turn to be 404 --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e5f65f..3bd7be2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,10 +34,10 @@ The environment will be ready to use in a few minutes. ### Local development environment -These are one time installations required to be able to test your changes locally as part of the pull request (PR) submission process. +These are one-time installations required to be able to test your changes locally as part of the pull request (PR) submission process. 1. install Go [through download](https://go.dev/doc/install) | [through Homebrew](https://formulae.brew.sh/formula/go) -1. [install golangci-lint](https://golangci-lint.run/usage/install/#local-installation) +1. [install golangci-lint](https://golangci-lint.run/welcome/install/#local-installation) ### Building the extension From 8b4ac5bf77063ba3be42ce7a56cb38784f256afa Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Fri, 13 Dec 2024 20:07:19 -0500 Subject: [PATCH 08/29] Add flag for terminal art only mode --- main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.go b/main.go index e815810..3880cc9 100644 --- a/main.go +++ b/main.go @@ -45,6 +45,7 @@ var ( full bool debug bool web bool + artOnly bool output string // new output path flag rootCmd = &cobra.Command{ @@ -106,6 +107,7 @@ func init() { 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().BoolVar(&artOnly, "art-only", false, "Generate only ascii art and suppress skyline header.") rootCmd.Flags().StringVarP(&output, "output", "o", "", "Output file path (optional)") } From 73feeb1eabf8f8031f922ccd28e3649efe7e24ca Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Fri, 13 Dec 2024 20:35:03 -0500 Subject: [PATCH 09/29] Add checks for new flag --- main.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/main.go b/main.go index 3880cc9..898fd7c 100644 --- a/main.go +++ b/main.go @@ -178,7 +178,7 @@ func generateSkyline(startYear, endYear int, targetUser string, full bool) error allContributions = append(allContributions, contributions) // Generate ASCII art for each year - asciiArt, err := ascii.GenerateASCII(contributions, targetUser, year, year == startYear) + asciiArt, err := ascii.GenerateASCII(contributions, targetUser, year, (year == startYear) && !artOnly) if err != nil { if warnErr := log.Warning("Failed to generate ASCII preview: %v", err); warnErr != nil { return warnErr @@ -204,14 +204,18 @@ func generateSkyline(startYear, endYear int, targetUser string, full bool) error } } - // 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) + 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 From c37b948a135242dad6b011250de52553d5cd87af Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Fri, 13 Dec 2024 20:52:45 -0500 Subject: [PATCH 10/29] Split up header and footer options --- ascii/generator.go | 12 +++++++----- main.go | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ascii/generator.go b/ascii/generator.go index cba3457..7ba2aeb 100644 --- a/ascii/generator.go +++ b/ascii/generator.go @@ -16,7 +16,7 @@ var ErrInvalidGrid = errors.New("invalid contribution grid") // GenerateASCII creates a 2D ASCII art representation of the contribution data. // It returns the generated ASCII art as a string and an error if the operation fails. // When includeHeader is true, the output includes the header template. -func GenerateASCII(contributionGrid [][]types.ContributionDay, username string, year int, includeHeader bool) (string, error) { +func GenerateASCII(contributionGrid [][]types.ContributionDay, username string, year int, includeHeader bool, includeUserInfo bool) (string, error) { if len(contributionGrid) == 0 { return "", ErrInvalidGrid } @@ -74,10 +74,12 @@ func GenerateASCII(contributionGrid [][]types.ContributionDay, username string, buffer.WriteRune('\n') } - // Add centered user info below - buffer.WriteString("\n") - buffer.WriteString(centerText(username)) - buffer.WriteString(centerText(fmt.Sprintf("%d", year))) + if includeUserInfo { + // Add centered user info below + buffer.WriteString("\n") + buffer.WriteString(centerText(username)) + buffer.WriteString(centerText(fmt.Sprintf("%d", year))) + } return buffer.String(), nil } diff --git a/main.go b/main.go index 898fd7c..3093434 100644 --- a/main.go +++ b/main.go @@ -178,7 +178,7 @@ func generateSkyline(startYear, endYear int, targetUser string, full bool) error allContributions = append(allContributions, contributions) // Generate ASCII art for each year - asciiArt, err := ascii.GenerateASCII(contributions, targetUser, year, (year == startYear) && !artOnly) + 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 From a0497c92e24944e019fd1b3bb306b04094e2be3a Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Fri, 13 Dec 2024 20:53:31 -0500 Subject: [PATCH 11/29] Add additional ascii generation tests --- ascii/generator_test.go | 62 ++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/ascii/generator_test.go b/ascii/generator_test.go index a0ed580..b35620b 100644 --- a/ascii/generator_test.go +++ b/ascii/generator_test.go @@ -9,39 +9,47 @@ import ( func TestGenerateASCII(t *testing.T) { tests := []struct { - name string - grid [][]types.ContributionDay - user string - year int - includeHeader bool - wantErr bool + name string + grid [][]types.ContributionDay + user string + year int + includeHeader bool + wantErr bool }{ { - name: "empty grid", - grid: [][]types.ContributionDay{}, - user: "testuser", - year: 2023, - includeHeader: false, - wantErr: true, + name: "empty grid", + grid: [][]types.ContributionDay{}, + user: "testuser", + year: 2023, + includeHeader: false, + wantErr: true, }, { - name: "valid grid", - grid: makeTestGrid(3, 7), - user: "testuser", - year: 2023, - includeHeader: false, - wantErr: false, + name: "valid grid", + grid: makeTestGrid(3, 7), + user: "testuser", + year: 2023, + includeHeader: false, + wantErr: false, }, + { + name: "no header", + grid: makeTestGrid(3, 7), + user: "testuser", + year: 2023, + includeHeader: false, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := GenerateASCII(tt.grid, tt.user, tt.year, tt.includeHeader) + result, err := GenerateASCII(tt.grid, tt.user, tt.year, tt.includeHeader, tt.includeHeader) if (err != nil) != tt.wantErr { t.Errorf("GenerateASCII() error = %v, wantErr %v", err, tt.wantErr) return } - if !tt.wantErr { + if !tt.wantErr && tt.includeHeader { // Existing validation code... if !strings.Contains(result, "testuser") { t.Error("Generated ASCII should contain username") @@ -52,7 +60,21 @@ func TestGenerateASCII(t *testing.T) { if !strings.Contains(result, string(EmptyBlock)) { t.Error("Generated ASCII should contain empty blocks") } + if !strings.Contains(result, HeaderTemplate) { + t.Error("Generated ASCII should contain header") + } } + if !tt.wantErr && !tt.includeHeader { + if strings.Contains(result, "testuser") { + t.Error("Generated ASCII should exclude username when requested") + } + if strings.Contains(result, "2023") { + t.Error("Generated ASCII should exclude year when requested") + } + if strings.Contains(result, HeaderTemplate) { + t.Error("Generated ASCII should exclude header when requested") + } + } }) } } From cfe26b0abcc9d069269c04090b7c702e65133e60 Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Fri, 13 Dec 2024 20:58:09 -0500 Subject: [PATCH 12/29] Reword cli help entry --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 3093434..b23ef4a 100644 --- a/main.go +++ b/main.go @@ -107,7 +107,7 @@ func init() { 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().BoolVar(&artOnly, "art-only", false, "Generate only ascii art and suppress skyline header.") + rootCmd.Flags().BoolVar(&artOnly, "art-only", false, "Generate only ASCII preview") rootCmd.Flags().StringVarP(&output, "output", "o", "", "Output file path (optional)") } From 31f691d3bc77205d481dc7faaa881c7d5dcbb708 Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Fri, 13 Dec 2024 21:11:16 -0500 Subject: [PATCH 13/29] Update CLI instructions in README --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 518bc6d..0470195 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ You can run the `gh skyline` command with the following flags: - Examples: `gh skyline --year 2020`, `gh skyline --year 2014-2024` - `-w`, `--web`: Open the GitHub profile for the authenticated or specified user. - Example: `gh skyline --web`, `gh skyline --user mona --web` +- `--art-only`: Show the ASCII art preview without generating an STL file. ### Examples @@ -78,6 +79,12 @@ Generate a skyline from the user's join year to the current year: gh skyline --full ``` +Generate only the ASCII preview for a skyline: + +```bash +gh skyline --art-only +``` + Enable debug logging: ```bash From 870c8163b0e05932f9cd3084b02a58e19a7d47a2 Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Fri, 13 Dec 2024 21:16:05 -0500 Subject: [PATCH 14/29] Incorporate formatter corrections --- ascii/generator.go | 12 +++---- ascii/generator_test.go | 80 ++++++++++++++++++++--------------------- main.go | 24 ++++++------- 3 files changed, 58 insertions(+), 58 deletions(-) diff --git a/ascii/generator.go b/ascii/generator.go index 7ba2aeb..fa72b6c 100644 --- a/ascii/generator.go +++ b/ascii/generator.go @@ -74,12 +74,12 @@ func GenerateASCII(contributionGrid [][]types.ContributionDay, username string, buffer.WriteRune('\n') } - if includeUserInfo { - // Add centered user info below - buffer.WriteString("\n") - buffer.WriteString(centerText(username)) - buffer.WriteString(centerText(fmt.Sprintf("%d", year))) - } + if includeUserInfo { + // Add centered user info below + buffer.WriteString("\n") + buffer.WriteString(centerText(username)) + buffer.WriteString(centerText(fmt.Sprintf("%d", year))) + } return buffer.String(), nil } diff --git a/ascii/generator_test.go b/ascii/generator_test.go index b35620b..8e547d5 100644 --- a/ascii/generator_test.go +++ b/ascii/generator_test.go @@ -9,37 +9,37 @@ import ( func TestGenerateASCII(t *testing.T) { tests := []struct { - name string - grid [][]types.ContributionDay - user string - year int - includeHeader bool - wantErr bool + name string + grid [][]types.ContributionDay + user string + year int + includeHeader bool + wantErr bool }{ { - name: "empty grid", - grid: [][]types.ContributionDay{}, - user: "testuser", - year: 2023, - includeHeader: false, - wantErr: true, + name: "empty grid", + grid: [][]types.ContributionDay{}, + user: "testuser", + year: 2023, + includeHeader: false, + wantErr: true, }, { - name: "valid grid", - grid: makeTestGrid(3, 7), - user: "testuser", - year: 2023, - includeHeader: false, - wantErr: false, + name: "valid grid", + grid: makeTestGrid(3, 7), + user: "testuser", + year: 2023, + includeHeader: false, + wantErr: false, + }, + { + name: "no header", + grid: makeTestGrid(3, 7), + user: "testuser", + year: 2023, + includeHeader: false, + wantErr: false, }, - { - name: "no header", - grid: makeTestGrid(3, 7), - user: "testuser", - year: 2023, - includeHeader: false, - wantErr: false, - }, } for _, tt := range tests { @@ -60,21 +60,21 @@ func TestGenerateASCII(t *testing.T) { if !strings.Contains(result, string(EmptyBlock)) { t.Error("Generated ASCII should contain empty blocks") } - if !strings.Contains(result, HeaderTemplate) { - t.Error("Generated ASCII should contain header") - } + if !strings.Contains(result, HeaderTemplate) { + t.Error("Generated ASCII should contain header") + } + } + if !tt.wantErr && !tt.includeHeader { + if strings.Contains(result, "testuser") { + t.Error("Generated ASCII should exclude username when requested") + } + if strings.Contains(result, "2023") { + t.Error("Generated ASCII should exclude year when requested") + } + if strings.Contains(result, HeaderTemplate) { + t.Error("Generated ASCII should exclude header when requested") + } } - if !tt.wantErr && !tt.includeHeader { - if strings.Contains(result, "testuser") { - t.Error("Generated ASCII should exclude username when requested") - } - if strings.Contains(result, "2023") { - t.Error("Generated ASCII should exclude year when requested") - } - if strings.Contains(result, HeaderTemplate) { - t.Error("Generated ASCII should exclude header when requested") - } - } }) } } diff --git a/main.go b/main.go index b23ef4a..bdc4f46 100644 --- a/main.go +++ b/main.go @@ -204,18 +204,18 @@ func generateSkyline(startYear, endYear int, targetUser string, full bool) error } } - 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 + 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 From a249572432b821678d63e386105bd70f279ab9c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:05:17 +0000 Subject: [PATCH 15/29] 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.0 to 7.2.1 - [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/e1cb86b6e8d119f789513668b4b30bf17fe1efe4...85f7611e0f7b53c8573cca84aa0ed4344f6f6a4d) --- updated-dependencies: - dependency-name: super-linter/super-linter dependency-type: direct:production update-type: version-update:semver-patch 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 8166258..bea4066 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@e1cb86b6e8d119f789513668b4b30bf17fe1efe4 # v7.2.0 + uses: super-linter/super-linter/slim@85f7611e0f7b53c8573cca84aa0ed4344f6f6a4d # v7.2.1 env: VALIDATE_ALL_CODEBASE: true DEFAULT_BRANCH: "main" From b39cfcc54f96f3f60331cba7d9979c777c9e2f60 Mon Sep 17 00:00:00 2001 From: Siddharth Koli Date: Sat, 21 Dec 2024 00:14:25 +0530 Subject: [PATCH 16/29] Fix divide by 0 in case of 0 contributions --- ascii/generator.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ascii/generator.go b/ascii/generator.go index cba3457..bbf63a7 100644 --- a/ascii/generator.go +++ b/ascii/generator.go @@ -60,7 +60,10 @@ func GenerateASCII(contributionGrid [][]types.ContributionDay, username string, if day.ContributionCount == -1 { asciiGrid[dayIdx][weekIdx] = FutureBlock } else { - normalized := float64(day.ContributionCount) / float64(maxContributions) + normalized := 0.0 + if maxContributions != 0 { + normalized = float64(day.ContributionCount) / float64(maxContributions) + } asciiGrid[dayIdx][weekIdx] = getBlock(normalized, dayIdx, nonZeroCount) } } From 460d4057709e97f050853c94f509a7a8e2180b8f Mon Sep 17 00:00:00 2001 From: Siddharth Koli Date: Sat, 21 Dec 2024 00:15:44 +0530 Subject: [PATCH 17/29] Improve readability of conditions --- main.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index e815810..9addd7e 100644 --- a/main.go +++ b/main.go @@ -190,8 +190,9 @@ func generateSkyline(startYear, endYear int, targetUser string, full bool) error lines := strings.Split(asciiArt, "\n") gridStart := 0 for i, line := range lines { - if strings.Contains(line, string(ascii.EmptyBlock)) || - strings.Contains(line, string(ascii.FoundationLow)) { + containsEmptyBlock := strings.Contains(line, string(ascii.EmptyBlock)) + containsFoundationLow := strings.Contains(line, string(ascii.FoundationLow)) + isNotOnlyEmptyBlocks := strings.Trim(line, string(ascii.EmptyBlock)) != "" gridStart = i break } From 64066af916f865432e47a6bf260a673396c8860b Mon Sep 17 00:00:00 2001 From: Siddharth Koli Date: Sat, 21 Dec 2024 00:16:07 +0530 Subject: [PATCH 18/29] Add condition to skip over empty blocks (when -f is passed) --- main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.go b/main.go index 9addd7e..79ce61d 100644 --- a/main.go +++ b/main.go @@ -193,6 +193,8 @@ func generateSkyline(startYear, endYear int, targetUser string, full bool) error 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 } From bb8c35fe0eb9e9efee2e948ccf88ae7c28841f2a Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Sat, 28 Dec 2024 10:50:09 +0000 Subject: [PATCH 19/29] Remove GitHub CodeQL extension from devcontainer configuration --- .devcontainer/devcontainer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6c71182..8883a33 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -14,7 +14,6 @@ "github.copilot-workspace", "GitHub.vscode-pull-request-github", "GitHub.remotehub", - "GitHub.vscode-codeql", "golang.Go" ] } From 9b837dc564168f5c71363ac35d914c3934b9b515 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Sat, 28 Dec 2024 11:03:35 +0000 Subject: [PATCH 20/29] Remove baseline validation workflow from GitHub Actions --- .github/workflows/baseline.yml | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 .github/workflows/baseline.yml diff --git a/.github/workflows/baseline.yml b/.github/workflows/baseline.yml deleted file mode 100644 index 8fa8481..0000000 --- a/.github/workflows/baseline.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Validate Repository Configuration - -permissions: - contents: read - -on: - push: - branches: [main] - pull_request: - branches: [main] - workflow_dispatch: - -jobs: - validate: - name: Validate Baseline Configuration - uses: chrisreddington/reusable-workflows/.github/workflows/baseline-validator.yml@d62f6e0cbe864707a620bba0be92695711514442 - with: - required-features: "ghcr.io/devcontainers/features/github-cli:1" From 05293138966f23b02f0e29abbe837a3ee5db2600 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Sat, 28 Dec 2024 11:37:17 +0000 Subject: [PATCH 21/29] Add ConfigError type and improve GitHub profile URL generation --- errors/errors.go | 1 + main.go | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/errors/errors.go b/errors/errors.go index fc52f8a..ec3bca3 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -15,6 +15,7 @@ const ( NetworkError ErrorType = "NETWORK" // Network communication errors GraphQLError ErrorType = "GRAPHQL" // GitHub GraphQL API errors STLError ErrorType = "STL" // STL file generation errors + ConfigError ErrorType = "CONFIG" // Configuration errors ) // SkylineError provides structured error information including type and context diff --git a/main.go b/main.go index 253bfdf..8f93b95 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "github.com/cli/go-gh/v2/pkg/api" "github.com/cli/go-gh/v2/pkg/browser" + "github.com/cli/go-gh/v2/pkg/config" "github.com/github/gh-skyline/ascii" "github.com/github/gh-skyline/errors" "github.com/github/gh-skyline/github" @@ -290,6 +291,16 @@ func openGitHubProfile(targetUser string, client GitHubClientInterface, b Browse targetUser = username } - profileURL := fmt.Sprintf("https://github.com/%s", targetUser) + cfg, err := config.Read(nil) + if err != nil { + return errors.New(errors.ConfigError, "failed to read gh config", err) + } + + hostname, _ := cfg.Get([]string{"host"}) + if hostname == "" { + hostname = "github.com" + } + + profileURL := fmt.Sprintf("https://%s/%s", hostname, targetUser) return b.Browse(profileURL) } From 31778c6befb383d5a9eb0e394bc379f8c72c3bb9 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Sat, 28 Dec 2024 11:48:01 +0000 Subject: [PATCH 22/29] Revert "Add ConfigError type and improve GitHub profile URL generation" --- errors/errors.go | 1 - main.go | 13 +------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/errors/errors.go b/errors/errors.go index ec3bca3..fc52f8a 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -15,7 +15,6 @@ const ( NetworkError ErrorType = "NETWORK" // Network communication errors GraphQLError ErrorType = "GRAPHQL" // GitHub GraphQL API errors STLError ErrorType = "STL" // STL file generation errors - ConfigError ErrorType = "CONFIG" // Configuration errors ) // SkylineError provides structured error information including type and context diff --git a/main.go b/main.go index 8f93b95..253bfdf 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,6 @@ import ( "github.com/cli/go-gh/v2/pkg/api" "github.com/cli/go-gh/v2/pkg/browser" - "github.com/cli/go-gh/v2/pkg/config" "github.com/github/gh-skyline/ascii" "github.com/github/gh-skyline/errors" "github.com/github/gh-skyline/github" @@ -291,16 +290,6 @@ func openGitHubProfile(targetUser string, client GitHubClientInterface, b Browse targetUser = username } - cfg, err := config.Read(nil) - if err != nil { - return errors.New(errors.ConfigError, "failed to read gh config", err) - } - - hostname, _ := cfg.Get([]string{"host"}) - if hostname == "" { - hostname = "github.com" - } - - profileURL := fmt.Sprintf("https://%s/%s", hostname, targetUser) + profileURL := fmt.Sprintf("https://github.com/%s", targetUser) return b.Browse(profileURL) } From 49c21c6d1b47fa966fe643c8923e82dcd284f2dd Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Sat, 28 Dec 2024 12:25:24 +0000 Subject: [PATCH 23/29] Add shorthand flag for 'art-only' option in CLI --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 956973a..2f3be30 100644 --- a/main.go +++ b/main.go @@ -107,7 +107,7 @@ func init() { 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().BoolVar(&artOnly, "art-only", false, "Generate only ASCII preview") + rootCmd.Flags().BoolVarP(&artOnly, "art-only", "a", false, "Generate only ASCII preview") rootCmd.Flags().StringVarP(&output, "output", "o", "", "Output file path (optional)") } From 6ad84d2bdec0d8c560229ecae597064521e3e882 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Sat, 28 Dec 2024 12:29:50 +0000 Subject: [PATCH 24/29] Include -a option in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0470195..1d92e06 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ You can run the `gh skyline` command with the following flags: - Examples: `gh skyline --year 2020`, `gh skyline --year 2014-2024` - `-w`, `--web`: Open the GitHub profile for the authenticated or specified user. - Example: `gh skyline --web`, `gh skyline --user mona --web` -- `--art-only`: Show the ASCII art preview without generating an STL file. +- `-a`, `--art-only`: Show the ASCII art preview without generating an STL file. ### Examples From 91fd83e0861a10f7a74e6401a22116ae29a5eaab Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Sat, 28 Dec 2024 13:02:51 +0000 Subject: [PATCH 25/29] Fix README.md formatting issue --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d92e06..c129177 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ You can run the `gh skyline` command with the following flags: - Examples: `gh skyline --year 2020`, `gh skyline --year 2014-2024` - `-w`, `--web`: Open the GitHub profile for the authenticated or specified user. - Example: `gh skyline --web`, `gh skyline --user mona --web` -- `-a`, `--art-only`: Show the ASCII art preview without generating an STL file. +- `-a`, `--art-only`: Show the ASCII art preview without generating an STL file. ### Examples From 3e4f27b3beb0880160240bfe26ce8aeeb0a45866 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Sat, 28 Dec 2024 14:07:38 +0000 Subject: [PATCH 26/29] Add tests for GenerateASCII function with zero contributions --- ascii/generator_test.go | 60 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/ascii/generator_test.go b/ascii/generator_test.go index a0ed580..758e4cb 100644 --- a/ascii/generator_test.go +++ b/ascii/generator_test.go @@ -97,3 +97,63 @@ func TestGetBlock(t *testing.T) { }) } } + +// TestGenerateASCIIZeroContributions tests the GenerateASCII function with zero contributions. +// It verifies that the skyline consists of empty blocks and appropriately handles the header. +func TestGenerateASCIIZeroContributions(t *testing.T) { + tests := []struct { + name string + includeHeader bool + }{ + { + name: "Zero contributions without header", + includeHeader: false, + }, + { + name: "Zero contributions with header", + includeHeader: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a test grid with zero contributions + grid := makeTestGrid(3, 7) + for i := range grid { + for j := range grid[i] { + grid[i][j].ContributionCount = 0 + } + } + + // Generate ASCII art + result, err := GenerateASCII(grid, "testuser", 2023, tt.includeHeader) + if err != nil { + t.Fatalf("GenerateASCII() returned an error: %v", err) + } + + lines := strings.Split(result, "\n") + + // Determine the starting line of the skyline + skylineStart := 0 + if tt.includeHeader { + // Assuming HeaderTemplate has a fixed number of lines + headerLines := strings.Count(HeaderTemplate, "\n") + skylineStart = headerLines + 1 // +1 for the additional newline after header + } + + // Verify the skyline has at least 7 lines + if len(lines) < skylineStart+7 { + t.Fatalf("Expected at least %d lines for skyline, got %d", skylineStart+7, len(lines)) + } + + // Check each line of the skyline for empty blocks + for i := skylineStart; i < skylineStart+7; i++ { + for _, ch := range lines[i] { + if ch != EmptyBlock { + t.Errorf("Expected empty block in skyline, got '%c' on line %d", ch, i+1) + } + } + } + }) + } +} From 10a6ff539a49e96036962270803f0b66aef89aad Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Sat, 28 Dec 2024 14:34:34 +0000 Subject: [PATCH 27/29] Refactor TestGenerateASCIIZeroContributions to include footer handling in the test cases --- ascii/generator_test.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ascii/generator_test.go b/ascii/generator_test.go index 0352c36..4ee56c9 100644 --- a/ascii/generator_test.go +++ b/ascii/generator_test.go @@ -121,19 +121,19 @@ func TestGetBlock(t *testing.T) { } // TestGenerateASCIIZeroContributions tests the GenerateASCII function with zero contributions. -// It verifies that the skyline consists of empty blocks and appropriately handles the header. +// It verifies that the skyline consists of empty blocks and appropriately handles the header and footer. func TestGenerateASCIIZeroContributions(t *testing.T) { tests := []struct { - name string - includeHeader bool + name string + includeHeaderAndFooter bool }{ { - name: "Zero contributions without header", - includeHeader: false, + name: "Zero contributions without header", + includeHeaderAndFooter: false, }, { - name: "Zero contributions with header", - includeHeader: true, + name: "Zero contributions with header", + includeHeaderAndFooter: true, }, } @@ -148,7 +148,7 @@ func TestGenerateASCIIZeroContributions(t *testing.T) { } // Generate ASCII art - result, err := GenerateASCII(grid, "testuser", 2023, tt.includeHeader) + result, err := GenerateASCII(grid, "testuser", 2023, tt.includeHeaderAndFooter, tt.includeHeaderAndFooter) if err != nil { t.Fatalf("GenerateASCII() returned an error: %v", err) } @@ -157,7 +157,7 @@ func TestGenerateASCIIZeroContributions(t *testing.T) { // Determine the starting line of the skyline skylineStart := 0 - if tt.includeHeader { + if tt.includeHeaderAndFooter { // Assuming HeaderTemplate has a fixed number of lines headerLines := strings.Count(HeaderTemplate, "\n") skylineStart = headerLines + 1 // +1 for the additional newline after header From 226902c99ca5712542596de5d2e883dc59496ad6 Mon Sep 17 00:00:00 2001 From: Chris Reddington <791642+chrisreddington@users.noreply.github.com> Date: Sat, 28 Dec 2024 15:13:13 +0000 Subject: [PATCH 28/29] Update year formatting in output to use YYYY-YY format --- main.go | 3 ++- main_test.go | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index f4c1f49..a5a0cdc 100644 --- a/main.go +++ b/main.go @@ -124,7 +124,8 @@ func formatYearRange(startYear, endYear int) string { if startYear == endYear { return fmt.Sprintf("%d", startYear) } - return fmt.Sprintf("%02d-%02d", startYear%100, endYear%100) + // 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 diff --git a/main_test.go b/main_test.go index be9ae03..242d6ef 100644 --- a/main_test.go +++ b/main_test.go @@ -39,7 +39,7 @@ func TestFormatYearRange(t *testing.T) { name: "different years", startYear: 2020, endYear: 2024, - want: "20-24", + want: "2020-24", }, } @@ -73,7 +73,7 @@ func TestGenerateOutputFilename(t *testing.T) { user: "testuser", startYear: 2020, endYear: 2024, - want: "testuser-20-24-github-skyline.stl", + want: "testuser-2020-24-github-skyline.stl", }, } From 06535d61f2d3c133b2916813f506fa2738115680 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2024 18:24:25 +0000 Subject: [PATCH 29/29] Bump github.com/thlib/go-timezone-local in the go-dependencies group Bumps the go-dependencies group with 1 update: [github.com/thlib/go-timezone-local](https://github.com/thlib/go-timezone-local). Updates `github.com/thlib/go-timezone-local` from 0.0.4 to 0.0.6 - [Release notes](https://github.com/thlib/go-timezone-local/releases) - [Commits](https://github.com/thlib/go-timezone-local/compare/v0.0.4...v0.0.6) --- updated-dependencies: - dependency-name: github.com/thlib/go-timezone-local dependency-type: indirect update-type: version-update:semver-patch dependency-group: go-dependencies ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8a2aa58..30744f5 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( 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/thlib/go-timezone-local v0.0.4 // indirect + github.com/thlib/go-timezone-local v0.0.6 // indirect golang.org/x/image v0.23.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.27.0 // indirect diff --git a/go.sum b/go.sum index 63ad29a..777916e 100644 --- a/go.sum +++ b/go.sum @@ -48,8 +48,8 @@ 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/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.4 h1:9oqkZLirWUtrFmhF/7WxR8Y0TBAGaThe95w6K3pLKBk= -github.com/thlib/go-timezone-local v0.0.4/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= +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/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=