欢迎光临
我们一直在努力

接手外包项目后的两件事:修复分裂的 Git 历史 + 搭建 Gitee 推送自动部署(含 10 个真实踩坑)

前言

接手一个外包开发的 PHP 项目,做了两件事:把分裂成两条的 Git 历史合回一条,以及打通「本地 push → Gitee → 宝塔服务器自动更新」的链路。

过程中踩的坑比预想多得多。网上教程大多只写理想路径,而真实环境里挡路的是自签证书、老版本 Git 的行为差异、插件路由未注册、面板反扫描机制这类细节。本文按实际排查顺序记录,每个坑都给出定位方法和判据。

环境:CentOS 7 + 宝塔面板 v11.8.1 + PHP 7.4 + Gitee 私有仓库。


一、两个 master:不是分支,是两棵树

现象

在 VSCode 的 Git 图里看到两条完全平行的提交线,各自带着一个 master 标签:一条是 origin/master(3 个提交),另一条是本地 master(74 个提交)。

定位

git merge-base master origin/master
# 无输出 —— 没有共同祖先

git rev-list –max-parents=0 –all
# 输出两个根提交

两个根提交,意味着这是两棵互不相干的历史树(unrelated histories),不是普通的分支分叉。

成因

接手项目时,没有沿用原有的 .git 目录,而是在工作目录里重新 git init + 一次性 “first commit” 推到了自己的新仓库。外包的 74 条提交历史被压扁成了一个提交。而本地的克隆仍然保留着原始历史,于是两边对不上。

修复

先确认两边的代码内容差异有多大:

git diff –stat master origin/master

如果差异只是几个文件,说明代码基本一致,只是历史被压扁了。那么把远程那几个「真实提交」摘到本地这条完整历史上,再覆盖远程:

git fetch origin
git cherry-pick <commit1> <commit2> # 只摘有内容的提交,跳过那个全量快照式的 first commit
git push –force-with-lease origin master

cherry-pick 只看提交的 diff,不要求共同祖先,所以跨历史树也能用。

强推前必须确认两件事:

  • 有没有别人也在用这个仓库?有的话,他们必须重新 clone。
  • 旧历史里有没有敏感信息?如果当初压扁历史正是为了抹掉密钥,那就不该恢复,应该反过来以远程为准。
  • 正确的接手姿势

    以后遇到「接手项目、换到自己的仓库」,保留原 .git 目录,直接改远程地址即可,历史不会断:

    git remote set-url origin <新仓库地址>
    git push -u origin master


    二、清理运行时产物

    老项目常见问题:缓存、日志、上传文件全被跟踪,每次 git status 都是一堆脏 diff。

    # 更新 .gitignore 后
    git rm –cached cache/xxx.json member/callback/xxx.log
    git commit -m "chore: 忽略运行时产物"

    ⚠️ 这里有个容易忽略的连锁反应:git rm –cached 提交之后,服务器执行 git pull 时会把这些文件从磁盘上删掉。缓存文件无所谓(会重新生成),但日志文件里的历史记录会丢。

    另一个判断点:不要无差别地忽略整个 uploads/。这类目录常常混着「后台上传的真实站点资产」(logo、广告图)和「程序生成的临时文件」。全忽略会导致重新部署时资产丢失。只忽略生成物:

    cache/
    uploads/sample_report_*.html
    *.log


    三、搭建 Gitee → 宝塔自动部署

    目标链路:

    本地 git push → Gitee WebHook → 宝塔 WebHook 插件 → 服务器 git fetch + reset –hard

    坑 1:Gitee 拒绝自签证书

    第一次配好 WebHook,测试报错:

    SSLHandshakeException: PKIX path building failed:
    sun.security.provider.certpath.SunCertPathBuilderException:
    unable to find valid certification path to requested target

    这个报错其实是好消息——TCP 连接已经建立,说明云服务器安全组和防火墙都放行了,卡住的只是 TLS 握手。

    原因:宝塔面板默认用自签证书,而 WebHook 地址填的是 IP。Gitee 的 Java 客户端校验证书链时找不到可信签发机构。自签证书天然不在任何信任库里,这是必然结果。

    解法:给面板绑一个子域名并申请 Let’s Encrypt 证书。

    # 1. DNS 加 A 记录:bt.example.com → 服务器 IP
    # 2. 验证解析生效(注意:本机若开着代理,ping 可能返回 198.18.x.x 这类 fake-ip,结果不可信)
    nslookup bt.example.com 223.5.5.5

    # 3. 面板 → 设置 → 常用设置 → 绑定域名 → 填 bt.example.com
    # 4. 面板 → 设置 → 常用设置 → 面板SSL配置 → Let's Encrypt → 文件验证 → 确定

    绑定域名前先记下解锁命令。一旦绑定,就只能通过域名访问面板;DNS 出问题会把自己关在门外:

    rm -f /www/server/panel/data/domain.conf && bt restart

    申请证书时选「文件验证」即可,宝塔会自动创建一个用于验证和续签的站点,不要手动删除它,删了 90 天后证书就续不上。

    验证证书是否真的生效,不要看浏览器(点过「继续访问」会留下陈旧例外,一直标红)。用 curl,它和 Gitee 一样走系统信任库:

    curl.exe -I https://bt.example.com:8888/

    正常返回 HTTP 头 = 证书受信任;报 SSL certificate problem = 没成功。

    坑 2:装完插件必须重启面板

    WebHook 插件装好、钩子也建了,请求 /hook 却返回 404:

    <html><head><title>404 Not Found</title></head>
    <body><center><h1>404 Not Found</h1></center>
    <hr><center>nginx</center></body></html>

    一开始怀疑是安全入口拦截。排查发现不是:

    ss -lntp | grep 8888
    # LISTEN 0 100 *:8888 users:(("BT-Panel",pid=xxx,fd=3))

    nginx -T 2>/dev/null | grep 8888
    # 无输出

    端口是 BT-Panel(Python)自己在监听,nginx 根本没参与。那个 Server: nginx 响应头是宝塔伪装的。所以 404 来自面板本体——/hook 路由没被加载。

    解法:

    bt restart

    重启后立刻返回 {"code": 1} + HTTP 200。插件安装后路由需要重新注册,这一步几乎所有教程都没提。

    坑 3:安全入口不影响 /hook

    顺带澄清一个常见误解:宝塔开启安全入口后,/hook 接口不受影响,不需要在 URL 里加入口前缀。实测加了反而还是 404。

    不要为了让 webhook 工作而关闭安全入口——那等于把面板登录页直接挂在公网上。

    坑 4:宝塔反扫描机制会让 curl 误判

    排查面板可达性时,用 curl 请求安全入口地址返回 404:

    curl -s -o /dev/null -w "%{http_code}\\n" https://bt.example.com:8888/a1b2c3d4
    # 404

    一度以为面板挂了。但服务端明明正常:

    bt status
    # Bt-Panel (pid 952) already running
    # Bt-Task (pid 965) already running

    cat /www/server/panel/data/admin_path.pl
    # /a1b2c3d4 —— 入口配置正确

    真相是宝塔的反扫描机制:检测到非浏览器 User-Agent 就一律返回 404,避免被扫描器探测到面板入口。加上浏览器 UA 立刻正常:

    curl -s -i -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \\
    (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
    \\
    https://bt.example.com:8888/a1b2c3d4 | head -5

    # HTTP/1.1 200 OK
    # Content-Type: text/html; charset=utf-8
    # Set-Cookie: xxx_ssl=…; HttpOnly; Path=/

    排查启示:用命令行工具测宝塔面板的 HTTP 可达性时,务必带浏览器 UA,否则得到的 404 完全没有诊断价值。这个特性也解释了排查 /hook 时看到的部分 404 噪音。

    另外,/login 这类路径在开启安全入口后会返回「请使用正确的入口登录面板」页面——这是正常的拦截,不是故障。完整访问地址必须带入口路径:

    https://bt.example.com:8888/a1b2c3d4

    忘了入口是什么,在服务器上执行:

    /etc/init.d/bt default


    四、服务器目录接管为 Git 仓库

    坑 5:dubious ownership

    cd /www/wwwroot/example.com
    git config –global –add safe.directory /www/wwwroot/example.com
    git init
    git remote add origin git@gitee.com:user/repo.git
    git fetch origin
    git checkout -f -b master –track origin/master
    chown -R www:www /www/wwwroot/example.com

    safe.directory 那条是必需的。Git 2.35+ 有防护:目录属主(www)与执行者(root)不一致时会拒绝操作。不加这条,后面钩子自动执行时也会撞上同样的问题。

    最后的 chown 同样不能省——Git 以 root 身份写入的文件属主是 root,而 PHP-FPM 跑在 www 下,不改会导致部分功能没权限读写。

    git init 和 git fetch 都不会动工作目录里的现有文件,真正覆盖文件的是 git checkout -f。这一步执行前务必有可用备份。

    认证用部署公钥,不用账号密码

    ssh-keygen -t ed25519 -C "deploy@server" -f /root/.ssh/gitee_deploy -N ""
    cat /root/.ssh/gitee_deploy.pub
    # 贴到 Gitee → 仓库 → 管理 → 部署公钥管理

    printf 'Host gitee.com\\n IdentityFile /root/.ssh/gitee_deploy\\n StrictHostKeyChecking no\\n' >> /root/.ssh/config
    ssh -T git@gitee.com

    两个细节:

    • -N "" 必须空密码。钩子是无人值守执行的,有密码就无法自动认证。
    • StrictHostKeyChecking no 避免首次连接弹出「是否信任此主机」的交互确认——自动执行时没人能回答,会直接卡死。

    部署公钥是只读的,服务器即使被拿下也改不了仓库,比账号密码安全得多。

    坑 6:Git 1.8 不更新 origin/master

    CentOS 7 自带 Git 1.8.3.1(2013 年)。钩子脚本这样写会永远部署不到最新代码:

    git fetch origin master
    git reset –hard origin/master # ❌ 重置到过期的引用

    原因:老版本里 git fetch origin master 只把结果写进 FETCH_HEAD,不更新 refs/remotes/origin/master。

    现象很具迷惑性:fetch 明明成功了,输出也正常,但 git log 显示的还是旧提交。

    坑 7:FETCH_HEAD 同名文件

    改成 git reset –hard FETCH_HEAD 后又报:

    fatal: ambiguous argument 'FETCH_HEAD': both revision and filename

    工作目录里恰好有个叫 FETCH_HEAD 的空文件(某次误操作的重定向产物),Git 无法判断你指的是文件还是引用。

    最终的稳妥写法——用显式 refspec,既绕开版本差异,也不依赖任何容易被同名文件干扰的特殊名字:

    #!/bin/bash
    set -e
    SITE_DIR=/www/wwwroot/example.com
    cd "$SITE_DIR"
    git fetch origin +master:refs/remotes/origin/master
    git reset –hard origin/master
    chown -R www:www "$SITE_DIR"
    echo "deployed: $(git rev-parse –short HEAD) at $(date)"

    +master:refs/remotes/origin/master 显式指定「把远程 master 强制写入本地的远程跟踪引用」,行为在所有 Git 版本上一致。


    五、三个安全问题

    坑 8:备份包放进了网站根目录

    cd /www/wwwroot
    tar czf ~/backup.tar.gz example.com # 结果落在了站点目录里

    宝塔默认的 nginx 敏感文件规则挡了 .sql、.bak、.log,但不挡 .tar.gz。也就是说全站源码(含 config/config.php 里的数据库密码)可以被任何人直接下载。

    写备份命令用绝对路径 + -C 参数:

    tar czf /root/backup-$(date +%F).tar.gz -C /www/wwwroot example.com

    坑 9:备份是空的

    更严重的问题:上面那个错位的备份,实际大小只有 45 字节——一个空 gzip 归档。整个接管过程实际上没有安全网。

    备份必须验证,不能假设它成功了:

    ls -lh /root/backup-*.tar.gz # 看大小
    tar tzf /root/backup-*.tar.gz | wc -l # 看文件数

    同理,备份命令里慎用 2>/dev/null——它会把「文件不存在」这类关键错误一并吞掉。

    坑 10:明文密码躺在服务器上

    cat /root/.git-credentials
    # https://user:password@gitee.com
    # https://user:password@codeup.aliyun.com

    git config –list | grep credential
    # credential.helper=store

    credential.helper=store 是全局配置,此后任何在这台机器上输入的 Git 密码都会明文追加进这个文件。而外包开发者有过这台服务器的 root 权限。

    改用 SSH 后清理:

    git config –global –unset credential.helper
    rm -f /root/.git-credentials

    先 unset 再删,否则下次触发认证又会重新生成。

    清理文件不等于凭据安全——密码已经暴露过,必须去平台后台轮换。同理,代码里的硬编码密钥即使被 refactor 掉了,只要旧值没换,历史记录里的那份依然有效。


    六、验证与恢复

    端到端验证

    # 本地
    git commit –allow-empty -m "test: 验证自动部署"
    git push origin master

    # 15 秒后在服务器
    cd /www/wwwroot/example.com && git log –oneline -1

    同时检查未被跟踪的配置文件是否安然无恙:

    ls -l config/config.php .user.ini # 修改时间应该还是很久以前

    以及敏感路径防护:

    for u in "https://example.com/" "https://example.com/.git/config" "https://example.com/.git/HEAD"; do
    printf "%-45s %s\\n" "$u" "$(curl -s -o /dev/null -w '%{http_code}' "$u")"
    done
    # 首页 200,.git 路径全部 404

    顺带一提:宝塔默认的 nginx 站点配置里已经包含了「敏感目录」规则,.git、.svn、.idea 等都在其中,通常不需要额外添加。加之前先读一遍现有配置——nginx 的正则 location 按定义顺序匹配,追加到文件末尾的规则很可能永远不会命中,成为死代码。

    误删文件的恢复

    被 git rm –cached 移出版本库、又被 reset –hard 从磁盘删掉的文件,内容仍然在 Git 历史里。从移除操作的父提交里取出来即可:

    git show <移除提交>^:path/to/file > /root/file.bak
    cp /root/file.bak path/to/file
    chown www:www path/to/file

    因为该文件现在已被 .gitignore 忽略,还原后不会再被后续部署删除。


    总结:Checklist

    搭建这类自动部署时,按这个顺序走能少踩很多坑:

  • ✅ 备份,并验证归档大小和文件数
  • ✅ 备份放 /root,绝不放网站根目录
  • ✅ 面板绑定域名 + Let’s Encrypt 证书(自签证书过不了 Gitee 的校验)
  • ✅ 用 curl 而非浏览器验证证书是否受信任
  • ✅ 用命令行测面板可达性时带浏览器 UA,否则反扫描机制会返回误导性的 404
  • ✅ WebHook 插件装完 bt restart
  • ✅ 用部署公钥(只读、空密码)而非账号密码
  • ✅ 配 safe.directory,部署后 chown
  • ✅ 脚本用显式 refspec,不依赖 origin/master 或 FETCH_HEAD 的隐式行为
  • ✅ 清理 .git-credentials 并去平台轮换密码
  • ✅ 空提交做端到端验证,确认配置文件未被触碰
  • 最大的教训其实不是技术性的:每一个「应该没问题」的环节都要有验证判据,而每一个报错也要先确认它是否真的是报错。

    这次遇到的两类问题恰好构成对照——备份「看起来成功了」,实际是 45 字节的空壳;面板「看起来挂了」,实际是反扫描机制的正常行为。前者要验大小、验文件数,后者要换个 UA 再试。假设和结论之间,永远需要一条可执行的判据。


    文中所有 IP、域名、密钥、路径均已替换为占位符。

    赞(0)
    未经允许不得转载:171主机测评 » 接手外包项目后的两件事:修复分裂的 Git 历史 + 搭建 Gitee 推送自动部署(含 10 个真实踩坑)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址