本仓库: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 # 可一次推多个标签
五、安全提示
- 令牌不要写进仓库文件(本文件中一律用
<你的令牌>占位)。令牌一旦提交到仓库,等同泄露,应立即到 Gitee「设置 → 私人令牌」吊销重建。 - 令牌写在 remote URL 里会明文保存在
.git/config中,仅适合个人电脑;多人共用环境建议用方式 2(单次指定)或 Git 凭据管理器。 - 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()