普通视图

发现新文章,点击刷新页面。
今天 — 2025年4月3日首页

git subtree 最佳实践

作者 bug修士
2025年4月3日 09:35

目录

1背景

1.1 痛点

目前业务主要有A端和B端两个系统,这两个系统技术栈是完全相同的,许多功能也相同。所以在日常的开发过程中,产生了大量的重复工作,一个需求在A端完成后,还需要复制到B端,这样往往容易出现疏漏。

1.2 解决思路

实现代码复用目前,有下面两种方法:

  • 抽象成NPM包进行复用

  • 使用Git的子仓库对代码进行复用

由于本项目要实现业务代码复用,抽成 npm 包的方式就不太合适。

1.3 什么是git子仓库

通俗上的理解, 一个Git仓库下面放了多个其他的Git仓库,其他的Git仓库就是我们父级仓库的子仓库。

通过使用git子仓库将公共的组件抽离出来,实现在一端更改后,另一端通过git去合并代码,将我们从繁重的复制粘贴中解放出来。同时,可以在后续的需求中放入公共组件,通过增量的方式去应用这个技术,不会影响以前的代码。

1.4 git的两种子仓库方案

目前git实现子仓库有下面两种方案:

  1. git submodule。 tdesign 使用的就是这种方案。

  2. git subtree

两种方案的对比如下:

维度 subtree submodule 优劣对比
空间占用 subtree 在初始化 add 时,会将子仓库 copy 到父仓库中,并产生至少一次 merge 记录。所以会占用大量父仓库空间 submodule 在初始化 add 时,会在父仓库新建一个 .gitmodules 文件,用于保存子仓库的 commit hash 引用。所以不会占用父仓库空间 submodule 更优
clone subtree add 至父仓库之后,后续的 clone 操作与单一仓库操作相同 后续 clone 时 submodule 还需要 init/update 操作,且 submodule 子仓库有自己的分支。 流水线部署时需要更改配置。 subtree 更优
update 子仓库更新后,父仓库需要 subtree pull 操作,且命令行略长,需要指定 --prefix 参数。由于无法感知子仓库的存在,可能会产生 merge 冲突需要处理 子仓库更新后,父仓库需要 submodule update 操作。父仓库只需变动子仓库 hash 引用,不会出现冲突 submodule 更优
commit 父仓库直接提交父子仓库目录里的变动。若修改了子仓库的文件,则需要执行 subtree push 父子仓库的变动需要单独分别提交。且注意先提交子仓库再提交父仓库 subtree 更优

用一句话来描述 Git Subtree 的优势就是:

经由 Git Subtree 来维护的子项目代码,对于父项目来说是透明的,所有的开发人员看到的就是一个普通的目录,原来怎么做现在依旧那么做,只需要维护这个 Subtree 的人在合适的时候去做同步代码的操作。

1.5 git subtree 对现有项目的影响

使用git subtree 无需改变现有工程结构,可以只在新需求中使用它去复用代码,相当于它只是一个复制粘贴的工具。

2方案设计

2.1 创建子仓库

建立一个单独的git仓库命名为 common , 可以创建如下的目录结构:

-common
  -utils 公共的工具函数
  -services 接口
  -components 公共的组件
  -hooks 公共的hooks

2.2 关联子仓库

然后在A端和B端添加common的远程仓库:

 git remote add common [common仓库地址]

建立父仓库和子仓库的依赖关系:

git subtree add --prefix=src/common common master

将common远程仓库的master分支拷贝到父仓库的 src/common 目录下, 这时在两个项目的src目录多一个 common 的文件夹,我们可以像一个本地目录一样去使用里面的代码。

--prefix 可以用 -P 来代替,见下文。

2.3 拉取子仓库更新

git subtree pull -P src/common common master

2.4 推送更改到子仓库

方法一 直接提交

git subtree push -P src/common common master

subtree push实际上是遍历本工程每一次提交,把提交文件涉及到subtree目录的挑出来,同步到subtree工程,如果提交有很多,速度会非常慢。

方法二 拆分代码再push[推荐]

git subtree split --rejoin -P src/common
git subtree push -P src/common common master

如果想要split成功,一定要去除 commit msg 的校验。

方法三 拆分代码到单独分支

git subtree split --rejoin -P src/common -b split-common
git push common split-common

首先将 common 拆分到父仓库的 split-common 分支,可以通过 checkout 到这个分支查看内容。

2.5 删除子仓库

git rm -r src/common

2.5 细节

在开发一个需求的时候, A端更改了 common 后,其他人只需要向以前一样在父仓库拉取代码。而当想在B端使用 common 代码,则需要将A端的代码同步到common 仓库,B拉取一下就行。

问题

git subtree split 无效

我们项目是基于 umi 脚手架开发的项目,这个脚手架自带了一个 gitHooks 会对 commit 的msg进行校验,而git subtree split 的原理就是通过 msg 进行判断。 解决方法:去掉 package.json 中的 commit 校验

{
  "gitHooks": {
  }
}

修改后没有同步

问题描述

修改一个后,没有push代码,慢慢导致后面两端的子仓库出现差异, 出现代码冲突。

解决方法

每次修改公共的代码都要 push 和 pull, 手动保持一致。

git subtree pull 冲突

错误信息如下

fatal: refusing to merge unrelated histories

解决方法, 在 git subtree pull 时添加 --squash 参数, 类似于 git push 的 --allow-unrelated-historie参数。

git subtree push 不上去

git push using:  common feature/20221214
cache for f1156335aca1314ff75ba328a850cbdd13affb5a already exists!

stackoverflow.com/questions/6…

暂时无法解决

参考文章

# 为什么你的公司不应该使用git submodule # Git subtree用法与常见问题分析 # 用 Git Subtree 在多个 Git 项目间双向同步子项目 # Git subtree 要不要使用 –squash 参数 # 掌握Git的subtree[译]

昨天 — 2025年4月2日首页

Caddy Web服务器初体验:简洁高效的现代选择

2025年4月2日 19:16

Caddy简介

Caddy是一款使用Go语言编写的开源Web服务器和反向代理服务器,旨在提供易于使用且高效的性能。它支持HTTP/HTTPS协议,并可作为反向代理服务器、负载均衡器和WebSocket支持等。Caddy的灵活性和模块化架构使其适合容器化环境和微服务架构。

Caddy的主要特点

  1. 默认启用HTTPS:Caddy集成了Let’s Encrypt,可以自动申请、更新和管理SSL证书,无需额外操作。
  2. 配置简洁:Caddy的配置文件(Caddyfile)简洁易读,降低了新手的学习成本。
  3. 动态配置管理:通过REST API,可以在运行时更改Caddy的配置,无需重新启动服务器。
  4. 现代化特性:支持Prometheus metrics,使用结构化的JSON作为访问日志。

Caddy与Nginx的对比

特性 Caddy Nginx
配置方式 Caddyfile, JSON, REST API Nginx配置文件(nginx.conf)
自动HTTPS支持 是,默认启用自动TLS证书管理 否,需手动配置SSL证书
适用范围 7层(应用层),反向代理和Web服务,内置负载均衡 支持4层(传输层)和7层(应用层)反向代理、负载均衡等
扩展性 插件化架构,支持扩展 模块化架构,支持静态编译的模块
性能 较高(适合轻量应用) 非常高(适合高并发应用)
配置简洁性 Caddyfile格式简洁,易于上手 配置相对复杂,灵活但不够直观
系统资源占用 较低 较低,适合高并发处理
编写语言 Go语言 C语言
Access日志格式 结构化,默认JSON格式,支持自定义 非结构化,默认标准日志格式,支持自定义

Caddy的基本用法

安装方式

  1. 二进制安装:下载Caddy的二进制文件并移动到PATH下即可使用。
  2. Docker Compose安装:使用Docker容器快速部署Caddy。

Docker Compose配置示例

version: "3.8"
services:
  caddy:
    image: caddy:latest
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    environment:
      - ACME_AGREE=true

volumes:
  caddy_data:
  caddy_config:

配置方式

  1. Caddyfile配置:简洁易读的配置文件。
  2. JSON配置:适合高级使用场景和动态配置。
  3. REST API配置:动态管理和变更配置。

Caddyfile示例

example.com {
    reverse_proxy 127.0.0.1:3000
    log {
        output file /var/log/caddy/access.log {
            mode 644
        }
        format json
    }
}

JSON配置示例

{
  "apps": {
    "http": {
      "servers": {
        "example": {
          "listen": [":80"],
          "routes": [
            {
              "match": [
                {
                  "host": ["example.com"]
                }
              ],
              "handle": [
                {
                  "handler": "static_response",
                  "body": "Hello, world!"
                }
              ]
            }
          ]
        }
      }
    }
  }
}

常见配置示例

  1. 直接回复

    localhost:2017 {
        respond "Hello, world!"
    }
    
  2. 配置静态文件

    localhost:2016 {
        root * /var/www/mysite
        file_server {
            browse
            hide .git
            precompressed zstd br gzip
        }
    }
    
  3. 配置反向代理

    example.com {
        reverse_proxy localhost:8000
    }
    
  4. 配置负载均衡

    example.com {
        reverse_proxy / backend1.example.com backend2.example.com
    }
    

Caddy的持久化存储

  1. 配置文件:自定义配置文件需要放置在合理的位置。
  2. 数据目录:用于存储TLS证书和其他关键数据。
  3. 配置目录:保存最后一次有效的配置。

在使用Docker容器时,需要挂载这些目录以确保数据持久化。

volumes:
  - ./Caddyfile:/etc/caddy/Caddyfile
  - caddy_data:/data
  - caddy_config:/config

Git简明指南:从入门到基本操作

作者 小old弟
2025年4月2日 09:20

Git作为当今最流行的分布式版本控制系统,已成为开发者必备的工具之一。本文基于Roger Dudler的《Git简明指南》,全面介绍Git的基本概念、安装方法和核心操作流程,帮助初学者快速掌握Git的使用技巧。

Git简介与安装

Git是一个开源的分布式版本控制系统,由Linus Torvalds为管理Linux内核开发而创建。与传统的集中式版本控制系统(如SVN)不同,Git的每个工作目录都是一个完整的代码仓库,拥有完整的历史记录和版本跟踪能力,不依赖于网络连接或中央服务器。

Windows系统安装步骤

  1. Git官网下载适合Windows 64位系统的安装包(如2.16.2版本)
  2. 运行安装程序,按照向导逐步完成安装
  3. 在"选择默认编辑器"步骤,建议选择Git自带的命令行工具(Git Bash)
  4. 在"选择传输协议"步骤,通常选择HTTPS协议
  5. 选择"使用MinTTY"作为终端模拟器,它提供了类似Linux的命令行体验
  6. 完成安装后,通过开始菜单中的"Git Bash"验证安装是否成功

安装完成后,需要进行基本的全局配置:

git config --global user.name "Your Name"
git config --global user.email "your_email@example.com"

这些信息将用于标识你的所有提交记录。

Git基础概念与工作流

Git的工作流程围绕三个核心区域构建:

  1. 工作目录(Working Directory):包含实际项目文件,你可以直接编辑这些文件。
  2. 暂存区(Index/Staging Area):临时保存你对工作目录所做的更改,准备下一次提交。
  3. 本地仓库(HEAD):指向你最近一次提交的版本,包含完整的项目历史。

这种三树结构使得Git能够精确控制版本管理过程,允许开发者选择性地暂存和提交更改,而不是一次性提交所有修改。

Git基本操作

创建与克隆仓库

要开始使用Git管理项目,首先需要初始化一个Git仓库:

git init

该命令会在当前目录创建一个隐藏的.git文件夹,用于存储Git的所有版本控制数据。

如果需要获取现有仓库的副本,可以使用克隆命令:

git clone /path/to/repository  # 克隆本地仓库
git clone username@host:/path/to/repository  # 克隆远程服务器上的仓库
git clone https://github.com/username/repository.git  # 克隆GitHub等平台上的仓库

添加更改与提交

Git的基本工作流程包括添加更改到暂存区,然后提交这些更改到本地仓库:

git add <filename>  # 添加特定文件到暂存区
git add *  # 添加所有更改文件到暂存区

git commit -m "提交信息"  # 将暂存区的更改提交到本地仓库

提交信息应当简明扼要地描述本次更改的内容,这有助于日后查看历史记录时理解每次提交的目的。

推送更改到远程仓库

本地提交完成后,需要将更改推送到远程仓库以实现共享和备份:

git push origin master  # 将本地master分支推送到远程origin仓库

如果是首次推送,可能需要先添加远程仓库:

git remote add origin <server>  # 添加远程仓库地址

分支管理

分支是Git最强大的功能之一,它允许开发者在不影响主代码线的情况下进行实验和开发新功能。

基本分支操作

git checkout -b feature_x  # 创建并切换到名为feature_x的新分支
git checkout master  # 切换回主分支
git branch -d feature_x  # 删除feature_x分支
git push origin <branch>  # 将分支推送到远程仓库

分支使得团队协作更加高效,不同开发者可以在各自的分支上工作,完成后将更改合并到主分支。

更新与合并

保持本地仓库与远程仓库同步是协作开发的关键:

git pull  # 获取远程更改并自动合并到当前分支

当需要将一个分支的更改合并到当前分支时:

git merge <branch>  # 合并指定分支到当前分支

Git会尝试自动合并更改,但有时会出现冲突(conflicts),需要手动解决。解决冲突后,需要标记文件为已解决:

git add <filename>  # 标记冲突已解决

在合并前,可以使用diff命令预览差异:

git diff <source_branch> <target_branch>  # 比较两个分支的差异

标签与版本管理

标签用于标记重要的版本发布点,如软件版本号:

git tag 1.0.0 1b2e1d63ff  # 为提交ID前10位为1b2e1d63ff的提交创建1.0.0标签

可以通过git log查看提交历史并获取提交ID:

git log  # 查看提交历史

错误恢复与撤销

Git提供了多种方式撤销更改或恢复到之前的状态:

git checkout -- <filename>  # 丢弃工作目录中对某个文件的修改,恢复到HEAD状态

如果需要完全丢弃本地所有更改并同步到远程最新状态:

git fetch origin
git reset --hard origin/master  # 危险操作:将丢弃所有未提交的本地更改

实用技巧与高级功能

Git还提供了一些有用的辅助功能:

gitk  # 启动图形化界面查看仓库历史
git config color.ui true  # 启用彩色输出
git config format.pretty oneline  # 简化日志输出格式
git add -i  # 交互式暂存更改

对于与IDE的集成,如IntelliJ IDEA,可以在设置中配置Git路径,并直接通过IDE界面执行版本控制操作,大大提高了开发效率。

总结

Git作为现代软件开发的核心工具,其分布式架构和强大的分支管理能力使其成为团队协作的理想选择。通过掌握基本的仓库创建、更改提交、分支管理和合并操作,开发者可以高效地管理项目代码。随着使用经验的积累,Git更高级的功能如rebase、stash和子模块等将进一步扩展你的版本控制能力。

记住,Git的学习曲线可能初期较为陡峭,但一旦掌握基本概念和常用命令,它将极大提升你的开发效率和项目管理能力。实践是最好的学习方法,建议在实际项目中应用这些Git命令,逐步熟悉其工作流程和强大功能。

昨天以前首页

🔥🔥🔥2.5W字!8个场景问题!带你了解最实用的 git 操作!!!

作者 北岛贰
2025年3月31日 23:14

版权声明:本人文章仅在掘金平台发布,请勿抄袭搬运,转载请注明作者及原文链接 🦉

阅读提示:网页版带有主题和代码高亮,阅读体验更佳 🍉

网上稍微搜一搜,每一篇 git 文章都在教你怎么 git add,git push,少数文章会给你说 git cherry-pick,git stash,但是很少有文章会给你说 git rebase。

从刚工作时起,你的 leader 前辈们都在告诉你,git rebase 很危险,平时就用 git merge 就行了,所以你工作了三五年,还没怎么用过 git rebase,有时候遇到 git 问题,不是删分支就是在删分支的路上,我猜你还会把需求代码先一点一点复制粘贴下来,然后删除旧分支再建新分支,再改回去,对吧?

stop!!!

诶棒油,你的头顶长了什么东西,我眼睛里没有~

那么今天,我们 git rebase 要讲,git cherry-pick、git stash 也要讲,而且要讲一些可能大家不知道的,实用的 git 知识和操作。

带着问题找答案,先来看看下面八个问题,你是否能解决。如果你都能解决,那么恭喜你,你的 git 已然化境:

(前四个问题只能算是开胃小菜)

  • 新需求忘记开新分支,提交到其它分支了,怎么办?
  • 新开发了一个功能,合并 master 了,产品过了一段时间说不要了,怎么办?(不可以殴打产品)
  • 功能开发了一半,需要拉一下 master,但是你又不想 commit 代码怎么办?
  • 刚才提交了一个 commit,发现 message 写得有问题,怎么办?
  • 需求开发提交了几个 commit,提交 master 时领导 review 后,说你第一笔 commit 代码有问题,让你改一下,怎么办?
  • 刚才提交了很多个 commit,发现最初的 commit meesage 写得有问题,怎么办?
  • 一个大需求开发了一个月,每天拉 master 代码合并到本地分支,发现这个需求自己提了十多个 commit,需求需要发布 merge 到 master 了,领导让你把十多个 commit 合并成一个,方便 code review,怎么办?
  • 昨天提交了一个 commit 到 master 了,今天发现 commit message 写错了,但是 master 上别人的提交已经有几十上百个了,领导说你 message 写错了,改一下,怎么办?

不能完全解答的朋友就请继续往下看。

为了方便演示,我现在新建一个 git-demo 的项目,新增 a.js、b.js、c.js、d.js 四个演示文件,每个文件一句代码,每个文件一次 commit 共四个 commit:

image.png

问题1:新需求忘记开新分支,提交到其它分支了,怎么办?

我当初犯过最严重的错误就是,同时开发两个需求,忙来忙去,两个需求的 commit 搞岔了,两个分支都各有另一个需求的 commit,当时带我的同事都懵了。

然后在他一顿猛如虎的操作下,才理清了我的 commit。

其实答案很简单,就是 cherry-pick,这倒不是什么新鲜知识,很多有点工作经验的都知道,那为什么我们还讲,一是照顾新同学,二是这个命令对我们后续的题目解答很重要。

git cherry-pcik 的作用,就是将任意分支的 commit 挑拣到当前分支,但是这个挑拣不会删除原分支的 commit。

使用方式,就是紧跟着我们某个 commit 的 hash 值,可以跟多个,每个 hash 使用空格分割:

git cherry-pick 123abc 456def 789ghi

不妨在演示项目中新建一个分支 test:

image.png

我们在 test 分支新增一个 commit:

image.png

然后我们复制 e.js 的 hash,切回 master 使用 cherry-pick 将其应用到 master 上:

image.png

而 test 分支的 e.js commit 不会受到影响:

image.png

这就是 cherry-pick 最简单的应用。

不过,你聪明的小脑袋瓜可能会问,cherry-pick 任意分支?那挑拣的 commit 如果是在本分支呢?

诶,问得好,我们不妨试试。

先将刚才的 e.js 从 master 撤回。然后将 e.js、d.js 做 cherry-pick,这里的 e.js 是 test 分支的,d.js 是 master 分支自己的,我们在 master 分支做 cherry-pick 操作:

image.png

注意看红框里的内容,第一个框里说的意思,就是上一个 cherry-pick 是空的,可能是解决冲突产生的,你可以直接执行 git commit --allow-empty 将没有任何内容更改的 commit 提交到当前分支。

image.png

假设我们直接执行 git commit --allow-empty,会弹出 vim 编辑器让我们编辑该 commit 的信息:

image.png

我们不做任何操作,直接按 esc 键,然后输入 :wq 并按回车键退出:

image.png

可以看到,d.js 有两次 commit:

image.png

回到前面,除了 git commit --allow-empty,git 还提示我们有其它几个命令可以选择:

  • git cherry-pick --skip
  • git cherry-pick --continue
  • git cherry-pick --abort

git cherry-pick --skip 的意思就是跳过,d.js 本身对比 master 自身来说没有任何修改,是空的,直接跳过,仅 cherry-pick e.js。

git cherry-pick --continue 的意思是说,当你使用 git cherry-pick 遇到冲突,解决冲突并把修改添加到暂存区(使用 git add)之后,就可以使用这个命令,让 git 继续执行 cherry-pick 操作。如果你没解决冲突,一直执行这个命令,是不会有任何有意义的效果的。

git cherry-pick --abort 则是放弃此次 cherry-pick,只要放弃了,所有内容都不会 pick,包括 e.js 也不会被 pick 过去,等于直接放弃本次 pick 操作。

综上,如果你在 cherry-pick 时不小心 pick 了本分支的 commit,且是空白内容没有实质性内容冲突,最好执行 git cherry-pick --skip。但是实际上,那么多个 commit,它只会提示你 The previous cherry-pick is now empty, possibly due to conflict resolution. ,你并不知道具体是哪个,直接跳过也不见得是正确的,也可以先 pick 过来,再决定取舍。

另外,cherry-pick 的 commit 理想状态下是没有冲突的,但是很多时候会有冲突,必须解决冲突了才能继续 cherry-pick。

具体点说,冲突解决完,需要继续 git add 文件1 文件2 ...,然后修改 commit message。

这两步做完,还需要继续执行:

git cherry-pick --continue

这样呢,一次 cherry-pick 的过程才算结束。

再多说一句,git cherry-pick 本就应该用于挑拣其它分支的 commit,所以用的时候不要挑拣本分支 commit。

问题2:新开发了一个功能,合并 master 了,产品过了一段时间说不要了,怎么办?

这个问题也不算太难,直接 git revert。

使用方式,也是跟 hash 值:

git revert 123abc 456def

当然,上面的用法是针对不连续的 commit 来说的,如果你是连续的多个 commit 一起撤回,可以这么用:

git revert <start_commit>(不包含)..<end_commit>(包含),例如:

git revert abcdef123..7890abcd

问题3:功能开发了一半,需要拉一下 master,但是你又不想 commit 代码怎么办?

git stash

又是一个高频使用的命令,重要性不言而喻。场景很多,比如,我在当前 bug_fix 分支,在修复一个 bug,突然来了一个优先级更高的 bug,那已经写的代码不能直接 commit 吧?你当然可以说建一个新分支呗,一个 bug 一个分支,这也是一个解决方案,不过各家公司有各家公司的要求,具体问题具体分析。不切换分支的情况下,就可以 git stash。

修复完这个紧急 bug 后,我们需要继续修复前一个 bug,就可以执行 git stash apply 将之前暂存的代码恢复,继续开发。

不过,这里需要注意,git stash apply 是应用最近一次 stash 的代码,如果你存了很多个,就必须指定。

我们可以通过 git stash list 命令查看所有的 stash:

stash@{0}: On main: stash1
stash@{1}: On feature: stash2
stash@{2}: On feature: stash3
stash@{3}: On main: stash4

越上面的越新,如果我们要应用某个旧的,指定一下即可:

git stash apply stash@{3}

git stash 和 git commit 一样也可以设置 message:

git stash -m 'stash: 这是一个 stash'

篇幅所限,我们用表格汇总下 stash 命令:

命令 作用 示例
git stashgit stash push 将当前工作目录和暂存区的修改保存到栈中 git stash push -m "保存修改"
git stash list 查看当前保存的所有 stash git stash list
git stash apply 应用最近一次保存的 stash,应用后 stash 仍保留在栈中 git stash apply
git stash apply <stash编号> 应用指定的 stash git stash apply stash@{1}
git stash pop 应用最近一次保存的 stash,并将其从栈中删除 git stash pop
git stash pop <stash编号> 应用指定的 stash 并将其从栈中删除 git stash pop stash@{1}
git stash drop 删除最近一次保存的 stash git stash drop
git stash drop <stash编号> 删除指定的 stash git stash drop stash@{1}
git stash clear 删除栈中所有的 stash git stash clear
git stash show 查看最近一次 stash 的差异 git stash show
git stash show <stash编号> 查看指定 stash 的差异 git stash show stash@{1}
git stash show -p <stash编号> 查看指定 stash 的详细差异内容 git stash show -p stash@{1}

问题4:刚才提交了一个 commit,发现 message 写得有问题,怎么办?

git commit --amend

当我们执行完命令后,会打开 vim 编辑器:

image.png

vim 编辑器的使用其实很简单,我们输入 i,底部会提示我们进入编辑模式:

image.png

我们使用箭头移动光标位置,输入新的 message:

image.png

修改完成,按 esc 键,再输入 :wq 回车,操作完成。

image.png

问题5:需求开发提交了几个 commit,提交 master 领导 review 后,说你第一笔 commit 代码有问题,让你改一下,怎么办?

这里的意思很简单,当我们辛苦开发了一阵子需求,提了好几个 commit 后,发现某一笔(非最新一笔)的代码有问题,需要修改,一般人的做法,就是库库一阵改了,再提个新的 commit 呗。

这其实也可行,但是还是那句话,不同的公司有不同的规范,commit 的管理尺度各不相同,所以不用太较真场景和问题的解决方法。

假设我们现在不新增 commit,就在原 commit 的基础上修改,这就需要用到 rebase。我们不讲深奥的理论,就只讲实操和现象,你先用起来再说。

现在,我们将 b.js 的 const a = 1 的内容修改一下,且不新增 commit。

我们需要执行命令git rebase -i <hash>

这里有个关键点,rebase -i 操作,后面跟的这个 hash 是一个开区间,也就是不包含在内的意思,假如,我要修改 b.js,hash 就是 a.js 的 hash,假如我要修改 c.js,hash 就至少得是 b.js 的 hash。

Talk is less,show me the code.

直接看结果,假设是 a.js 的 hash:

git rebase -i 081b0c26a4d7deb04ed1625b2a84f31f24d5fbe8

image.png

那么我们就可以编辑 b、c、d 三个提交。

假设是 b.js 的 hash:

git rebase -i b8cd836ef539457228a86fca8ddeb3ec2b52017e

image.png

那么我们就可以编辑 c、d 两个提交。

其实就是在这个开区间的 hash 范围内,所有的 commit 都可以被操作。

现在,我们就修改一下 b.js 的内容,不新增 commit:

image.png

这里绿色区域的文字,默认进入是 pick,当我们要操作某个 commit 时,将其替换为对应的操作,例如:

image.png

edit 的意思就是编辑该 commit。务必记得,在 vim 编辑器里,需要先键入 i 才能进入编辑模式。然后我们继续 esc + :wq 退出:

image.png

到了这一步,我们相当于穿越时空,来到了 b.js 提交的那个时空,我们在当前可以任意修改文件,任意操作代码,然后执行 commit,或者使用 git commit --amend 修改当前 b.js 的 message。

我们将 b.js 代码内容修改一下:

image.png

修改完代码,务必将修改后的代码暂存:

image.png

暂存完,这一步我们需要修改 commit message,如果我们不修改 commit message,我们直接 git rebase --continue,系统还是会弹出 vim 编辑器让我们修改,不修改的话我们直接 esc + :wq 退出:

image.png

仔细观察红框中的内容,首先是 commit 没有什么变化,其次,master 分支名旁边的表示 rebase 进程的提示没有了。

image.png

那么,我们针对历史 commit 代码的修改就结束了。

好了,这时候聪明的你又会问,除了 edit,还有其它的操作吗?

有的,有的兄弟,这样的命令一共有 11 种:

image.png

pick 就是默认的,reword 是只修改 commit message 不修改代码,squash 是将多个 commit 合并为一个。

squash 这里比较重要,我们继续来举个例子,假设我一共 4 个 commit,那么到底什么场景下我会需要把它合并成一个呢?

首先,开发时间长,每天都可能要处理不同需求和 bug,有时就只能先 commit,然后去做其它事情。这样就会产生多个 commit,当我们开发完了,我们需要提代码给 leader review,你一个需求不能整个五六个、七八个 commit 给他看吧,这时候就需要合并 commit。

其次,假设我现在修改 bug,先写了一版,推上去发现有问题,我需要继续修改,此时我当然可以使用上面的 git rebase 的 edit 方法,不过你也可以再新增一个 commit,然后合并 commit,这也是一种方案。

OK,废话不多说。

假设我现在需要把 b.js、c.js 的 commit 合并为一个:

此时是 a.js 的 hash,才能选择 b.js 与 c.js。

git rebase -i 081b0c26a4d7deb04ed1625b2a84f31f24d5fbe8

image.png

(s 是 squash 的缩写)

但是你发现,为什么第一个 s 是红色的?

其实是因为,在 squash 操作中,第一个 commit 不允许被 squash,第一个默认就是 pick。

假设合并 b & c,需要这样:

image.png

假设合并 c & d,需要这样:

image.png

假设合并 b & c & d,需要这样:

image.png

那么继续 esc + :wq 退出,如果有冲突会按照冲突流程处理,前面我们已经处理过。

可能看到这里,你又有疑问,如果 git rebase -i <hash> 的 hash 是开区间,那我就是要编辑或者合并 a.js 的 commit 怎么办?

可以使用参数 --root 解决:

git rebase -i --root

必须这样执行,不需要加 hash。

image.png

这样又引出了新的问题,就是,我操作完了,发现搞错了,我怎么反悔?得回到 squash 前的状态啊!!

这里就涉及到 git 的 back 操作,想必你工作中也用到过,前面的内容中我们也提到了 revert。

那这里的回退,我们卖个关子,放在后面讲。

现在以表格整理一下这些命令,其余命令大家可以实操试试,毕竟实践出真知。

简称 英文全称 解释
p pick 按原样应用指定的提交,让提交按原有的顺序和内容应用到变基后的分支上。
r reword 应用该提交,但会暂停以允许修改提交信息,可用于完善之前提交时写得不够清晰或有错误的提交信息。
e edit 应用该提交,但会暂停以便修改提交内容,你能对此次提交所做的更改进行调整,之后使用 git commit --amend 更新提交。
s squash 将该提交与前一个提交合并,并且可以编辑合并后的提交信息,有助于把多个相关的小提交合并成一个更有意义的大提交。
f fixup 类似于 squash,将该提交合并到前一个提交,但会丢弃当前提交的提交信息,只保留前一个提交的信息。
x exec 在处理到该提交时执行一个 shell 命令,允许在变基过程中插入自定义操作,如运行测试脚本等。
d drop 移除该提交,即不将此提交应用到变基后的分支中,可用于去除不必要的提交。
b break 在该提交处暂停变基,让你可以手动检查状态或执行额外操作,之后使用 git rebase --continue 继续变基。
l label 在当前位置创建一个新的标签,类似于 git label 命令,方便后续引用该位置。
t reset 返回到指定的标签位置,撤销自该标签之后的所有变基操作。
m merge 引入指定的标签或提交,将其与当前分支合并,就像执行了一次 git merge 操作。

问题6:刚才提交了很多个 commit,发现最初的 commit meesage 写得有问题,怎么办?

通过前面的学习案例,后续的问题我们直接给出参考答案,减少演示带来的阅读负担。

第一个你应该想到的思路,就是 git rebase -i,并执行 reword 或者 edit 命令。

第二个思路,就是直接 revert 该 commit,然后重新 commit 时修改 message。

问题7:一个大需求开发了一个月,每天拉 master 代码合并到本地分支,发现这个需求自己提了十多个 commit,需求需要发布 merge 到 master 了,领导让你把十多个 commit 合并成一个,方便 code review,怎么办?

git rebase -i,执行 squash 操作。

当然了,这是一个理想状态,你的需求的 commit 是连续的,只有你自己的 commit。

可现实开发中,一个大需求绵延一个月,我们每天还要更新 master 的代码,我们自己的 commit 可能散落在各个时间点上,不能直接 squash,不然你就会面对几百个上千个 commit,无异于大海捞针。

最正确的做法,是我先将开发完成的分支 push 到 remote 仓库,然后我新建一个 merge request,不要直接确认,我们只是需要其 diff 出分支上的更改。因为此时 gitlab 等托管平台已经会自动 diff 出本分支的 commit,这样你不用再去翻找一月前的那些 commit 了。

然后,你新建一个分支,将原来分支上所有的 commit (这就是为什么要在原分支新建 merge request,方便你复制 commit hash)使用 cherry-pick 转移到新分支上来。

接着使用 squash 操作将其合并为一个 commit,再推送新分支到 remote,发起真正的 merge request。

怎么样!!这个思路是不是很 sao ~。

问题8:昨天提交了一个 commit 到 master 了,今天发现 commit message 写错了,但是 master 上别人的提交已经有几十上百个了,领导说你 message 写错了,改一下,怎么办?

你可能会说,git rebase -i,执行 reword 或者 edit。

是的,这不算错。

但是这和第 6 题不同,这是已经在 master 上的修改了,不是本地的 commit。

如果我们执行 rebase 操作,因为涉及到变基,代码 push 到托管平台后,会 diff 出你的变更,它会把该 commit 后的所有 commit 都识别为你的变更,你会发现你这个分支提上去 merge 到 master 时会有 N 个 commit,那些 commit 都是你同事的,且已经是在 master 上了。

理论上你直接合并也没有问题,因为你本身没有修改过任何东西,哪怕合上去起了冲突,冲突解决完就行了。但是你的领导看着你的这个操作,明明只是修改了一个 commit message,但是却多出来 N 个已经在 master 的 commit,他看到肯定会懵逼,几乎不会同意你的 merge request。

所以,最好的解决方案是就是:

将错就错~?

哈哈,大概率现实是这样的,你的 leader 会和你说,错就错了,一个 commit message 而已。

这其实没毛病,但是如果管理严格一点的公司呢?

或者说,这个问题难道真的没法解决吗?

其实方案我们在第 6 题也提过。

解决办法就是先对其 revert,然后重新 commit,这是最稳妥的解决办法。

对,思路就是这么简单,不要拘泥于 git rebase 强大的历史 commit 能力,最简单朴素的方式或许是最安全的。

一点点后续:我想反悔,怎么办?

假如我们 git rebase 出错了怎么办?

假如你还没执行完,你的分支显示是这样的:master|REBASE,那么你可以直接执行 git rebase --abort。这会直接放弃本次 rebase。

其它的情况,例如 cherry-pick 也是同理。

但是,假如你的分支变成了 ((0903e23230...)) 或者其它你看不懂的样子,并且 commit 也不见了一些,别慌!!

git 其实为我们保存了所有的历史记录。

我们使用 git reflog 可以查看:

image.png

想退出此模式的话直接 q 或者 wq

假设现在我想回到下面红框时候的 commit 状态:

image.png

此时,我只需要先复制它的 hash —— c7d7bbb,然后 reset:

git reset --hard c7d7bbb

就能回到最初的时候了!~

image.png

不过使用 --hard 参数会丢失暂存区的更改和影响工作目录,如果你不是很明确地想要回退到某个历史记录上,建议使用 git reset --soft。下面也列一下 reset 的参数:

参数 作用描述
--soft 仅移动分支指针到指定提交,暂存区和工作目录的内容保持不变,不改变文件的修改状态,暂存的内容依然保留。
--mixed 默认参数,移动分支指针到指定提交,同时重置暂存区,使其与指定提交时的状态一致,但工作目录中的文件内容不会被修改。
--hard 移动分支指针到指定提交,并将暂存区和工作目录的内容都重置为指定提交时的状态,会覆盖未提交的修改,使用需谨慎。
--merge 用于处理合并冲突后,将分支指针重置回合并前的状态,同时保留工作目录和暂存区中已解决的冲突内容。
--keep 尝试将分支指针移动到指定提交,同时保留工作目录中的修改。若工作目录中的修改与指定提交存在冲突,重置操作会失败。
--abort 终止一个正在进行的--merge--keep重置操作。

给个三连吧!!!!!!!!!!!!

image.png

往期推荐

爆肝两个月,我用flutter开发了一款免费音乐app 80+ 👍🏻 102+ 💚

搭建一个快速开发油猴脚本的前端工程 24+ 👍🏻 42+ 💚

金九银十招聘季,IT 打工人,该怎么识别烂公司好公司? 70+ 👍🏻 80+ 💚

为什么就这个文件的 ESLint 检查失效了?

学会 TypeScript 体操,轻松看懂开源项目代码

别人休息我努力,悄悄写个 cli 工具,必须提升效率,skr~ 60+ 👍🏻 110+ 💚

一文掌握 eslint,再也不怕项目报错 20+ 👍🏻 30+ 💚

开发一个 npm 库应该做哪些工程配置? 40+ 👍🏻 50+ 💚

分享我在前端学习与开发中用到的神仙网站和工具 40+ 👍🏻 110+ 💚

uniapp 踩坑记录(二) 130+ 👍🏻 150+ 💚

闲来无事,摸鱼时让 chatgpt 帮忙,写了一个 console 样式增强库并发布 npm 100+ 👍🏻 110+ 💚

uniapp 初体验踩坑记录 30+ 👍🏻 60+ 💚

两小时学会 JS 正则表达式,终身不忘 50+ 👍🏻

【一年前端必知必会】如何写出简洁清晰的代码 50+ 👍🏻

【一年前端必知必会】了解 Blob,ArrayBuffer,Base64 40+ 👍🏻 90+ 💚

❌
❌