feat(updater): 引入 24 小时静默更新检测与一键升级功能
- 新增 `updater` 模块实现 GitHub Release 版本异步检查与本地缓存 - 新增 `git air update` 与 `git air version` 命令支持一键升级及版本检查 - 优化 CLI 终端交互,在首页渲染时提供友好的新版本升级提示卡片
This commit is contained in:
1 parent
10cbfc39a5
commit
7632652b7b
9 files changed
+398
-64
No files matched your search
@@ -214,6 +214,20 @@ git air hook uninstall
|
||||
|
||||
---
|
||||
|
||||
### 5. 版本查看与一键自动升级 (`git air update`)
|
||||
|
||||
`git-air` 内置了**轻量级 24 小时静默更新检测**,当 GitHub 发布新版本时会在终端底部自动提醒;你也可以随时手动检查与一键升级:
|
||||
|
||||
```bash
|
||||
# 查看当前版本并在线检查更新
|
||||
git air version
|
||||
|
||||
# 一键自动拉取并升级到最新版本
|
||||
git air update
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 配置文件说明
|
||||
|
||||
`git-air` 支持多层配置覆盖,优先级从高到低为:
|
||||
|
||||
@@ -211,6 +211,20 @@ git air hook uninstall
|
||||
|
||||
---
|
||||
|
||||
### 5. Version Check & One-Click Upgrade (`git air update`)
|
||||
|
||||
`git-air` comes with **lightweight 24-hour background update detection** and alerts you in the terminal whenever a new release is available. You can also manually check or upgrade anytime:
|
||||
|
||||
```bash
|
||||
# Check current version and check for updates
|
||||
git air version
|
||||
|
||||
# One-click upgrade to the latest release
|
||||
git air update
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration Hierarchy
|
||||
|
||||
`git-air` searches for configurations in the following priority order:
|
||||
|
||||
+67
-63
@@ -65,80 +65,46 @@ $ git air
|
||||
ユーザー認証ハンドラーに Redis キャッシュ層を追加し、Context のタイムアウト伝播を修正。
|
||||
|
||||
#### 詳細レビュー結果
|
||||
- [BLOCKER] internal/service/user.go:45 - 驥榊、ァ縺ェ繧サ繧ュ繝・繝ェ繝�ぅ�# 迚ケ螳壹�繝輔ぃ繧、繝ォ縺ョ縺ソ繧偵Ξ繝薙Η繝シ
|
||||
git air internal/service/chat.go
|
||||
- [BLOCKER] internal/service/user.go:45 - 重大なセキュリティ脆弱性: ループ内で生SQL文字列を直接結合しています。SQLインジェクションのリスクがあります。
|
||||
// 推奨修正コード:
|
||||
db.Where("username = ?", inputName).First(&user)
|
||||
|
||||
# 荳譎ら噪縺ォ繝「繝�Ν繧Пrovider繧呈欠螳壹@縺ヲ螳溯。�
|
||||
git air --provider deepseek --model deepseek-chat
|
||||
- [WARNING] internal/service/user.go:82 - 潜在的リスク: Redis 接続エラーを握りつぶしています。キャッシュ障害時にDBへ負荷が集中します。
|
||||
|
||||
- [WARNING] internal/service/user.go:103 - 並行処理の欠陥: 早期リターン時に Mutex の Unlock を忘れています(Mutex Leak)。デッドロックの危険性があります。
|
||||
|
||||
#### 結論
|
||||
- 判定: [REJECT]
|
||||
- スコア: 60 / 100
|
||||
|
||||
📊 Token: 入力 1,243 / 出力 412 | ≈ $0.0025
|
||||
─────────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. AI 繧ウ繝溘ャ繝医Γ繝�そ繝シ繧ク閾ェ蜍慕函謌� (`git air msg`)
|
||||
|
||||
繧ウ繝溘ャ繝医Γ繝�そ繝シ繧ク縺ョ菴懈�縺ォ謔ゥ繧蠢�ヲ√�繧ゅ≧縺ゅj縺セ縺帙s縲Agit-air` 縺後せ繝��繧ク繝ウ繧ー縺輔l縺溷キョ蛻�r蛻�梵縺励,onventional Commits 貅匁侠縺ョ繝。繝�そ繝シ繧ク繧�**螟夊ィ隱櫁�蜍暮←蠢�**縺ァ逕滓�縺励∪縺呻シ�
|
||||
## 📦 インストール
|
||||
|
||||
### 方法 1: Go Install(推奨)
|
||||
```bash
|
||||
# 1. 繧キ繧ケ繝�Β險隱槭r閾ェ蜍墓、懷�縺励※逕滓� & 蟇セ隧ア蝙九さ繝溘ャ繝�
|
||||
git air msg
|
||||
go install github.com/unclesam-ly/git-air@latest
|
||||
```
|
||||
|
||||
# 2. 闍ア隱槭〒逕滓��医が繝シ繝励Φ繧ス繝シ繧ケ繝サ蝗ス髫帙�繝ュ繧ク繧ァ繧ッ繝亥髄縺托シ�
|
||||
git air msg -l en
|
||||
|
||||
# 3. 譌・譛ャ隱� / 髻灘嵜隱槭〒逕滓�
|
||||
git air msg -l ja
|
||||
git air msg -l ko
|
||||
|
||||
# 4. 遒コ隱阪↑縺励〒逶エ謗・ git commit 繧貞ョ溯。�
|
||||
git air msg -c
|
||||
### 方法 2: ソースコードからビルド
|
||||
```bash
|
||||
git clone https://github.com/unclesam-ly/git-air.git
|
||||
cd git-air
|
||||
go build -o git-air .
|
||||
sudo mv git-air /usr/local/bin/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 繝ッ繝ウ繧ソ繝�メ縺ァ Pre-commit 繝輔ャ繧ッ繧堤匳骭イ
|
||||
## 🚀 クイックスタート
|
||||
|
||||
Git 繝ェ繝昴ず繝医Μ逶エ荳九〒莉・荳九r螳溯。鯉シ�
|
||||
```bash
|
||||
git air hook install
|
||||
```
|
||||
- **繧ウ繝溘ャ繝郁�蜍輔ヶ繝ュ繝�け讖溯�**�壹Ξ繝薙Η繝シ縺ァ `[BLOCKER]` 驥榊、ァ谺�髯・繧� `[REJECT]` 蛻、螳壹′讀懷�縺輔l縺溷�エ蜷医~git-air` 縺ッ髱槭ぞ繝ュ縺ョ邨ゆコ�さ繝シ繝峨〒**繧ウ繝溘ャ繝茨シ�git commit`�峨r閾ェ蜍慕噪縺ォ荳ュ譁ュ繝サ繝悶Ο繝�け**縺励∪縺呻シ�
|
||||
- **蜴ウ譬シ繝「繝シ繝� (`--strict`)**�啻[WARNING]` 隴ヲ蜻翫〒繧ゅさ繝溘ャ繝医r繝悶Ο繝�け縺励◆縺��エ蜷医� `git air --strict` 繧剃スソ逕ィ��
|
||||
- **荳譎ら噪縺ェ繝悶Ο繝�け隗」髯、**�夂キ頑・縺ァ繧ウ繝溘ャ繝医r蠑キ蛻カ縺励◆縺��エ蜷医�縲;it 讓呎コ悶� `git commit --no-verify` 縺セ縺溘� `git air --no-block` 繧剃サ倅ク弱@縺セ縺吶�
|
||||
### 1. モデルと API Key の設定
|
||||
|
||||
繝輔ャ繧ッ繧定ァ」髯、縺吶k蝣エ蜷茨シ�
|
||||
```bash
|
||||
git air hook uninstall
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 笞呻ク� 險ュ螳壹�蜆ェ蜈磯��ス�
|
||||
|
||||
1. **繧ウ繝槭Φ繝峨Λ繧、繝ウ蠑墓焚** (`--key`, `--model`, `--provider`, `--prompt`, `--lang`, `--price-input`, `--price-output`, `--strict`, `--no-block`)
|
||||
2. **迺ー蠅�、画焚** (`GIT_AIR_API_KEY`, `GIT_AIR_PROVIDER`, `GIT_AIR_MODEL`, `GIT_AIR_COMMIT_LANG`)
|
||||
3. **繝励Ο繧ク繧ァ繧ッ繝亥句挨險ュ螳�** (繝ェ繝昴ず繝医Μ逶エ荳九� `./config.yaml` 縺セ縺溘� `./.git-air.yaml`)
|
||||
4. **繧ー繝ュ繝シ繝舌Ν險ュ螳�** (`~/.git-air/config.yaml`)
|
||||
|
||||
### `config.yaml` 險ュ螳壹ヵ繧。繧、繝ォ萓具シ�
|
||||
```yaml
|
||||
# 繝励Ο繝舌う繝繝シ: gemini, claude, grok, deepseek, qwen, zhipu, moonshot, siliconflow, ollama, openai, custom
|
||||
provider: "gemini"
|
||||
api_key: "YOUR_API_KEY_HERE"
|
||||
model: "gemini-3.7-flash"
|
||||
|
||||
# 繧ォ繧ケ繧ソ繝� API 繧ィ繝ウ繝峨�繧、繝ウ繝� (莉サ諢�)
|
||||
# base_url: "https://api.deepseek.com/v1"
|
||||
|
||||
# 繧ォ繧ケ繧ソ繝� System Prompt (莉サ諢�)
|
||||
# custom_prompt: ""
|
||||
|
||||
# 繧ォ繧ケ繧ソ繝� Token 譁咎≡險ュ螳� (莉サ諢上∝腰菴�: 邀ウ繝峨Ν / 1M Tokens)
|
||||
# price_input: 0.75
|
||||
# price_output: 3.75
|
||||
|
||||
# AI 繧ウ繝溘ャ繝医Γ繝�そ繝シ繧ク險隱櫁ィュ螳� (莉サ諢�: auto, zh, en, ja, ko)
|
||||
# commit_lang: "auto"
|
||||
```LLM縺ョ繧ィ繝ウ繝峨�繧、繝ウ繝医→謗ィ螂ィ繝「繝�Ν繧呈ィ呎コ悶�繝ェ繧サ繝�ヨ縺励※縺�∪縺吶A--provider` 繧呈欠螳壹☆繧九□縺代〒邁。蜊倥↓蛻�j譖ソ縺医i繧後∪縺呻シ�
|
||||
`git-air` は国内外の主要LLMのエンドポイントと推奨モデルを標準プリセットしています。`--provider` を指定するだけで簡単に切り替えられます:
|
||||
|
||||
```bash
|
||||
# 1. Google Gemini (推奨: 高速・低コスト)
|
||||
@@ -209,7 +175,28 @@ git air --provider deepseek --model deepseek-chat
|
||||
|
||||
---
|
||||
|
||||
### 3. 繝ッ繝ウ繧ソ繝�メ縺ァ Pre-commit 繝輔ャ繧ッ繧堤匳骭イ
|
||||
### 3. AI コミットメッセージ自動生成 (`git air msg`)
|
||||
|
||||
コミットメッセージの作成に悩む必要はもうありません。`git-air` がステージングされた差分を分析し、Conventional Commits 準拠のメッセージを**多言語自動適応**で生成します:
|
||||
|
||||
```bash
|
||||
# 1. システム言語を自動検出して生成 & 対話型コミット
|
||||
git air msg
|
||||
|
||||
# 2. 英語で生成(オープンソース・国際プロジェクト向け)
|
||||
git air msg -l en
|
||||
|
||||
# 3. 日本語 / 韓国語で生成
|
||||
git air msg -l ja
|
||||
git air msg -l ko
|
||||
|
||||
# 4. 確認なしで直接 git commit を実行
|
||||
git air msg -c
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. ワンタッチで Pre-commit フックを登録
|
||||
|
||||
Git リポジトリ直下で以下を実行:
|
||||
```bash
|
||||
@@ -226,10 +213,24 @@ git air hook uninstall
|
||||
|
||||
---
|
||||
|
||||
### 5. バージョン確認とワンクリック自動更新 (`git air update`)
|
||||
|
||||
`git-air` には**軽量な24時間サイレント更新検出機能**が組み込まれており、新しいリリースが公開されるとターミナル下部で自動通知されます。手動での確認やアップデートも可能です:
|
||||
|
||||
```bash
|
||||
# 現在のバージョンを確認し、更新をチェック
|
||||
git air version
|
||||
|
||||
# 最新バージョンへ一括アップデート
|
||||
git air update
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 設定の優先順位
|
||||
|
||||
1. **繧ウ繝槭Φ繝峨Λ繧、繝ウ蠑墓焚** (`--key`, `--model`, `--provider`, `--prompt`, `--price-input`, `--price-output`, `--strict`, `--no-block`)
|
||||
2. **迺ー蠅�、画焚** (`GIT_AIR_API_KEY`, `GIT_AIR_PROVIDER`, `GIT_AIR_MODEL`)
|
||||
1. **コマンドライン引数** (`--key`, `--model`, `--provider`, `--prompt`, `--lang`, `--price-input`, `--price-output`, `--strict`, `--no-block`)
|
||||
2. **環境変数** (`GIT_AIR_API_KEY`, `GIT_AIR_PROVIDER`, `GIT_AIR_MODEL`, `GIT_AIR_COMMIT_LANG`)
|
||||
3. **プロジェクト個別設定** (リポジトリ直下の `./config.yaml` または `./.git-air.yaml`)
|
||||
4. **グローバル設定** (`~/.git-air/config.yaml`)
|
||||
|
||||
@@ -249,6 +250,9 @@ model: "gemini-3.7-flash"
|
||||
# カスタム Token 料金設定 (任意、単位: 米ドル / 1M Tokens)
|
||||
# price_input: 0.75
|
||||
# price_output: 3.75
|
||||
|
||||
# AI コミットメッセージ言語設定 (任意: auto, zh, en, ja, ko)
|
||||
# commit_lang: "auto"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
+14
-1
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/unclesam-ly/git-air/internal/llm"
|
||||
"github.com/unclesam-ly/git-air/internal/reviewer"
|
||||
"github.com/unclesam-ly/git-air/internal/ui"
|
||||
"github.com/unclesam-ly/git-air/internal/updater"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@@ -21,6 +22,9 @@ var rootCmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// 后台非阻塞异步检查新版本
|
||||
updateCh := updater.CheckAsync()
|
||||
|
||||
// 1. 检查并获取 Diff
|
||||
if flagCached, _ := cmd.Flags().GetBool("cached"); flagCached {
|
||||
args = append([]string{"--cached"}, args...)
|
||||
@@ -96,7 +100,16 @@ var rootCmd = &cobra.Command{
|
||||
|
||||
ui.PrintFooter()
|
||||
|
||||
// 8. 门禁阻断检测 (Pre-commit 拦截逻辑)
|
||||
// 8. 检查新版本提醒(非阻塞)
|
||||
select {
|
||||
case info := <-updateCh:
|
||||
if info != nil {
|
||||
ui.PrintUpdateBanner(info.CurrentVersion, info.LatestVersion, info.ReleaseURL)
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// 9. 门禁阻断检测 (Pre-commit 拦截逻辑)
|
||||
outText := reviewOutput.String()
|
||||
hasBlocker := strings.Contains(outText, "[BLOCKER]") || strings.Contains(outText, "[CRITICAL]")
|
||||
hasReject := strings.Contains(outText, "[REJECT]")
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/unclesam-ly/git-air/internal/ui"
|
||||
"github.com/unclesam-ly/git-air/internal/updater"
|
||||
)
|
||||
|
||||
var updateCmd = &cobra.Command{
|
||||
Use: "update",
|
||||
Aliases: []string{"upgrade"},
|
||||
Short: "一键升级 git-air 到最新版本",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("🔍 正在检查并拉取最新版本 (当前: %s)...\n", updater.CurrentVersion)
|
||||
|
||||
updateCmd := exec.Command("go", "install", "github.com/unclesam-ly/git-air@latest")
|
||||
updateCmd.Stdout = os.Stdout
|
||||
updateCmd.Stderr = os.Stderr
|
||||
|
||||
if err := updateCmd.Run(); err != nil {
|
||||
ui.PrintError("升级失败: %v (提示: 请确保已安装 Go 并配置环境变量)", err)
|
||||
return
|
||||
}
|
||||
|
||||
ui.PrintSuccess("git-air 已成功升级至最新版本!🎉")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(updateCmd)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/unclesam-ly/git-air/internal/ui"
|
||||
"github.com/unclesam-ly/git-air/internal/updater"
|
||||
)
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "查看当前 git-air 版本信息及更新检测",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("git-air %s (%s/%s, Go %s)\n",
|
||||
updater.CurrentVersion,
|
||||
runtime.GOOS,
|
||||
runtime.GOARCH,
|
||||
runtime.Version(),
|
||||
)
|
||||
|
||||
checkFlag, _ := cmd.Flags().GetBool("check")
|
||||
if checkFlag {
|
||||
info := updater.CheckUpdate(true)
|
||||
if info != nil {
|
||||
ui.PrintUpdateBanner(info.CurrentVersion, info.LatestVersion, info.ReleaseURL)
|
||||
} else {
|
||||
ui.PrintSuccess("当前已是最新版本!")
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
versionCmd.Flags().BoolP("check", "c", true, "向 GitHub 检查是否有可用新版本")
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
@@ -52,6 +52,19 @@ func PrintUsage(inputTokens, outputTokens int, costUSD float64) {
|
||||
)
|
||||
}
|
||||
|
||||
// PrintUpdateBanner 打印新版本升级提醒卡片
|
||||
func PrintUpdateBanner(currentVer, latestVer, releaseURL string) {
|
||||
fmt.Println()
|
||||
fmt.Println(colorYellow + "┌─────────────────────────────────────────────────────────────┐" + colorReset)
|
||||
fmt.Printf(colorYellow+"│"+colorReset+" 🚀 %sgit-air 新版本发布: %s%s%s → %s%s%s\n",
|
||||
colorBold, colorGray, currentVer, colorReset,
|
||||
colorGreen, latestVer, colorReset,
|
||||
)
|
||||
fmt.Println(colorYellow + "│" + colorReset + " 运行以下命令一键升级: " + colorYellow + "│" + colorReset)
|
||||
fmt.Printf(colorYellow+"│"+colorReset+" %sgo install github.com/unclesam-ly/git-air@latest%s "+colorYellow+"│"+colorReset+"\n", colorCyan, colorReset)
|
||||
fmt.Println(colorYellow + "└─────────────────────────────────────────────────────────────┘" + colorReset)
|
||||
}
|
||||
|
||||
// PrintError 打印错误信息
|
||||
func PrintError(format string, a ...interface{}) {
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CurrentVersion 当前编译版本号
|
||||
const CurrentVersion = "v1.0.4"
|
||||
|
||||
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 || resp.StatusCode != http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsNewerVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
latest string
|
||||
current string
|
||||
expected bool
|
||||
}{
|
||||
{"v1.0.5", "v1.0.4", true},
|
||||
{"v1.1.0", "v1.0.4", true},
|
||||
{"v2.0.0", "v1.9.9", true},
|
||||
{"v1.0.4", "v1.0.4", false},
|
||||
{"v1.0.3", "v1.0.4", false},
|
||||
{"v1.0.4.1", "v1.0.4", true},
|
||||
{"1.0.5", "1.0.4", true},
|
||||
{"", "v1.0.4", false},
|
||||
{"v1.0.4", "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := IsNewerVersion(tt.latest, tt.current)
|
||||
if got != tt.expected {
|
||||
t.Errorf("IsNewerVersion(%q, %q) = %v, expected %v", tt.latest, tt.current, got, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user