Gitee 推送、打标签(附注备注)操作指南

本仓库:https://gitee.com/<你的用户名>/<你的仓库名>(分支 main)
本文说明如何用 Gitee 私人令牌完成「提交 → 推送 → 打附注标签(含变更备注)→ 推送标签」的完整流程。

一、本次 v1.4 发布实际执行的命令(完整记录)

# 1. 暂存本次变更(新文档 + 测试页)
git add mes-report-platform api-test.html docs/gitee-push-tag.md

# 2. 提交(标题 + 详细说明)
git commit -m "docs: 新增 MES 接口探测文档三件套与免登录接口测试页" -m "变更内容见标签 v1.4 备注"

# 3. 创建附注标签(-a 附注类型,-m 备注内容,可多个 -m 分段)
git tag -a v1.4 -m "MES 接口文档与免登录工具集 v1.4" -m "变更内容:..." 

# 4. 推送主分支
git push origin main

# 5. 推送标签(附注标签的备注会一并推到 Gitee)
git push origin v1.4

也可以一步推送分支+全部标签:git push origin main --follow-tags
(--follow-tags 只推送附注标签,普通轻量标签不会被推送。)

二、令牌(私人令牌)的三种使用方式

你的仓库远程地址已经内置了令牌(git remote -v 可见):

https://oauth2:<你的令牌>@gitee.com/<你的用户名>/<你的仓库名>.git

因此 git push / git pull 直接执行即可,无需再输入账号密码。其他两种写法:

方式 1:直接修改远程地址(一次配置,永久生效)

git remote set-url origin https://oauth2:<你的令牌>@gitee.com/<你的用户名>/<你的仓库名>.git

方式 2:单次推送时临时指定(不改配置,最安全)

git push https://oauth2:<你的令牌>@gitee.com/<你的用户名>/<你的仓库名>.git main
git push https://oauth2:<你的令牌>@gitee.com/<你的用户名>/<你的仓库名>.git v1.4

方式 3:Gitee 用户名 + 令牌作密码

git remote set-url origin https://<Gitee用户名>:<你的令牌>@gitee.com/<你的用户名>/<你的仓库名>.git

三种方式等价。oauth2:<令牌> 是 Gitee 官方推荐的令牌嵌入格式。

三、标签(Tag)常用命令速查

3.1 创建附注标签(推荐,带备注)

# 附注标签:存储为独立对象,包含备注/作者/日期,可推送到远程
git tag -a v1.4 -m "版本说明第一段" -m "变更内容第二段"

# 给历史某次提交补打标签
git tag -a v1.3-fix <commit-hash> -m "备注"

3.2 创建轻量标签(无备注,仅一个指针,一般不用)

git tag v1.4-lite

3.3 推送标签到 Gitee

git push origin v1.4        # 推送单个标签
git push origin --tags      # 推送所有本地标签
git push origin main --follow-tags   # 推送分支 + 附注标签

3.4 查看标签

git tag                     # 列出所有标签
git tag -l "v1.*"           # 按模式过滤
git show v1.4               # 查看标签的完整备注 + 对应提交
git tag -n                  # 每个标签显示一行备注

3.5 删除标签

git tag -d v1.4                     # 删除本地标签
git push origin --delete v1.4       # 删除远程标签
git push origin :refs/tags/v1.4     # 同上(旧写法)

3.6 修改已打标签的备注

标签不能"编辑",只能删了重打:

git tag -d v1.4
git tag -a v1.4 -m "新的备注内容"
git push origin --delete v1.4
git push origin v1.4


四、日常发布流程(标准操作,可复制)

# ① 查看改动
git status
git diff

# ② 暂存 + 提交
git add .
git commit -m "feat: 本次变更标题" -m "详细说明…"

# ③ 推送主分支
git push origin main

# ④ 需要发版时打附注标签并推送
git tag -a v1.5 -m "v1.5 变更内容:……"
git push origin v1.4 v1.5     # 可一次推多个标签


五、安全提示

  1. 令牌不要写进仓库文件(本文件中一律用 <你的令牌> 占位)。令牌一旦提交到仓库,等同泄露,应立即到 Gitee「设置 → 私人令牌」吊销重建。
  2. 令牌写在 remote URL 里会明文保存在 .git/config 中,仅适合个人电脑;多人共用环境建议用方式 2(单次指定)或 Git 凭据管理器。
  3. Gitee 私人令牌具有你账号的完整仓库读写权限,泄露后他人可直接推送/删除代码。

六、可视化发布脚本(Python)

配套的 git_release_tool.py 把上述流程做成了图形界面,仅依赖 Python 自带的 tkinter,无需安装第三方包:

python git_release_tool.py

功能一览:

  • 左右分栏:左侧按 ①查看暂存 → ②提交推送 → ③版本标签 → ④一键发布 分区操作,右侧深色控制台实时显示每条命令及输出
  • 每个操作都标注【本地】/【远程】徽章,并附一句话说明(做什么、信息填在哪个输入框)
  • 输入框下方均有填写指引:提交标题必填、标签名必填、说明和备注可选
  • 中文输出:「刷新状态」生成中文报告(待提交 / 未暂存 / 新文件分类 + 下一步指引);推送被拒、认证失败、标签已存在等常见英文信息自动附「→ 意思是:…」中文解释
  • 一键发布:暂存 → 提交 → 推送分支 → 打标签 → 推送标签,任一步失败自动中止并标红原因
  • 界面不会被裁掉:启动时按内容实际高度自动撑开窗口,一次看全所有步骤;屏幕不够高时左侧面板可用鼠标滚轮或滚动条上下查看(日志区独立滚动,互不干扰)
脚本通过参数列表直接调用 git,不经过 PowerShell 解析,天然规避中文提交信息的编码问题(见上文常见问题)。

完整源码:git_release_tool.py

复制保存为 git_release_tool.py,运行 python git_release_tool.py 即可使用:

#!/usr/bin/env python3
"""Git 发布助手 —— 可视化完成 提交 → 推送 → 打附注标签 → 推送标签 全流程。

仅依赖 Python 标准库(tkinter),无需安装第三方包。
用法:python git_release_tool.py
"""
import os
import queue
import subprocess
import threading
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, ttk

# ---------------- 配色与字体 ----------------
C_BG = "#f1f5f9"          # 窗口背景
C_CARD = "#ffffff"        # 卡片背景
C_TEXT = "#0f172a"        # 主文字
C_HINT = "#64748b"        # 说明文字
C_PRIMARY = "#2563eb"     # 主按钮(蓝)
C_PRIMARY_DARK = "#1d4ed8"
C_SUCCESS = "#16a34a"     # 提交按钮(绿)
C_SUCCESS_DARK = "#15803d"
C_HEADER = "#1e293b"      # 顶部栏(深蓝灰)
C_BORDER = "#e2e8f0"      # 卡片边框

FONT = ("Microsoft YaHei UI", 9)
FONT_BOLD = ("Microsoft YaHei UI", 10, "bold")
FONT_HINT = ("Microsoft YaHei UI", 8)
FONT_MONO = ("Consolas", 9)

# 操作范围徽章:本地=只改你电脑上的仓库;远程=会上传到 Gitee
BADGES = {
    "local": ("#e0e7ff", "#3730a3", "本地"),
    "remote": ("#dcfce7", "#15803d", "远程"),
    "both": ("#fef3c7", "#92400e", "本地+远程"),
}


def _decode(data):
    # 中文 Windows 上 git 输出多为 GBK,先按 UTF-8 再按 GBK 解码
    for enc in ("utf-8", "gbk"):
        try:
            return data.decode(enc)
        except UnicodeDecodeError:
            continue
    return data.decode("utf-8", errors="replace")


def _fmt(args):
    # 仅用于日志展示:含空格的参数加引号
    return " ".join(a if " " not in a else f'"{a}"' for a in args)


def run_git(repo, *args):
    """在 repo 目录执行 git 命令,返回 (是否成功, 合并后的输出)。"""
    # core.quotepath=false 让中文文件名正常显示而非 \xxx 转义
    # LC_ALL=C 强制 git 输出英文,保证下面的中文翻译规则稳定命中
    cmd = ["git", "-c", "core.quotepath=false", *args]
    env = {**os.environ, "LC_ALL": "C", "LANG": "C"}
    try:
        proc = subprocess.run(cmd, cwd=repo, capture_output=True, env=env)
    except OSError as exc:
        return False, f"无法执行 git:{exc}"
    parts = [_decode(proc.stdout).strip(), _decode(proc.stderr).strip()]
    return proc.returncode == 0, "\n".join(p for p in parts if p)


# ---------------- 中文翻译层 ----------------
# porcelain 状态码 → 中文(第 1 列=暂存区,第 2 列=工作区)
_XY_CN = {"M": "修改", "A": "新增", "D": "删除", "R": "重命名", "C": "复制", "T": "类型改变"}


def _explain_branch(text):
    """翻译 porcelain -b 的分支行,如 main...origin/main [ahead 1]"""
    if text.startswith("HEAD"):
        return "当前分支:(分离头指针,不在任何分支上)"
    ahead = behind = 0
    if "[" in text and text.endswith("]"):
        for item in text[text.index("[") + 1 : -1].split(","):
            item = item.strip()
            if item.startswith("ahead"):
                ahead = int(item.split()[1])
            elif item.startswith("behind"):
                behind = int(item.split()[1])
    name, _, upstream = text.partition("...")
    upstream = upstream.split(" ")[0]
    info = f"当前分支:{name}" + (f"(跟踪远程 {upstream})" if upstream else "(未设置远程跟踪分支)")
    if upstream:
        if ahead and behind:
            info += f":本地领先 {ahead} 个、落后 {behind} 个提交(先 git pull 再推送)"
        elif ahead:
            info += f":本地领先远程 {ahead} 个提交,点「推送分支」上传"
        elif behind:
            info += f":本地落后远程 {behind} 个提交,需先 git pull 合并"
        else:
            info += ":本地与远程一致"
    return info


def explain_status(output):
    """把 git status --porcelain -b 的输出翻译成中文报告(返回行列表)。"""
    branch_info = ""
    entries = []
    for line in output.splitlines():
        if line.startswith("## "):
            branch_info = _explain_branch(line[3:])
        elif line.strip():
            entries.append(line)

    staged, unstaged, untracked, conflicts = [], [], [], []
    for line in entries:
        code, path = line[:2], line[3:]
        if "U" in code or code in ("AA", "DD"):
            conflicts.append(path)
        elif code == "??":
            untracked.append(path)
        else:
            if code[0] != " ":
                staged.append(f"[{_XY_CN.get(code[0], code[0])}] {path}")
            if code[1] != " ":
                unstaged.append(f"[{_XY_CN.get(code[1], code[1])}] {path}")

    lines = []
    if branch_info:
        lines.append(branch_info)
    if staged:
        lines.append(f"【待提交】已暂存 {len(staged)} 个文件 —— 点「提交」会保存这些改动:")
        lines += ["    " + s for s in staged]
    if unstaged:
        lines.append(f"【未暂存】已修改 {len(unstaged)} 个文件 —— 需要先点「暂存全部」:")
        lines += ["    " + s for s in unstaged]
    if untracked:
        lines.append(f"【新文件】未跟踪 {len(untracked)} 个文件 —— Git 还不认识它们,暂存后才纳入管理:")
        lines += ["    " + s for s in untracked]
    if conflicts:
        lines.append(f"【冲突】{len(conflicts)} 个文件有合并冲突,需手动解决后才能提交:")
        lines += ["    " + s for s in conflicts]
    if not (staged or unstaged or untracked or conflicts):
        lines.append("工作区干净:没有任何改动需要提交。")

    if untracked or unstaged:
        lines += ["", "下一步:点「暂存全部」→ 在 ② 填写提交标题 → 点「提交」→「推送分支」。"]
    elif staged:
        lines += ["", "下一步:在 ② 填写提交标题 → 点「提交」→「推送分支」。"]
    return lines


# 常见英文输出 → 中文解释(子串匹配,命中就追加一行「→ 意思是:…」)
EXPLAIN_RULES = [
    (("Everything up-to-date",), "远程已是最新,没有新提交需要推送", "info"),
    (("[rejected]", "non-fast-forward", "fetch first"), "推送被拒绝:远程有你本地没有的提交。先执行 git pull --rebase origin 分支名,合并后再推送", "err"),
    (("Authentication failed", "could not read Username", "invalid username or password", "Access denied"), "认证失败:检查私人令牌配置是否正确(见上文《令牌认证配置方式》)", "err"),
    (("Could not resolve host", "unable to access", "Failed to connect", "timed out"), "网络连接失败:无法访问 Gitee,请检查网络后重试", "err"),
    (("Repository not found",), "仓库不存在,或令牌没有该仓库的权限", "err"),
    (("not a git repository",), "这个目录不是 Git 仓库:请重新选择项目根目录(里面有 .git 文件夹)", "err"),
    (("nothing to commit, working tree clean",), "没有可提交的改动:工作区是干净的", "info"),
    (("nothing added to commit",), "有改动或新文件但还没暂存:先点「暂存全部」再提交", "info"),
    (("files changed", "file changed"), "提交成功:改动已存档到本地仓库", "ok"),
    (("already exists",), "标签已存在:换一个标签名;或先删除旧标签(git tag -d 标签名)再重打", "err"),
    (("[new tag]",), "远程新建标签成功:Gitee 的「标签 / 发行版」页面已可见", "ok"),
    (("[new branch]",), "远程新建分支成功", "ok"),
    (("error: failed to push some refs",), "推送失败:具体原因见上方红色信息", "err"),
    (("Your branch is ahead of",), "本地领先远程:点「推送分支」把提交上传到 Gitee", "info"),
]


def explain(output):
    """扫描 git 输出,返回命中的中文解释 [(文本, 颜色标签)]。"""
    return [("→ 意思是:" + note, tag) for keys, note, tag in EXPLAIN_RULES
            if any(k in output for k in keys)]


def next_step_hint(args, ok):
    """命令成功后的下一步引导。"""
    if not ok or not args:
        return None
    cmd = args[0]
    if cmd == "add":
        return ("→ 下一步:在 ② 填写提交标题,点「提交」", "info")
    if cmd == "commit":
        return ("→ 下一步:点「推送分支」上传到 Gitee;发版的话继续在 ③ 打标签", "info")
    if cmd == "tag" and "-a" in args:
        return ("→ 下一步:点「推送标签」上传到 Gitee(标签不会跟着推分支自动上传!)", "info")
    return None


class ReleaseTool(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Git 发布助手")
        # 宽度尽量大(描述少换行、内容更矮),高度随后按内容实际高度自适应
        self._win_w = min(1240, self.winfo_screenwidth() - 40)
        self.geometry(f"{self._win_w}x{min(860, self.winfo_screenheight() - 40)}")
        self.minsize(980, 560)
        self.configure(bg=C_BG)
        self._buttons = []
        self._logq = queue.Queue()
        self._setup_styles()
        self._build_header()
        self._build_body()
        self._fit_window()
        self.after(100, self._poll_log)
        self._log("使用顺序:确认仓库路径 → ① 刷新状态 → ② 填提交标题、提交并推送 → ③ 填标签名、打标签并推送;或直接「一键发布」。", "info")
        self._log("【本地】= 只改你电脑上的仓库;【远程】= 会上传到 Gitee。推送前请先用私人令牌配置好远程地址。", "info")
        self._log("英文看不懂没关系:「刷新状态」输出中文报告,其他命令的常见结果会附「→ 意思是:…」中文解释。", "info")

    # ---------------- 样式 ----------------
    def _setup_styles(self):
        style = ttk.Style(self)
        style.theme_use("clam")
        style.configure(".", font=FONT, background=C_CARD, foreground=C_TEXT)
        style.configure("Root.TFrame", background=C_BG)
        style.configure("TFrame", background=C_CARD)
        style.configure("TLabelframe", background=C_CARD, bordercolor=C_BORDER, relief="solid", borderwidth=1)
        style.configure("TLabelframe.Label", background=C_BG, foreground=C_TEXT, font=FONT_BOLD)
        style.configure("TLabel", background=C_CARD, foreground=C_TEXT, font=FONT)
        style.configure("Hint.TLabel", background=C_CARD, foreground=C_HINT, font=FONT_HINT)
        style.configure("TEntry", fieldbackground="#ffffff", bordercolor="#cbd5e1", lightcolor="#cbd5e1", darkcolor="#cbd5e1", padding=4)
        style.configure("TButton", font=FONT, padding=(10, 3), background="#ffffff", foreground=C_TEXT, bordercolor="#cbd5e1")
        style.map("TButton", background=[("active", "#e2e8f0"), ("disabled", "#f8fafc")], foreground=[("disabled", "#94a3b8")])
        style.configure("Primary.TButton", background=C_PRIMARY, foreground="#ffffff", bordercolor=C_PRIMARY_DARK, padding=(14, 6), font=FONT_BOLD)
        style.map("Primary.TButton", background=[("active", C_PRIMARY_DARK), ("disabled", "#bfdbfe")], foreground=[("disabled", "#ffffff")])
        style.configure("Success.TButton", background=C_SUCCESS, foreground="#ffffff", bordercolor=C_SUCCESS_DARK, padding=(10, 3))
        style.map("Success.TButton", background=[("active", C_SUCCESS_DARK), ("disabled", "#bbf7d0")], foreground=[("disabled", "#ffffff")])

    # ---------------- 界面 ----------------
    def _build_header(self):
        header = tk.Frame(self, bg=C_HEADER)
        header.pack(fill="x")
        self._header = header
        tk.Label(header, text="Git 发布助手", bg=C_HEADER, fg="#ffffff",
                 font=("Microsoft YaHei UI", 13, "bold")).pack(side="left", padx=(16, 14), pady=7)
        tk.Label(header, text="提交 → 推送 → 打标签 → 推送标签    |    【本地】只影响你的电脑    【远程】会上传到 Gitee",
                 bg=C_HEADER, fg="#94a3b8", font=FONT_HINT).pack(side="left", pady=7)

    def _build_body(self):
        body = ttk.Frame(self, style="Root.TFrame")
        body.pack(fill="both", expand=True, padx=10, pady=8)
        left = self._build_scroll_panel(body)
        right = ttk.Frame(body, style="Root.TFrame")
        right.pack(side="left", fill="both", expand=True, padx=(10, 0))
        self._build_repo_card(left)
        self._build_stage_card(left)
        self._build_commit_card(left)
        self._build_tag_card(left)
        self._build_release_card(left)
        self._build_log_card(right)

    def _build_scroll_panel(self, parent):
        """左侧可滚动面板:内容比窗口高时,用鼠标滚轮或滚动条查看全部步骤。"""
        holder = ttk.Frame(parent, style="Root.TFrame")
        holder.pack(side="left", fill="y")
        self._canvas = tk.Canvas(holder, bg=C_BG, highlightthickness=0, bd=0,
                                 width=640, yscrollincrement=20)
        vsb = ttk.Scrollbar(holder, orient="vertical", command=self._canvas.yview)
        self._canvas.configure(yscrollcommand=vsb.set)
        self._canvas.pack(side="left", fill="both", expand=True)
        vsb.pack(side="right", fill="y")
        inner = ttk.Frame(self._canvas, style="Root.TFrame")
        item = self._canvas.create_window((0, 0), window=inner, anchor="nw")
        inner.bind("<Configure>",
                   lambda e: self._canvas.configure(scrollregion=self._canvas.bbox("all")))
        self._canvas.bind("<Configure>",
                          lambda e: self._canvas.itemconfigure(item, width=e.width))
        self.bind_all("<MouseWheel>", self._on_wheel)
        return inner

    def _on_wheel(self, event):
        # 日志区(Text 控件)自带滚动,其余区域的滚轮都用来滚动左侧面板
        if isinstance(event.widget, tk.Text):
            return
        units = 3 * max(1, abs(event.delta) // 120)
        self._canvas.yview_scroll(-units if event.delta > 0 else units, "units")

    def _fit_window(self):
        """按内容实际高度调整窗口并居中放置:能放下就全部展示,放不下才需要滚动。"""
        self.update_idletasks()
        region = self._canvas.bbox("all")
        if not region:
            return
        content_h = region[3] - region[1]
        need = content_h + self._header.winfo_reqheight() + 16 + 40   # 40 ≈ 标题栏与边框
        h = max(560, min(need, self.winfo_screenheight() - 40))
        # 必须显式居中:只给宽高时窗口可能被放到偏低的位置,底部内容会跑到屏幕外
        x = max(0, (self.winfo_screenwidth() - self._win_w) // 2)
        y = max(0, (self.winfo_screenheight() - h) // 2)
        self.geometry(f"{self._win_w}x{h}+{x}+{y}")

    # ---- 通用小部件 ----
    def _card(self, parent, title):
        frame = ttk.LabelFrame(parent, text=title, padding=(8, 4))
        frame.pack(fill="x", pady=(0, 5))
        return frame

    def _cell(self, parent, text, width=72):
        # 固定宽度的标签单元格,保证各行输入框左对齐
        box = ttk.Frame(parent, width=width)
        box.pack(side="left")
        box.pack_propagate(False)
        ttk.Label(box, text=text).pack(anchor="w")

    def _badge(self, parent, scope):
        bg, fg, text = BADGES[scope]
        return tk.Label(parent, text=text, bg=bg, fg=fg,
                        font=("Microsoft YaHei UI", 8, "bold"), padx=6, pady=1)

    def _button(self, parent, text, command, style=None):
        kw = {"style": style} if style else {}
        btn = ttk.Button(parent, text=text, command=command, **kw)
        self._buttons.append(btn)
        return btn

    def _hint(self, parent, text, indent=78):
        # 输入框下方的一行填写指引
        ttk.Label(parent, text=text, style="Hint.TLabel", wraplength=530,
                  justify="left").pack(anchor="w", padx=(indent, 0))

    def _field(self, parent, label, var, hint):
        row = ttk.Frame(parent)
        row.pack(fill="x", pady=(3, 0))
        self._cell(row, label)
        ttk.Entry(row, textvariable=var).pack(side="left", fill="x", expand=True)
        self._hint(parent, "↑ " + hint)

    def _action(self, parent, text, command, scope, desc, style=None):
        row = ttk.Frame(parent)
        row.pack(fill="x", pady=1)
        btn = self._button(row, text, command, style)
        btn.configure(width=8)
        btn.pack(side="left")
        self._badge(row, scope).pack(side="left", padx=(8, 6))
        ttk.Label(row, text=desc, style="Hint.TLabel",
                  wraplength=480, justify="left").pack(side="left", fill="x", expand=True)

    # ---- 各功能区 ----
    def _build_repo_card(self, parent):
        card = self._card(parent, "仓库设置 —— 所有命令都在这个目录执行")
        self.repo_var = tk.StringVar(value=os.path.dirname(os.path.abspath(__file__)))
        row = ttk.Frame(card)
        row.pack(fill="x", pady=1)
        self._cell(row, "仓库路径:")
        ttk.Entry(row, textvariable=self.repo_var).pack(side="left", fill="x", expand=True)
        self._button(row, "浏览…", self.choose_repo).pack(side="left", padx=(6, 0))
        self._hint(card, "↑ 默认是脚本所在目录;换项目就点「浏览…」选根目录(含 .git)")

        self.branch_var = tk.StringVar(value="main")
        row = ttk.Frame(card)
        row.pack(fill="x", pady=(4, 1))
        self._cell(row, "分支名:")
        ttk.Entry(row, textvariable=self.branch_var, width=14).pack(side="left")
        ttk.Label(row, text="推送目标分支;不确定就点「刷新状态」看日志第一行",
                  style="Hint.TLabel", wraplength=410, justify="left").pack(side="left", padx=(8, 0))

    def _build_stage_card(self, parent):
        card = self._card(parent, "① 查看与暂存 —— 提交前先确认改了什么")
        self._action(card, "刷新状态", self.on_status, "local",
                     "git status:中文报告列出改动,不改任何东西")
        self._action(card, "暂存全部", self.on_add, "local",
                     "git add .:把改动放进待提交区(像加入购物车)")

    def _build_commit_card(self, parent):
        card = self._card(parent, "② 提交与推送 —— 提交信息填在下方输入框")
        self.title_var = tk.StringVar()
        self._field(card, "提交标题:", self.title_var,
                    "必填,如 feat: 新增导出功能 / fix: 修复登录超时")
        self.body_var = tk.StringVar()
        self._field(card, "提交说明:", self.body_var, "选填,会作为提交信息的第二段")
        self._action(card, "提交", self.on_commit, "local",
                     "git commit:存档到本地仓库,Gitee 上还看不到", "Success.TButton")
        self._action(card, "推送分支", self.on_push, "remote",
                     "git push origin main:上传到 Gitee,成功后网页可见")

    def _build_tag_card(self, parent):
        card = self._card(parent, "③ 版本标签 —— 标签信息填在下方输入框")
        self.tag_var = tk.StringVar()
        self._field(card, "标签名:", self.tag_var, "必填,如 v1.5;重名会失败,可先点「标签列表」查看")
        self.tag_msg_var = tk.StringVar()
        self._field(card, "标签备注:", self.tag_msg_var, "建议填:这版改了什么,会显示在 Gitee Releases")
        self._action(card, "打标签", self.on_tag, "local",
                     "git tag -a:带备注的版本快照,只在本地")
        self._action(card, "推送标签", self.on_push_tag, "remote",
                     "git push origin v1.5:标签必须单独推送,不跟分支走!")
        self._action(card, "标签列表", self.on_list_tags, "local",
                     "git tag -n:查看已有标签和备注")

    def _build_release_card(self, parent):
        card = self._card(parent, "④ 一键发布 —— 按顺序执行上面全部步骤")
        self._button(card, "一键发布:暂存 → 提交 → 推送分支 → 打标签 → 推送标签",
                     self.on_release, "Primary.TButton").pack(fill="x", pady=1)
        row = ttk.Frame(card)
        row.pack(fill="x", pady=(3, 0))
        self._badge(row, "both").pack(side="left")
        ttk.Label(row, text="需先填好提交标题和标签名;任一步失败自动中止,日志标红原因",
                  style="Hint.TLabel", wraplength=480, justify="left").pack(side="left", padx=(6, 0))

    def _build_log_card(self, parent):
        card = ttk.LabelFrame(parent, text="日志 —— 执行的每条命令和结果都显示在这里", padding=(6, 4))
        card.pack(fill="both", expand=True)
        self.log_text = scrolledtext.ScrolledText(
            card, height=10, state="disabled", font=FONT_MONO, wrap="word",
            bg="#0f172a", fg="#e2e8f0", insertbackground="#e2e8f0",
            relief="flat", padx=8, pady=8,
        )
        self.log_text.pack(fill="both", expand=True)
        self.log_text.tag_configure("cmd", foreground="#7dd3fc", font=("Consolas", 9, "bold"))
        self.log_text.tag_configure("err", foreground="#fca5a5")
        self.log_text.tag_configure("ok", foreground="#86efac")
        self.log_text.tag_configure("info", foreground="#94a3b8")
        self.log_text.tag_configure("step", foreground="#fbbf24", font=("Consolas", 9, "bold"))

    # ---------------- 工具方法 ----------------
    def choose_repo(self):
        path = filedialog.askdirectory(title="选择 Git 仓库目录(项目根目录,含 .git)")
        if path:
            self.repo_var.set(os.path.normpath(path))

    def _repo(self):
        repo = self.repo_var.get().strip()
        if not repo or not os.path.isdir(repo):
            messagebox.showwarning("提示", "请先选择有效的仓库路径")
            return ""
        return repo

    def _branch(self):
        return self.branch_var.get().strip() or "main"

    def _log(self, text, tag=""):
        # 工作线程只往队列写,UI 更新统一在主线程轮询执行
        self._logq.put((text, tag))

    def _poll_log(self):
        while True:
            try:
                item = self._logq.get_nowait()
            except queue.Empty:
                break
            if item is None:
                for btn in self._buttons:
                    btn.configure(state="normal")
            else:
                text, tag = item
                self.log_text.configure(state="normal")
                if tag:
                    self.log_text.insert("end", text + "\n", tag)
                else:
                    self.log_text.insert("end", text + "\n")
                self.log_text.configure(state="disabled")
                self.log_text.see("end")
        self.after(100, self._poll_log)

    def _exec(self, repo, *args):
        """执行一条 git 命令并写日志,返回是否成功(仅在工作线程中调用)。"""
        self._log("$ git " + _fmt(args), "cmd")
        ok, output = run_git(repo, *args)
        if ok and not output:
            output = "(命令执行成功,git 成功时通常不输出内容)"
        self._log(output, "ok" if ok else "err")
        for note, tag in explain(output):
            self._log(note, tag)
        hint = next_step_hint(args, ok)
        if hint:
            self._log(*hint)
        return ok

    def _run_async(self, job):
        for btn in self._buttons:
            btn.configure(state="disabled")

        def worker():
            try:
                job()
            except Exception as exc:
                self._log(f"执行出错:{exc}", "err")
            self._logq.put(None)

        threading.Thread(target=worker, daemon=True).start()

    # ---------------- 按钮动作 ----------------
    def on_status(self):
        repo = self._repo()
        if not repo:
            return

        def job():
            self._log("$ git status --porcelain -b(机器可读格式,下方已翻译成中文)", "cmd")
            ok, output = run_git(repo, "status", "--porcelain=v1", "-b")
            if ok:
                for line in explain_status(output):
                    self._log(line)
            else:
                self._log(output or "(无输出)", "err")
                for note, tag in explain(output):
                    self._log(note, tag)

        self._run_async(job)

    def on_add(self):
        repo = self._repo()
        if not repo:
            return

        def job():
            self._exec(repo, "add", ".")

        self._run_async(job)

    def on_commit(self):
        repo = self._repo()
        if not repo:
            return
        title = self.title_var.get().strip()
        if not title:
            messagebox.showwarning("提示", "请填写提交标题(在「② 提交与推送」的第一个输入框)")
            return
        args = ["commit", "-m", title]
        body = self.body_var.get().strip()
        if body:
            args += ["-m", body]

        def job():
            self._exec(repo, *args)

        self._run_async(job)

    def on_push(self):
        repo = self._repo()
        if not repo:
            return
        branch = self._branch()

        def job():
            self._exec(repo, "push", "origin", branch)

        self._run_async(job)

    def on_tag(self):
        repo = self._repo()
        if not repo:
            return
        name = self.tag_var.get().strip()
        if not name:
            messagebox.showwarning("提示", "请填写标签名(在「③ 版本标签」的第一个输入框),例如 v1.5")
            return
        msg = self.tag_msg_var.get().strip() or name

        def job():
            self._exec(repo, "tag", "-a", name, "-m", msg)

        self._run_async(job)

    def on_push_tag(self):
        repo = self._repo()
        if not repo:
            return
        name = self.tag_var.get().strip()
        if not name:
            messagebox.showwarning("提示", "请先在「③ 版本标签」填写标签名,再推送")
            return

        def job():
            self._exec(repo, "push", "origin", name)

        self._run_async(job)

    def on_list_tags(self):
        repo = self._repo()
        if not repo:
            return

        def job():
            self._exec(repo, "tag", "-n")

        self._run_async(job)

    def on_release(self):
        repo = self._repo()
        if not repo:
            return
        title = self.title_var.get().strip()
        if not title:
            messagebox.showwarning("提示", "一键发布需要填写提交标题(② 的第一个输入框)")
            return
        tag = self.tag_var.get().strip()
        if not tag:
            messagebox.showwarning("提示", "一键发布需要填写标签名(③ 的第一个输入框)")
            return
        branch = self._branch()
        body = self.body_var.get().strip()
        tag_msg = self.tag_msg_var.get().strip() or tag
        commit_args = ["commit", "-m", title] + (["-m", body] if body else [])
        steps = [
            ("暂存全部(本地)", ["add", "."]),
            ("提交(本地)", commit_args),
            ("推送分支(远程)", ["push", "origin", branch]),
            ("打附注标签(本地)", ["tag", "-a", tag, "-m", tag_msg]),
            ("推送标签(远程)", ["push", "origin", tag]),
        ]

        def job():
            self._log(f"========== 一键发布 {tag} 开始 ==========", "step")
            for step_name, args in steps:
                self._log(f"---- {step_name} ----", "step")
                if not self._exec(repo, *args):
                    self._log(f"已中止:{step_name} 失败,请检查上方红色日志处理后重试。", "err")
                    return
            self._log(f"========== {tag} 发布完成 ==========", "ok")

        self._run_async(job)


if __name__ == "__main__":
    ReleaseTool().mainloop()

返回首页