十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

内网代码托管选型指南:私有化、分支管控与制品库落地实践

内网代码托管选型指南:私有化、分支管控与制品库落地实践 去年我帮一家做工业软件的公司做研发基础设施选型团队的要求非常统一代码必须放内网三个产品线经常在同一个文件上打架每次合并都像拆弹管理层还想把固件包、算法库、Android安装包统一管起来别再让运维拿U盘拷来拷去。这三个条件放在一起其实就是标题里的十二个字——能私有化、管得住分支和制品。市面上叫得出名字的版本控制工具不算少但很多人在选型时只盯着“Git仓库放哪”结果上了台之后才发现分支策略、制品存储、权限模型全都绕不开。这篇文章把我的选型过程、工具对比、分支管控的实际操作和制品库落地经验整理了一遍适合正在做内网代码托管选型的技术负责人也适合被分支合并折磨的开发同学。1. 先想明白私有化、分支、制品为什么要放到一起选型1.1 我遇到的一次真实选型上面说的那家公司最初并不是没有版本控制。他们用一台共享电脑放了一个裸 Git 仓库开发用命令行直接 push配合群聊通知来提醒别人拉代码。听起来很草台班子但二十人的团队确实撑了一年多。直到有一次嵌入式组和算法组同时改同一个协议头文件一个在 Windows 上一个在 Linux 上两边都不熟悉合并冲突硬生生手工合并了三天最后还弄丢了一段配置。从那以后他们决定认真选工具。我介入时列出的条件就是私有化、分支受控、制品要能管。注意这三个条件不是三个问题而是一个问题的三个侧面。原因是只要团队超过十个人代码托管平台就同时承担了“代码仓库”“协作流程”“交付物仓库”三种角色。你选一个工具实际上是在选一套研发协作基础设施。1.2 版本控制工具的边界正在变宽十几年前版本控制工具的任务很单一记录历史、提供分支、支持合并。SVN时代分支就是完整目录拷贝合并不存在智能三方合并Git 把分支变成指针合并算法也强得多但工具本身依然不管“人应该怎么协作”。如今团队对工具的要求早就越过了存储代码这条线。我习惯把这四件事放在一起看代码存放、分支与合并规则、制品保留与分发、权限与审计。私有化部署本身就意味着你必须自己承担这些环节的配置和维护。有人以为 GitLab/Gitea 只是一个网页壳子写完代码照样用命令行 push所以随便选一个就行但真到了团队协作“分支怎么进 main”“谁能发布制品”“CI 构建产物存在哪”这些规则才是维护成本的大头。1.3 分支问题是规则问题不是工具功能问题你随便搜“git 切换分支”“idea 合并分支到 test”能搜出一大堆操作求助帖。说明很多人对分支操作的困扰并不是工具不支持而是不知道在哪个界面上点。但我想先说一个更根本的东西分支管理的第一步不是学命令是定规则。比如你的团队是双周发版那么 main或者 master上面就不应该允许任何人直接 push所有改动要先经过 Merge Request/Pull Request代码评审通过后再进。比如你有 dev 分支做集成测试那就规定 feature 分支必须先合并到 dev 验证再由 dev 进 main而不是每个人绕过 dev 直接往 main 怼。这些规则一旦定了才轮到工具去执行——用保护分支、审批人数、Push Rule 把它固化下来。这也是为什么我推荐工具时特别看重“分支保护”和“MR/PR 流程”的强度而不是它有多少种花哨的分支操作动画。工具是把人的规则变成强制约束少了这个再好的 Git 命令也救不了混乱。1.4 制品是一个独立资产类别最后说制品。很多人会问制品不就是构建产物吗放到 Git 仓库里不就行了这个问题我在第 4 章会详细展开。这里先给一个结论制品和源码的生命周期完全不同。源码是持续变化的制品是某个时间点的不可变快照源码要频繁拉取、diff、合并制品只关心版本、校验和、部署依赖关系。如果团队只是写内部脚本确实不用操心制品。但只要涉及到 Java/golang/Node 的包、Docker 镜像、移动端安装包、固件就必须有独立的制品仓库。版本控制工具和制品库工具是两套系统有些一体化平台比如 GitLab把两者揉在了一起有些则要外部配合。选型时把这个边界想清楚后面能省掉很多扯皮。2. 四个候选产品的横向评估与取舍2.1 GitLab CE功能最全的“全家桶”路线先给结论如果团队规模中上、对流程有要求GitLab CE社区版自托管是绕不开的选项。它一个产品里面同时包含代码仓库、MR 评审、分支保护、CI/CD、Container Registry、Package Registry几乎把“代码-流程-制品”闭环都做了。部署条件上官方对小型安装的建议是至少 4GB 内存但以我的实测一个几十人的内网实例给 4 核 8G 会更稳。用 Docker Compose 或者 Omnibus 安装包都行内网域名、HTTPS 证书一配就能跑起来。GitLab CE 免费没有用户数限制但像安全合规仪表盘、多级审批这类企业级功能需要升级到 Premium。分支管控方面GitLab 的 Protected Branches 可以限制谁能 push、谁能合并配合 Merge Request 的 Approval Rules能做到“必须两人 review 且流水线通过才能合入 main”。制品方面项目级 Container Registry 和 Package Registry 直接集成在同一个项目里CI 里推送镜像、Maven 包都可以通过内置变量完成。对没太多精力维护多条工具链的团队这是最省事的一条路。2.2 Gitea轻量到可以长期自己养Gitea 是我个人挺喜欢的一个产品。它是 Go 写的整个服务编译成一个二进制文件加上一个 SQLite 库就能跑。我在一台 1 核 1G 的云主机上跑过 Gitea 加一个 Gitea Runner日常完全没压力。这对很多小团队是实实在在的好处——不用专门维护一台 8G 内存的服务器GitLab 跑起来光系统自带进程就吃掉一半资源而 Gitea 经常只占几百 MB。功能上Gitea 有仓库、Issue、Pull Request、Review、分支保护、LFS最近几个版本还内置了 CI/CDGitea Actions。分支保护基本够用可以禁止直接 push、要求 PR 审批、限制合并方式。唯一明显短板是制品仓库虽然每个 Release 可以挂二进制附件但那不是正经的包管理器源。你要是给前端组提供 npm 私服给 Java 组提供 Maven 中央代理指望 Gitea 自己干不现实得上 Nexus 或 Artifactory。适用场景很清晰30 人以内、没有严格合规要求、想要低维护成本的团队。我之前见过一个十人的技术兴趣小组用 Gitea 维护了四五年升级基本都是替换二进制重启犯错的概率非常低。2.3 Gerrit为严格评审而生的流派Gerrit 是个老牌但仍然有一批忠实用户的代码评审系统。它的逻辑和 GitLab/Gitea 完全不同正常 Git push 直接推 refs/heads/xxxGerrit 则要求大家推的是 refs/for/xxx每个提交进系统后生成一个 Change必须被 review 并通过 Verify/Approve才能 submit 进入目标分支。也就是说“不评审不能合”是它的核心架构不是可选项。这种模式适合对代码质量和审计要求极高的场景比如汽车电子、医疗器械、银行核心系统或者大厂内部的基础设施团队。代价也很明显学习曲线陡很多人第一次提交会觉得“为什么我明明 push 了但分支上没有”插件生态以 Java 为主二次开发门槛高它本身没有制品管理能力需要外挂一套制品库。说句公道话如果团队能接受 reject 和 review 的文化并愿意付出时间训练Gerrit 养出来的纪律性是 GitLab 那套可选审批比不了的。但对于大多数中小团队我建议谨慎进入因为它的整个工作流改起来比较痛苦人员流动后培训成本也高。2.4 选型对照与建议把上面三家以及“Git 外部制品库”这种组合放在一张表里方便对照维度GitLab CEGiteaGerritGit Nexus 组合服务器资源要求高建议 4 核 8G 起很低1 核 1G 可行中需要 JVM 与数据库中至少 2 个服务分支保护能力强保护分支 MR 审批 Push Rules中基础保护和 PR 审批极强不评审不能合入弱基本靠钩子制品管理内置容器镜像和包仓库弱仅 Release 附件无由 Nexus/Artifactory 承担运维成本较高升级和大仓库维护有活很低二进制替换方便中等Java 生态高链路靠手工组合适合团队20 人以上需要流程和制品1-20 人低维护50 人以上强评审文化有专门 Devops 人力我的取舍逻辑很简单先看流程诉求。只求能放代码、能管分支Gitea 足够需要强制代码评审且公司允许用不同工作流Gerrit 值得投成本既想流程好管又不想养一堆系统直接 GitLab CE。制品库这件事无论选哪个我都建议单独考虑 Nexus 或者直接依赖 GitLab 自带的 Registry别让源码仓库兼职当文件服务器。3. 分支管控实测保护规则、合并流程、跨 IDE 操作3.1 先落一套大家都认的分支策略网上关于分支策略的资料很多Git Flow、GitHub Flow、Trunk-Based各有拥趸。我建议别再纠结理论先按团队发布节奏选一个能落地的。如果团队是固定周期发版比如双周、每月用简化版 Git Flowmain 放稳定版本dev 做集成feature/* 从 dev 拉出来hotfix/* 从 main 拉出来。每个 feature 分支做完后先回 dev验证完再由 dev 合入 main 并打 tag。如果团队是持续交付、每天多次上线用 Trunk-Based所有人都在 main 上开发分支生命周期控制在几天以内通过特性开关控制发布。这种模式下不需要长期维护 dev 分支合入频率高冲突反而少。我给那个工业软件团队定的是简化版 Git Flow因为产品线多、发版节奏固定、硬件迭代不能天天上线。定了规则以后再把保护分支和审批配置配好接下来就看工具能不能执行。3.2 用保护分支把规则写进服务器先看 GitLab。进入“项目 - Settings - Repository - Protected branches”添加保护分支。以 main 为例“Allowed to merge”选 Maintainers“Allowed to push”选 No one。这意味所有人不能直接 push main只能走 Merge Request 合入。如果还想限制谁能推到 feature 分支也可以单独添加规则。光有保护分支还不够。回到“Settings - General - Merge requests”把“Pipelines must succeed”“All threads must be resolved”打开这样 CI 没跑完或 review 留言没解决都不允许点 Merge。Approval Rules 里可以指定需要 1-2 个审批人审批人最好不是被评审代码的提交者。实测下来这三项配合基本能堵住“直接改 main”和“不跑 CI 强合”两个高频事故。Gitea 的位置在“仓库 - 设置 - 分支”不同版本中文翻译可能略有差异添加规则选择分支名称勾选“禁止直接推送”“需要 Pull Request 审查通过”“可推送的用户/团队白名单”。Gitea 的 PR 审批可以指定最低审查人数但选项比 GitLab 少一些胜在简单。3.3 高频操作对照命令行、IDEA、VS Code、TortoiseGit、Eclipse这一节把平时被问最多的操作列出来用命令行和图形界面双轨对照。不用全背知道每个工具在哪点就行。创建新分支并推送命令行git checkout -b feature/login然后git push -u origin feature/loginIDEAVCS - Git - Branches - New Branch输入分支名勾选 Checkout new branchVS CodeCtrlShiftP 输入Git: Create Branch...输入名称选择基于当前分支推送用源代码管理视图右上角的“Publish Branch”TortoiseGit右键 - TortoiseGit - Switch/Checkout...勾选 Create new branch输入名称后 Switch切换分支命令行git switch devGit 2.23旧版git checkout devIDEA右下角分支名 - Branches - Remote Branches - origin/dev - CheckoutVS Code点击左下角分支名 - Checkout to...或命令面板Git: Checkout to...TortoiseGit右键 - Switch/Checkout... - Branch - 选分支 - OK合并分支把 dev 合并到 testIDEA 里先切换到 test 分支VCS - Git - Merge Changes... - 选 dev - Merge冲突时 IDEA 会弹出冲突对话框可以 Accept Yours/Theirs 或打开 Merge 工具逐块处理VS Code切到 test 后命令面板Git: Merge Branch...- 选 dev冲突时源代码管理视图有“合并编辑器”入口TortoiseGit右键 - Merge... - 选择要合并进来的分支勾选 Merge optionsEclipse在目标分支上Team - Merge... - 选源分支三方合并在 Workspace 中会标记冲突必须手动改完提交清理已删除的分支远程分支被人删了本地还残留一堆 remote 缓存git remote prune origin或git fetch --prune删除本地分支git branch -d local-dev没合并过的用 -D 强制VS Code 命令面板Git: Delete Branch...选本地分支IDEA Branches 弹窗里选中本地分支 - Delete删除远程分支git push origin --delete feature/loginGitLab 网页端 Repository - Branches - 点击删除图标这里特别提醒切分支前把未提交改动处理好要么提交到临时分支要么git stash。我见过不止一个人在 dev 上写了半天直接 checkout 到 test 发现所有改动跟着到了 test原因是这些改动还没 commitGit 的 switch 会把它们带到目标分支一旦冲突很难抽身。养成“先提交或 stash 再切换”的习惯比什么都重要。3.4 master 被 revert 之后其他分支二次合并的冲突复盘这是一个非常典型但我发现很多人没意识到的坑网上相关热词也在反复出现master 分支 revert 后其他分支再合并 master冲突比第一次还激烈。这个完整复盘值得单独写。场景是这样的feature/login 开发完通过 MR 以 merge commit 的方式合入 master。上线后发现功能有严重 bug团队决定回滚在 master 上执行了git revert merge-commit-id。注意 revert 一个 merge commit 必须带 -m 参数通常用git revert -m 1 merge-commit-id代表保留第一父提交也就是主线把整个合并引入的改动撤销掉。这时 master 的代码回到合并前的状态但 feature/login 分支并没有消失它还在继续开发。等 login 分支改完想再次合到 master问题来了master 上已经存在了一个“revert 提交”Git 会把功能改动看成“曾经引入、后来又被回滚”当你再发起合并时Git 要同时处理这堆互相矛盾的历史结果就是成片冲突错误率极高。更坏的情况是有人图省事直接 force push 覆盖冲突把 revert 弄丢了。我的处理思路分两种场景临时回滚功能还会再上在 revert 完成之后立即让 feature 分支同步 master。命令是git checkout feature/login git merge master把回滚的“反向提交”吸收到 feature 分支里。这样 feature 分支就站在回滚后的代码上继续开发下一次合并时Git 看到的 diff 是“回滚基础上的新增功能”冲突会小很多。别等改了好几天后才处理这个同步问题拖得越久越乱。彻底放弃该功能revert 完 master 之后直接把 feature/login 分支清理掉成员改从最新 master 重新拉分支。不要留一个和 master 历史明显冲突的旧分支不处理。另外如果你的 revert 没有带 -m 导致报错可以用git log --oneline --graph查看提交图确认要保留哪个 parent普通提交的 revert 不需要 -m。这个坑的根本原因不是 Git 不行而是回滚和分支开发两条时间线没有及时对齐。工具再强规矩也得人来定。4. 制品仓库把“管得住制品”落到实处4.1 为什么制品不能和源码塞在同一个 Git 里先说一个最直观的例子。某个固件包 50MB团队一天编译 5 次如果把每次编译结果都提交到 Git一天就是 250MB 的裸对象。Git 为了效率会为每个文件存储不可变的 blob哪怕新版本只改了一个字节也会新存一份完整的 50MB。仓库膨胀到几个 GB 之后clone 要几分钟CI 环境每次还得全量拉这种体验谁都受不了。制品和源码的性质也确实不同。源码适合 diff、合并、回溯制品则讲究“不可变 可复现”release-1.2.3 就是 release-1.2.3内容不能变变了就无法审计。用 Git 分支做制品存储分支是可以移动的、会被清理的语义上就容易乱。更不用说权限了——源码仓库里的可见用户通常都能克隆而正式的制品库应该只有部署系统能拉取。所以让专业系统干专业事别让 Git 兼职当下载站。4.2 直接用 GitLab 自带的 Registry 管镜像和包既然标题强调“版本控制工具能管制品”GitLab 最值得说的就是它在同一个项目里集成了 Container Registry 和 Package Registry。容器镜像仓库用来放 Docker 镜像包仓库用来放 Maven、npm、PyPI 这些软件包不需要额外再搭一套 JFrog 或 Nexus。使用容器镜像仓库的流程是这样的项目设置里启用 Container RegistryCI 里用 CI/CD 变量登录、构建、推送。.gitlab-ci.yml里核心脚本大概长这样build: stage: build image: docker:24.0.9 services: - docker:24.0.9-dind variables: IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker build -t $IMAGE_TAG . - docker push $IMAGE_TAGPackage Registry 对 Maven 的用法则是把发布目标指向项目内部的 packages/maven 地址认证用 CI_JOB_TOKEN 或用户的 Personal Access Token这部分在 GitLab 项目页面里有现成的命令片段照着填即可。但要说清楚GitLab 自带的 Registry 适合“流程已经圈在 GitLab 里”的团队。如果你们还维护着自建的 Jenkins、多个旧版工具链或者希望制品仓库与代码仓库权限完全隔离那么上独立制品库更合适。4.3 用 Nexus 搭一套私有 Maven/npm 仓库Nexus Repository Manager 3 是自建制品库里很稳妥的选择。用 Docker 启动非常方便docker run -d --name nexus --restartalways \ -p 8081:8081 \ -v nexus-data:/nexus-data \ sonatype/nexus3启动后访问http://your-host:8081第一次管理员密码在容器里的/nexus-data/admin.password文件里或者用docker exec nexus cat /nexus-data/admin.password查看。登录后第一件事是改密码、关闭匿名的完全访问权限。以 Maven 仓库为例建议建三个仓库一个maven2(hosted)用来放私有 jar 包命名比如 maven-releases一个maven2(proxy)代理 Maven 中央仓库降低外网依赖再建一个maven2(group)把 hosted 和 proxy 合并成一个访问地址比如 maven-public。客户端就这样配置settings server idmaven-public/id usernamedeployer/username password你的部署账号密码/password /server mirrors mirror idnexus/id urlhttp://your-host:8081/repository/maven-public//url mirrorOf*/mirrorOf /mirror /mirrors /settings这里有个经验不要把 admin 账号给开发同学做 deploy单独建 deployer 用户只授权 maven-releases 的写权限访问代理仓库可以开放匿名读或者用只读账号。Gradle 项目则在 build 中配置 repository 地址和凭据原理一样。4.4 制品保留策略给垃圾回收上一个闹钟制品库最容易被忽略的是清理策略。默认所有东西都留着一个月后磁盘就报警。Nexus 里可以在“Administration - Cleanup Policies”创建策略按“上次拉取时间超过 N 天”或“保留最新 N 个版本”来清理然后挂载到对应的 repository 上。我一般对 proxy 仓库设置缓存保留 30 天对 hosted 仓库保留最近 20 个 release 版本snapshot 仓库保留最近 50 个快照。GitLab 的 Container Registry 也有 Cleanup Policy在项目设置里填正则、保留数量、最早保留时间让后台定期跑。不同版本的位置和开关可能略有差异但思路都一样。我因为以前没配CI 每构建一次 push 一个镜像一年后 registry 目录里多出几百个 tag 镜像排查问题都很困难。如果你是 Docker 镜像还要注意镜像的不可变 tag 问题。tag 别用 latest 占位要用构建号/commit 短哈希作为版本清理策略按 tag 正则会好写很多。5. 迁移落地时容易翻车的几个环节5.1 仓库搬家和提交历史迁移如果是从 GitHub 迁到内网 GitLab/Gitea只搬仓库历史的话很简单git clone --mirror gitgithub.com:someorg/repo.git cd repo.git git push --mirror gityour-gitlab.example.com:group/repo.git镜像克隆会把所有分支、tag、refs 原样推过去。但如果还要迁移 Merge Request、Issue、评论手工就做不到了需要用到 GitLab 后台的导入功能或者用第三方脚本。Gitea 对大型 GitLab 仓库的导入有时候会丢一些数据我的建议是文档、MR 历史不太重要时用镜像克隆如果历史数据必须完整先在小项目上试导入一遍检查关键 MR 和权限再全量迁移。从 SVN 迁移到 Git更推荐用 git-svn 或者至少保留作者邮箱映射如果之前仓库存了大量二进制迁移前先清理不然历史里全是几十 MB 的对象后面想瘦身就得做历史重写了。5.2 仓库体积失控LFS 和二进制文件我接手过的一个项目Git 仓库 11GB原因就是历史上有人把设计稿、压缩包、模型文件直接 commit 进去了。先用git count-objects -vH看仓库体积用git rev-list --objects --all找不到大文件时可以配合脚本计算每个对象的大小定位后基本就两条路要么用 BFG Repo-Cleaner 或 git filter-repo 重写历史把那些大文件从历史中抹掉要么从现在开始给大文件换用 Git LFS。Git LFS 的思路是用文本指针替换大文件内容真正的二进制内容存到 LFS 服务端。GitLab 和 Gitea 都支持 LFS安装配置不复杂。但我的建议是LFS 解决的是“以后别把大文件塞进历史”没法删历史里已经存在的垃圾。历史已经膨胀的项目该重写还是得重写。而且 LFS 不是灵丹妙药如果二进制文件非常频繁变化LFS 存储和带宽也会吃紧这时候要考虑独立的对象存储或制品库而不是继续往版本控制系统塞。日常习惯更要紧在 GitLab 的 Push Rules 里设置“最大文件大小 10MB”Gitea 可以在服务端 hook 里做类似限制能挡住的就提前挡住。5.3 备份、权限与密钥管理私有化最怕的就是数据丢。GitLab 的备份用自带命令gitlab-rake gitlab:backup:create备份文件会生成在/var/opt/gitlab/backups目录恢复时用gitlab-rake gitlab:backup:restore BACKUP时间戳。Gitea 更省事gitea dump一条命令就能把配置、仓库、数据库、LFS、附件打成 zip 包。Nexus 的数据全在 sonatype-work 目录下Docker 里是/nexus-data备份这个目录基本就够了如果想更稳连同配置文件和数据库一起备份。权限模型我建议按三条线分开设计代码仓库的可见性和角色GitLab 里 Guest/Reporter/Developer/Maintainer/Owner、分支保护规则谁能合并进 main、制品库的部署与拉取权限独立的 deploy/read 账号。不要把同一个密码用在一堆地方。GitLab 的 Personal Access Token、Gitea 的 Token、Nexus 的用户密码全部通过 CI/CD 的 Secrets 变量注入不要写死在代码或配置仓库里。5.4 把“规则”用钩子和 Push Rules 写死最后聊一点细节。光有保护分支和审查还不够很多团队规则因为没人强制最后变成纸面文章。GitLab 的 Push Rules 就是用来干这个的它允许你直接拒绝包含特定文件名的提交、拒绝超过大小限制的文件、校验提交信息是否符合正则。如果你的 GitLab 版本没有 Push Rules用服务端 pre-receive 钩子也能实现类似效果。比如“提交信息必须以单号开头否则 reject”这样就不需要再从 MR 里人工检查。如果想进一步控制可以在服务端 Git 钩子比如 pre-receive里写检查逻辑但普通团队我不建议一上来就碰钩子维护脚本的时间不如先把 Push Rules 用熟。我见过某个团队用 pre-receive 钩子检查每次 push 是否包含临时文件结果没做异常处理误伤很多正常提交最后只能回滚配置。规则要渐进式落地别一次堆太多。从选型、分支策略、保护规则、制品库到最后的钩子和备份这整条链路都理顺后那家工业软件公司最终选型是 GitLab CE 加 Nexus代码、分支、制品各归其位。个人的体会是工具能帮你把规则执行到位但规则本身必须人先想清楚。如果让我现在总结一点所谓“经验”我会说先定义“main 怎么进、feature 分支活多久、制品怎么命名、保留几份”这四个问题的答案再去装工具。只要你把规则想清楚了GitLab 和 Gitea 都能帮你落地规则没想清楚上再贵的平台也是把原来的混乱搬到新界面里。我给团队做内网版本控制工具选型时最想做的一次性投入就是留出半天时间把所有分支、制品、备份的规则写成文档并让工具真正拦住违反规则的分支操作这几件小事比选哪个品牌更能决定团队以后几个月过得好不好。
返回列表