# 极递 GitGo
**Repository Path**: deepin20/GitGo
## Basic Information
- **Project Name**: 极递 GitGo
- **Description**: 图形化 Git 推送工具
- **Primary Language**: Python
- **License**: MIT
- **Default Branch**: dev
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 1
- **Created**: 2026-06-21
- **Last Updated**: 2026-10-11
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 极递 GitGo
**基于 Python + Tkinter 的图形化 Git 客户端**,为 deepin / 统信 UOS 桌面而做。
把「多个仓库各自同步」「推上去之前先检查」「给这次改动发个发行版」这三件日常事,
变成点几下就能完成——不用记住 `rebase`、`--set-upstream`、`force-with-lease` 这些参数。
| | |
| --- | --- |
| **当前版本** | `2.3.1`(版本号唯一来源:`src/utils/constants.py` 的 `APP_VERSION`) |
| **运行环境** | Python ≥ 3.7、Tk(`python3-tk`)、`git` |
| **平台** | Linux 为主(deepin / UOS 深度适配);Windows / macOS 有构建脚手架,未在对应平台实测 |
| **主页** | |
| **许可** | MIT |
| **测试** | 992 项全绿(`unittest`,不需图形环境) |
---
## 目录
- [能做什么](#能做什么)
- [快速开始](#快速开始)
- [打包与安装](#打包与安装)
- [自动同步的语义](#自动同步的语义)
- [配置文件](#配置文件)
- [凭证与安全](#凭证与安全)
- [界面适配与线程模型](#界面适配与线程模型)
- [项目结构](#项目结构)
- [常见问题](#常见问题)
- [开发与测试](#开发与测试)
- [贡献与发版](#贡献与发版)
- [许可证](#许可证)
---
## 能做什么
### 仓库管理
| 功能 | 说明 |
| --- | --- |
| **多仓库** | 每个仓库独立配置、独立同步策略;下拉框按分组排序,可加 `[组]` 前缀 |
| **添加仓库** | 本地目录**手动选**,远程仓库**从 Gitee 列表选**(自动拉取账号可见的仓库,含协作与组织仓库) |
| **新建仓库** | 一次做完「本地目录(不存在则创建并 `git init`)+ Gitee 远程(可新建到组织,也可跳过)+ 首次同步」 |
| **克隆仓库** | 地址框接受**整条 `git clone ...` 命令**(从 Gitee 页面复制的通常就是这种);自动识别 HTTPS / SSH / 本地路径,流式显示进度 |
| **自动跟随改名 / 移动** | 配置里存着 `.git` 的身份指纹(`st_dev:st_ino`),目录在外被改名或搬走也能认出它搬到哪儿 |
| **批量操作** | 一键对所有登记仓库拉取或推送;无关历史不替你决定,标成「需要单独处理」后跳过 |
### 自动同步
| 功能 | 说明 |
| --- | --- |
| **方向可选** | 双向同步 / 仅本地 → 远程 / 仅远程 → 本地,三者语义见[下文](#自动同步的语义) |
| **文件变更自动提交推送** | `add → commit → pull --rebase → push`,带防抖(默认 5 秒) |
| **定时拉取** | 按分钟间隔自动 `pull --rebase` |
| **策略限流** | 时间段(支持跨零点)+ 分支白名单(`fnmatch` 通配)+ WiFi 白名单,任一不满足即整轮跳过 |
| **同步前钩子** | 推送前依次执行你的命令(格式化、测试…),任何一条失败即中止推送,本地提交保留 |
| **桌面通知** | 推送成功 / 失败 / 被钩子中止 / 同步冲突时发 `notify-send`,按仓库开关 |
### 提交安全
| 功能 | 说明 |
| --- | --- |
| **智能提交信息** | 按 porcelain 变更自动生成中文说明(新增/修改/删除/重命名分别计数,同目录聚合成 `目录/*`) |
| **变更预览(选择性提交)** | 逐文件勾选,只 `git add` 选中那几个;刷新时勾选状态按路径保留 |
| **大文件检测** | 超过阈值(默认 100MB,可改)提示改用 Git LFS |
| **敏感信息扫描** | 命中密钥 / 令牌 / 私钥块 / 连接串等格式时先弹确认。已跟踪改动扫 `git diff HEAD`,**未跟踪文件逐个读开头**——它们不出现在任何 diff 里,恰恰最容易顺手提交 |
| **冲突可视化解决** | 交给 Meld / KDiff3 / Beyond Compare / VS Code 等外部工具;工具退出后**自动复查**是否还留着 `<<<<<<<` 标记,再决定「继续 / 跳过 / 中止」 |
### 历史、分支与协作
- **提交历史**:按作者 / 关键词 / 日期区间 / 条数过滤;双击看完整 diff;可把某个提交**拣选(cherry-pick)到当前分支**。
- **整理提交历史**:压缩最近 N 个提交(`reset --soft` + 重新提交)、改写最新一条信息(`commit --amend`)——只做**无中间态**的安全子集,完整 `rebase -i` 不搬进 GUI。
- **分支管理 / 分支对比**:切换时保证本地与远程同属一个分支;任选两个 ref 比较文件差异与 ± 行数,双击看单文件 diff。
- **标签 / Stash / 贡献统计**。
- **子模块管理**:列出(状态标记翻成中文)/ 添加 / 更新(含 `--init`、`--recursive`)/ 移除。
- **Issue / PR**:从远程地址识别 GitHub / GitLab / Gitee(含自建实例与 GitLab 嵌套分组),在 GitGo 内查看与新建 Issue、PR / MR。
- **.gitignore 图形化编辑**:内置 11 套模板(Python / Node / Java / C / Go / Rust / IDE / Linux / macOS / Windows / 通用),逐条增删,右侧**实时预览**会忽略哪些文件。
- **仓库健康度仪表盘**:0–100 分 + 问题清单 + 关键指标;分数低时清单里**一定**有对应原因。
- **插件系统**:`~/.config/gitgo/plugins/*.py`,定义 `on_before_commit` / `on_after_push` / `on_before_pull` 即可参与流程(单插件出错只停用它自己)。
- **发行版发布**:自动查找最新 deb,在 Gitee 一键建 Release 并上传附件。
- **检查更新**:查 Gitee 最新发行版并比较版本,**只给下载地址**,绝不自动替换程序文件。
---
## 快速开始
```bash
git clone https://gitee.com/deepin20/GitGo.git
cd GitGo
pip install -r requirements.txt # 唯一第三方依赖:watchdog(缺了只是「自动同步」不可用)
python run.py
```
### 首次启动:配好提交身份
程序会检测全局 `user.name` / `user.email`,缺失时弹窗引导并执行 `git config --global`。
**跳过输入也可以**——GitGo 不会替你填 `GitGo User` 之类的占位身份。错误的作者信息一旦写进提交历史就难以批量修正,所以此时它会明确警告并**跳过自动同步**;补好身份前请手动执行:
```bash
git config --global user.name "你的姓名"
git config --global user.email "你的邮箱"
```
---
## 打包与安装
### 1. deb(推荐,统信商店规范)
```bash
sudo apt install rsync dpkg-dev # 打包工具
bash build_deb.sh # 产物:build-deb/com.gitee.deepin20.gitgo_<版本>_<架构>.deb
sudo dpkg -i build-deb/com.gitee.deepin20.gitgo_*.deb
sudo apt install -f # 依赖缺失时补齐
```
包布局遵循商店规范:文件全部落在 `/opt/apps//`,根下只有 `entries/`(desktop 与图标,安装时由系统软链到 `/usr/share/applications` 等目录)、`files/`(应用本体,命令行入口 `files/bin/`)与 `info`(JSON 元数据)三样。
几个关键取舍:
- **版本号与 appid 的唯一来源**都是 `src/utils/constants.py`(`APP_VERSION` / `APP_ID`),脚本用 sed 解析,不写死。
- **零维护者脚本**:商店禁止含「改系统」钩子的包;`entries/` → 系统目录的软链由 `deepin-home-appstore-daemon` 的 dpkg 触发器完成。
- **不建虚拟环境**:UOS 默认不带 `python3-venv`,靠它反而把「装不上」变成「跑不起来」。硬依赖只有 `python3 (>= 3.7)`、`python3-tk`、`git`;`watchdog` 与 `meld` 都降为 `Recommends`。
- **Deepin 25「磐石」**:`/usr` 只读、`/opt` 可写,所以载荷只放 `/opt`(写 `/usr/share` 会 EROFS——装成功却没图标)。
- `info` 的 version 是四段纯数字(`2.3.1` → `2.3.1.0`),与 deb 的 `Version` 是两回事;desktop 的 `Icon` 必须等于 appid,不允许绝对路径。
- 与旧包(装到 `/opt/GitGo`)互斥:control 里声明了 `Conflicts/Replaces/Breaks/Provides: gitgo`,装新包会自动卸旧包。
- 架构不写死(`dpkg --print-architecture`),UOS 的 arm64 / loongarch64 只差这一行。
- 打包前把 `TMPDIR` 指到构建目录内(系统 `/tmp` 常是 10MB tmpfs,否则 `lzma write error`)。
规范判定(版本号规范化、`info`/`desktop`/`control` 生成、`md5sums`)是 `src/utils/packaging.py` 里的纯函数,由 `tests/test_packaging.py` 兜底。
### 2. 直接运行(不打包)
```bash
pip install -r requirements.txt
bash run.sh # 自动查找可用的 Python 解释器后启动
```
### 3. 其他平台 / 分发方式(`packaging/`)
| 目录 | 产物 | 前置 |
| --- | --- | --- |
| `packaging/appimage/build_appimage.sh` | 单个 `.AppImage` | linuxdeploy + PyInstaller 产物,免安装、双击即用;在 Linux 上执行 |
| `packaging/windows/` | `.exe` + Inno Setup 安装包 | `build_windows.bat` + `gitgo.spec` + `gitgo.iss`;在 Windows 上执行 |
| `packaging/macos/` | `.dmg`(arm64 / x86_64 各一份) | `build_macos.sh` + `gitgo.spec`;在 macOS 上执行 |
| `packaging/README-ppa.md` | apt 源说明 | 自建 flat apt 源的做法(含签名与 `sources.list.d` 片段) |
> **注意**:Windows / macOS / AppImage 三个脚本只在各自目标平台验证过流程,**未在 CI 上实跑**(本仓库的开发与验证环境是 Linux)。当作起点读,别当"已验证的发布流水线";各目录的 README 写清了取舍与已知限制。
### 4. 可选:PyInstaller
```bash
pip install pyinstaller
pyinstaller --onefile --windowed --add-data "src/resources:resources" run.py -n GitGo
```
### 为什么不能 `pip install .`
GitGo **不作为 Python 库分发**。`setup.py` 只保留元数据(版本、描述、作者),并在安装命令上显式拒绝。
原因是源码里的导入写作 `from core...` / `from gui...` / `from utils...` 这种**顶层**形式(`run.py` 把 `src` 加进 `sys.path`)。真按库安装,就会把 `core` / `gui` / `utils` / `app` 这四个**通用名字**打进 `site-packages`——`core` 与 `utils` 是别的发行版也在用的名字,轻则互相覆盖文件,重则把别的工具的导入劫持到 GitGo 的模块上。
请用 deb 包或「直接运行」两种方式,它们都只读 `src/`,不向系统 Python 环境写入任何东西。
---
## 自动同步的语义
### 两个开关 + 一个方向
| 开关 | 作用 |
| --- | --- |
| `auto_sync` | 文件变更后自动提交并推送。**只有勾选时才动作**——控制的是对远程有副作用的提交推送 |
| `auto_pull` | 定时自动拉取远程更新 |
两者互相独立,可以只开一个:只开 `auto_sync` 则文件一变就「提交 → 先拉后推」但不定时拉取;只开 `auto_pull` 则只做定时拉取(文件监控不启动,改文件不会被自动提交);都开时两条链路共用一把操作锁,不会有两个 git 进程同时改索引。
`sync_mode` 是凌驾于两个开关之上的**方向约束**,实际动作取交集:
| 同步方式 | 文件变更自动提交推送 | 定时拉取 |
| --- | --- | --- |
| 双向同步(默认) | 按开关,且推送前先 `pull --rebase` | 按开关 |
| 仅本地 → 远程 | 按开关,**提交后不再拉取** | 禁用 |
| 仅远程 → 本地 | 禁用 | 按开关 |
被模式压制的那一项会在界面上**禁用并取消勾选**——但这只是**显示**上的联动:你那一下勾选会被记住(配置存的是你的意愿,实际会不会做由「模式 ∩ 意愿」现算),切回允许它的模式时自动恢复,不必重新勾一次。
「仅本地 → 远程」下若远程有新提交,推送会因非快进被拒绝——这正是该模式的定义(不把远程改动合到本地),日志会如实说明,**不会**悄悄改成拉取。需要在一条线上请改用「双向同步」。
> 同步方式只约束**自动**行为;手动点「提交并推送」始终按你的意图执行。
### 策略限流(时间段 / 分支 / WiFi)
任一条件不满足,本轮自动同步与自动拉取**整轮跳过**(不提交也不推送)。时间支持跨零点窗口,分支支持 `fnmatch` 通配。
**WiFi 只在配了白名单时才去探测**——`nmcli` 是真实进程调用(最长 5 秒),不该为不存在的约束每次去问网络。
被策略挡下的改动有个坑:之后不再有文件事件,就永远躺着。定时拉取是现成的周期性心跳,程序借它在窗口打开时**补做**一次。
### 冲突与「脏工作区」的区别
自动路径(没有人在旁边)撞上冲突时,程序立刻 `git rebase --abort` 把仓库还原到冲突之前——**工作区是干净的,既不在中间态也没有待提交内容**,本地提交和未提交改动都还在。随后**暂停**对应开关并同步取消界面勾选,避免"看着开着其实没在跑"。
这与「工作区有未提交的改动,本轮自动拉取已跳过」**不是一回事**:后者只是 `rebase` 要求已跟踪文件干净,而防抖窗口内本来就常处于「刚改完还没提交」——跳过本轮即可,不误报冲突、也不会关掉自动拉取。未跟踪的新文件不影响变基。
手动路径(提交面板、历史拣选)撞上冲突则弹「冲突解决」窗口,由你决定继续 / 跳过 / 中止。
---
## 配置文件
`~/.config/gitgo/config.json`,每个仓库独立存储:
```json
{
"repositories": {
"/path/to/repo": {
"alias": "myproject",
"remote_url": "https://gitee.com/user/repo.git",
"last_commit_msg": "更新代码",
"last_branch": "main",
"auto_sync": true,
"auto_pull": true,
"sync_mode": "both",
"pull_interval": 5,
"debounce_delay": 5,
"ignore_temp_files": true,
"fingerprint": "2065:1234567",
"merge_tool": "auto",
"group": "工作",
"pre_sync_hooks": "# 推送前依次执行\npytest -q",
"sync_time_start": "09:00",
"sync_time_end": "18:00",
"sync_branches": "main,release/*",
"sync_wifi": "Office-WiFi",
"large_file_mb": 100,
"secret_scan": true,
"notify_enabled": true
}
},
"current_repo": "/path/to/repo"
}
```
| 字段 | 含义 |
| --- | --- |
| `auto_sync` | 见[上文](#自动同步的语义) |
| `auto_pull` | 定时自动拉取远程更新 |
| `sync_mode` | `both` / `push` / `pull`;认不出来的值一律当 `both` |
| `pull_interval` | 定时拉取间隔(分钟) |
| `debounce_delay` | 文件变更防抖延迟(秒),界面可调 |
| `ignore_temp_files` | 是否忽略 `.tmp` / `.log` / `.swp` 等临时文件 |
| `fingerprint` | 仓库身份指纹(`.git` 的 `st_dev:st_ino`)。**空串表示还没记下**,此时不做自动跟随 |
| `merge_tool` | 冲突解决用的外部工具;`auto` = 自动挑一个图形工具 |
| `group` | 仓库分组标签(自由文本),下拉框按组展示 |
| `pre_sync_hooks` | 推送前依次执行的命令,每行一条,`#` 为注释 |
| `sync_time_start` / `sync_time_end` | 允许自动同步的时间段(`HH:MM`,两端都空为不限,支持跨零点) |
| `sync_branches` | 分支白名单(逗号分隔,支持 `fnmatch`;空为不限) |
| `sync_wifi` | WiFi 白名单(逗号分隔 SSID;空为不限网络) |
| `large_file_mb` | 大文件阈值(MB) |
| `secret_scan` | 敏感信息扫描开关 |
| `notify_enabled` | 桌面通知开关 |
v2.3.0 新增字段在读取时按默认值补齐,**旧配置文件无需手工迁移**。
写入采用「临时文件 + 原子替换」(`os.replace`),进程中断不会留下半个 JSON;字段更新走 `update_repo_config()`,不经过「读 → 改 → 写」两次加锁,避免并发丢更新。
---
## 凭证与安全
| 文件 | 内容 |
| --- | --- |
| `~/.config/gitgo/credentials` | 用户名 + 私人令牌,**所有仓库共用一份**(格式与 git `store` 后端一致,权限 `600`) |
| `<仓库>/.git/config` | 每个仓库一条 `credential.helper = store --file=<上面的路径>`(`--local`,不改写全局 `~/.gitconfig`) |
| `<仓库>/.git/gitgo-credentials` | **旧版**位置。首次接管该仓库时会被合并进共享文件并删除 |
入口:工具栏「凭证」,或菜单「工具 → Gitee 账号凭证(只输一次)」。保存时会调用 Gitee 接口验证令牌,能区分「令牌无效」与「网络不通」。
**注意 `store` 后端是明文存储**:文件不会被提交进版本库,但任何能读到主目录的进程都能看到它。对安全要求高的场景请改用 SSH 或系统 keyring 凭证助手。
> 令牌**绝不**写进日志或提示文案:远程地址里若含 `user:pass@`,界面显示前会经 `redact_remote_url()` 打码成 `https://***@host/path`。
---
## 界面适配与线程模型
### 跟随统信原生应用的外观
`src/utils/theme.py` 在启动时读系统外观并统一应用:
- **字体**:候选以「思源黑体」(= 系统 Noto Sans CJK)开头。Tk **不做字体回退**,候选一旦缺字就是方框,所以含 CJK 的字体必须排在 `Noto Sans` 这类纯西文字体**前面**;等宽同理(日志里有中文)。
- **缩放**:按 `Xft.dpi` 算 `tk scaling`(dpi / 72)。缺了这步,125% 缩放机器上的界面会比原生应用小一圈。
- **深浅色**:读 `com.deepin.dde.appearance gtk-theme`(`deepin-dark` 即深色),两套调色板取自 DTK6。经典 Tk 控件靠 `option_add` 生效,因此主题必须在构建界面**之前**应用。
- **强调色**:DTK 的 `#0081FF`,只给真正的「主操作」(提交并推送、对话框里的创建/应用/验证)。工具栏按钮一律中性色——原生应用的工具按钮本来就不着色。
- **任务栏集成**:`Tk(className=APP_ID)` 决定 `WM_CLASS`,与桌面项的 `StartupWMClass` 对齐(都取 `constants.APP_ID`);少了这对齐,任务栏会把窗口当成另一个程序、图标显示成默认齿轮。
颜色 / 字体 / 缩放**只从** `utils/theme.py` 与 `constants.py` 取,界面文案只允许 BMP 字符(码点 ≤ U+FFFF)——Tk 8.6 遇 emoji 会抛 `TclError`,外部文本进控件前统一经 `helpers.to_bmp()`。
### 线程模型
Tkinter 只能在主线程操作。文件监控(watchdog 线程)、定时拉取(`threading.Timer`)与后台状态查询都在**工作线程**里跑 git 命令,结果一律经 `utils/threading_utils.MainThreadDispatcher`(队列 + 主线程轮询)派发回主线程,避免跨线程操作控件的偶发崩溃。
对话框里的耗时操作走 `run_dialog_task(widget, fn, on_done, poll_ms, guard)`:工作线程执行、结果回主线程、`guard` 用来丢弃陈旧结果(例:连续点两次「刷新」,先发的那次结果到达时已过时)。
---
## 项目结构
```
GitGo/
├── run.py # 开发入口:python run.py
├── run.sh / gitgo # 启动壳(自动查找可用解释器);商店包的入口在 files/bin/
├── build_deb.sh # 按统信商店规范构建 .deb
├── setup.py # 仅元数据;**不支持 pip 安装**,见上文
├── requirements.txt
├── packaging/ # AppImage / Windows / macOS / apt 源 构建脚手架
├── docs/changelog/ # 每个版本一份 CHANGELOG_v<版本>.md
├── src/
│ ├── app.py # 应用主类:装配 + 工作线程派发
│ ├── core/ # 核心逻辑(不依赖 GUI)
│ │ ├── git_utils.py # Git 命令封装(subprocess / 流式输出 / 中间态识别 / fetch 锁)
│ │ ├── config.py # 配置读写(原子写 + 加锁 + update_repo_config)
│ │ ├── credentials.py # 共享凭证:一次输入、所有仓库共用 + 旧凭证迁移
│ │ ├── repo_manager.py # 仓库状态查询(带缓存)+ 路径状态 / 改名 / inode 指纹跟随
│ │ ├── gitee.py # Gitee API:令牌校验、仓库列表、新建仓库、地址拼装
│ │ ├── remote_setup.py # 远程对接:配 origin + 首次同步的规划与执行
│ │ ├── repo_create.py # 新建仓库:路径判定 + 本地初始化 + 接远程
│ │ ├── auto_sync.py # 自动同步:watchdog + 定时拉取 + 同步方式门控
│ │ ├── sync_policy.py # 同步策略:时间段 / 分支白名单 / WiFi 白名单
│ │ ├── hooks.py # 同步前钩子:推送前依次执行用户命令,失败即中止
│ │ ├── batch_ops.py # 多仓库批量拉取 / 推送
│ │ ├── smart_commit.py # 智能提交信息生成
│ │ ├── precommit.py # 提交前检查:大文件 + 敏感信息扫描
│ │ ├── history.py # 提交历史:过滤参数拼装 + 输出解析 + 单提交详情
│ │ ├── history_tools.py # 整理提交历史:压缩最近 N 个 / 改写最新一条
│ │ ├── compare.py # 分支对比:numstat + name-status 合并、方向语义
│ │ ├── gitignore.py # .gitignore:模板 / 规则增删 / 忽略预览
│ │ ├── submodules.py # 子模块:添加 / 更新 / 移除
│ │ ├── merge_tools.py # 外部合并工具:探测、选择与调起
│ │ ├── forges.py # GitHub / GitLab / Gitee 识别 + Issue / PR(MR)
│ │ ├── release.py # Gitee 发行版发布(tag 对齐 + 附件上传)
│ │ ├── update_check.py # 检查更新:查最新发行版并比较版本
│ │ ├── health.py # 仓库健康度:分数 + 问题清单
│ │ ├── stats.py # 贡献统计(一次 git log --all --numstat 聚合)
│ │ ├── notify.py # 桌面通知(尽力而为)
│ │ └── plugins.py # 插件系统(隔离加载)
│ ├── gui/ # GUI 组件
│ │ ├── main_window.py # 主窗口(含自适应折行工具栏 FlowToolbar)
│ │ ├── repo_selector.py # 仓库下拉框 + 添加 / 管理
│ │ ├── branch_selector.py # 分支选择器(保证本地与远程同分支)
│ │ ├── sync_controls.py # 自动同步控制面板(模式 + 两个开关 + 参数)
│ │ ├── commit_panel.py # 提交面板
│ │ ├── log_viewer.py # 日志面板
│ │ ├── status_bar.py # 状态栏(远程连接 / 分支 / 领先落后)
│ │ └── dialogs/ # 23 个对话框
│ │ ├── branch_dialog.py / branch_compare_dialog.py
│ │ ├── tag_dialog.py / stash_dialog.py / submodule_dialog.py
│ │ ├── history_dialog.py / history_tools_dialog.py / diff_viewer_dialog.py
│ │ ├── commit_preview_dialog.py / stats_dialog.py
│ │ ├── gitignore_dialog.py / conflict_dialog.py
│ │ ├── remote_picker_dialog.py / create_repo_dialog.py / clone_repo_dialog.py
│ │ ├── repo_manager_dialog.py / repo_settings_dialog.py
│ │ ├── credential_dialog.py / release_dialog.py
│ │ ├── health_dashboard_dialog.py / batch_ops_dialog.py
│ │ ├── forge_dialog.py / unrelated_history_dialog.py
│ │ └── __init__.py
│ └── utils/
│ ├── constants.py # 常量 + 版本号 / appid 的唯一来源
│ ├── helpers.py # 纯函数(to_bmp、名称规整、字体解析、折行……)
│ ├── theme.py # 主题层:跟随系统的深浅色 / 字体 / 缩放
│ ├── packaging.py # 商店打包规范的纯函数(info / desktop / control)
│ └── threading_utils.py # 工作线程 → Tk 主线程的派发
└── tests/ # 34 个测试模块(unittest / pytest 均可运行)
```
---
## 常见问题
### Q: 「克隆仓库」「新建仓库」「添加仓库」有什么不同?
分界在起点,三者前置条件互不重叠:
| 入口 | 本地目录 | 远程仓库 | 适用场景 |
| --- | --- | --- | --- |
| **克隆仓库** | **不应该**已存在(存在且非空时 git 会拒绝) | 必须已存在 | 把远程仓库拉一份到本地 |
| **新建仓库** | 可以还不存在(自动创建并 `git init`) | 可以还不存在(可在 Gitee 新建,也可跳过) | 从零开一个新项目 |
| **添加仓库** | **必须**已存在 | 从 Gitee 列表里选 | 本地已有仓库,只是接上远程 |
克隆窗口的地址框接受**整条 `git clone ...` 命令**;目标目录已存在且非空时会在点「克隆」之前就说清楚,不会让你等几分钟再看 git 报错。
### Q: 首次同步提示「切换到分支 X 失败(本地若存在与远程同名的未跟踪文件,请先移走或改名)」?
这是**保护**,不是故障。你给的目录里已经有和远程同名的文件(例如远程有 `README.md`,本地那份还没 `git add` 过),直接拉下来会把本地那份原样覆盖、事后无法恢复。
GitGo 把这一步交给 `git checkout -b <分支> origin/<分支>` 而不是 `git reset --hard`,于是 git 自带的保护会在覆盖前中止,失败原因如实告诉你。
处理办法二选一:把本地那份先备份/改名后重试,或确认远程那份才是你要的、删掉本地同名文件后重试。本地独有的、路径不冲突的未跟踪文件不受影响。
### Q: 状态栏的「领先 / 落后」数字准吗?
数字来自真实的 `git fetch`。若网络不通、认证失败或超时,会在状态栏与日志中明确提示「无法连接远程,数字可能滞后」——此时显示的仍是在现有远程引用上算出的值,仅供参考。
### Q: 手动「提交并推送」撞上冲突怎么办?
弹「冲突解决」窗口,不再把你赶回终端。窗口里能看到当前处在哪个中间态(变基 / 合并 / 拣选 / 还原)与还有哪些文件冲突;用外部合并工具处理后,程序会**自动复查**是否还留着 `<<<<<<<` 标记(实测「工具没保存就退出」会让 git 把文件标成已解决,接着一次提交就把冲突标记提交进去了);然后点「继续」完成变基(拣选冲突多一个「跳过此提交」),或点「中止」回到操作前。
冲突没解决就关窗,程序不会继续推送,会明确告诉你仓库还停在中间态。
> 本机没装图形合并工具时,窗口里给出可照抄的安装命令(如 `sudo apt install meld`);只有终端类工具(`vimdiff` 等)时不会被自动选中——图形程序没有控制终端,拉起来只会得到一片空白。
### Q: 我把仓库目录在文件管理器里改了名,GitGo 还能认出来吗?
能——前提是这个仓库此前在 GitGo 里**添加、新建、克隆或刷新过**(那时会记下 `.git` 的身份指纹 `st_dev:st_ino`)。目录名变了但 inode 不变,程序启动时就能按指纹在旧路径附近认出它搬到哪儿,问过你之后把配置改到新路径。
三种情形处理方式不同:在 GitGo 里改目录名(管理所有仓库 → 重命名目录)一步到位;在外面改名/搬家则按指纹自动跟随;同一路径下换成另一个仓库则指纹对不上,什么都不做。
指纹必须在改名**之前**存下来,改完就无从知道了——所以每次刷新都会顺手补写一次。老配置里没有指纹的条目**不猜**,会如实报「路径不存在」,用「重新定位」手动指认。
### Q: 私人令牌存在哪里?
只有一份,在 `~/.config/gitgo/credentials`(权限 `600`,明文),所有仓库共用;每个仓库只保留一条指向它的 `credential.helper`。不会写入 `~/.git-credentials`,也不会改写全局 `~/.gitconfig`。详见[凭证与安全](#凭证与安全)。
### Q: 为什么工具栏按钮有时会换行?
窗口宽度不够时按钮**自动折到下一行**,而不是被截掉。旧实现把 11 个按钮全挤一行,窄窗口下后面的按钮会整排消失(「创建发行版」消失事故),现在是 `FlowToolbar` 自适应折行——**只换行,绝不截断**。
---
## 开发与测试
```bash
pip install -r requirements.txt
pip install pytest pyflakes # pyflakes 是**必需**依赖,见下
python -m pytest tests -q # 或:python -m unittest discover -s tests -t .
```
> `pyflakes` 不是可选项:`tests/test_imports.py::TestNoUndefinedNames` 靠它抓「函数体里引用了没导入的名字」(用户点按钮时才抛 `NameError`)。缺了它会**直接失败**并提示安装方式——以前是静默 skip,界面看着「绿」而守卫根本没跑,比没有守卫更容易让人放松警惕。
**测试不需要图形环境**(Tk 只被 import,不会被实例化)。覆盖范围包括:配置读写(并发「原子字段更新」不丢更新、条目按路径迁移)、Git 命令封装与推送 upstream 行为、首次同步的规划真值表与**同名未跟踪文件的保护**、凭证作用范围与共享凭证(用真实 `git credential fill` 验证落盘格式)、Gitee 仓库列表分页、新建仓库请求形状、同步方式的合成规则与真实仓库上的方向差异、自动同步的门控 / 脏工作区跳过 / 冲突后暂停与文案、同步策略与钩子、批量操作计划、智能提交信息生成、提交前的大文件与敏感信息扫描、提交历史过滤与解析、分支对比的双命令合并、`.gitignore` 模板与预览、合并工具探测、冲突态识别与继续/中止映射、子模块、健康度、多平台 forge 的 API 差异、插件加载隔离、仓库指纹与自动跟随,以及一组**整体冒烟守卫**:
- 模块导入 + 无未定义名字(pyflakes)
- 界面文案无非 BMP 字符
- 对话框按钮栏不被压扁(`pack` 先分配者先占)
- 新入口都接在真实方法上(菜单/工具栏名字 ↔ 类方法)
- 索引里没有孤立 gitlink
- 源码里没有写死的颜色
只有真正需要 `Observer` 的监控生命周期用例会因缺 watchdog 而跳过;开关、事件处理与冲突处理这类最容易出错的部分都不依赖 watchdog。
### 真实 X 会话渲染探针
布局类改动离线测不出来,另外用真实 X 会话跑探针(脚本在 `.workbuddy/tmp/`,不进仓库):
```bash
DISPLAY=:0 HOME=$HOME /usr/bin/python3 .workbuddy/tmp/probe_gui_survey.py # 主窗口全控件可见性(ismapped / 遮挡 / 越界)
DISPLAY=:0 HOME=$HOME /usr/bin/python3 .workbuddy/tmp/probe_async_gui.py # 对话框异步化后按钮是否恢复、日志是否回填
DISPLAY=:0 HOME=$HOME /usr/bin/python3 .workbuddy/tmp/probe_p2_fixes.py # 外部路径净化 / 轮询链续期 / fetch 开关
```
量尺寸前必须等窗口管理器(`update()` 一次只会量到「内容自然尺寸」);探针的 root 必须 `withdraw()`,否则它会叠在被测窗口上制造假遮挡。
> **注意**:GUI 探针要用 `/usr/bin/python3`,不要用虚拟环境里的解释器——managed python 的 Tk 解析不到 CJK 字体,量出来的宽度会失真(实测 886 ↔ 1188px)。
---
## 贡献与发版
欢迎提交 Issue 与 Pull Request。请保持 PEP 8 风格,并为改动补上单元测试。
每个版本的改动记录在 `docs/changelog/CHANGELOG_v<版本>.md`(自 v2.1.2 起,每次发版新增一份,文件名与 `constants.APP_VERSION` 对齐)。
**发版流程**:改 `src/utils/constants.py` 的 `APP_VERSION` → 补一份 CHANGELOG → `bash build_deb.sh` → 打 tag 并在 Gitee 创建发行版(也可用程序里的「创建发行版」一键完成)。
---
## 许可证
MIT License
Copyright (c) 2026 温暖断章
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.