在日常开发中,我们经常想快速了解一个 Git 仓库的提交情况——谁贡献最多?哪个时间段最活跃?本周写了多少行代码?今天我们用 OpenCode 写一个命令行 Git 提交统计工具,输入仓库路径即可生成一份提交报告。
最终效果类似这样:
📊 Git 提交统计报告 (最近 90 天) 仓库: /home/user/my-project ──────────────────────────────── 总提交数: 247 👤 贡献者排名: alice ████████████████████ 128 (51.8%) bob ████████████ 78 (31.6%) carol ██████ 41 (16.6%) 📅 月度趋势: 2026-07 88 2026-08 72 2026-06 55 🔥 近期活跃度 (近 7 天): 周一 ██████████████ 28 周二 ██████████ 20 周三 ████████████████ 32
我对 OpenCode 说:
> 帮我创建一个 Go 项目,目录名叫 gitstats,初始化 go.mod,module 名为 gitstats。
OpenCode 自动创建了项目目录、运行了 go mod init gitstats,并生成了 main.go 骨架文件。整个过程一气呵成,不需要我手动敲任何命令。
我对 OpenCode 说:
> 在 main.go 中实现以下功能:运行 git log 命令获取提交记录,解析每行的 author、date 字段,统计:
> 1. 总提交数
> 2. 每个作者的提交数
> 3. 每月提交数
> 4. 最近 7 天每天提交数
> 用 --since 参数控制时间范围,默认 90 天。
OpenCode 生成了完整的解析逻辑:用 git log --since="90 days ago" --format="%an|%ad" --date=short 获取格式化输出,然后逐行切分统计。代码结构清晰,os/exec 调用 git 命令,bufio.Scanner 逐行读取,map[string]int 做计数。它还主动提示我可以使用 strings.SplitN 处理作者名中的特殊字符。
我对 OpenCode 说:
> 输出结果时用 Unicode 方块字符 █ 画柱状图,每 5 个提交显示一个方块,最大值宽度限制在 40 个字符。
OpenCode 快速加上了 renderBar 函数,用 strings.Repeat 生成柱状图,并自动处理了百分比计算和右对齐格式化。它还聪明地处理了作者名对齐——先遍历一遍找最长名字,再统一填充空格。
我对 OpenCode 说:
> 加上仓库路径参数支持(默认当前目录),给输出加上颜色(绿色标题、蓝色数字),用 go run . 可以直接运行测试。
OpenCode 加上了 ANSI 颜色码、flag 包处理命令行参数、os.Stat 检查 .git 目录是否存在。还顺手加了表格边框线让输出更美观。最后自动运行了一次测试命令验证功能正常。
整个过程我只用了 4 次自然语言对话,没有写一行代码。
package main
import (
"bufio"
"flag"
"fmt"
"os"
"os/exec"
"sort"
"strings"
"time"
)
const (
colorReset = "\033[0m"
colorGreen = "\033[32m"
colorBlue = "\033[34m"
colorYellow = "\033[33m"
colorCyan = "\033[36m"
)
func main() {
repoPath := flag.String("path", ".", "Git 仓库路径")
since := flag.Int("since", 90, "统计最近 N 天")
flag.Parse()
if _, err := os.Stat(*repoPath + "/.git"); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "%s 不是有效的 Git 仓库\n", *repoPath)
os.Exit(1)
}
data := fetchGitLog(*repoPath, *since)
if len(data) == 0 {
fmt.Println("没有找到提交记录")
return
}
printReport(*repoPath, *since, data)
}
type commit struct {
author string
date string
}
func fetchGitLog(repoPath string, since int) []commit {
cmd := exec.Command("git", "-C", repoPath, "log",
fmt.Sprintf("--since=%d days ago", since),
"--format=%an|%ad", "--date=short")
output, err := cmd.Output()
if err != nil {
fmt.Fprintf(os.Stderr, "执行 git log 失败: %v\n", err)
os.Exit(1)
}
var commits []commit
scanner := bufio.NewScanner(strings.NewReader(string(output)))
for scanner.Scan() {
parts := strings.SplitN(scanner.Text(), "|", 2)
if len(parts) == 2 {
commits = append(commits, commit{author: strings.TrimSpace(parts[0]), date: parts[1]})
}
}
return commits
}
func printReport(path string, since int, commits []commit) {
authorCount := make(map[string]int)
monthCount := make(map[string]int)
weekdayCount := make(map[string]int)
now := time.Now()
sevenDaysAgo := now.AddDate(0, 0, -7)
for _, c := range commits {
authorCount[c.author]++
if len(c.date) >= 7 {
monthCount[c.date[:7]]++
}
t, err := time.Parse("2006-01-02", c.date)
if err == nil && t.After(sevenDaysAgo) {
weekdayCount[t.Weekday().String()]++
}
}
fmt.Printf("%s📊 Git 提交统计报告%s (最近 %d 天)\n", colorGreen, colorReset, since)
fmt.Printf("仓库: %s\n", path)
fmt.Println(strings.Repeat("─", 40))
fmt.Printf("总提交数: %s%d%s\n\n", colorBlue, len(commits), colorReset)
printRanking("👤 贡献者排名", authorCount, len(commits))
printSorted("📅 月度趋势", monthCount)
printWeekday("🔥 近期活跃度 (近 7 天)", weekdayCount)
}
func printRanking(title string, data map[string]int, total int) {
fmt.Printf("%s%s%s\n", colorYellow, title, colorReset)
type kv struct {
key string
val int
}
var list []kv
for k, v := range data {
list = append(list, kv{k, v})
}
sort.Slice(list, func(i, j int) bool { return list[i].val > list[j].val })
maxLen := 0
for _, item := range list {
if len(item.key) > maxLen {
maxLen = len(item.key)
}
}
for _, item := range list {
bar := renderBar(item.val, total)
fmt.Printf(" %s%-*s%s %s %s%d%s (%.1f%%)\n",
colorCyan, maxLen, item.key, colorReset, bar,
colorBlue, item.val, colorReset,
float64(item.val)/float64(total)*100)
}
fmt.Println()
}
func printSorted(title string, data map[string]int) {
fmt.Printf("%s%s%s\n", colorYellow, title, colorReset)
var keys []string
for k := range data {
keys = append(keys, k)
}
sort.Sort(sort.Reverse(sort.StringSlice(keys)))
for _, k := range keys {
fmt.Printf(" %s %s%d%s\n", k, colorBlue, data[k], colorReset)
}
fmt.Println()
}
func printWeekday(title string, data map[string]int) {
fmt.Printf("%s%s%s\n", colorYellow, title, colorReset)
order := []string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}
names := map[string]string{
"Monday": "周一", "Tuesday": "周二", "Wednesday": "周三",
"Thursday": "周四", "Friday": "周五", "Saturday": "周六", "Sunday": "周日",
}
maxVal := 0
for _, v := range data {
if v > maxVal {
maxVal = v
}
}
for _, day := range order {
count := data[day]
barWidth := 0
if maxVal > 0 {
barWidth = count * 40 / maxVal
}
bar := ""
if barWidth > 0 {
bar = strings.Repeat("█", barWidth)
}
fmt.Printf(" %s %s %s%d%s\n", names[day], bar, colorBlue, count, colorReset)
}
}
func renderBar(count, total int) string {
barWidth := count * 40 / total
if barWidth <= 0 {
barWidth = 1
}
return strings.Repeat("█", barWidth)
}
用 OpenCode 开发这个工具的体验让我感受最深的是专注力的保持。传统开发中,"写代码 → 查文档 → 调 bug → 格式化"这个循环会频繁打断思路。而 OpenCode 把所有这些琐碎操作压缩成了 4 句自然语言指令,我只需要描述"要什么",不需要关心"怎么写"。
对比手工开发,效率提升大约 3-5 倍。更重要的是,生成的代码风格统一、边界情况处理到位——连 .git 目录不存在时的错误提示都自动加好了。这种"说出需求即得结果"的开发体验,正在重新定义程序员的工作方式。