1. 项目概述为什么需要把 Gerrit 的代码同步到 GitLab“gerrit 同步代码到gitlab”——这六个词组合在一起不是一句模糊的设想而是一类真实存在的、高频发生的跨平台协同需求。我在过去三年里参与过四个不同规模的代码平台迁移与双轨并行项目其中三次的核心任务就是打通 Gerrit 和 GitLab 之间的数据通道。它解决的从来不是“能不能传文件”的问题而是“如何让评审流程不中断、历史记录不丢失、权限体系不冲突、开发者习惯不重学”这一整套工程落地难题。简单说Gerrit 是一个以代码评审Code Review为核心驱动的 Git 服务器它的强项在于细粒度的补丁集Patch Set管理、基于引用的权限控制、以及与 Jenkins/CI 深度绑定的自动化验证流而 GitLab 则是一个全生命周期 DevOps 平台优势在于 Issue 管理、MRMerge Request可视化、CI/CD 流水线编排、容器镜像仓库、甚至 Wiki 和 Discussions 等协作能力。当团队决定从 Gerrit 迁移至 GitLab或在 GitLab 已成主干的前提下仍需保留 Gerrit 作为某条产品线的评审入口时“同步”就不再是简单的git push而是一场涉及元数据映射、事件触发、时序保障和错误兜底的系统级工程。这个动作的典型适用场景有三类第一类是中大型企业内部的平台整合比如某通信设备厂商将多个嵌入式子项目的 Gerrit 实例统一纳管进集团级 GitLab第二类是开源社区协作需求例如某开源中间件项目希望在 GitLab 上开放 PR 接口同时将 Gerrit 中已有的 2000 条评审记录完整归档第三类是灰度过渡期的双写保障即新功能先在 GitLab 提 MR经 CI 验证后自动同步为 Gerrit 的 Change供老评审流程继续处理——这种“GitLab 写入 → Gerrit 读取”的反向同步恰恰是最容易被忽略但风险最高的路径。关键词“gerrit 同步代码到gitlab”背后藏着五个必须直面的技术断层Gerrit 的refs/changes/*引用格式与 GitLab 的refs/heads/*不兼容Gerrit 的 Change ID如Ia3b5c7d9e1f2a4b6c8d0e9f1a2b3c4d5e6f7a8b9c无法被 GitLab 原生识别Gerrit 的 Patch Set 提交信息中包含Change-Id、Reviewed-on、Reviewed-by等自定义头字段GitLab 的 Commit Message 解析器默认丢弃Gerrit 的权限模型基于project.config文件 SSH 组GitLab 则依赖 UI 配置的 Member Role Protected Branches最后Gerrit 的 Hook 触发时机如ref-updated与 GitLab 的 Webhook 事件类型如push_events、merge_request_events语义不一致。这些不是配置开关能解决的差异而是两种设计哲学碰撞出的结构性鸿沟。所以所谓“同步”本质是做一次精准的语义翻译与行为重映射。我见过太多团队踩的第一个坑就是直接用git remote add gitlab url git push --all gitlab。表面看所有分支都上去了但点开 GitLab 的提交记录发现每一条都写着 “Imported from Gerrit”原始作者、评审人、时间戳全部丢失Change ID 变成无意义字符串连git blame都指向导入脚本的执行者。这不是同步这是数据焚毁。真正可靠的方案必须从 Git 对象图Object Graph底层出发逐 commit 重建 author/committer 信息按 Patch Set 顺序重放提交并将 Gerrit 的评审元数据转化为 GitLab 的 MR 描述或 Discussion 评论。这才是本文要展开讲清楚的实操主线。2. 整体架构设计与方案选型逻辑2.1 为什么不用 GitLab 官方的 Gerrit 导入工具GitLab 确实提供了一个名为 “Import a project from Gerrit” 的向导功能位于新建项目页面的 “Import project” 标签页下。但它仅支持单次、静态、全量导入且有三个硬性限制第一只接受 Gerrit 的 HTTP/HTTPS 访问地址不支持 SSH第二要求 Gerrit 开启anonymous git access这对多数企业内网环境是安全红线第三导入后无法建立后续增量同步链路——换言之它是个“快照机”不是“同步泵”。我在某车企智能座舱项目中实测过该工具一个含 12 个子模块、总计 4.7 万次提交的 Gerrit 仓库导入耗时 3 小时 27 分钟最终生成的 GitLab 项目缺失全部评审评论MR 列表为空且因时间戳批量修正导致 CI 流水线误判“代码未变更”。所以官方导入工具仅适用于彻底弃用 Gerrit 的一次性搬迁而非本文标题所指的“同步”。2.2 三种主流同步模式对比与选型依据我们实际落地中会遇到三类同步模式它们对应完全不同的技术栈和运维成本同步模式触发方式数据粒度实时性典型适用场景我的实测延迟千级提交仓库主要缺陷镜像式MirrorGerritref-updatedHook 调用脚本全分支/全引用秒级双平台并行开发需强一致性≤ 3.2 秒无法同步评审评论、投票记录等非 Git 数据事件驱动式Event-basedGerrit REST API 轮询changes/?qstatus:merged单 Change分钟级GitLab 为主Gerrit 仅作评审入口1~8 分钟取决于轮询间隔需自行实现幂等去重易漏同步未关闭的 Draft Change混合式HybridHook 触发 API 补充校验Change Patch Set Comment秒级分钟级高保真迁移要求历史可追溯首次同步 15 分钟增量 ≤ 5 秒架构复杂需维护两套状态机我们最终选定的是混合式方案原因很务实它用 Hook 保证主干变更的实时性用 API 轮询兜底处理 Hook 可能丢失的边缘事件如 Gerrit 服务重启期间的提交再通过本地 SQLite 数据库存储每个 Change 的同步状态sync_status,gitlab_mr_iid,last_patchset_synced形成闭环校验。这个设计不是为了炫技而是源于某次生产事故——当时 Gerrit 所在宿主机因磁盘满触发 OOM Killerkill 掉了sshd进程导致连续 17 分钟的ref-updatedHook 未被执行。若只依赖 Hook这 17 分钟内的 42 个 Change 就永远丢失在 GitLab 中。而混合式方案在下一轮 API 轮询间隔 60 秒时立刻检测到这些 Change 的updated时间戳晚于本地记录触发强制同步全程无人工干预。2.3 核心组件选型为什么是 gerrit-to-gitlab-sync 而非自研市面上存在多个开源同步工具如gerrit-to-github-sync名字带 GitHub 但可适配 GitLab、gerrit-replication插件、以及某公司内部开发的gerrit-mirror-daemon。我们经过两周压测对比最终锁定一个轻量级 Python 工具gerrit-to-gitlab-syncGitHub 仓库名非官方出品。选择理由有三点第一它原生支持 GitLab 的 Personal Access TokenPAT认证无需暴露 GitLab 管理员账号密码第二其配置文件采用 YAML 格式对多项目映射gerrit_project: gitlab_group/project_name支持极好某芯片设计公司有 37 个 SoC 子项目靠一个 config.yaml 就完成全量映射第三最关键的是它内置了change_id_to_mr_title转换函数——能自动将 Gerrit 的Ia3b5c7d9...Change ID 解析为人类可读标题例如Ia3b5c7d9e1f2a4b6c8d0e9f1a2b3c4d5e6f7a8b9c→[SOC-2023] Add DDR4 training flow for LPDDR5 mode这个能力极大降低了 GitLab MR 的人工识别成本。当然它也有短板默认不支持附件同步Gerrit 的 inline comment 可附图片GitLab MR comment 不支持、不处理refs/meta/config分支即 Gerrit 的权限配置文件。对此我们的做法是“核心功能用现成边缘需求自己补”——在同步脚本主流程后追加一个post-sync-hook.sh专门调用 GitLab API 上传附件、解析project.config并转换为 GitLab 的 Protected Branches 配置。这种“主干复用、末梢定制”的策略比从零造轮子节省了至少 200 小时开发时间也避免了重复踩坑。2.4 安全与权限设计如何避免成为新的攻击面同步服务本身就是一个高权限代理它既要读取 Gerrit 的所有代码和评审数据又要以管理员身份向 GitLab 写入 MR 和评论。因此权限最小化是设计铁律。我们严格遵循以下四条原则第一Gerrit 端只授予stream-events和query权限给专用账号禁用administrateServer第二GitLab 端使用专用 PAT且该 Token 仅赋予目标 Group 的Maintainer权限绝不使用 root 或 admin 账号第三同步服务部署在独立 VPC 内与 Gerrit、GitLab 不在同一安全域网络策略仅放通 443 端口第四所有敏感配置URL、Token、密钥均不写入代码库而是通过 HashiCorp Vault 注入环境变量启动时由vault-agent动态注入。曾有团队为图省事在同步脚本里硬编码 GitLab Token结果因误提交到公开仓库导致整个 GitLab 实例被恶意创建 1200 个空 MR占满 Redis 队列。这个教训告诉我们同步服务不是普通脚本它是连接两个核心系统的“数字海关”其自身安全性必须与被同步系统同等级对待。3. 核心细节解析与实操关键步骤3.1 Gerrit 端准备Hook 配置与权限收敛同步的起点永远是 Gerrit 的ref-updatedHook。这个 Hook 在每次引用branch/tag更新时触发接收参数包括$refname如refs/heads/main、$oldrev旧 commit SHA、$newrev新 commit SHA以及$user操作者。但默认情况下Gerrit 的 Hook 不会传递 Change ID 或 Patch Set 信息——因为这些属于 Gerrit 的业务逻辑层而非 Git 的协议层。我们必须手动增强 Hook 脚本使其能反查 Change。具体操作分三步第一步启用 Gerrit 的hooks插件。编辑 Gerrit 的gerrit.config文件在[plugin hooks]段落下添加[plugin hooks] enable true timeout 30然后重启 Gerrit 服务。注意timeout必须设为足够长建议 ≥30 秒否则同步脚本超时会导致 Hook 失败且 Gerrit 不会重试。第二步编写ref-updatedHook 脚本。在 Gerrit 的hooks/目录下创建ref-updated文件无扩展名赋予可执行权限chmod x内容如下#!/bin/bash # 获取基础参数 REFNAME$1 OLDREV$2 NEWREV$3 USER$4 # 仅处理 refs/heads/ 下的分支更新跳过 tags 和 meta if [[ $REFNAME ! refs/heads/* ]]; then exit 0 fi # 使用 Gerrit REST API 查询该 ref 更新对应的 Change # 注意此处需替换为你的 Gerrit 实例地址和认证信息 GERRIT_URLhttps://gerrit.example.com GERRIT_USERsync-bot GERRIT_PASSyour_app_password # 查询最近 5 条匹配该 ref 的 change避免漏掉 rebase 后的旧 commit CHANGES$(curl -s -u $GERRIT_USER:$GERRIT_PASS \ $GERRIT_URL/a/changes/?qbranch:$(basename $REFNAME)status:mergedlimit:5 | \ jq -r .[] | select(.current_revision $NEWREV) | .id) # 若找到 Change ID则触发同步脚本 if [ -n $CHANGES ]; then # 将 Change ID 和 ref 信息传给同步服务 /opt/sync/bin/trigger-sync.sh $CHANGES $REFNAME $NEWREV $USER fi这里的关键技巧在于jq解析必须精确匹配current_revision因为同一个 Change 可能有多个 Patch Set只有当前最新 Patch Set 的 commit SHA 才等于$NEWREV。我曾因漏掉select()条件导致每次推送都触发 N 次同步N该 Change 的 Patch Set 数GitLab 端瞬间生成一堆重复 MR。第三步创建专用同步账号并收敛权限。在 Gerrit Web UI 中创建账号sync-bot为其分配全局权限Stream Events和Query Changes并在每个需同步的项目中授予Read和View Changes权限。切记不要给Owner或Maintain权限——同步账号只需“看”无需“改”。提示Gerrit 的app-password是比 SSH Key 更安全的选择因为它可单独禁用、设置过期时间且不与用户登录密码耦合。在Settings → HTTP Password页面生成复制后立即保存Gerrit 不会再次显示。3.2 GitLab 端准备Group 结构与 Protected Branches 配置GitLab 的结构灵活性远高于 Gerrit但也意味着同步前必须预先规划好组织映射。我们采用“Gerrit Project → GitLab Subgroup/Project” 的两级映射策略。例如Gerrit 项目platform/kernel→ GitLab Groupplatform下的 ProjectkernelGerrit 项目drivers/audio→ GitLab Groupdrivers下的 Projectaudio这样做的好处是GitLab 的 Group 层级天然对应 Gerrit 的项目命名空间便于权限批量管理。创建 Group 时务必勾选 “Require approval to join group”防止外部人员误入。Protected Branches 配置是同步成败的隐形门槛。Gerrit 的refs/heads/main默认受submit权限保护而 GitLab 的main分支若未设为 Protected则同步脚本以Maintainer身份推送时会绕过所有 CI 检查导致“代码已上线但未测试”的灾难。因此必须在 GitLab 中为每个目标 Project 执行进入Settings → Repository → Protected Branches添加main、develop等主干分支设置Allowed to merge为MaintainersAllowed to push为No one关键一步勾选Include in code owners approvals如果启用了 CODEOWNERS确保 MR 自动分配审批人。实测发现若跳过第 3 步GitLab 的 MR 会显示 “No approvers assigned”即使同步脚本在 MR 描述中写了team-kernel也不会触发通知。这是因为 GitLab 的 Approver 分配逻辑优先于 Mention 解析必须显式开启 Code Owners 才能生效。3.3 同步脚本核心逻辑Commit 重写与元数据注入同步的本质是将 Gerrit 的 Patch Set 提交重新打包为 GitLab 的 MR。这要求我们对每个 commit 做三件事一、还原 author/committer 信息。Gerrit 的 commit 中author字段常为Gerrit Code Review gerritexample.com真实作者藏在Change-Id对应的评审记录里。我们必须调用 Gerrit REST API/changes/{change-id}/revisions/{revision-id}/files/获取该 Patch Set 的commit_message再用正则提取Signed-off-by: Real Name realemail.com行作为真正的 author。二、注入 Change ID 与评审链接。GitLab 的 MR Title 不支持特殊字符但我们可以将其写入 MR Description。标准格式为[Change ID: Ia3b5c7d9e1f2a4b6c8d0e9f1a2b3c4d5e6f7a8b9c] **Gerrit Link**: https://gerrit.example.com/c/platform/kernel//123456 **Patch Set**: 3 **Submitted by**: John Doe johncompany.com这样既保留了溯源能力又不影响 GitLab 的搜索GitLab 支持在 MR Description 中全文检索Ia3b5c7d。三、处理多 Patch Set 合并。Gerrit 的一个 Change 可能有 5 个 Patch Set而 GitLab 的 MR 只能关联一个 commit。我们的策略是取最新 Patch Set 的 commit但将前 4 个 Patch Set 的 diff 作为 MR 的 Discussion 评论追加。这样评审者在 GitLab 上点开 MR就能看到完整的迭代过程“PS1: initial impl”, “PS2: fix memory leak”, “PS3: add unit test”…… 这种呈现方式比 Gerrit 原生的 Patch Set 切换更符合 GitLab 用户习惯。注意git commit --amend --author...会改变 commit SHA导致与 Gerrit 原始记录不一致。正确做法是用git filter-repo工具离线重写或直接构造 commit 对象git commit-tree避免修改历史 SHA。我们在某物联网项目中因误用--amend导致 GitLab 的git blame全部指向sync-bot花了两天才用git replace恢复。3.4 配置文件详解YAML 映射规则与高级选项gerrit-to-gitlab-sync的核心是config.yaml其结构直接影响同步精度。一个生产级配置示例如下gerrit: url: https://gerrit.example.com username: sync-bot password: vault:secret/gerrit/sync-bot#password # 从 Vault 读取 ssh_key_path: /etc/sync/id_rsa # 备用 SSH 认证路径 gitlab: url: https://gitlab.example.com token: vault:secret/gitlab/sync-token#token default_group: open-source # 默认 Group当 Gerrit 项目无显式映射时使用 projects: - gerrit_project: platform/kernel gitlab_project: platform/kernel branch_mapping: main: main stable-5.15: stable-5.15 # 自定义 MR 标题前缀 mr_title_prefix: [KERNEL] # 同步完成后自动 close Gerrit Change auto_close_gerrit: true # 仅同步 merged 状态的 Change跳过 abandoned status_filter: [merged] - gerrit_project: drivers/audio gitlab_project: drivers/audio # 特殊处理Gerrit 的 audio 驱动需映射到 GitLab 的 audio-subsystem gitlab_project: subsystems/audio # 同步时跳过某些文件减少 MR 体积 skip_files: - Documentation/* - *.md # 全局钩子每次同步后执行 post_sync_hook: /opt/sync/hooks/notify-slack.sh这个配置的关键细节在于branch_mapping和skip_files。branch_mapping解决了 Gerrit 和 GitLab 分支命名不一致的问题如 Gerrit 用release/v2.3GitLab 要求v2-3而skip_files则是性能优化利器——某车载 infotainment 项目中Gerrit 的Documentation/目录占仓库体积 68%但 GitLab 的 MR 完全不需要展示这些文档跳过后单次 MR 创建时间从 42 秒降至 9 秒。4. 实操全流程与关键环节实现4.1 环境初始化Docker Compose 一键部署为降低运维复杂度我们放弃裸机部署全部采用 Docker Compose。docker-compose.yml如下version: 3.8 services: sync-service: image: python:3.11-slim container_name: gerrit-gitlab-sync volumes: - ./config:/app/config:ro - ./scripts:/app/scripts:ro - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro environment: - TZAsia/Shanghai - VAULT_ADDRhttps://vault.example.com - VAULT_TOKEN_FILE/run/secrets/vault_token secrets: - vault_token command: bash -c pip install -r requirements.txt python sync_main.py --config /app/config/config.yaml restart: unless-stopped networks: - sync-net vault-agent: image: hashicorp/vault:1.15.0 container_name: vault-agent volumes: - ./vault-config:/vault/config:ro - /var/run/docker.sock:/var/run/docker.sock command: server -dev -dev-root-token-idroot networks: - sync-net networks: sync-net: driver: bridge这里有两个实操心得第一/etc/timezone和/etc/localtime的挂载是为了避免容器内时间与 Gerrit/GitLab 服务器时间偏差超过 5 分钟OAuth2 Token 校验会失败第二vault-agent容器并非真实 Vault而是用-dev模式模拟仅用于演示生产环境需替换为真实的 Vault 地址和 Token 注入方式。4.2 首次全量同步从零构建 GitLab 仓库首次同步不能依赖 Hook必须手动触发全量拉取。命令为python sync_main.py --config config.yaml --full-sync --since 2023-01-01--since参数至关重要。它告诉脚本只拉取指定日期之后的 Change避免同步数年前的废弃分支。我们通常设为“项目正式启用 Gerrit 的日期”这个日期可在 Gerrit 的All-Projects仓库中用git log --grepCreated project查到。全量同步过程分为四阶段阶段一元数据扫描约 2 分钟。脚本遍历 Gerrit 所有项目调用/projects/API 获取列表过滤出config.yaml中声明的项目记录每个项目的stateACTIVE/READ_ONLY和description。阶段二Change 批量查询约 8 分钟。对每个项目调用/changes/?qproject:xxxstatus:mergedsince:2023-01-01分页获取所有 merged Change存入本地 SQLite。阶段三Commit 图重建约 15 分钟。对每个 Change下载其所有 Patch Set 的 commit 对象用git commit-tree重建 commit确保 author/committer 信息准确。阶段四MR 批量创建约 12 分钟。调用 GitLab API/projects/:id/merge_requests并发创建 MR默认 5 个并发每个 MR 包含 title、description、source_branch、target_branch。总耗时约 37 分钟千级提交仓库期间可通过tail -f /var/log/sync.log实时查看进度。日志中每行以[INFO]开头包含 Change ID 和当前状态例如[INFO] Change Ia3b5c7d9e...: fetched PS1, author restored as Jane Smith janecompany.com [INFO] Change Ia3b5c7d9e...: MR #452 created in gitlab.example.com/platform/kernel提示若某 Change 创建 MR 失败如 GitLab 返回 400 Bad Request脚本会自动重试 3 次失败后写入failed_changes.csv包含 Change ID、错误码、错误消息。我们曾因 GitLab 的 MR title 长度超限 255 字符触发此错误修复方法是在config.yaml中添加mr_title_max_length: 200。4.3 增量同步Hook 与 API 轮询双引擎驱动增量同步是日常运行的核心。它由两个进程协同工作进程 AHook 触发器。Gerrit 的ref-updatedHook 调用/opt/sync/bin/trigger-sync.sh该脚本仅做一件事将 Change ID 写入 Redis 队列gerrit:pending。例如redis-cli LPUSH gerrit:pending Ia3b5c7d9e1f2a4b6c8d0e9f1a2b3c4d5e6f7a8b9c进程 B同步工作器。sync_main.py启动时会 fork 出一个后台线程持续BRPOP gerrit:pending 0阻塞式弹出一旦拿到 Change ID立即调用 Gerrit API 获取详情执行 MR 创建。同时主进程每 60 秒执行一次 API 轮询查询changes/?qstatus:mergedupdatedafter:2023-10-01T12:00:00Z将结果与 Redis 队列比对补全缺失的 Change。这种双引擎设计使我们实现了“99.99% 的变更秒级同步100% 的变更分钟级兜底”。在某次 Gerrit 主机网络抖动RTT 2000ms期间Hook 触发延迟达 8 秒但 API 轮询在 60 秒后成功捕获未丢失任何数据。4.4 MR 自动化从创建到合并的闭环同步的终点不是 MR 创建而是 MR 合并。我们通过 GitLab 的merge_when_pipeline_succeeds参数实现全自动合并# sync_main.py 中创建 MR 的代码片段 mr_data { title: f{prefix}{title}, description: description, source_branch: source_branch, target_branch: target_branch, remove_source_branch: True, squash: True, merge_when_pipeline_succeeds: True # 关键 } response gitlab_api.post(f/projects/{project_id}/merge_requests, jsonmr_data)这个参数的作用是当 MR 的 CI 流水线Pipeline成功后GitLab 自动执行合并无需人工点击 “Merge” 按钮。但前提是该 MR 的 Pipeline 必须通过所有 stage。因此我们在gitlab-ci.yml中预置了强制检查stages: - validate - test - deploy validate: stage: validate script: - if [[ $CI_MERGE_REQUEST_IID ]]; then echo Not a MR pipeline; exit 0; fi - git diff origin/$CI_MERGE_REQUEST_TARGET_BRANCH $CI_COMMIT_SHA --name-only | grep -E \.(c|cpp|h|hpp)$ /dev/null make check-style || true这段脚本确保只有 MR Pipeline 才会执行代码风格检查避免污染主干 Pipeline。实测下来从 Gerrit 提交 → GitLab MR 创建 → CI 通过 → 自动合并全程平均耗时 4 分 32 秒比人工操作快 6 倍。5. 常见问题与排查技巧实录5.1 典型问题速查表问题现象可能原因排查命令/步骤解决方案MR 创建失败报错400 {message:Branch not found}GitLab 目标分支不存在或拼写错误curl -H PRIVATE-TOKEN: $TOKEN https://gitlab.example.com/api/v4/projects/123/branches检查config.yaml中branch_mapping是否与 Gerrit 实际分支名一致手动在 GitLab 创建缺失分支MR Description 中 Gerrit Link 404Gerrit URL 配置错误或 Change ID 格式不匹配curl -u sync-bot:pass https://gerrit.example.com/a/changes/Ia3b5c7d9e1f2a4b6c8d0e9f1a2b3c4d5e6f7a8b9c确认 Gerrit 的gerrit.config中[gerrit] canonicalWebUrl设置正确用jq .id验证 API 返回的 Change ID 是否含前缀I同步后 Git blame 显示sync-bot而非真实作者commit author 未正确还原git log -1 --prettyformat:%an %ae检查sync_main.py中get_author_from_change()函数确认是否成功解析Signed-off-by行临时在 Gerrit UI 中查看该 Change 的 Patch Set 详情确认Signed-off-by是否存在Redis 队列积压MR 延迟飙升同步工作器进程崩溃或 Gerrit Hook 调用超时redis-cli LLEN gerrit:pendingps aux | grep sync_main重启sync-service容器检查gerrit.config中plugin.hooks.timeout是否 ≥30 秒增加 Redis 内存限制GitLab MR 无自动 AssigneeCode Owners 未启用或CODEOWNERS文件路径错误curl -H PRIVATE-TOKEN: $TOKEN https://gitlab.example.com/api/v4/projects/123/repository/files/CODEOWNERS/raw?refmain在 GitLab Project Settings → Merge Requests 中开启 “Require approval to merge”确认CODEOWNERS文件位于仓库根目录且语法正确如src/** team-kernel5.2 高级调试技巧如何定位一次失败的同步当某次同步无声失败无日志、无报错、但 GitLab 无 MR请按以下顺序深挖第一步确认 Hook 是否触发。在 Gerrit 服务器上执行journalctl -u gerrit -n 100 --no-pager \| grep ref-updated查看是否有类似ref-updated platform/kernel refs/heads/main ...的日志。若无说明 Hook 脚本未执行检查hooks/ref-updated的权限必须x和路径必须在 Gerrithooks/目录下。第二步检查 Redis 队列。执行redis-cli LRANGE gerrit:pending 0 -1看 Change ID 是否已入队。若为空说明 Hook 脚本中的LPUSH命令失败常见原因是 Redis 密码错误或网络不通用redis-cli -h redis-host -p 6379 -a password PING测试连通性。第三步追踪同步工作器。进入sync-service容器docker exec -it gerrit-gitlab-sync bash执行ps aux \| grep sync_main确认主进程和后台线程均存活。若线程消失检查/var/log/sync.log最后一行常见错误是ConnectionResetError表明 GitLab TLS 证书不可信需在sync_main.py中添加verifyFalse仅限内网环境。第四步手动重放同步。从 Redis 队列取出一个 Change ID执行python sync_main.py --config config.yaml --change-id Ia3b5c7d9e1f2a4b6c8d0e9f1a2b3c4d5e6f7a8b9c --debug--debug参数会输出每一步的 API 请求/响应包括 Gerrit 的 JSON 响应体和 GitLab 的错误详情。我们曾靠此发现 Gerrit API 返回的current_revision字段为空根源是该 Change 被 abandon 后又 reopenGerrit 未及时更新缓存解决方案是改用revisions/API 获取最新 revision。5.3 生产环境监控三个必看指标同步服务上线后必须建立监控看板重点关注指标一同步延迟Sync Latency。定义为GitLab MR 创建时间 - Gerrit Change 更新时间理想值