Skip to content

Year to date support #73

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 59 additions & 9 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ import (

// 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
yearRange string
user string
full bool
debug bool
web bool
artOnly bool
output string
yearToDate bool
until string
)

// rootCmd is the root command for the GitHub Skyline CLI tool.
Expand All @@ -47,7 +49,20 @@ 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.`,
to create a "building" effect, with empty spaces (no contributions) at the top.

Examples:
# Generate skyline for current year
gh skyline

# Generate skyline for a specific year
gh skyline --year 2023

# Generate skyline for the last 12 months up to today
gh skyline --year-to-date

# Generate skyline for the last 12 months up to a specific date
gh skyline --year-to-date --until 2024-02-29`,
RunE: handleSkylineCommand,
}

Expand All @@ -74,6 +89,21 @@ func initFlags() {
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)")
flags.BoolVar(&yearToDate, "year-to-date", false, "Generate contribution graph for the last 12 months")
flags.StringVar(&until, "until", "", "End date for year-to-date period in YYYY-MM-DD format (defaults to today)")

// Mark mutually exclusive flags
rootCmd.MarkFlagsMutuallyExclusive("year", "year-to-date")
rootCmd.MarkFlagsMutuallyExclusive("year", "full")
rootCmd.MarkFlagsMutuallyExclusive("full", "year-to-date")

// Add validation to ensure --until is only used with --year-to-date
rootCmd.PreRunE = func(cmd *cobra.Command, _ []string) error {
if cmd.Flags().Changed("until") && !yearToDate {
return fmt.Errorf("--until can only be used with --year-to-date")
}
return nil
}
}

// executeRootCmd is the main execution function for the root command.
Expand All @@ -100,12 +130,32 @@ func handleSkylineCommand(_ *cobra.Command, _ []string) error {
return nil
}

if yearToDate {
// Parse custom end date if provided
now := time.Now()
endDate := now
if until != "" {
parsedEnd, err := time.Parse("2006-01-02", until)
if err != nil {
return fmt.Errorf("invalid until date format, expected YYYY-MM-DD: %v", err)
}
if parsedEnd.After(now) {
return fmt.Errorf("until date cannot be in the future")
}
endDate = parsedEnd
}

// Calculate start date as 12 months before end date
startDate := endDate.AddDate(0, -12, 0)
yearRange = fmt.Sprintf("%d-%d", startDate.Year(), endDate.Year())
}

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)
return skyline.GenerateSkyline(startYear, endYear, user, full, output, artOnly, until)
}

// Browser interface matches browser.Browser functionality.
Expand Down
130 changes: 129 additions & 1 deletion cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"fmt"
"testing"
"time"

"github.com/github/gh-skyline/internal/testutil/mocks"
)
Expand Down Expand Up @@ -34,14 +35,141 @@ func TestRootCmd(t *testing.T) {

func TestInit(t *testing.T) {
flags := rootCmd.Flags()
expectedFlags := []string{"year", "user", "full", "debug", "web", "art-only", "output"}
expectedFlags := []string{
"year",
"user",
"full",
"debug",
"web",
"art-only",
"output",
"year-to-date",
"until",
}
for _, flag := range expectedFlags {
if flags.Lookup(flag) == nil {
t.Errorf("expected flag %s to be initialized", flag)
}
}
}

func TestMutuallyExclusiveFlags(t *testing.T) {
tests := []struct {
name string
args []string
wantErr bool
}{
{
name: "year and year-to-date are mutually exclusive",
args: []string{"--year", "2024", "--year-to-date"},
wantErr: true,
},
{
name: "year and full are mutually exclusive",
args: []string{"--year", "2024", "--full"},
wantErr: true,
},
{
name: "full and year-to-date are mutually exclusive",
args: []string{"--full", "--year-to-date"},
wantErr: true,
},
{
name: "until requires year-to-date",
args: []string{"--until", "2024-03-21"},
wantErr: true,
},
{
name: "valid year-to-date with until",
args: []string{"--year-to-date", "--until", "2024-03-21"},
wantErr: false,
},
{
name: "valid single year",
args: []string{"--year", "2024"},
wantErr: false,
},
{
name: "valid year-to-date",
args: []string{"--year-to-date"},
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := rootCmd
cmd.SetArgs(tt.args)
err := cmd.Execute()
if (err != nil) != tt.wantErr {
t.Errorf("Execute() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func TestYearToDatePeriodCalculation(t *testing.T) {
tests := []struct {
name string
untilDate string
wantStartYear int
wantEndYear int
wantErr bool
}{
{
name: "default to current date",
untilDate: "",
wantStartYear: time.Now().AddDate(0, -12, 0).Year(),
wantEndYear: time.Now().Year(),
wantErr: false,
},
{
name: "specific date in current year",
untilDate: time.Now().Format("2006-01-02"),
wantStartYear: time.Now().AddDate(0, -12, 0).Year(),
wantEndYear: time.Now().Year(),
wantErr: false,
},
{
name: "invalid date format",
untilDate: "2024/03/21",
wantStartYear: 0,
wantEndYear: 0,
wantErr: true,
},
{
name: "future date",
untilDate: time.Now().AddDate(0, 1, 0).Format("2006-01-02"),
wantStartYear: 0,
wantEndYear: 0,
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Reset flags before each test
yearToDate = true
until = tt.untilDate
yearRange = ""

err := handleSkylineCommand(rootCmd, nil)
if (err != nil) != tt.wantErr {
t.Errorf("handleSkylineCommand() error = %v, wantErr %v", err, tt.wantErr)
return
}

if !tt.wantErr {
// Check if the yearRange was set correctly
expectedYearRange := fmt.Sprintf("%d-%d", tt.wantStartYear, tt.wantEndYear)
if yearRange != expectedYearRange {
t.Errorf("yearRange = %v, want %v", yearRange, expectedYearRange)
}
}
})
}
}

// TestOpenGitHubProfile tests the openGitHubProfile function
func TestOpenGitHubProfile(t *testing.T) {
tests := []struct {
Expand Down
Loading