mirror of
https://github.com/taigrr/gico.git
synced 2026-04-13 08:28:22 -07:00
Compare commits
3 Commits
cd/add-tes
...
cd/stdlib-
| Author | SHA1 | Date | |
|---|---|---|---|
| 70c16e9774 | |||
| ef0804254d | |||
| 028e9a9cac |
@@ -2,71 +2,72 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/taigrr/gico/commits"
|
||||
"github.com/taigrr/gico/graph/svg"
|
||||
)
|
||||
|
||||
type DayCount [366]int
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
logger := func(h http.Handler) http.Handler {
|
||||
return handlers.LoggingHandler(os.Stdout, h)
|
||||
}
|
||||
r.Use(mux.MiddlewareFunc(logger))
|
||||
r.HandleFunc("/weekly.svg", func(w http.ResponseWriter, r *http.Request) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("GET /weekly.svg", func(w http.ResponseWriter, r *http.Request) {
|
||||
author := r.URL.Query().Get("author")
|
||||
highlight := r.URL.Query().Get("highlight")
|
||||
shouldHighlight := highlight != ""
|
||||
|
||||
w.Header().Add("Content-Type", "text/html")
|
||||
repoPaths, err := commits.GetRepos()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
http.Error(w, "failed to get repos", http.StatusInternalServerError)
|
||||
log.Printf("error getting repos: %v", err)
|
||||
return
|
||||
}
|
||||
week, err := repoPaths.GetWeekFreq([]string{author})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
http.Error(w, "failed to get weekly frequency", http.StatusInternalServerError)
|
||||
log.Printf("error getting weekly freq: %v", err)
|
||||
return
|
||||
}
|
||||
svg := svg.GetWeekSVG(week, shouldHighlight)
|
||||
svg.WriteTo(w)
|
||||
w.Header().Set("Content-Type", "image/svg+xml")
|
||||
svgData := svg.GetWeekSVG(week, shouldHighlight)
|
||||
svgData.WriteTo(w)
|
||||
})
|
||||
r.HandleFunc("/stats.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
mux.HandleFunc("GET /stats.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
year := time.Now().Year()
|
||||
yst := r.URL.Query().Get("year")
|
||||
author := r.URL.Query().Get("author")
|
||||
y, err := strconv.Atoi(yst)
|
||||
if err == nil {
|
||||
if y, err := strconv.Atoi(yst); err == nil {
|
||||
year = y
|
||||
}
|
||||
repoPaths, err := commits.GetRepos()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
http.Error(w, "failed to get repos", http.StatusInternalServerError)
|
||||
log.Printf("error getting repos: %v", err)
|
||||
return
|
||||
}
|
||||
freq, err := repoPaths.FrequencyChan(year, []string{author})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
http.Error(w, "failed to get frequency", http.StatusInternalServerError)
|
||||
log.Printf("error getting freq: %v", err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(freq); err != nil {
|
||||
log.Printf("error encoding response: %v", err)
|
||||
}
|
||||
b, _ := json.Marshal(freq)
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.Write(b)
|
||||
})
|
||||
r.HandleFunc("/yearly.svg", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
mux.HandleFunc("GET /yearly.svg", func(w http.ResponseWriter, r *http.Request) {
|
||||
year := time.Now().Year()
|
||||
yst := r.URL.Query().Get("year")
|
||||
author := r.URL.Query().Get("author")
|
||||
highlight := r.URL.Query().Get("highlight")
|
||||
shouldHighlight := highlight != ""
|
||||
y, err := strconv.Atoi(yst)
|
||||
if err == nil {
|
||||
if y, err := strconv.Atoi(yst); err == nil {
|
||||
if year != y {
|
||||
shouldHighlight = false
|
||||
}
|
||||
@@ -74,19 +75,23 @@ func main() {
|
||||
}
|
||||
repoPaths, err := commits.GetRepos()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
http.Error(w, "failed to get repos", http.StatusInternalServerError)
|
||||
log.Printf("error getting repos: %v", err)
|
||||
return
|
||||
}
|
||||
freq, err := repoPaths.FrequencyChan(year, []string{author})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
http.Error(w, "failed to get frequency", http.StatusInternalServerError)
|
||||
log.Printf("error getting freq: %v", err)
|
||||
return
|
||||
}
|
||||
svg := svg.GetYearSVG(freq, shouldHighlight)
|
||||
w.Header().Add("Content-Type", "text/html")
|
||||
svg.WriteTo(w)
|
||||
w.Header().Set("Content-Type", "image/svg+xml")
|
||||
svgData := svg.GetYearSVG(freq, shouldHighlight)
|
||||
svgData.WriteTo(w)
|
||||
})
|
||||
|
||||
err := http.ListenAndServe(":8822", r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Println("gico-server listening on :8822")
|
||||
if err := http.ListenAndServe(":8822", mux); err != nil {
|
||||
log.Fatalf("server error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
99
commits/cache_test.go
Normal file
99
commits/cache_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package commits
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/taigrr/gico/types"
|
||||
)
|
||||
|
||||
func TestHashSliceDeterministic(t *testing.T) {
|
||||
a := hashSlice([]string{"foo", "bar", "baz"})
|
||||
b := hashSlice([]string{"baz", "foo", "bar"})
|
||||
if a != b {
|
||||
t.Error("hashSlice should be order-independent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashSliceDifferentInputs(t *testing.T) {
|
||||
a := hashSlice([]string{"foo", "bar"})
|
||||
b := hashSlice([]string{"foo", "baz"})
|
||||
if a == b {
|
||||
t.Error("different inputs should produce different hashes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheRepoRoundTrip(t *testing.T) {
|
||||
path := "/test/cache-repo"
|
||||
head := "deadbeef123"
|
||||
commits := []types.Commit{
|
||||
{Hash: head, Author: types.Author{Name: "test"}, TimeStamp: time.Now()},
|
||||
}
|
||||
|
||||
// Should miss before caching
|
||||
_, ok := GetCachedRepo(path, head)
|
||||
if ok {
|
||||
t.Error("expected cache miss before storing")
|
||||
}
|
||||
|
||||
CacheRepo(path, commits)
|
||||
|
||||
// Should hit after caching
|
||||
cached, ok := GetCachedRepo(path, head)
|
||||
if !ok {
|
||||
t.Error("expected cache hit after storing")
|
||||
}
|
||||
if len(cached) != 1 || cached[0].Hash != head {
|
||||
t.Error("cached data doesn't match stored data")
|
||||
}
|
||||
|
||||
// Different head should miss
|
||||
_, ok = GetCachedRepo(path, "differenthead")
|
||||
if ok {
|
||||
t.Error("expected cache miss for different head")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheGraphRoundTrip(t *testing.T) {
|
||||
year := 2025
|
||||
authors := []string{"alice@example.com"}
|
||||
paths := []string{"/repo/one", "/repo/two"}
|
||||
freq := types.Freq{1, 2, 3, 0, 0}
|
||||
|
||||
// Should miss before caching
|
||||
_, ok := GetCachedGraph(year, authors, paths)
|
||||
if ok {
|
||||
t.Error("expected cache miss before storing")
|
||||
}
|
||||
|
||||
CacheGraph(year, authors, paths, freq)
|
||||
|
||||
// Should hit
|
||||
cached, ok := GetCachedGraph(year, authors, paths)
|
||||
if !ok {
|
||||
t.Error("expected cache hit")
|
||||
}
|
||||
if len(cached) != len(freq) {
|
||||
t.Errorf("expected freq length %d, got %d", len(freq), len(cached))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheReposAuthorsRoundTrip(t *testing.T) {
|
||||
paths := []string{"/repo/author-test"}
|
||||
authors := []string{"alice", "bob"}
|
||||
|
||||
_, ok := GetCachedReposAuthors(paths)
|
||||
if ok {
|
||||
t.Error("expected cache miss")
|
||||
}
|
||||
|
||||
CacheReposAuthors(paths, authors)
|
||||
|
||||
cached, ok := GetCachedReposAuthors(paths)
|
||||
if !ok {
|
||||
t.Error("expected cache hit")
|
||||
}
|
||||
if len(cached) != 2 {
|
||||
t.Errorf("expected 2 authors, got %d", len(cached))
|
||||
}
|
||||
}
|
||||
@@ -54,10 +54,7 @@ func (paths RepoSet) GetRepoAuthors() ([]string, error) {
|
||||
}
|
||||
|
||||
func (paths RepoSet) GetRepoCommits(year int, authors []string) ([][]types.Commit, error) {
|
||||
yearLength := 365
|
||||
if year%4 == 0 {
|
||||
yearLength++
|
||||
}
|
||||
yearLength := types.YearLength(year)
|
||||
|
||||
commits := make([][]types.Commit, yearLength)
|
||||
for i := 0; i < yearLength; i++ {
|
||||
@@ -109,10 +106,6 @@ func (paths RepoSet) GetRepoCommits(year int, authors []string) ([][]types.Commi
|
||||
}
|
||||
|
||||
func (paths RepoSet) FrequencyChan(year int, authors []string) (types.Freq, error) {
|
||||
yearLength := 365
|
||||
if year%4 == 0 {
|
||||
yearLength++
|
||||
}
|
||||
cache, ok := GetCachedGraph(year, authors, paths)
|
||||
if ok {
|
||||
return cache, nil
|
||||
@@ -152,10 +145,7 @@ func (paths RepoSet) FrequencyChan(year int, authors []string) (types.Freq, erro
|
||||
}
|
||||
|
||||
func YearFreqFromChan(cc chan types.Commit, year int) types.Freq {
|
||||
yearLength := 365
|
||||
if year%4 == 0 {
|
||||
yearLength++
|
||||
}
|
||||
yearLength := types.YearLength(year)
|
||||
freq := make([]int, yearLength)
|
||||
for commit := range cc {
|
||||
freq[commit.TimeStamp.YearDay()-1]++
|
||||
@@ -206,10 +196,7 @@ func (repo Repo) GetCommitChan() (chan types.Commit, error) {
|
||||
}
|
||||
|
||||
func FreqFromChan(cc chan types.Commit, year int) types.Freq {
|
||||
yearLength := 365
|
||||
if year%4 == 0 {
|
||||
yearLength++
|
||||
}
|
||||
yearLength := types.YearLength(year)
|
||||
freq := make([]int, yearLength)
|
||||
for commit := range cc {
|
||||
if commit.TimeStamp.Year() != year {
|
||||
|
||||
@@ -26,10 +26,7 @@ func (paths RepoSet) GetWeekFreq(authors []string) (types.Freq, error) {
|
||||
return types.Freq{}, err
|
||||
}
|
||||
freq = append(curFreq, freq...)
|
||||
today += 365
|
||||
if curYear%4 == 0 {
|
||||
today++
|
||||
}
|
||||
today += types.YearLength(curYear)
|
||||
}
|
||||
|
||||
week := freq[today-6 : today+1]
|
||||
@@ -37,10 +34,7 @@ func (paths RepoSet) GetWeekFreq(authors []string) (types.Freq, error) {
|
||||
}
|
||||
|
||||
func (paths RepoSet) Frequency(year int, authors []string) (types.Freq, error) {
|
||||
yearLength := 365
|
||||
if year%4 == 0 {
|
||||
yearLength++
|
||||
}
|
||||
yearLength := types.YearLength(year)
|
||||
gfreq := make(types.Freq, yearLength)
|
||||
for _, p := range paths {
|
||||
repo, err := OpenRepo(p)
|
||||
@@ -115,11 +109,7 @@ func (repo Repo) GetCommitSet() (CommitSet, error) {
|
||||
}
|
||||
|
||||
func (cs CommitSet) ToYearFreq() types.Freq {
|
||||
year := cs.Year
|
||||
yearLength := 365
|
||||
if year%4 == 0 {
|
||||
yearLength++
|
||||
}
|
||||
yearLength := types.YearLength(cs.Year)
|
||||
freq := make([]int, yearLength)
|
||||
for _, v := range cs.Commits {
|
||||
freq[v.TimeStamp.YearDay()-1]++
|
||||
|
||||
212
commits/commits_test.go
Normal file
212
commits/commits_test.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package commits
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/taigrr/gico/types"
|
||||
)
|
||||
|
||||
func makeCommit(name, email string, ts time.Time) types.Commit {
|
||||
return types.Commit{
|
||||
Author: types.Author{Name: name, Email: email},
|
||||
TimeStamp: ts,
|
||||
Hash: "abc123",
|
||||
Repo: "/test/repo",
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitSetFilterByYear(t *testing.T) {
|
||||
cs := CommitSet{
|
||||
Commits: []types.Commit{
|
||||
makeCommit("a", "a@x.com", time.Date(2024, 3, 15, 10, 0, 0, 0, time.UTC)),
|
||||
makeCommit("b", "b@x.com", time.Date(2025, 6, 1, 12, 0, 0, 0, time.UTC)),
|
||||
makeCommit("c", "c@x.com", time.Date(2024, 12, 31, 23, 59, 0, 0, time.UTC)),
|
||||
makeCommit("d", "d@x.com", time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
}
|
||||
|
||||
filtered := cs.FilterByYear(2024)
|
||||
if filtered.Year != 2024 {
|
||||
t.Errorf("expected Year=2024, got %d", filtered.Year)
|
||||
}
|
||||
if len(filtered.Commits) != 2 {
|
||||
t.Errorf("expected 2 commits for 2024, got %d", len(filtered.Commits))
|
||||
}
|
||||
|
||||
filtered = cs.FilterByYear(2025)
|
||||
if len(filtered.Commits) != 1 {
|
||||
t.Errorf("expected 1 commit for 2025, got %d", len(filtered.Commits))
|
||||
}
|
||||
|
||||
filtered = cs.FilterByYear(2000)
|
||||
if len(filtered.Commits) != 0 {
|
||||
t.Errorf("expected 0 commits for 2000, got %d", len(filtered.Commits))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitSetFilterByAuthorRegex(t *testing.T) {
|
||||
cs := CommitSet{
|
||||
Commits: []types.Commit{
|
||||
makeCommit("Alice", "alice@example.com", time.Now()),
|
||||
makeCommit("Bob", "bob@example.com", time.Now()),
|
||||
makeCommit("Charlie", "charlie@example.com", time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
// Filter by name regex
|
||||
filtered, err := cs.FilterByAuthorRegex([]string{"^Ali"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(filtered.Commits) != 1 {
|
||||
t.Errorf("expected 1 commit matching ^Ali, got %d", len(filtered.Commits))
|
||||
}
|
||||
|
||||
// Filter by email regex
|
||||
filtered, err = cs.FilterByAuthorRegex([]string{"bob@"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(filtered.Commits) != 1 {
|
||||
t.Errorf("expected 1 commit matching bob@, got %d", len(filtered.Commits))
|
||||
}
|
||||
|
||||
// Multiple patterns
|
||||
filtered, err = cs.FilterByAuthorRegex([]string{"Alice", "Charlie"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(filtered.Commits) != 2 {
|
||||
t.Errorf("expected 2 commits matching Alice|Charlie, got %d", len(filtered.Commits))
|
||||
}
|
||||
|
||||
// Invalid regex
|
||||
_, err = cs.FilterByAuthorRegex([]string{"[invalid"})
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid regex, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitSetToYearFreq(t *testing.T) {
|
||||
// Regular year (2025)
|
||||
cs := CommitSet{
|
||||
Year: 2025,
|
||||
Commits: []types.Commit{
|
||||
makeCommit("a", "a@x.com", time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC)),
|
||||
makeCommit("a", "a@x.com", time.Date(2025, 1, 1, 14, 0, 0, 0, time.UTC)),
|
||||
makeCommit("b", "b@x.com", time.Date(2025, 3, 15, 12, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
}
|
||||
|
||||
freq := cs.ToYearFreq()
|
||||
if len(freq) != 365 {
|
||||
t.Errorf("expected 365 days for 2025, got %d", len(freq))
|
||||
}
|
||||
if freq[0] != 2 {
|
||||
t.Errorf("expected 2 commits on Jan 1, got %d", freq[0])
|
||||
}
|
||||
// March 15 = day 74 (31+28+15=74, index 73)
|
||||
if freq[73] != 1 {
|
||||
t.Errorf("expected 1 commit on Mar 15, got %d", freq[73])
|
||||
}
|
||||
|
||||
// Leap year (2024)
|
||||
csLeap := CommitSet{
|
||||
Year: 2024,
|
||||
Commits: []types.Commit{
|
||||
makeCommit("a", "a@x.com", time.Date(2024, 12, 31, 10, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
}
|
||||
freqLeap := csLeap.ToYearFreq()
|
||||
if len(freqLeap) != 366 {
|
||||
t.Errorf("expected 366 days for 2024, got %d", len(freqLeap))
|
||||
}
|
||||
}
|
||||
|
||||
func TestYearFreqFromChan(t *testing.T) {
|
||||
cc := make(chan types.Commit, 5)
|
||||
cc <- makeCommit("a", "a@x.com", time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC))
|
||||
cc <- makeCommit("a", "a@x.com", time.Date(2025, 1, 1, 14, 0, 0, 0, time.UTC))
|
||||
cc <- makeCommit("b", "b@x.com", time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC))
|
||||
close(cc)
|
||||
|
||||
freq := YearFreqFromChan(cc, 2025)
|
||||
if len(freq) != 365 {
|
||||
t.Errorf("expected 365, got %d", len(freq))
|
||||
}
|
||||
if freq[0] != 2 {
|
||||
t.Errorf("expected 2 on Jan 1, got %d", freq[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFreqFromChan(t *testing.T) {
|
||||
cc := make(chan types.Commit, 5)
|
||||
cc <- makeCommit("a", "a@x.com", time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC))
|
||||
cc <- makeCommit("b", "b@x.com", time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC)) // wrong year
|
||||
cc <- makeCommit("c", "c@x.com", time.Date(2025, 12, 31, 23, 0, 0, 0, time.UTC))
|
||||
close(cc)
|
||||
|
||||
freq := FreqFromChan(cc, 2025)
|
||||
if len(freq) != 365 {
|
||||
t.Errorf("expected 365, got %d", len(freq))
|
||||
}
|
||||
if freq[0] != 1 {
|
||||
t.Errorf("expected 1 on Jan 1 (skip wrong year), got %d", freq[0])
|
||||
}
|
||||
if freq[364] != 1 {
|
||||
t.Errorf("expected 1 on Dec 31, got %d", freq[364])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterCChanByYear(t *testing.T) {
|
||||
in := make(chan types.Commit, 5)
|
||||
in <- makeCommit("a", "a@x.com", time.Date(2025, 3, 1, 10, 0, 0, 0, time.UTC))
|
||||
in <- makeCommit("b", "b@x.com", time.Date(2024, 3, 1, 10, 0, 0, 0, time.UTC))
|
||||
in <- makeCommit("c", "c@x.com", time.Date(2025, 7, 4, 10, 0, 0, 0, time.UTC))
|
||||
close(in)
|
||||
|
||||
out := FilterCChanByYear(in, 2025)
|
||||
count := 0
|
||||
for range out {
|
||||
count++
|
||||
}
|
||||
if count != 2 {
|
||||
t.Errorf("expected 2 commits for 2025, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterCChanByAuthor(t *testing.T) {
|
||||
in := make(chan types.Commit, 5)
|
||||
in <- makeCommit("Alice", "alice@x.com", time.Now())
|
||||
in <- makeCommit("Bob", "bob@x.com", time.Now())
|
||||
in <- makeCommit("Charlie", "charlie@x.com", time.Now())
|
||||
close(in)
|
||||
|
||||
out, err := FilterCChanByAuthor(in, []string{"Alice", "Charlie"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
count := 0
|
||||
for range out {
|
||||
count++
|
||||
}
|
||||
if count != 2 {
|
||||
t.Errorf("expected 2 commits, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenRepoNonExistent(t *testing.T) {
|
||||
_, err := OpenRepo("/nonexistent/path")
|
||||
if err == nil {
|
||||
t.Error("expected error for nonexistent path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenRepoNonDirectory(t *testing.T) {
|
||||
// Use a known file (not a directory)
|
||||
_, err := OpenRepo("/dev/null")
|
||||
if err == nil {
|
||||
t.Error("expected error for non-directory path")
|
||||
}
|
||||
}
|
||||
3
go.mod
3
go.mod
@@ -8,8 +8,6 @@ require (
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/go-git/go-git/v5 v5.16.5
|
||||
github.com/gorilla/handlers v1.5.2
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/muesli/termenv v0.16.0
|
||||
github.com/taigrr/mg v0.1.1
|
||||
github.com/taigrr/simplecolorpalettes v0.9.8
|
||||
@@ -31,7 +29,6 @@ require (
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.7.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
|
||||
6
go.sum
6
go.sum
@@ -53,8 +53,6 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
@@ -69,10 +67,6 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8J
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
||||
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
|
||||
func TestMinMax(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []int
|
||||
wantMin int
|
||||
wantMax int
|
||||
name string
|
||||
input []int
|
||||
wantMin int
|
||||
wantMax int
|
||||
}{
|
||||
{
|
||||
name: "normal values",
|
||||
|
||||
@@ -48,6 +48,19 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
// IsLeapYear returns true if year is a leap year per the Gregorian calendar.
|
||||
func IsLeapYear(year int) bool {
|
||||
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
|
||||
}
|
||||
|
||||
// YearLength returns 366 for leap years and 365 otherwise.
|
||||
func YearLength(year int) int {
|
||||
if IsLeapYear(year) {
|
||||
return 366
|
||||
}
|
||||
return 365
|
||||
}
|
||||
|
||||
func (c Commit) String() string {
|
||||
return fmt.Sprintf("%s\t%s\t%s\t%s",
|
||||
c.TimeStamp.Format("0"+time.Kitchen),
|
||||
|
||||
@@ -122,6 +122,45 @@ func TestFreqMerge(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsLeapYear(t *testing.T) {
|
||||
tests := []struct {
|
||||
year int
|
||||
want bool
|
||||
}{
|
||||
{2024, true}, // divisible by 4
|
||||
{2025, false}, // not divisible by 4
|
||||
{1900, false}, // divisible by 100 but not 400
|
||||
{2000, true}, // divisible by 400
|
||||
{2100, false}, // divisible by 100 but not 400
|
||||
{2400, true}, // divisible by 400
|
||||
{1996, true}, // divisible by 4
|
||||
{2023, false}, // not divisible by 4
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(time.Date(tt.year, 1, 1, 0, 0, 0, 0, time.UTC).Format("2006"), func(t *testing.T) {
|
||||
got := IsLeapYear(tt.year)
|
||||
if got != tt.want {
|
||||
t.Errorf("IsLeapYear(%d) = %v, want %v", tt.year, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestYearLength(t *testing.T) {
|
||||
if YearLength(2024) != 366 {
|
||||
t.Error("expected 366 for 2024")
|
||||
}
|
||||
if YearLength(2025) != 365 {
|
||||
t.Error("expected 365 for 2025")
|
||||
}
|
||||
if YearLength(1900) != 365 {
|
||||
t.Error("expected 365 for 1900 (century year, not leap)")
|
||||
}
|
||||
if YearLength(2000) != 366 {
|
||||
t.Error("expected 366 for 2000 (divisible by 400)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataSetOperations(t *testing.T) {
|
||||
ds := NewDataSet()
|
||||
now := time.Now().Truncate(24 * time.Hour)
|
||||
|
||||
10
ui/ui.go
10
ui/ui.go
@@ -5,6 +5,8 @@ import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"github.com/taigrr/gico/types"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -66,12 +68,10 @@ func (m model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// YearLen returns the number of days in a year.
|
||||
// Deprecated: Use types.YearLength instead.
|
||||
func YearLen(year int) int {
|
||||
yearLen := 365
|
||||
if year%4 == 0 {
|
||||
yearLen++
|
||||
}
|
||||
return yearLen
|
||||
return types.YearLength(year)
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
Reference in New Issue
Block a user