Added bento :)
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
package reports
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/atridad/wrapped-cli/pkg/config"
|
||||
"github.com/atridad/wrapped-cli/pkg/models"
|
||||
"github.com/fogleman/gg"
|
||||
)
|
||||
|
||||
const (
|
||||
canvasW = 1400
|
||||
canvasH = 1160
|
||||
gridGap = 20
|
||||
radius = 24
|
||||
unitCol = 325
|
||||
twoCol = 670
|
||||
fourCol = 1360
|
||||
)
|
||||
|
||||
var (
|
||||
bgColor = color.RGBA{9, 9, 11, 255}
|
||||
cardBg = color.RGBA{24, 24, 27, 255}
|
||||
cardBgPurple = color.RGBA{30, 20, 36, 255}
|
||||
cardBgBlue = color.RGBA{15, 23, 42, 255}
|
||||
accentMagenta = color.RGBA{192, 132, 252, 255}
|
||||
accentCyan = color.RGBA{45, 212, 191, 255}
|
||||
accentOrange = color.RGBA{251, 146, 60, 255}
|
||||
accentGreen = color.RGBA{74, 222, 128, 255}
|
||||
accentBlue = color.RGBA{96, 165, 250, 255}
|
||||
textPrimary = color.RGBA{244, 244, 245, 255}
|
||||
textSecondary = color.RGBA{161, 161, 170, 255}
|
||||
textMuted = color.RGBA{113, 113, 122, 255}
|
||||
borderColor = color.RGBA{39, 39, 42, 255}
|
||||
barTrack = color.RGBA{39, 39, 42, 255}
|
||||
)
|
||||
|
||||
type bentoCard struct {
|
||||
x, y, w, h float64
|
||||
bg color.RGBA
|
||||
accent color.RGBA
|
||||
}
|
||||
|
||||
func col(n int) float64 {
|
||||
return float64(gridGap + n*(unitCol+gridGap))
|
||||
}
|
||||
|
||||
func cardLayout() []bentoCard {
|
||||
r1y := float64(gridGap)
|
||||
r1h := float64(240)
|
||||
r2y := r1y + r1h + gridGap
|
||||
r2h := float64(240)
|
||||
r3y := r2y + r2h + gridGap
|
||||
r3h := float64(300)
|
||||
r4y := r3y + r3h + gridGap
|
||||
r4h := float64(280)
|
||||
|
||||
return []bentoCard{
|
||||
{col(0), r1y, twoCol, r1h, cardBgPurple, accentMagenta},
|
||||
{col(2), r1y, unitCol, r1h, cardBg, accentOrange},
|
||||
{col(3), r1y, unitCol, r1h, cardBg, accentCyan},
|
||||
{col(0), r2y, unitCol, r2h, cardBg, accentGreen},
|
||||
{col(1), r2y, unitCol, r2h, cardBg, accentOrange},
|
||||
{col(2), r2y, twoCol, r2h, cardBgBlue, accentBlue},
|
||||
{col(0), r3y, fourCol, r3h, cardBg, accentMagenta},
|
||||
{col(0), r4y, fourCol, r4h, cardBg, accentCyan},
|
||||
}
|
||||
}
|
||||
|
||||
func loadFont(dc *gg.Context, size float64) {
|
||||
paths := []string{
|
||||
"/System/Library/Fonts/Helvetica.ttc",
|
||||
"/System/Library/Fonts/SFNS.ttf",
|
||||
"/System/Library/Fonts/Supplemental/Arial.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
||||
}
|
||||
for _, p := range paths {
|
||||
if err := dc.LoadFontFace(p, size); err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func drawCard(dc *gg.Context, c bentoCard) {
|
||||
dc.DrawRoundedRectangle(c.x, c.y, c.w, c.h, radius)
|
||||
dc.SetColor(c.bg)
|
||||
dc.Fill()
|
||||
dc.DrawRoundedRectangle(c.x, c.y, c.w, c.h, radius)
|
||||
dc.SetColor(borderColor)
|
||||
dc.SetLineWidth(1)
|
||||
dc.Stroke()
|
||||
}
|
||||
|
||||
func label(dc *gg.Context, c bentoCard, text string) {
|
||||
loadFont(dc, 12)
|
||||
dc.SetColor(textSecondary)
|
||||
dc.DrawString(text, c.x+24, c.y+32)
|
||||
}
|
||||
|
||||
func bigNum(dc *gg.Context, c bentoCard, value, sub string) {
|
||||
loadFont(dc, 64)
|
||||
dc.SetColor(c.accent)
|
||||
dc.DrawString(value, c.x+24, c.y+110)
|
||||
|
||||
if sub != "" {
|
||||
loadFont(dc, 14)
|
||||
dc.SetColor(textSecondary)
|
||||
dc.DrawString(sub, c.x+24, c.y+136)
|
||||
}
|
||||
}
|
||||
|
||||
func bigText(dc *gg.Context, c bentoCard, value string, size float64) {
|
||||
loadFont(dc, size)
|
||||
dc.SetColor(c.accent)
|
||||
dc.DrawString(value, c.x+24, c.y+110)
|
||||
}
|
||||
|
||||
func filledBar(dc *gg.Context, x, y, totalW, h, pct float64, fill color.RGBA) {
|
||||
dc.DrawRoundedRectangle(x, y, totalW, h, 6)
|
||||
dc.SetColor(barTrack)
|
||||
dc.Fill()
|
||||
|
||||
fillW := totalW * pct
|
||||
if fillW < 8 {
|
||||
fillW = 8
|
||||
}
|
||||
dc.DrawRoundedRectangle(x, y, fillW, h, 6)
|
||||
dc.SetColor(fill)
|
||||
dc.Fill()
|
||||
}
|
||||
|
||||
func renderCommitsCard(dc *gg.Context, c bentoCard, stats *models.UserStats) {
|
||||
label(dc, c, "TOTAL COMMITS")
|
||||
bigNum(dc, c, formatNumber(stats.TotalCommits),
|
||||
fmt.Sprintf("%.1f commits per day on average", stats.AverageCommitsPerDay))
|
||||
|
||||
dc.DrawRoundedRectangle(c.x+24, c.y+c.h-40, c.w-48, 10, 5)
|
||||
dc.SetColor(color.RGBA{50, 40, 60, 255})
|
||||
dc.Fill()
|
||||
dc.DrawRoundedRectangle(c.x+24, c.y+c.h-40, (c.w-48)*0.72, 10, 5)
|
||||
dc.SetColor(c.accent)
|
||||
dc.Fill()
|
||||
}
|
||||
|
||||
func renderReposCard(dc *gg.Context, c bentoCard, stats *models.UserStats) {
|
||||
label(dc, c, "REPOSITORIES")
|
||||
bigNum(dc, c, formatNumber(stats.TotalRepositories), "worked on this year")
|
||||
|
||||
y := c.y + 160
|
||||
maxW := c.w - 48
|
||||
for i, repo := range stats.TopRepositories {
|
||||
if i >= 3 {
|
||||
break
|
||||
}
|
||||
loadFont(dc, 12)
|
||||
dc.SetColor(textMuted)
|
||||
dc.DrawString(truncate(repo.Name, maxW, dc), c.x+24, y)
|
||||
y += 24
|
||||
}
|
||||
}
|
||||
|
||||
func renderActiveDayCard(dc *gg.Context, c bentoCard, stats *models.UserStats) {
|
||||
label(dc, c, "MOST ACTIVE DAY")
|
||||
bigText(dc, c, stats.MostActiveDay, 48)
|
||||
|
||||
if len(stats.CommitsByWeekday) > 0 {
|
||||
loadFont(dc, 14)
|
||||
dc.SetColor(textSecondary)
|
||||
dc.DrawString(
|
||||
fmt.Sprintf("%d commits that day", stats.CommitsByWeekday[0].Count),
|
||||
c.x+24, c.y+136,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func renderAvgCard(dc *gg.Context, c bentoCard, stats *models.UserStats) {
|
||||
label(dc, c, "AVG COMMITS / DAY")
|
||||
bigNum(dc, c, fmt.Sprintf("%.1f", stats.AverageCommitsPerDay), "daily average")
|
||||
}
|
||||
|
||||
func renderActiveMonthCard(dc *gg.Context, c bentoCard, stats *models.UserStats) {
|
||||
label(dc, c, "MOST ACTIVE MONTH")
|
||||
bigText(dc, c, stats.MostActiveMonth, 44)
|
||||
}
|
||||
|
||||
func renderTopRepoCard(dc *gg.Context, c bentoCard, stats *models.UserStats) {
|
||||
label(dc, c, "TOP REPOSITORY")
|
||||
|
||||
if len(stats.TopRepositories) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
top := stats.TopRepositories[0]
|
||||
maxW := c.w - 48
|
||||
|
||||
loadFont(dc, 32)
|
||||
dc.SetColor(c.accent)
|
||||
dc.DrawString(truncate(top.Name, maxW, dc), c.x+24, c.y+100)
|
||||
|
||||
loadFont(dc, 13)
|
||||
dc.SetColor(textSecondary)
|
||||
dc.DrawString("your #1 repository this year", c.x+24, c.y+126)
|
||||
|
||||
y := c.y + 164
|
||||
for i := 1; i < len(stats.TopRepositories) && i < 4; i++ {
|
||||
loadFont(dc, 12)
|
||||
dc.SetColor(textMuted)
|
||||
dc.DrawString(
|
||||
fmt.Sprintf("#%d %s", i+1, truncate(stats.TopRepositories[i].Name, maxW-30, dc)),
|
||||
c.x+24, y,
|
||||
)
|
||||
y += 24
|
||||
}
|
||||
}
|
||||
|
||||
func renderLanguagesCard(dc *gg.Context, c bentoCard, stats *models.UserStats) {
|
||||
label(dc, c, "LANGUAGES")
|
||||
|
||||
if len(stats.Languages) == 0 {
|
||||
loadFont(dc, 15)
|
||||
dc.SetColor(textSecondary)
|
||||
dc.DrawString("No language data available.", c.x+24, c.y+100)
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
numCols = 2
|
||||
rowsPerCol = 5
|
||||
rowH = 48.0
|
||||
startY = 60.0
|
||||
labelW = 110.0
|
||||
countW = 90.0
|
||||
barH = 14.0
|
||||
)
|
||||
|
||||
maxCount := stats.Languages[0].Count
|
||||
halfW := (c.w - 48 - gridGap) / 2
|
||||
|
||||
for i, lang := range stats.Languages {
|
||||
if i >= numCols*rowsPerCol {
|
||||
break
|
||||
}
|
||||
|
||||
colIdx := i / rowsPerCol
|
||||
rowIdx := i % rowsPerCol
|
||||
|
||||
baseX := c.x + 24 + float64(colIdx)*(halfW+float64(gridGap))
|
||||
baseY := c.y + startY + float64(rowIdx)*rowH
|
||||
|
||||
pct := float64(lang.Count) / float64(maxCount)
|
||||
barW := halfW - labelW - countW - 10
|
||||
|
||||
loadFont(dc, 13)
|
||||
dc.SetColor(textPrimary)
|
||||
dc.DrawString(truncatePx(lang.Language, labelW-8, dc), baseX, baseY+12)
|
||||
|
||||
filledBar(dc, baseX+labelW, baseY+2, barW, barH, pct, c.accent)
|
||||
|
||||
loadFont(dc, 12)
|
||||
dc.SetColor(textSecondary)
|
||||
dc.DrawString(
|
||||
fmt.Sprintf("%d %.1f%%", lang.Count, lang.Percent),
|
||||
baseX+labelW+barW+10, baseY+13,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func renderActivityCard(dc *gg.Context, c bentoCard, stats *models.UserStats) {
|
||||
label(dc, c, "ACTIVITY PATTERNS")
|
||||
|
||||
const (
|
||||
startY = 54.0
|
||||
rowH = 28.0
|
||||
labelW = 44.0
|
||||
countW = 50.0
|
||||
barH = 14.0
|
||||
)
|
||||
|
||||
halfW := (c.w - 48 - float64(gridGap)) / 2
|
||||
lx := c.x + 24
|
||||
|
||||
loadFont(dc, 12)
|
||||
dc.SetColor(textSecondary)
|
||||
dc.DrawString("By Weekday", lx, c.y+startY)
|
||||
|
||||
if len(stats.CommitsByWeekday) > 0 {
|
||||
maxC := stats.CommitsByWeekday[0].Count
|
||||
barW := halfW - labelW - countW - 10
|
||||
|
||||
for i, day := range stats.CommitsByWeekday {
|
||||
if i >= 7 {
|
||||
break
|
||||
}
|
||||
by := c.y + startY + 24 + float64(i)*rowH
|
||||
|
||||
loadFont(dc, 12)
|
||||
dc.SetColor(textPrimary)
|
||||
dc.DrawString(day.Day[:3], lx, by+12)
|
||||
|
||||
pct := float64(day.Count) / float64(maxC)
|
||||
filledBar(dc, lx+labelW, by+2, barW, barH, pct, accentCyan)
|
||||
|
||||
loadFont(dc, 11)
|
||||
dc.SetColor(textMuted)
|
||||
dc.DrawString(formatNumber(day.Count), lx+labelW+barW+8, by+12)
|
||||
}
|
||||
}
|
||||
|
||||
rx := c.x + 24 + halfW + float64(gridGap)
|
||||
|
||||
loadFont(dc, 12)
|
||||
dc.SetColor(textSecondary)
|
||||
dc.DrawString("By Month", rx, c.y+startY)
|
||||
|
||||
if len(stats.CommitsByMonth) > 0 {
|
||||
maxC := stats.CommitsByMonth[0].Count
|
||||
barW := halfW - labelW - countW - 10
|
||||
|
||||
for i, month := range stats.CommitsByMonth {
|
||||
if i >= 7 {
|
||||
break
|
||||
}
|
||||
by := c.y + startY + 24 + float64(i)*rowH
|
||||
|
||||
loadFont(dc, 12)
|
||||
dc.SetColor(textPrimary)
|
||||
dc.DrawString(month.Date.Format("Jan"), rx, by+12)
|
||||
|
||||
pct := float64(month.Count) / float64(maxC)
|
||||
filledBar(dc, rx+labelW, by+2, barW, barH, pct, accentOrange)
|
||||
|
||||
loadFont(dc, 11)
|
||||
dc.SetColor(textMuted)
|
||||
dc.DrawString(formatNumber(month.Count), rx+labelW+barW+8, by+12)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func formatNumber(n int) string {
|
||||
switch {
|
||||
case n >= 1_000_000:
|
||||
return fmt.Sprintf("%.1fM", float64(n)/1_000_000)
|
||||
case n >= 1_000:
|
||||
return fmt.Sprintf("%.1fK", float64(n)/1_000)
|
||||
default:
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
}
|
||||
|
||||
func truncate(s string, maxPx float64, dc *gg.Context) string {
|
||||
return truncatePx(s, maxPx, dc)
|
||||
}
|
||||
|
||||
func truncatePx(s string, maxPx float64, dc *gg.Context) string {
|
||||
w, _ := dc.MeasureString(s)
|
||||
if w <= maxPx {
|
||||
return s
|
||||
}
|
||||
for len(s) > 1 {
|
||||
s = s[:len(s)-1]
|
||||
w, _ = dc.MeasureString(s + "…")
|
||||
if w <= maxPx {
|
||||
return s + "…"
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func ExportReportAsImage(stats *models.UserStats, format string) (string, error) {
|
||||
resultsDir, err := config.GetResultsDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
dc := gg.NewContext(canvasW, canvasH)
|
||||
dc.SetColor(bgColor)
|
||||
dc.Clear()
|
||||
|
||||
cards := cardLayout()
|
||||
for _, c := range cards {
|
||||
drawCard(dc, c)
|
||||
}
|
||||
|
||||
renderCommitsCard(dc, cards[0], stats)
|
||||
renderReposCard(dc, cards[1], stats)
|
||||
renderActiveDayCard(dc, cards[2], stats)
|
||||
renderAvgCard(dc, cards[3], stats)
|
||||
renderActiveMonthCard(dc, cards[4], stats)
|
||||
renderTopRepoCard(dc, cards[5], stats)
|
||||
renderLanguagesCard(dc, cards[6], stats)
|
||||
renderActivityCard(dc, cards[7], stats)
|
||||
|
||||
now := time.Now()
|
||||
timestamp := now.Format("2006-01-02 15-04-05")
|
||||
filenameWithExt := fmt.Sprintf("%s - Gitea Wrapped.%s", timestamp, format)
|
||||
filename := filepath.Join(resultsDir, filenameWithExt)
|
||||
|
||||
switch format {
|
||||
case "png":
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
if err := png.Encode(f, dc.Image()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case "jpg", "jpeg":
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
if err := jpeg.Encode(f, dc.Image(), &jpeg.Options{Quality: 92}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported format: %s", format)
|
||||
}
|
||||
|
||||
return filenameWithExt, nil
|
||||
}
|
||||
|
||||
func GenerateReportImage(stats *models.UserStats, format string) (string, error) {
|
||||
return ExportReportAsImage(stats, format)
|
||||
}
|
||||
|
||||
func stripANSI(s string) string {
|
||||
b := make([]byte, 0, len(s))
|
||||
inEscape := false
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '\x1b' {
|
||||
inEscape = true
|
||||
} else if inEscape && s[i] == 'm' {
|
||||
inEscape = false
|
||||
} else if !inEscape {
|
||||
b = append(b, s[i])
|
||||
}
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func getTextColor(line string) color.Color {
|
||||
if strings.Contains(line, "Overview") ||
|
||||
strings.Contains(line, "Languages") ||
|
||||
strings.Contains(line, "Activity") {
|
||||
return color.RGBA{0, 255, 255, 255}
|
||||
}
|
||||
return color.RGBA{200, 200, 200, 255}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package reports
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/atridad/wrapped-cli/pkg/config"
|
||||
"github.com/atridad/wrapped-cli/pkg/models"
|
||||
)
|
||||
|
||||
type ReportMetadata struct {
|
||||
Timestamp time.Time `yaml:"timestamp"`
|
||||
Username string `yaml:"username"`
|
||||
TotalCommits int `yaml:"total_commits"`
|
||||
Repositories int `yaml:"repositories"`
|
||||
Stats *models.UserStats `yaml:"stats"`
|
||||
}
|
||||
|
||||
type Report struct {
|
||||
Metadata *ReportMetadata
|
||||
MarkdownID string
|
||||
MetadataID string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
func SaveReport(stats *models.UserStats) (string, error) {
|
||||
resultsDir, err := config.GetResultsDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
timestamp := now.Format("2006-01-02 15-04-05")
|
||||
metadataID := fmt.Sprintf("%s.yaml", now.Format("2006-01-02-15-04-05"))
|
||||
imageID := fmt.Sprintf("%s - Gitea Wrapped.png", timestamp)
|
||||
|
||||
metadata := &ReportMetadata{
|
||||
Timestamp: now,
|
||||
Username: stats.Username,
|
||||
TotalCommits: stats.TotalCommits,
|
||||
Repositories: stats.TotalRepositories,
|
||||
Stats: stats,
|
||||
}
|
||||
|
||||
metadataPath := filepath.Join(resultsDir, metadataID)
|
||||
data, err := yaml.Marshal(metadata)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(metadataPath, data, 0o644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := ExportReportAsImage(stats, "png"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to generate image: %v\n", err)
|
||||
}
|
||||
|
||||
return imageID, nil
|
||||
}
|
||||
|
||||
func ListReports() ([]Report, error) {
|
||||
resultsDir, err := config.GetResultsDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(resultsDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []Report{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var reports []Report
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
if filepath.Ext(entry.Name()) == ".yaml" {
|
||||
metadataPath := filepath.Join(resultsDir, entry.Name())
|
||||
data, err := os.ReadFile(metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var metadata ReportMetadata
|
||||
if err := yaml.Unmarshal(data, &metadata); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
reports = append(reports, Report{
|
||||
Metadata: &metadata,
|
||||
MetadataID: entry.Name(),
|
||||
Timestamp: metadata.Timestamp,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(reports, func(i, j int) bool {
|
||||
return reports[i].Timestamp.After(reports[j].Timestamp)
|
||||
})
|
||||
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
func GetReport(metadataID string) (*ReportMetadata, error) {
|
||||
resultsDir, err := config.GetResultsDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metadataPath := filepath.Join(resultsDir, metadataID)
|
||||
data, err := os.ReadFile(metadataPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var metadata ReportMetadata
|
||||
if err := yaml.Unmarshal(data, &metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &metadata, nil
|
||||
}
|
||||
|
||||
func generateMarkdown(stats *models.UserStats) string {
|
||||
md := fmt.Sprintf("# Gitea Wrapped — %s\n\n", time.Now().Format("January 2, 2006"))
|
||||
|
||||
md += fmt.Sprintf("**User**: %s\n\n", stats.Username)
|
||||
|
||||
md += "## Overview\n\n"
|
||||
md += fmt.Sprintf("- **Total Commits**: %d\n", stats.TotalCommits)
|
||||
md += fmt.Sprintf("- **Total Repositories**: %d\n", stats.TotalRepositories)
|
||||
md += fmt.Sprintf("- **Average Commits/Day**: %.2f\n", stats.AverageCommitsPerDay)
|
||||
md += fmt.Sprintf("- **Most Active Day**: %s\n", stats.MostActiveDay)
|
||||
md += fmt.Sprintf("- **Most Active Month**: %s\n\n", stats.MostActiveMonth)
|
||||
|
||||
md += "## Languages\n\n"
|
||||
for i, lang := range stats.Languages {
|
||||
if i >= 10 {
|
||||
break
|
||||
}
|
||||
md += fmt.Sprintf("- %s: %d repos (%.1f%%)\n", lang.Language, lang.Count, lang.Percent)
|
||||
}
|
||||
md += "\n"
|
||||
|
||||
md += "## Top Repositories\n\n"
|
||||
for i, repo := range stats.TopRepositories {
|
||||
md += fmt.Sprintf("%d. [%s](%s)\n", i+1, repo.Name, repo.HTMLURL)
|
||||
}
|
||||
md += "\n"
|
||||
|
||||
md += "## Most Active Days\n\n"
|
||||
for i, day := range stats.CommitsByWeekday {
|
||||
if i >= 7 {
|
||||
break
|
||||
}
|
||||
md += fmt.Sprintf("- %s: %d commits\n", day.Day, day.Count)
|
||||
}
|
||||
md += "\n"
|
||||
|
||||
md += "## Most Active Months\n\n"
|
||||
for i, month := range stats.CommitsByMonth {
|
||||
if i >= 12 {
|
||||
break
|
||||
}
|
||||
md += fmt.Sprintf("- %s: %d commits\n", month.Date.Format("January 2006"), month.Count)
|
||||
}
|
||||
|
||||
return md
|
||||
}
|
||||
Reference in New Issue
Block a user