Files
unclesam b04d70b99a feat: 拆分评审与提交消息提示词并增加 hook 备份与 JSON 输出
- 新增 custom_reviewer_prompt 和 custom_commit_msg_prompt,兼容旧 custom_prompt
- pre-commit hook 安装时备份已有 hook,卸载时恢复
- 支持 --format json 输出评审结果
- diff 发送前自动脱敏敏感信息
2026-08-23 22:31:30 +08:00

178 lines
4.0 KiB
Go

package updater
import (
"context"
"encoding/json"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
// CurrentVersion 当前编译版本号
const CurrentVersion = "v1.0.7"
const (
githubAPIURL = "https://api.github.com/repos/unclesam-ly/git-air/releases/latest"
checkInterval = 24 * time.Hour
httpTimeout = 1500 * time.Millisecond
)
type UpdateState struct {
LastChecked int64 `json:"last_checked"`
LatestVer string `json:"latest_ver"`
ReleaseURL string `json:"release_url"`
}
type UpdateInfo struct {
CurrentVersion string
LatestVersion string
ReleaseURL string
}
func getCachePath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
dir := filepath.Join(home, ".git-air")
_ = os.MkdirAll(dir, 0755)
return filepath.Join(dir, "update_check.json"), nil
}
func readCache() (*UpdateState, error) {
path, err := getCachePath()
if err != nil {
return nil, err
}
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var state UpdateState
if err := json.Unmarshal(data, &state); err != nil {
return nil, err
}
return &state, nil
}
func writeCache(state *UpdateState) {
path, err := getCachePath()
if err != nil {
return
}
data, err := json.Marshal(state)
if err == nil {
_ = os.WriteFile(path, data, 0600)
}
}
// CheckUpdate 检查是否有新版本(带 24 小时本地缓存与极短超时,静默处理所有异常)
func CheckUpdate(force bool) *UpdateInfo {
now := time.Now().Unix()
// 1. 如果不是强制检查,优先读取 24 小时内的本地缓存
if !force {
if state, err := readCache(); err == nil && state != nil {
if now-state.LastChecked < int64(checkInterval.Seconds()) {
if IsNewerVersion(state.LatestVer, CurrentVersion) {
return &UpdateInfo{
CurrentVersion: CurrentVersion,
LatestVersion: state.LatestVer,
ReleaseURL: state.ReleaseURL,
}
}
return nil
}
}
}
// 2. 发起轻量级 GitHub API 请求
ctx, cancel := context.WithTimeout(context.Background(), httpTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", githubAPIURL, nil)
if err != nil {
return nil
}
req.Header.Set("User-Agent", "git-air-cli")
req.Header.Set("Accept", "application/vnd.github.v3+json")
client := &http.Client{Timeout: httpTimeout}
resp, err := client.Do(req)
if err != nil || resp == nil {
return nil
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil
}
var release struct {
TagName string `json:"tag_name"`
HTMLURL string `json:"html_url"`
}
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil || release.TagName == "" {
return nil
}
// 3. 写入本地缓存
writeCache(&UpdateState{
LastChecked: now,
LatestVer: release.TagName,
ReleaseURL: release.HTMLURL,
})
// 4. 比对版本
if IsNewerVersion(release.TagName, CurrentVersion) {
return &UpdateInfo{
CurrentVersion: CurrentVersion,
LatestVersion: release.TagName,
ReleaseURL: release.HTMLURL,
}
}
return nil
}
// CheckAsync 后台并发静默检查,返回 channel 供主流程结束前快速获取
func CheckAsync() <-chan *UpdateInfo {
ch := make(chan *UpdateInfo, 1)
go func() {
info := CheckUpdate(false)
ch <- info
}()
return ch
}
// IsNewerVersion 语义化版本号比对 (如 "v1.0.5" > "v1.0.4")
func IsNewerVersion(latest, current string) bool {
latestClean := strings.TrimPrefix(strings.TrimSpace(latest), "v")
currentClean := strings.TrimPrefix(strings.TrimSpace(current), "v")
if latestClean == "" || currentClean == "" || latestClean == currentClean {
return false
}
latestParts := strings.Split(latestClean, ".")
currentParts := strings.Split(currentClean, ".")
for i := 0; i < len(latestParts) && i < len(currentParts); i++ {
lNum, err1 := strconv.Atoi(latestParts[i])
cNum, err2 := strconv.Atoi(currentParts[i])
if err1 != nil || err2 != nil {
return latestClean > currentClean
}
if lNum > cNum {
return true
} else if lNum < cNum {
return false
}
}
return len(latestParts) > len(currentParts)
}