GitHub又打不开了。这个想法最近在开发群里出现的频率明显变高,于是“自己搭一套代码托管平台”重新成了热门话题。如果你已经在 Linux 服务器上装好了 Gitea,或者正打算用 Docker 部署一个,那么接下来最现实的问题就是:怎么把 GitHub 上积累的项目,干净、完整、不丢历史地搬到本地 Gitea 上?
这篇博客只讲迁移本身,不讨论访问加速、镜像网站那些事。内容会覆盖迁移场景判断、Gitea 部署基线、单仓库迁移、带 Issue/PR 的迁移、几十个仓库的批量脚本化方案,以及迁移之后最容易翻车的几个细节。适合两类读者:一是个人开发者想把 GitHub 仓库迁入自建 Gitea 做备份或完全切换的;二是团队管理员负责把公司项目从公网 GitHub 搬到内网 Gitea 的。
1. 迁移这件事,先分清你属于哪一种场景
1.1 三种迁移动机和对应的路线
我见过太多人一上来就搜“Gitea 怎么从 GitHub 迁移”,然后照着某一篇教程一顿操作,最后发现教程里的方法完全不适合自己的情况。原因很简单:迁移这个词背后,藏着三种完全不同的需求。
第一种是个人仓库备份。GitHub 上积累了几年甚至十几年的项目,担心哪天账号出问题,或者单纯想给本地留一份可用的历史档案。这种需求的核心是代码本身,分支、标签、提交历史一个都不能少,但 Issue、PR 这些其实无所谓。对这种场景,命令行的git clone --bare+git push --mirror就是最优解,简单、可靠、不依赖任何平台特性。
第二种是团队/公司代码库搬家。比如企业内部要做代码托管私有化,或者国产化替代项目要求所有代码资产进入内网。这种场景除了代码,通常还希望把 Issue、PR、里程碑、Wiki 这些项目资产一起带走,不然历史讨论全部丢失,后续追溯会很麻烦。这种情况下要用 Gitea 内置的“外部仓库迁移”功能,它可以连 GitHub 的 Issue/PR 一起拉到目标仓库。
第三种是带持续同步的平滑切换。团队暂时没法一步到位放弃 GitHub,希望先在 Gitea 上建一份镜像,GitHub 继续作为主仓跑一段时间,两边的提交保持同步。这种场景用的是 Gitea 的 Pull Mirror 功能,属于迁移完成后的过渡策略。
判断清楚自己是哪一种,再选路线,才不会浪费时间。很多人直接去点 Gitea 的“迁移外部仓库”,却发现一次只能迁一个,几百个仓库要手动点几百次,点完还中途断掉,体验非常差。
1.2 为什么自托管选 Gitea 而不是 Gogs
迁移目标平台的选型,其实也会影响迁移方案。我早年用过 Gogs,它启动快、占用资源极低,单机跑起来确实轻巧。但遇到迁移这件事,Gogs 和 Gitea 的差距就出来了。
Gitea 是从 Gogs 分叉出来的社区版本,继承了轻量级基因的同时,多了几个对迁移至关重要的能力:
- 内建的外部仓库迁移功能,支持从 GitHub、GitLab、Bitbucket 等平台导入,包括 Issue/PR 元数据。
- 原生支持Git LFS,迁移大文件仓库时不用额外搭 LFS Server。
- 内置Actions,迁移后用 GitHub 风格的 workflow 基本无缝衔接。
- 社区版本迭代快,API 接口完善,批量脚本化操作非常顺手。
所以这篇内容全部基于 Gitea。如果你还在用 Gogs,建议先换到 Gitea 再谈迁移。
2. 环境准备:把 Gitea 调到能“接得住”迁移的状态
2.1 一套能跑起来的 Docker Compose 基线
虽说题目默认 Gitea 已经部署好了,但我在迁移实操中发现,不少人的 Gitea 是“能登录但没配到位”的状态。迁移一上来就失败,根因往往是部署环境没准备好。
这里给一套我常用、实测过从 1.20 到 1.22 版本都稳定的 Docker Compose 基线配置:
services: gitea: image: gitea/gitea:1.22.3 container_name: gitea environment: - USER_UID=1000 - USER_GID=1000 - GITEA__server__DOMAIN=gitea.example.com - GITEA__server__ROOT_URL=https://gitea.example.com/ - GITEA__server__SSH_DOMAIN=gitea.example.com - GITEA__server__SSH_PORT=2222 - GITEA__server__LFS_START_SERVER=true - GITEA__service__DISABLE_REGISTRATION=true restart: always volumes: - ./gitea:/data ports: - "3000:3000" - "2222:22"几个容易被忽略的点:
SSH_PORT=2222是给容器内 sshd 听的端口,映射到宿主机用2222:22。如果宿主机 22 端口没被占用,可以改成22:22,否则直接按上面的写法走。LFS_START_SERVER=true必须开,否则后面用 LFS 的项目推上去会直接报LFS is not supported之类的错误。GITEA__server__ROOT_URL里的域名要和实际访问域名一致,否则 clone 时 Gitea 显示的仓库地址会是错的。
数据库我一般先用内置的 SQLite。仓库数量不多、团队规模不超过几十人的场景,SQLite 完全没有问题,还能省一层运维。如果一开始就规划几百个仓库、几十个并发用户,那就在 Compose 里加 MySQL 或 PostgreSQL,但这不是迁移的必要条件,可以先跑起来再说。
2.2 迁移前必须确认的三个软硬件条件
部署能跑起来以后,先别急着点迁移,花两分钟确认下面三件事:
磁盘空间。一个带完整历史的 git 仓库,bare 体积大概是工作区文件的 2 到 4 倍。迁移前在 GitHub 上看看所有仓库加起来的体积,再到服务器上执行:
df -h /var/lib/gitea至少预留 2 倍的空间。我遇到过一次最尴尬的情况是,仓库迁到一半,磁盘满了,push 直接卡死,最后只能删掉半成品重来。
git 版本。服务器上的 git 版本不要太老。Gitea 对 git 底层依赖很强,服务器 git 版本低于 2.20 的话,某些阶段式推送和 partial clone 特性会出问题。执行git --version确认一下,Debian/Ubuntu 上一般装上官方源的最新版就没问题。
网络连通性。迁移机器要能访问 GitHub。如果你的运行环境访问 GitHub 不稳定,先把要迁移的仓库在本地网速好的机器上git clone --bare下来,再通过内网传输,这比直接在服务器上边下边推稳定得多。后面第 7 节还会讲离线的 bundle 方案。
2.3 Gitea 对 LFS、附件、仓库体积的支持边界
迁移之前,一定要知道你目标平台的能力边界,不然迁到一半发现“这个 Gitea 不支持”,会非常被动。
- LFS 支持:Gitea 默认不开 LFS,但通过
LFS_START_SERVER=true可以打开。LFS 对象存储会占用磁盘空间,迁移前估算时要把这些算进去。 - 仓库体积限制:Gitea 默认不限制单个仓库体积,但 HTTP 传输时太大容易断。后面我会说怎么调。
- Release 附件:Gitea 支持 Release 附件,但迁移工具不会自动把 GitHub 的 Release 附件拉过来,要手动补。这个很多人漏掉。
- Wiki:Gitea 内置迁移可以带 Wiki,但如果你原来 GitHub 的 Wiki 是独立仓库,迁移时注意选上对应选项。
把这些边界搞清楚,后面跑起来就顺了。
3. 单个仓库的完整迁移:代码、标签、分支一次搬干净
3.1 为什么核心命令是 git clone --bare + git push --mirror
如果你只需要搬代码历史,不关心 Issue 和 PR,那最核心的命令组合就两条:
git clone --bare git@github.com:user/repo.git cd repo.git git push --mirror git@gitea.local:user/repo.git很多人会问:git clone不就行了,为什么要加--bare?为什么 push 的时候要用--mirror?
--bare的意思是克隆一个没有工作区的裸仓库,也就是只保留.git目录里的内容。这样你拿到的是完整的 git 内部对象,包括所有对象数据库、refs、HEAD、配置等,而不是一份“能检出的代码”。打个比方:普通 clone 是拿到一栋房子的钥匙和家具清单,bare clone 是把整栋房子连地基一起搬走。
--mirror则更严格:它不止推送当前所有分支,还会把远端所有 refs(分支、标签、远程跟踪分支等)按原样镜像过去,并且如果目标仓库里有源仓库不存在的引用,--mirror会直接删除它们。这样目标仓库和源仓库在 refs 层面做到完全一致,没有多余的历史残留。
还有一点很重要:提交历史里的作者、提交时间、SHA 值在迁移后保持不变,因为 git 的 commit 对象是内容寻址的,我们只是把对象数据库整体搬过去,没有重写任何东西。
3.2 实操链路与常见的“空仓库初始化”陷阱
完整操作流程是这样的:
- 在 GitHub 上确认要迁移的仓库,拿到 clone 地址(建议用 SSH 形式)。
- 本地执行
git clone --bare,得到一个以.git结尾的目录。 - 在 Gitea 上手动创建一个同名空仓库,注意不要勾选“初始化仓库”,也就是不选添加 README、.gitignore 这些选项。
- 进入
repo.git目录,执行git push --mirror git@gitea.local:user/repo.git。 - 在 Gitea 上刷新页面,确认分支、标签都出现了。
第 3 步特别容易踩坑。很多人创建仓库的时候顺手勾了“初始化仓库”,Gitea 会自动生成一次初始提交。这时候再执行git push --mirror,会因为两个仓库的历史完全不相关而失败,或者即便成功也会带上一个多余的初始提交。我见过有人迁移后仓库里多了一个 “Initial commit”,怎么看怎么别扭。
正确的核对方式是:创建仓库时把初始化仓库那个选项保持不选中,页面上会明确提示“保留仓库为空,稍后推送代码”。
如果已经建了带初始化的仓库,最简单的办法是删掉重建。反正刚建的空仓库,删了也不会有损失。
3.3 大型仓库传输调优
如果你要迁移的仓库超过 1GB,或者历史非常长,直接推送可能会遇到超时或传输中断。几个实测有效的调优手段:
优先走 SSH 协议。HTTP push 在仓库大时容易因为缓存区不足报错,走 SSH 要稳定得多。Gitea 的 SSH 地址有两种格式:
- 标准格式:
git@gitea.local:user/repo.git(SSH 端口是 22) - 非标准端口格式:
ssh://git@gitea.local:2222/user/repo.git
如果你按第 2 节的 Compose 配置把 SSH 映射到了 2222 端口,clone 地址一定要用第二种格式,否则会连到默认 22 端口然后被拒。这也是后面第 6 节要重点讲的坑。
如果必须走 HTTP,可以调大 postBuffer:
git config --global http.postBuffer 524288000这个值单位是字节,524288000 就是 500MB。大仓库建议直接调大,避免上传过程中报RPC failed; curl 56或HTTP 413。
还有一个小技巧:如果 push 到一半断了,不用太担心,git push --mirror是幂等的。网络恢复后重新执行同样的命令,git 会把缺失的对象继续传过去,直到两端完全对齐。这一点在批量迁移时特别有用。
4. 要连 Issue/PR 一起搬:用 Gitea 内置的外部仓库迁移
4.1 内置迁移能带过来什么,带不过来什么
命令行迁移只能搬 git 数据,Issue、PR、Labels、Milestones 这些元数据是搬不过来的。如果你的项目在 GitHub 上有大量历史 Issue 和讨论,这显然不能接受。Gitea 内置的迁移功能就是为这种场景准备的。
在 Gitea 页面右上角点击+→迁移外部仓库,选择 GitHub 作为迁移源,填上仓库地址和 Token,下面有一排复选框,可以选:
- 迁移 Issue
- 迁移拉取请求
- 迁移标签
- 迁移里程碑
- 迁移 Wiki
- 获取版本发布信息
我实测下来的迁移质量还是不错的,Issue 的标题、正文、评论、标签、状态都能对应过来,PR 的讨论也能保留。不过有两个东西它带不过来,需要特别注意:
Release 附件带不过来。GitHub Release 页面上上传的二进制附件,Gitea 迁移时不会下载,只会创建一个 Release 记录。如果那些附件很重要,只能手动下载再传到 Gitea 的 Release 页面。
Webhook、分支保护规则、Deploy Keys 带不过来。这些属于仓库级别的配置,GitHub 和 Gitea 的数据模型差异太大,迁移工具不会处理。需要你在 Gitea 上对照原来的配置手工重建。
GitHub 的 Projects(项目管理看板)也迁不过来,但 Gitea 自己有 Projects 功能,可以到那边重新建立。
4.2 GitHub Token 的正确生成方式与限流应对
使用内置迁移时,强烈建议填写 GitHub Personal Access Token。不填也能迁,但 GitHub 的 API 速率限制很严格:未认证状态下,每个 IP 每小时的 API 请求上限是 60 次,迁移两三个小仓库可能刚好够,仓库稍微多几个或者 Issue 数量大一点,就会撞上 Rate Limit,迁移中断在中间某一个 PR 上,十分难受。
Token 的生成路径:GitHub 右上角头像 → Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new token。
Classic token 的权限选择repo即可,这是读仓库内容的最低完整权限。如果你担心安全问题,可以用 Fine-grained token,仓库权限选择Contents: Read-only、Issues: Read-only、Pull requests: Read-only、Metadata: Read-only。
不过 Fine-grained token 有个坑:它只能访问被授权的仓库,如果组织里有很多仓库但你授权了指定仓库,迁移时会提示仓库不存在或没有权限。个人项目几个仓库的话用 Fine-grained 没问题;要批量迁移一个组织下所有仓库,classic token 反而省事。
Gitea 迁移页面填 Token 时,它会先去 GitHub API 拉取可迁移的仓库列表,如果列表为空或报 401,先检查 Token 权限。
4.3 内置迁移和命令迁移怎么搭配
内置迁移虽然能带元数据,但它只适合仓库数量不多的场景。一次迁一个,点选选项,等它跑完,再点下一个。十几个仓库还扛得住,几百个仓库就完全不现实了。
所以我的建议是分情况:
- 仓库数量少(个位数到十几个),且以后不一定再频繁加仓库:直接用内置迁移,最省事,元数据完整。
- 仓库数量多(几十个以上),或者以后要持续增加仓库:先写脚本用命令迁移把代码搬过去,再单独处理少数几个有大量 Issue/PR 的“重点仓库”做内置迁移。不过要提醒一句:如果先用命令迁移推了代码,再用内置迁移去导同一个仓库,Gitea 有时候会因为仓库已存在而拒绝导入。反过来先内置迁移再命令行 mirror push 会更顺,但内置迁移的代码和命令行 mirror 的代码如果 refs 有细微差异,可能会有 conflict。我个人的经验是:大小仓库都走命令行 mirror 搬代码,Issue/PR 历史通过 Gitea 的迁移功能单独迁到另一个临时仓库,需要查找时再去临时仓库看。元数据完整性和代码一致性,总得取舍一个。
5. 批量迁移几十个仓库:脚本化方案的完整落地
5.1 先获取源仓库清单
当你需要迁移一个 GitHub 账号或组织下几十个仓库时,手动点击已经完全不可行。正确的做法是:先用脚本获取仓库清单,再用 Gitea API 批量创建空仓库,最后逐仓推送。
获取仓库清单的方式是调用 GitHub REST API。下面这个脚本会把指定 GitHub 用户下所有非 fork 仓库的 clone URL 输出到一个文件:
#!/usr/bin/env bash set -euo pipefail GH_USER="${1:?Usage: $0 <github-user> <gitea-url>}" GITEA_URL="${2:?Usage: $0 <github-user> <gitea-url>}" # 遍历 GitHub API 分页,取所有非 fork 仓库 PAGE=1 : > /tmp/repos.txt while true; do RESP=$(curl -s -H "Authorization: token ${GH_TOKEN}" \ "https://api.github.com/users/${GH_USER}/repos?per_page=100&page=${PAGE}") COUNT=$(echo "${RESP}" | jq length) if [ "${COUNT}" -eq 0 ]; then break fi echo "${RESP}" | jq -r '.[] | select(.fork == false) | .clone_url' >> /tmp/repos.txt PAGE=$((PAGE + 1)) done echo "共发现 $(wc -l < /tmp/repos.txt) 个仓库"这个脚本需要curl和jq。如果没有 jq,先安装:
apt install curl jq -y # Debian/Ubuntu脚本里select(.fork == false)用于过滤 fork 出来的仓库,这些通常不需要迁移。如果确实要把 fork 也搬过去,删掉这个条件即可。
5.2 对接 Gitea API 自动建库并推送
拿到清单之后,下一步是循环处理每个仓库。完整脚本如下:
#!/usr/bin/env bash set -euo pipefail GITEA_URL="${1:?Usage: $0 <github-user> <gitea-url>}" GH_USER="${2:?Usage: $0 <github-user> <gitea-url>}" # 需要先设置环境变量 # GH_TOKEN=<GitHub PAT> # GITEA_TOKEN=<Gitea PAT> while read -r clone_url; do repo_name=$(basename "${clone_url}" .git) echo "================ 开始迁移: ${repo_name} ================" # 1. 在 Gitea 创建同名空仓库 CREATE_RESP=$(curl -s -X POST "${GITEA_URL}/api/v1/user/repos" \ -H "accept: application/json" \ -H "authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/json" \ -d "{\"name\": \"${repo_name}\", \"private\": true, \"auto_init\": false}" || true) if echo "${CREATE_RESP}" | grep -q "already exists"; then echo "仓库已存在,跳过创建步骤" fi # 2. bare 克隆 + mirror 推送 WORK_DIR="/tmp/migrate-${repo_name}.git" rm -rf "${WORK_DIR}" git clone --bare "${clone_url}" "${WORK_DIR}" cd "${WORK_DIR}" git push --mirror "${GITEA_URL}/${GH_USER}/${repo_name}.git" cd /tmp rm -rf "${WORK_DIR}" echo "================ 完成: ${repo_name} ================" done < /tmp/repos.txt echo "全部迁移完成"这段脚本有几个细节值得注意:
创建仓库时auto_init必须为false,否则相当于给空仓库加了一个初始提交,第 2 步的 mirror push 会出问题,和第 3 节说的情况一样。
每次循环前rm -rf清掉上一个仓库的临时裸仓库,避免磁盘膨胀。迁移大仓库时,这些临时 bare 目录体积不小。
push 的目标地址用的是https形式:${GITEA_URL}/${GH_USER}/${repo_name}.git。这样需要认证,Gitea 会弹交互式认证,脚本会卡住。有两种解决方式:
方式一:在~/.git-credentials里写入凭证:
http://用户名:令牌@gitea.local然后执行一次git config --global credential.helper store。
方式二:在 push 的 URL 里直接带令牌:
git push --mirror "http://${GH_USER}:${GITEA_TOKEN}@${GITEA_URL#http://}/${GH_USER}/${repo_name}.git"方式二更快,但令牌会出现在命令历史里,如果服务器是多人共用的,要谨慎。我一般用方式一,或者用GIT_ASKPASS指向一个临时脚本,这样既不会在历史里暴露令牌,也不会交互卡住。
如果目标仓库在 Gitea 的某个组织(Organization)下面,把创建 API 从/api/v1/user/repos改成/api/v1/orgs/{org}/repos,脚本里的目标路径也改成对应的 org 名。
5.3 幂等设计与中断恢复
批量迁移几十个仓库,网络抖动是常态。脚本跑一半挂了怎么办?不要慌,脚本本身是幂等的:某个仓库已经创建过,再创建时会返回 “already exists”,脚本会跳过;某个仓库已经 push 过,再次git push --mirror会把缺失的对象补齐,不会重复产生内容。
所以我每次跑批量迁移,不会追求一次成功。挂了就重新执行一遍脚本,它会接着断点继续。如果某个仓库连续几次都失败,可能是源仓库有问题,先把它的 clone_url 从/tmp/repos.txt里剔除,让其他仓库继续跑,最后单独排查那几个。
批量迁移完成后,把 Gitea 的仓库体积统计一下,和 GitHub 侧对比,数字差距特别大的仓库一定有问题:
du -sh /var/lib/gitea/git/repositories/*6. 迁移后最容易翻车的四个地方
6.1 SSH 端口冲突:克隆失败的排查链路
这是我在帮别人排查时遇到最多的问题。现象是:迁移完成后,在 Gitea 页面点复制 clone 地址,拿到的是git@gitea.local:user/repo.git,但本地执行git clone报错:Permission denied (publickey)或者直接Connection refused,而浏览器访问 Gitea 页面完全正常。
别急着怀疑 SSH 密钥配错了。先看端口:如果 Gitea 跑在 Docker 里,按第 2 节的配置把容器内 22 端口映射到了宿主机 2222,那么标准格式的git@gitea.local:user/repo.git会默认连 22 端口,这时候要么连到宿主机的 sshd,要么 Connection refused,反正到不了 Gitea。
正确的 clone 地址是这种格式:
ssh://git@gitea.local:2222/user/repo.git如果你不想每次 clone 都打长长的一串,可以在~/.ssh/config里配一个别名:
Host gitea HostName gitea.local Port 2222 User git配置之后,clone 地址就变成了:
git@gitea:user/repo.git另外还有个小坑:Gitea 的 SSH 密钥指纹和 GitHub 不一样,本地第一次 clone 时会提示主机密钥未知,这是正常的。如果之前 known_hosts 里已经有同名主机的记录,会直接报REMOTE HOST IDENTIFICATION HAS CHANGED,需要先删掉旧记录再重试:
ssh-keygen -R gitea.local6.2 大小写不敏感历史:git status 莫名“全乱”
这个坑很隐蔽,也很容易让人怀疑人生。现象是:同一个仓库,迁移到 Gitea 后,在 Windows 或者 macOS 上克隆,跑git status,大量文件显示为 modified,但打开文件内容根本没变。
问题出在文件系统的大小写敏感性上。Windows 和 macOS 的默认文件系统(NTFS、APFS)对大小写不敏感,而 Linux 文件系统是敏感的。如果上游仓库历史里存在README.md和readme.md这种只在大小写上有区别的路径,在 Linux 上 checkout 时没问题,因为两个文件可以共存;但在 Windows/macOS 上 checkout 就会互相覆盖,git status 看到的是一堆“文件被修改”的假象。
排查命令也很简单:
git ls-tree -r HEAD --name-only | sort -f | uniq -di如果这条命令输出了内容,说明仓库历史里确实存在大小写冲突的文件。严格来说这不是迁移本身造成的,而是仓库历史就带着这个雷,只是迁移前你一直在 Linux 环境或者一直没触发。
处理方式分两种情况:
- 仓库还在活跃开发:在迁移后的仓库里,用
git mv把冲突路径规范化,例如把readme.md改成README.md,然后提交推送。这样后续开发者的工作区就干净了。 - 仓库已经归档不用再改:直接在开发机器上设置
git config core.ignorecase true,让 git 忽略大小写差异,也能避免大量假改动。
迁移前最好就把这个查一遍,省得迁移后团队每个人第一次 clone 都被吓一跳。
6.3 分支规则、Webhook、Release 附件不会自动带过来
我在第 4.1 节提到过一次,这里再展开说,因为它的影响面太大了。
迁移工具(无论命令行还是内置迁移)只搬 git 数据和 issue 数据。分支保护规则、Webhook、Deploy Keys、环境变量(如果有)、Release 附件,全部不会跟着走。
分支保护规则这种尤其重要。原来 GitHub 上 main 分支禁止直接 push、要求 PR review 的仓库,迁移到 Gitea 后如果忘了重建规则,第一个手快的同事可能就直接 push 上去了。在 Gitea 的仓库设置 → 分支 里,把受保护分支重新配置一遍:
- 分支名称:
main或master - 禁止直接推送
- 要求 PR 审核通过后才能合并
- 指定允许推送的用户/团队
Webhook 的迁移要重点核对路径。GitHub 的 Webhook URL 如果是https://ci.example.com/hooks/github,Gitea 默认发送的 Payload 格式虽然不是 100% 一样,但大多数 CI 平台同时兼容两种格式。如果原来的 CI 只认 GitHub 格式,需要在 CI 侧增加一个 Gitea 格式的 webhook 入口。
Release 附件只能手动处理。在 GitHub Releases 页面把需要的二进制下载下来,再到 Gitea 的 Release 里重新上传。如果项目发布频繁、附件多,这里会很耗时间,建议先确认哪些 Release 的附件还有人用,别一口气全传。
6.4 只允许访问指定仓库的 Token 配置
迁移完成后,你可能要给某些自动化脚本、CI 系统或者外部合作者签发一个 Token,要求它只能读某一个仓库,不能碰其他任何仓库。GitHub 的 Fine-grained Token 能做仓库级限定,但如果你现在要签的是 Gitea 侧的 Token,操作路径不太一样。
Gitea 的 Token 创建入口在:右上角头像 → 设置 → 应用 → 生成新令牌。
关键在权限范围设置:
- 选择 “自定义权限”
- Repository 权限设置为只读(Read:Repository)
- 权限范围下方有一个选项,可以选择这个 Token 能访问哪些仓库,如果你只选其中一个仓库,生成的 Token 就只能对这个仓库做认证,访问其他仓库会返回 403。
用这个 Token 做 clone 或 push 时,URL 格式要注意,Gitea 的 HTTP 认证是用户名 + 令牌的方式:
http://用户名:令牌@gitea.local/user/repo.git不是 GitHub 那种x-access-token:令牌的格式。我第一次在用 CI 系统对接 Gitea 时就在这里卡了半天,填写oauth2:token或者git:token都不对,正确写法就是用户名:令牌@。
还要强调一点:Token 一旦生成,Gitea 只会展示一次,刷新页面后就看不到了,要把它保存到密码管理器里。写进脚本或 CI 配置时,不要直接硬编码,用环境变量或 CI 的 Secret 机制代替。
7. 离线/受限网络下的搬运方案与同步收尾
7.1 git bundle:适合内网隔离环境的离线迁入
有些团队做代码托管私有化时,目标环境是完全的内网,服务器连不上 GitHub,甚至迁移人员的工作机也只能通过特定路径访问 GitHub。这种“物理隔离”的环境,直接在服务器上执行git clone --bare行不通。
这时候用git bundle打包整个仓库历史,再通过 U 盘或内部文件服务器搬运到目标环境,是最稳妥的方式。
在能访问 GitHub 的机器上执行:
git clone --bare git@github.com:user/repo.git cd repo.git git bundle create /tmp/repo.bundle --all--all表示把所有 refs 都打进 bundle,相当于一个仓库历史快照。这个.bundle文件是一个单一的二进制文件,可以拷到 U 盘或者通过内网文件系统传输。
到了内网服务器,先创建一个工作目录:
git clone /tmp/repo.bundle repo cd repo git remote add origin git@gitea.local:user/repo.git git push --mirror originbundle 方案的优点是:不依赖任何外部网络,传输链路完全可控,适合迁移大批量仓库时逐个稳定搬运。缺点是:如果仓库有 LFS 对象,git bundle不会把 LFS 文件打进去,还是要在能访问 GitHub 的机器上先执行git lfs fetch --all和git lfs push --all才能搬 LFS 对象。LFS 的离线搬运比较麻烦,通常需要单独拷贝 LFS 存储目录,这里就不展开了。
7.2 过渡期用 Pull Mirror 做自动同步
迁移完成后,不一定立刻把 GitHub 上的原仓库删掉。很多人会保留 GitHub 仓库作为备份,团队新代码推到 Gitea。这时候为了让两边的代码不越差越远,可以在 Gitea 上给每个仓库配置 Pull Mirror。
操作路径:仓库设置 → 镜像同步 → 拉取镜像 → 填写源仓库地址https://github.com/user/repo.git和 Token,同步间隔选一个最小值(通常是 1 小时),启用即可。
Pull Mirror 会定期从 GitHub 拉取新的提交、分支、标签到 Gitea。实测中需要注意:Pull Mirror 只同步 git refs,不会同步 GitHub 上的 Issue 评论、PR 状态。如果你团队的协作讨论已经全面转移到 Gitea,这个无关紧要;如果 GitHub 上还在产生 Issue,这部分内容要人工迁移,或让团队成员直接在 Gitea 建新 Issue。
还有一种方案是反过来,在 GitHub 侧 action 里配置自动 push 到 Gitea,但那是把 Gitea 当镜像,适合以 GitHub 为主仓的场景。对比下来,我建议大部分过渡场景用 Gitea 的 Pull Mirror,少量重点仓库手动定期git push --mirror同步一次,更加可控。
7.3 迁移完成后的验证清单
最后给大家一份我每次迁移完都会跑的验证清单,照着做一遍,基本能确认迁移是否真的成功:
| 检查项目 | 命令/操作 | 通过标准 |
|---|---|---|
| 代码对象完整性 | git fsck --full | 无 error,只有少量 dangling 警告可忽略 |
| 分支数量 | git branch -r或 Gitea 页面 | 与 GitHub 一致 |
| 标签数量 | git tag或 Gitea 页面 | 与 GitHub 一致 |
| 提交数量 | git log --oneline --all | wc -l | 与 GitHub 侧一致 |
| LFS 对象 | git lfs fsck --all | 所有对象校验通过 |
| 工作区干净 | clone 一个新副本,执行git status | 无任何改动 |
| Issue/PR 数量 | Gitea 仓库页面 Issue 标签页 | 与 GitHub 数量一致(若用内置迁移) |
| Webhook 回调 | 在 Gitea 仓库设置里点击“测试” | CI 能收到事件 |
| 权限 | 用只读 Token clone | 访问成功,且访问其他仓库返回 403 |
代码对象完整性是最重要的一项。git fsck --full会逐对象校验仓库的完整性,如果迁移过程中网络中断导致对象缺失,这步会直接报出来。不想在服务器上装 grep 复杂命令的话,直接在 Gitea 仓库页面看列表对照也够用。
我在实际负责的几次迁移项目里,还会专门拿一个仓库做一次“模拟开发流程”验证:从 Gitea clone,新建分支,push,提 PR,合并,确认整个流程顺畅。因为迁移不是把数据搬过去就完了,团队的日常协作流程在 Gitea 上能不能跑通,才是关键。
跑完这份清单,代码、元数据、权限、CI 四层都确认无误,迁移工作才算真正画上句号。后续要不要删 GitHub 上的原仓库,取决于团队自己的备份策略,我个人的习惯是保留至少三个月,确认 Gitea 完全稳定后再做清理。