├── .github └── workflows │ └── relese.yml ├── .gitignore ├── LICENSE ├── README.md ├── config.json ├── go.mod └── main.go /.github/workflows/relese.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' # 触发条件:推送的标签匹配版本号模式,例如 v1.0.0 7 | workflow_dispatch: 8 | 9 | jobs: 10 | create-release: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Create Release 15 | id: create_release 16 | uses: actions/create-release@v1 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }} 19 | with: 20 | tag_name: ${{ github.ref }} 21 | release_name: Release ${{ github.ref }} 22 | draft: true 23 | prerelease: false 24 | outputs: 25 | upload_url: ${{ steps.create_release.outputs.upload_url }} 26 | 27 | 28 | 29 | 30 | build: 31 | needs: create-release 32 | runs-on: ubuntu-latest 33 | 34 | strategy: 35 | matrix: 36 | goos: [ linux, darwin, windows ] 37 | goarch: [ amd64, arm64 ] 38 | 39 | steps: 40 | - name: Checkout code 41 | uses: actions/checkout@v2 42 | 43 | - name: Set up Go 44 | uses: actions/setup-go@v3 45 | with: 46 | go-version: '1.21.4' # 替换为你的Go版本 47 | 48 | - name: Build the project 49 | run: | 50 | GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -ldflags="-s -w" -o post-commit-${{ matrix.goos }}-${{ matrix.goarch }} main.go 51 | 52 | - name: Archive the build output 53 | run: | 54 | zip git-daily-${{ matrix.goos }}-${{ matrix.goarch }}.zip post-commit-${{ matrix.goos }}-${{ matrix.goarch }} config.json 55 | 56 | - name: Upload Release Asset 57 | uses: actions/upload-release-asset@v1 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }} 60 | with: 61 | upload_url: ${{ needs.create-release.outputs.upload_url }} 62 | asset_path: ./git-daily-${{ matrix.goos }}-${{ matrix.goarch }}.zip 63 | asset_name: git-daily-${{ matrix.goos }}-${{ matrix.goarch }}.zip 64 | asset_content_type: application/zip 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | vendor/ 17 | 18 | # Go workspace file 19 | go.work 20 | 21 | # IDE specific files 22 | .idea/ 23 | .vscode/ 24 | *.swp 25 | *.swo 26 | 27 | # OS generated files 28 | .DS_Store 29 | .DS_Store? 30 | ._* 31 | .Spotlight-V100 32 | .Trashes 33 | ehthumbs.db 34 | Thumbs.db 35 | 36 | # Log files 37 | *.log 38 | 39 | # Environment variables file 40 | .env 41 | .env.local 42 | .env.development.local 43 | .env.test.local 44 | .env.production.local 45 | 46 | # Build output directory 47 | bin/ 48 | dist/ 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 zhangwt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Git Daily 📝 2 | 3 | 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/zhangwt-cn/git-daily)](https://goreportcard.com/report/github.com/zhangwt-cn/git-daily) 5 | [![GitHub release](https://img.shields.io/github/v/release/zhangwt-cn/git-daily)](https://github.com/zhangwt-cn/git-daily/releases) 6 | [![License](https://img.shields.io/github/license/zhangwt-cn/git-daily)](LICENSE) 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) 8 | 9 | **基于 Git commit hooks 的智能工作日报生成工具** 10 | 11 | 12 | 13 | ## ✨ 特性 14 | 15 | - 🔄 基于 Git post-commit hook 实现自动触发 16 | - 🤖 利用 AI 智能分析代码变动内容 17 | - 📊 生成结构化的工作日报 18 | - ⚙️ 支持自定义配置 19 | - 🚀 轻量级,低资源占用 20 | 21 | ## 📦 安装 22 | 23 | ### 前置条件 24 | 25 | - ✅ Git 26 | 27 | ### 安装步骤 28 | 29 | #### 方法一:直接使用预编译的二进制文件(推荐) 🚀 30 | 31 | 1. 从 [Release 页面](https://github.com/zhangwt-cn/git-daily/releases) 下载适合你操作系统的最新版本 32 | 33 | 2. 解压下载的文件: 34 | ```bash 35 | # 🪟 Windows 36 | unzip git-daily-windows-amd64.zip 37 | 38 | # 🐧 Linux/🍎 MacOS 39 | tar -xzf git-daily-linux-amd64.tar.gz 40 | ``` 41 | 42 | 3. 将解压出的 `git-daily` 二进制文件复制到你的 Git 项目的 hooks 目录: 43 | ```bash 44 | # 进入你的项目目录 45 | cd your-project 46 | 47 | # 复制 git-daily、config.json 到 .git/hooks/post-commit 48 | cp path/to/post-commit .git/hooks/post-commit 49 | cp path/to/config.json .git/hooks/config.json 50 | ``` 51 | 52 | 4. 确保 hook 文件具有执行权限(Linux/MacOS): 53 | ```bash 54 | chmod +x .git/hooks/post-commit 55 | ``` 56 | 57 | #### 方法二:从源码编译(可选) 🛠️ 58 | 59 | 如果你有 Go 开发环境(Go 1.21.4),也可以从源码编译: 60 | 61 | 1. 克隆项目并编译: 62 | ```bash 63 | git clone https://github.com/zhangwt-cn/git-daily.git 64 | cd git-daily 65 | go build 66 | ``` 67 | 68 | 2. 将编译好的二进制文件复制到你的项目的 hooks 目录: 69 | ```bash 70 | cp git-daily /path/to/your-project/.git/hooks/post-commit 71 | cp config.json /path/to/your-project/.git/hooks/config.json 72 | ``` 73 | 74 | ## 🚀 使用方法 75 | 76 | ### 1️⃣ 配置 77 | 78 | 在项目根目录创建 `config.json` 配置文件: 79 | ```json 80 | { 81 | "openai_base_url": "https://api.oaipro.com/v1", 82 | "openai_api_key": "sk-xxx", 83 | "moderation_model": "claude-3-5-sonnet-20241022", 84 | "daily_path": "" 85 | } 86 | ``` 87 | 88 | ### 2️⃣ 自动运行 89 | 90 | 完成配置后,工具会自动在每次 commit 后运行: 91 | - 🔍 自动分析本次提交的代码变动 92 | - 🤖 使用 AI 模型生成变更说明 93 | - 📝 生成日报内容 94 | 95 | ### 3️⃣ 输出说明 96 | 97 | 默认情况下,日报将保存在项目根目录的 `daily-report.md` 文件中: 98 | - 📂 可以通过 `daily_path` 配置项修改输出位置 99 | - 🔄 每次提交后会自动更新日报内容 100 | 101 | ### 4️⃣ 使用示例 102 | 103 | ```bash 104 | git add . 105 | git commit -m "feat: add new feature" 106 | # git-daily 会自动运行并生成日报 107 | ``` 108 | 109 | ## ⚙️ 配置说明 110 | 111 | 配置文件 (`config.json`) 参数说明: 112 | 113 | | 参数 | 说明 | 默认值 | 114 | |------|------|--------| 115 | | `openai_base_url` | OpenAI API 基础 URL | `https://api.oaipro.com/v1` | 116 | | `openai_api_key` | OpenAI API 密钥 | - | 117 | | `moderation_model` | AI 模型选择 | `claude-3-5-sonnet-20241022` | 118 | | `daily_path` | 日报输出路径 | 当前目录 | 119 | 120 | ## 📋 日报示例 121 | 122 | ```markdown 123 | # Daily Git Report - 2024-11-22 124 | 125 | ### Repo: git-daily 126 | **Commit Hash:** 5e645887f32be6129b1aef7413d000f2809df4a6 127 | **Date:** 2024-11-22 09:37:39 128 | **Message:** feat: 增加日志颜色输出 129 | **Analysis:** 130 | 今日工作进展: 131 | 1. 增强日志输出功能 132 | - 通过引入日志颜色输出功能,优化了系统的可读性和使用体验。不同颜色的日志信息可以帮助开发人员和运维人员快速区分和识别关键信息、警告和错误,从而提高问题诊断效率。 133 | - 具体改进方面,在终端输出中新增了红色(错误)、蓝色(信息)、绿色(成功)、黄色(警告)等颜色表示,能够使日志分析更加直观和快速,减少了人工筛选和判断时间。 134 | - 此功能不仅提升了日志的友好性,同时也预见性地减少了由于信息误读带来的潜在业务风险。后续我们计划根据实际的使用反馈,进一步调整和优化颜色配置,以适应更多场景需求。 135 | ``` 136 | 137 | ## 🔧 故障排除 138 | 139 | ### 常见问题 140 | 141 | 1. **🔴 权限问题** 142 | ```bash 143 | # 解决方案 144 | chmod +x .git/hooks/post-commit 145 | ``` 146 | 147 | 2. **🔴 配置文件无法读取** 148 | - 确保 `config.json` 在.git/hooks目录下 149 | - 检查 JSON 格式是否正确 150 | 151 | 3. **🔴 Failed to fetch analysis** 152 | - 验证 API 密钥是否正确 153 | - 检查网络连接 154 | - 确认 API 代理地址可访问 155 | 156 | 157 | ## 🤝 贡献指南 158 | 159 | 1. Fork 本项目 160 | 2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) 161 | 3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) 162 | 4. 推送到分支 (`git push origin feature/AmazingFeature`) 163 | 5. 提交 Pull Request 164 | 165 | ## 📄 许可证 166 | 167 | 本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情 168 | 169 | ## 👥 作者 170 | 171 | - zhangwt-cn 172 | 173 | ## 🙏 致谢 174 | 175 | 感谢所有贡献者对本项目的支持! 176 | 177 | --- 178 | 179 | 180 | 如果这个项目对你有帮助,请给一个 ⭐️ Star ⭐️ 181 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "openai_base_url": "https://api.oaipro.com/v1", 3 | "openai_api_key": "sk-xxx", 4 | "moderation_model": "claude-3-5-sonnet-20241022", 5 | "daily_path": "" 6 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module git-daily 2 | 3 | go 1.21.4 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | // ChatResponse 结构体用于解析API响应 17 | type ChatResponse struct { 18 | ID string `json:"id"` 19 | Object string `json:"object"` 20 | Created int `json:"created"` 21 | Choices []struct { 22 | Message struct { 23 | Role string `json:"role"` 24 | Content string `json:"content"` 25 | } `json:"message"` 26 | FinishReason string `json:"finish_reason"` 27 | Index int `json:"index"` 28 | } `json:"choices"` 29 | } 30 | 31 | // Config 结构体定义,字段与 JSON 结构对应 32 | type Config struct { 33 | OpenAIBaseURL string `json:"openai_base_url"` 34 | OpenAIAPIKey string `json:"openai_api_key"` 35 | ModerationModel string `json:"moderation_model"` 36 | DailyPath string `json:"daily_path"` 37 | } 38 | 39 | var config Config 40 | 41 | const ( 42 | Red = "\033[31m" 43 | Green = "\033[32m" 44 | Yellow = "\033[33m" 45 | Blue = "\033[34m" 46 | Reset = "\033[0m" 47 | ) 48 | 49 | func main() { 50 | loadConfig(`.git/hooks/config.json`) 51 | 52 | // 定义日报文件路径 53 | date := time.Now().Format("2006-01-02") 54 | dailyPath := config.DailyPath 55 | if dailyPath == "" { 56 | dailyPath = "./daily" 57 | } 58 | 59 | dailyReportFile := fmt.Sprintf("%s/git_report_%s.md", config.DailyPath, date) 60 | 61 | // 获取最新提交的 diff 内容 62 | diffContent, err := getGitDiff() 63 | if err != nil || strings.TrimSpace(diffContent) == "" { 64 | fmt.Println(Blue + "No diff content found. Skipping analysis." + Blue) 65 | return 66 | } 67 | 68 | // 获取提交信息 69 | commitMsg, commitHash, commitDate, repoName, err := getGitCommitInfo() 70 | if err != nil { 71 | fmt.Println(Red+"Failed to get commit info:"+Red, err) 72 | return 73 | } 74 | 75 | // 检查日报文件是否已存在,没有则创建文件并写入标题 76 | if _, err := os.Stat(dailyReportFile); os.IsNotExist(err) { 77 | err = os.WriteFile(dailyReportFile, []byte(fmt.Sprintf("# Daily Git Report - %s\n", date)), 0644) 78 | if err != nil { 79 | fmt.Println(Red+"Failed to create daily report file:"+Red, err) 80 | return 81 | } 82 | } 83 | 84 | // 检查是否已记录该提交 85 | reportContent, _ := os.ReadFile(dailyReportFile) 86 | if strings.Contains(string(reportContent), commitHash) { 87 | fmt.Println(Blue+"Commit already recorded:"+Blue, commitHash) 88 | return 89 | } 90 | 91 | // 调用 OpenAI API 分析提交信息和 diff 内容 92 | analysis, err := analyzeGitDiff(commitMsg, diffContent) 93 | if err != nil || strings.TrimSpace(analysis) == "" { 94 | fmt.Println(Red+"Failed to fetch analysis: "+Red, err) 95 | return 96 | } 97 | 98 | // 追加分析内容到日报文件 99 | err = appendToReport(dailyReportFile, repoName, commitHash, commitDate, commitMsg, analysis) 100 | if err != nil { 101 | fmt.Println(Red+"Failed to append to report file:"+Red, err) 102 | } 103 | } 104 | 105 | func getGitDiff() (string, error) { 106 | cmd := exec.Command("git", "diff", "HEAD^", "HEAD") 107 | output, err := cmd.CombinedOutput() 108 | return string(output), err 109 | } 110 | 111 | // 获取提交信息 112 | func getGitCommitInfo() (string, string, string, string, error) { 113 | commitMsgCmd := exec.Command("git", "log", "-1", "--pretty=%B") 114 | commitMsg, err := commitMsgCmd.CombinedOutput() 115 | if err != nil { 116 | return "", "", "", "", err 117 | } 118 | 119 | commitHashCmd := exec.Command("git", "log", "-1", "--pretty=%H") 120 | commitHash, err := commitHashCmd.CombinedOutput() 121 | if err != nil { 122 | return "", "", "", "", err 123 | } 124 | 125 | commitDate := time.Now().Format("2006-01-02 15:04:05") 126 | repoName := "git project" 127 | if dirName, err := os.Getwd(); err == nil { 128 | repoName = filepath.Base(dirName) 129 | } 130 | 131 | return strings.TrimSpace(string(commitMsg)), strings.TrimSpace(string(commitHash)), commitDate, strings.TrimSpace(repoName), nil 132 | } 133 | 134 | func loadConfig(filename string) { 135 | // 打开文件 136 | file, err := os.Open(filename) 137 | if err != nil { 138 | fmt.Println(Red+"打开配置文件错误: %w"+Red, err) 139 | } 140 | defer func(file *os.File) { 141 | err := file.Close() 142 | if err != nil { 143 | fmt.Println(Red+"关闭文件错误: %w"+Red, err) 144 | } 145 | }(file) 146 | 147 | // 创建 JSON 解码器并解码 148 | decoder := json.NewDecoder(file) 149 | if err := decoder.Decode(&config); err != nil { 150 | fmt.Println(Red+"解析配置文件错误: %w"+Red, err) 151 | } 152 | } 153 | 154 | func analyzeGitDiff(commitMsg, diffContent string) (string, error) { 155 | apiURL := config.OpenAIBaseURL + "/chat/completions" 156 | apiKey := config.OpenAIAPIKey 157 | payload := map[string]interface{}{ 158 | "model": config.ModerationModel, 159 | "messages": []map[string]string{ 160 | {"role": "system", "content": "你是一位擅长技术管理沟通的专家。请基于git代码变动内容,生成一份面向领导的工作日报。要突出业务价值和工作成效,用管理视角描述技术变更。\n\n## 分析重点\n1. 将技术变动转化为业务价值:\n - 功能如何服务业务目标\n - 变更带来的具体改进\n - 对用户体验的提升\n - 对系统性能的优化\n - 对维护成本的影响\n\n2. 重点关注:\n - 项目进展和里程碑\n - 重要功能的完成情况\n - 系统改进和优化\n - 问题修复的效果\n - 潜在的业务影响\n\n## 输出要求\n1. 使用管理层易于理解的语言\n2. 突出业务价值和实际效果\n3. 避免过多技术细节\n4. 注意工作表述的积极性\n5. 体现主动性和规划性\n\n## 输出格式\n今日工作进展:\n1. [重要进展/完成的功能]\n - 业务价值说明\n - 具体改进效果\n - (如有)后续规划\n\n## 示例\n\n输入:\n```diff\ndiff --git a/src/services/search.js b/src/services/search.js\n--- a/src/services/search.js\n+++ b/src/services/search.js\n@@ -15,6 +15,14 @@ class SearchService {\n+ async searchWithCache(keyword) {\n+ const cacheKey = `search:${keyword}`;\n+ const cached = await cache.get(cacheKey);\n+ if (cached) return cached;\n+ const result = await this.search(keyword);\n+ await cache.set(cacheKey, result, 3600);\n+ return result;\n+ }\n+\n async search(keyword) {\n- return await db.query(keyword);\n+ return await this.searchWithCache(keyword);\n }\n}\n\ndiff --git a/src/services/product.js b/src/services/product.js\n--- a/src/services/product.js\n+++ b/src/services/product.js\n@@ -8,6 +8,10 @@ class ProductService {\n+ if (product.stock < 10) {\n+ await notificationService.notify('stock-warning', {\n+ productId: product.id,\n+ currentStock: product.stock\n+ });\n+ }\n }\n }\n```\n\n相关commit信息:\n```\nperf: implement search caching\nfeat: add low stock notification\n```\n\n输出:\n今日工作进展:\n1. 优化系统搜索性能\n - 实现智能缓存机制,显著提升用户搜索响应速度\n - 预计可减少50%以上的数据库查询压力\n - 已完成开发和测试,准备在下一版本发布\n\n2. 完善库存管理预警\n - 新增低库存自动预警功能,支持及时补货决策\n - 有效预防断货风险,提升库存周转效率\n - 系统自动监控,无需人工干预\n"}, 161 | {"role": "user", "content": fmt.Sprintf("提交信息:\n'%s'\n\n代码变动:\n'%s'\n\nProvide a detailed analysis:", commitMsg, diffContent)}, 162 | }, 163 | } 164 | data, _ := json.Marshal(payload) 165 | req, _ := http.NewRequest("POST", apiURL, bytes.NewBuffer(data)) 166 | req.Header.Set("Authorization", "Bearer "+apiKey) 167 | req.Header.Set("Content-Type", "application/json") 168 | 169 | client := &http.Client{} 170 | resp, err := client.Do(req) 171 | if err != nil { 172 | return "", err 173 | } 174 | defer resp.Body.Close() 175 | 176 | if resp.StatusCode != http.StatusOK { 177 | return "", fmt.Errorf("HTTP Status: %d", resp.StatusCode) 178 | } 179 | 180 | body, _ := io.ReadAll(resp.Body) 181 | var openAIResp ChatResponse 182 | err = json.Unmarshal(body, &openAIResp) 183 | if err != nil || len(openAIResp.Choices) == 0 { 184 | return "", fmt.Errorf("invalid response from OpenAI") 185 | } 186 | 187 | return openAIResp.Choices[0].Message.Content, nil 188 | } 189 | 190 | func appendToReport(filePath, repoName, commitHash, commitDate, commitMsg, analysis string) error { 191 | content, _ := os.ReadFile(filePath) 192 | updatedContent := string(content) 193 | 194 | repoHeader := fmt.Sprintf("### Repo: %s", repoName) 195 | entry := fmt.Sprintf("**Commit Hash:** %s\n**Date:** %s\n**Message:** %s\n**Analysis:**\n%s\n\n---\n", commitHash, commitDate, commitMsg, analysis) 196 | 197 | if strings.Contains(updatedContent, repoHeader) { 198 | updatedContent = strings.Replace(updatedContent, repoHeader, repoHeader+"\n"+entry, 1) 199 | } else { 200 | updatedContent += fmt.Sprintf("\n%s\n%s", repoHeader, entry) 201 | } 202 | 203 | return os.WriteFile(filePath, []byte(updatedContent), 0644) 204 | } 205 | --------------------------------------------------------------------------------