Skip to content

Commit 0413e06

Browse files
committed
feat: Introduce TUI panels for contribution graph, disk usage, and commit timeline.
1 parent 34b05d4 commit 0413e06

File tree

11 files changed

+1054
-6
lines changed

11 files changed

+1054
-6
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,8 @@
2222
.DS_Store
2323
Thumbs.db
2424

25-
/materials
25+
/materials
26+
demo.gif
27+
demo.tap
28+
git-scope
29+
git-scope-test

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
- 📊 **Dashboard**: Shows branch, staged/unstaged counts, and last commit
2525
- ⌨️ **Keyboard-driven**: Vim-like navigation (`j`/`k`) and sorting (`s`)
2626
- 🚀 **Quick Jump**: Open any repo in your editor (VSCode, nvim, etc.) with `Enter`
27+
- 🌿 **Contribution Graph**: GitHub-style heatmap of your local commits (`g`)
28+
- 💾 **Disk Usage**: See `.git` and `node_modules` sizes at a glance (`d`)
29+
-**Timeline**: What were you working on? See recent activity (`t`)
2730

2831
## Installation
2932

@@ -79,6 +82,10 @@ editor: code # or nvim, vim, helix
7982
| `c` | **Clear** search & filters |
8083
| `Enter` | **Open** repo in editor |
8184
| `r` | **Rescan** directories |
85+
| `g` | Toggle **Grass** (contribution graph) |
86+
| `d` | Toggle **Disk** usage panel |
87+
| `t` | Toggle **Timeline** panel |
88+
| `Esc` | Close panel |
8289
| `q` | **Quit** |
8390

8491
## Roadmap

cmd/git-scope/main.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
"github.com/Bharath-code/git-scope/internal/tui"
1515
)
1616

17-
const version = "0.3.1"
17+
const version = "1.0.0"
1818

1919
func usage() {
2020
fmt.Fprintf(os.Stderr, `git-scope v%s — A fast TUI to see the status of all git repositories
@@ -44,16 +44,24 @@ Flags:
4444
func main() {
4545
flag.Usage = usage
4646
configPath := flag.String("config", config.DefaultConfigPath(), "Path to config file")
47+
showVersion := flag.Bool("v", false, "Show version")
48+
flag.Bool("version", false, "Show version")
4749
flag.Parse()
4850

51+
// Handle version flag
52+
if *showVersion || isFlagPassed("version") {
53+
fmt.Printf("git-scope v%s\n", version)
54+
return
55+
}
56+
4957
args := flag.Args()
5058
cmd := ""
5159
dirs := []string{}
5260

5361
// Parse command and directories
5462
if len(args) >= 1 {
5563
switch args[0] {
56-
case "scan", "tui", "help", "init", "scan-all", "-h", "--help":
64+
case "scan", "tui", "help", "init", "scan-all", "-h", "--help", "-v", "--version":
5765
cmd = args[0]
5866
dirs = args[1:]
5967
default:
@@ -69,6 +77,12 @@ func main() {
6977
return
7078
}
7179

80+
// Handle version
81+
if cmd == "-v" || cmd == "--version" {
82+
fmt.Printf("git-scope v%s\n", version)
83+
return
84+
}
85+
7286
// Handle init command
7387
if cmd == "init" {
7488
runInit()
@@ -115,6 +129,17 @@ func main() {
115129
}
116130
}
117131

132+
// isFlagPassed checks if a flag was explicitly passed on the command line
133+
func isFlagPassed(name string) bool {
134+
found := false
135+
flag.Visit(func(f *flag.Flag) {
136+
if f.Name == name {
137+
found = true
138+
}
139+
})
140+
return found
141+
}
142+
118143
// expandDirs converts relative paths and ~ to absolute paths
119144
func expandDirs(dirs []string) []string {
120145
result := make([]string, 0, len(dirs))

git-scope-test

135 KB
Binary file not shown.

internal/stats/contributions.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package stats
2+
3+
import (
4+
"os/exec"
5+
"strconv"
6+
"strings"
7+
"time"
8+
9+
"github.com/Bharath-code/git-scope/internal/model"
10+
)
11+
12+
// ContributionData holds commit counts per day for the heatmap
13+
type ContributionData struct {
14+
Days map[string]int // "2024-01-15" -> 5 commits
15+
TotalCommits int
16+
WeeksCount int
17+
StartDate time.Time
18+
EndDate time.Time
19+
MaxDaily int // Max commits in a single day (for scaling)
20+
}
21+
22+
// GetContributions aggregates commits from all repos for the last N weeks
23+
func GetContributions(repos []model.Repo, weeks int) (*ContributionData, error) {
24+
data := &ContributionData{
25+
Days: make(map[string]int),
26+
WeeksCount: weeks,
27+
EndDate: time.Now(),
28+
StartDate: time.Now().AddDate(0, 0, -7*weeks),
29+
}
30+
31+
sinceDate := data.StartDate.Format("2006-01-02")
32+
33+
for _, repo := range repos {
34+
commits, err := getRepoCommits(repo.Path, sinceDate)
35+
if err != nil {
36+
continue // Skip repos with errors
37+
}
38+
39+
for _, date := range commits {
40+
data.Days[date]++
41+
data.TotalCommits++
42+
if data.Days[date] > data.MaxDaily {
43+
data.MaxDaily = data.Days[date]
44+
}
45+
}
46+
}
47+
48+
return data, nil
49+
}
50+
51+
// getRepoCommits returns a list of commit dates (YYYY-MM-DD) from a repo
52+
func getRepoCommits(repoPath, sinceDate string) ([]string, error) {
53+
cmd := exec.Command("git", "log", "--since="+sinceDate, "--format=%ad", "--date=short")
54+
cmd.Dir = repoPath
55+
out, err := cmd.Output()
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
61+
dates := make([]string, 0, len(lines))
62+
for _, line := range lines {
63+
line = strings.TrimSpace(line)
64+
if line != "" {
65+
dates = append(dates, line)
66+
}
67+
}
68+
69+
return dates, nil
70+
}
71+
72+
// GetIntensityLevel returns 0-4 based on commit count relative to max
73+
func (d *ContributionData) GetIntensityLevel(date string) int {
74+
count := d.Days[date]
75+
if count == 0 {
76+
return 0
77+
}
78+
if d.MaxDaily == 0 {
79+
return 1
80+
}
81+
82+
// Scale to 1-4 based on percentage of max
83+
ratio := float64(count) / float64(d.MaxDaily)
84+
switch {
85+
case ratio >= 0.75:
86+
return 4
87+
case ratio >= 0.5:
88+
return 3
89+
case ratio >= 0.25:
90+
return 2
91+
default:
92+
return 1
93+
}
94+
}
95+
96+
// GetDayCount returns commit count for a specific date
97+
func (d *ContributionData) GetDayCount(date string) int {
98+
return d.Days[date]
99+
}
100+
101+
// FormatDate formats a time.Time to the key format
102+
func FormatDate(t time.Time) string {
103+
return t.Format("2006-01-02")
104+
}
105+
106+
// ParseDate parses a date string
107+
func ParseDate(s string) (time.Time, error) {
108+
return time.Parse("2006-01-02", s)
109+
}
110+
111+
// GetWeeksData returns contribution data organized by weeks for rendering
112+
// Returns a slice of weeks, each containing 7 days (Sun-Sat)
113+
func (d *ContributionData) GetWeeksData() [][]string {
114+
weeks := make([][]string, 0, d.WeeksCount)
115+
116+
// Find the Sunday before or on the start date
117+
current := d.StartDate
118+
for current.Weekday() != time.Sunday {
119+
current = current.AddDate(0, 0, -1)
120+
}
121+
122+
for current.Before(d.EndDate) || current.Equal(d.EndDate) {
123+
week := make([]string, 7)
124+
for i := 0; i < 7; i++ {
125+
week[i] = FormatDate(current)
126+
current = current.AddDate(0, 0, 1)
127+
}
128+
weeks = append(weeks, week)
129+
}
130+
131+
return weeks
132+
}
133+
134+
// GetMonthLabels returns month labels for the heatmap header
135+
func (d *ContributionData) GetMonthLabels() []string {
136+
months := make([]string, 0, 12)
137+
current := d.StartDate
138+
lastMonth := ""
139+
140+
for current.Before(d.EndDate) || current.Equal(d.EndDate) {
141+
monthLabel := current.Format("Jan")
142+
if monthLabel != lastMonth {
143+
months = append(months, monthLabel)
144+
lastMonth = monthLabel
145+
}
146+
current = current.AddDate(0, 0, 7) // Skip by week
147+
}
148+
149+
return months
150+
}
151+
152+
// FormatCount returns a formatted string for display
153+
func FormatCount(n int) string {
154+
if n == 0 {
155+
return "0"
156+
}
157+
return strconv.Itoa(n)
158+
}

0 commit comments

Comments
 (0)