Skip to content

Git 实用技巧

作者:Atom
字数统计:5.5k 字
阅读时长:19 分钟

Git 中有很多实用的技巧, 让提交代码变成更愉悦的事, 本文将持续记录作者积累的一些技巧

~^ 的区别

Commit Log

avatr

每条线上的星号(*)右侧是对应的 CommitID, 开始和结尾交叉代表公共提交。可以使用参数--decorate--graph来简化显示 GIT 提交历史

sh
git log --graph --decorate

上图第一条线拥有 165f5a1->3cb9272->de87e10->b6de943这几条提交记录:

下面是等价的表示方法:

git-graph.png

rust
A = = A^0
B = A^ = A^1 = A~1
C = A^2
D = A^^ = A^1^1 = A~2
E = B^2 = A^^2
F = B^3 = A^^3
G = A^^^ = A^1^1^1 = A~3
H = D^2 = B^^2 = A^^^2 = A~2^2
I = F^ = B^3^ = A^^3^
J = F^2 = B^3^2 = A^^3^2

HEAD^[num]

用于选择第几条为主线,例如使用:

sh
git reset --hard HEAD^2

这个将从左往右选取第二条为主线,且回退一个版本,commit-id 将为:

sh
ae3d768->02c1b1b->b6de943

HEAD~[num]

用于在当前主线『默认 master』,回退版本,例如使用:

sh
git reset --hard HEAD~1

这个将选取当前 master 主线,且回退一个版本,commit-id 将为:

sh
3cb9272->de87e10->b6de943

这种使用方式 HEAD 后面可以跟多个^,每个表示回退一个版本,效果同 HEAD~[num]

sh
git reset --hard HEAD^^

这个将选取第一条主线,且回退两个版本,commit-id 将为:

sh
de87e10->b6de943

HEAD~~

这种使用方式 HEAD 后面可以跟多个~,每个表示回退一个版本,效果同 HEAD~[num]

sh
git reset --hard HEAD~~

这个将选取第一条主线,且回退两个版本,commit-id 将为:

sh
de87e10->b6de943

如何选用

波浪符号~在外观上几乎是线性的,并且想沿直线向后走; 而插入符号^表示一棵有趣的树段或道路上的一个岔路口

  • 大部分时间使用~——回溯几代,通常是你想要的
  • 用于^合并提交——因为它们有两个或更多(直接)父级

代码统计

如果想了解多人团队中同事或者自己的代码统计情况, git提供了相关的命令, 方便且直观

sh
# 查看团队每个人的代码提交量
git shortlog -s -n

Fast-Forward 合并

Git 中的 fast-forward(快速前进)是一种合并(merge)分支的方式,它通常用于将一个分支的更改合并到另一个分支上。

Fast-forward 合并的效果是,目标分支(主分支)的指针直接移动到源分支(开发分支)的最新提交,因此看起来就像是主分支“快速前进”到了开发分支的状态

Fast-forward 合并通常是一种简单且干净的合并方式,因为它不会创建合并提交(merge commit),但它只适用于特定的情况,即主分支没有新的提交。如果主分支有新的提交,你可能需要执行普通的合并,这将创建一个合并提交以整合来自其他分支的更改

Fast-forward 合并只会发生在特定条件下

  • 合并到主分支

假设你有一个开发分支,比如 feature-branch,并且你在该分支上做了一些更改。当你想将这些更改合并到主分支(通常是 master 分支)时,如果在合并时没有新的提交到主分支,Git 将执行 fast-forward 合并

  • 没有冲突

fast-forward 合并中,没有冲突会发生。这是因为在 fast-forward 合并时,Git 简单地将目标分支(主分支)指向源分支(开发分支)的最新提交,这不会导致冲突

更多相关的内容可以查阅Merging vs Rebasing

变基 Rebase

笔者总结了rebase的最常用三种用法(注意区间前开后闭):

合并 commit

在开发中, 可能自己的分支进行了多次的提交, 为了保持合入公共开发分支中自己的 commit 的整洁性, 可以使用 commit 合并

注意

一般使用场景是自己的本地分支, 最好不要在远端公共分支上进行这种操作, 是极其危险的

sh
# 区间前开后闭, 一般可以使用 commitid~1来包含该提交
git rebase -i  <start-commit>  <end-commit>
# 此时, 会进入一个匿名分支, 需要切出一个新分支
git checkout -b <name>
# 推送到自己的远端
git push origin HEAD

修改 commit

如果前几次已经git commit到本地, 此时想修改他们提交信息, 也可以是用rebase -i命令

sh
# 例如 git rebase -i HEAD~1 就是修改最近一次提交
git rebase -i <start-commit>  <end-commit>

此时在vim模式下, 与合并commit所选择使用的pick选项不同, 而是选择使用reword选项来修改提交信息。

rebase -i 支持的选项有下图中的几种

修改提交信息

特殊情况

如果只修改最新一次的提交信息, 可以使用git commit --amend命令

此时等同于git rebase -i HEAD~1

复制 commit

可以复制某一个分支的一串 commit 到当前分支, 类似于cherry-pick

  • base: 分支名称
  • from: 待合并片段的起始 commitId(不包含)
  • to: 待合并片段的结束 commitId(包含)

语法:

git rebase --onto base from to

sh
# 被复制的分支上执行
git checkout -b  <new-branch>  <end-commit>

git rebase --onto  <target-branch>  <start-commit>^

合并远端分支

拉取远端代码的具体分支时, 拉取后与本地进行fast-forward合并, 可以使用

sh
git pull --rebase origin <target-branch>

重设分支基点

团队协作中, 主分支经常会更新一些内容, 因此我们提交到主分支前一般都需要先合并主分支的内容到开发分支, 主要使用两种方式合并

如果使用merge来合并主分支, 那么会产生一条多余的无关合并记录, 从而污染了开发分支, 如图

Merging-main-into-feature.svg

为了解决上述的问题, 就得使用rebase重设开发分支的基点, 使得开发分支的提交记录变得干净

sh
git checkout feature
git rebase main

这样就将整个feature分支从分支的顶端开始,有效地将所有新提交合并到main。 但是,变基不是使用合并提交,而是通过为原始分支中的每个提交创建全新的提交来重写项目历史记录

合并后的分支图如下

Rebasing-the-feature-branch-into-main.svg

Rebase vs Merge 对比

RebaseMerge
历史线线性, 干净保留分叉和合并节点
commit 记录看起来像在 main 最新节点上顺序开发会产生额外的 merge commit
冲突解决逐个 commit 解决, 可能需要多次一次性解决所有冲突
可追溯性原始分支点信息丢失保留完整的分支合并拓扑
commit SHA会改变(历史改写)不变

开源项目中的 Rebase 工作流

大多数开源项目(如 Linux kernel)要求贡献者在提交 PR 前先 rebase main, 以保持主分支历史的线性和干净。典型流程如下:

sh
# 1. Fork 并 clone 项目后, 从 main 拉出特性分支
git checkout main
git pull origin main
git checkout -b feature/my-feature

# 2. 在特性分支上开发, 产生多个 commit
git commit -m "feat: add X"
git commit -m "fix: adjust Y"

# 3. 开发完成, 合入前先 rebase 远端最新的 main
git fetch origin
git rebase origin/main

# 4. 如果有冲突, 逐个解决(rebase 会逐个 commit 重放)
git add .
git rebase --continue
# 重复直到所有 commit 重放完毕

# 5. 可选: 交互式 rebase 整理 commit, 把零碎提交合并成语义完整的提交
git rebase -i origin/main
# 在编辑器中将多个 commit 标记为 squash 或 fixup

# 6. 推送到自己的远程特性分支(历史被改写, 需要 force push)
git push --force-with-lease origin feature/my-feature

# 7. 创建 PR, 等待 review 后合入 main

黄金法则

只 rebase 自己的特性分支, 绝不 rebase 公共分支(如 main)

rebase 会改写 commit SHA。如果在 main 上执行 rebase, 所有基于 main 工作的协作者的本地历史都会与远程不一致, 导致大面积冲突

用图来理解这个原则:

初始状态:
main:    A - B - C
                 \
feature:          D - E     (你的 commit)

[正确] feature rebase onto main
main:    A - B - C          <- main 没变
                 \
feature:          D' - E'   <- 只有你的 commit SHA 变了

[错误] main rebase onto feature
main:    D - E - A' - B' - C'  <- 所有人依赖的 commit 全变了

关于 --force-with-lease

rebase 后需要 force push, 但应使用 --force-with-lease 而非 --force。前者会检查远程是否有别人的新推送, 更加安全

同步落后的本地开发分支

实际开发中经常遇到本地开发分支同时落后于远端同名分支和 main 分支的情况:

main:              A---B---C---D---E          (远端已前进)
                            \
origin/feature:              F---G---H        (远端开发分支已前进)
                            \
local/feature:               F---G            (本地落后)

标准处理流程如下:

Step 1: 拉取远端最新信息

sh
# fetch 只拉取元数据, 不修改本地工作区, 永远安全
git fetch origin

Step 2: 同步远端同名分支的最新提交

sh
# 先合入协作者在同一分支上的新提交
git rebase origin/feature/xxx

Step 3: 同步 main 分支的最新变更

sh
# 将开发分支的基点重设到最新的 main
git rebase origin/main

Step 4: 解决冲突(如有)

sh
# 逐个解决冲突文件
git add <resolved-file>
git rebase --continue

# 如果冲突太复杂想放弃
git rebase --abort

Step 5: 推送到远端

sh
# rebase 改写了历史, 需要 force-push
# --force-with-lease 比 --force 安全: 远端有未见过的新提交时会拒绝推送
git push --force-with-lease origin feature/xxx

速记命令(个人分支场景):

sh
git fetch origin && git rebase origin/main && git push --force-with-lease

个人分支 vs 共享分支

  • 个人特性分支 → 用 rebase, 保持历史线性
  • 多人共享分支 → 用 merge, 避免改写他人历史

如果是共享分支, 将上述 rebase 替换为 merge:

sh
git fetch origin
git merge origin/main
git push origin feature/xxx

删除本地无效的分支

远端有很多分支已经被删除, 而本地仍然存在, 删除本地的无效的分支可以使用prune命令

sh
# 列出已经失效的引用分支
git remote prune show  origin

# 删除失效的分支
git remote prune origin

恢复被删除的 stash 代码

有时候不小心清空了stash list中的备用代码, 想找回来怎么办? 可以使用以下命令:

sh
# 撤销git stash clear的操作
git fsck --unreachable | grep commit | cut -d ' ' -f3 | xargs git log --merges --no-walk --grep=WIP

文件换行符

windowsunix等各种系统上面采用了不同的换行符, 换行符的统一在团队合作时尤为关键

查看换行符

如果未统一换行符,那么必然会导致一些问题, 下面的命令可以检索出所有文件的换行符类型

详细的列出当前工作目录中所有文件的换行符类型, 总的来说, 换行符有以下几种常见格式

  • LFUnix 风格的换行符(\n)
  • CRLFWindows 风格的换行符(\r\n)
  • CRMac OS 风格的换行符(\r)
  • none:二进制文件或没有换行符的文件
sh
git ls-files --eol

换行符配置

跨平台协作开发是常有的,不统一的换行符确实对跨平台的文件交换带来了麻烦。最大的问题是,在不同平台上,换行符发生改变时,Git 会认为整个文件被修改,这就造成我们没法 diff,不能正确反映本次的修改。还好 Git 在设计时就考虑了这一点,其提供了一个 autocrlf 的配置项,用于在提交和检出时自动转换换行符,该配置有三个可选项:

  • true: 提交时转换为 LF,检出时转换为 CRLF
  • false: 提交检出均不转换
  • input: 提交时转换为LF,检出时不转换

用如下命令即可切换三种配置

sh
# 提交时转换为LF,检出时转换为CRLF

# 全局配置
git config --global core.autocrlf true
# 局部配置
git config --local core.autocrlf true
sh
# 提交时转换为LF,检出时不转换

# 全局配置
git config --global core.autocrlf input
# 局部配置
git config --local core.autocrlf input
sh
# 提交检出均不转换

# 全局配置
git config --global core.autocrlf false
# 局部配置
git config --local core.autocrlf false

如果把 autocrlf 设置为 false 时,那另一个配置项 safecrlf 最好设置为 ture。该选项用于检查文件是否包含混合换行符,其有三个可选项:

  • true: 拒绝提交包含混合换行符的文件
  • false: 允许提交包含混合换行符的文件
  • warn: 提交包含混合换行符的文件时给出警告

用如下命令即可切换三种配置

sh
# 拒绝提交包含混合换行符的文件

# 全局配置
git config --global core.safecrlf true
# 局部配置
git config --local core.safecrlf true
sh
# 允许提交包含混合换行符的文件

# 全局配置
git config --global core.safecrlf false
# 局部配置
git config --local core.safecrlf false
sh
# 提交包含混合换行符的文件时给出警告

# 全局配置
git config --global core.safecrlf warn
# 局部配置
git config --local core.safecrlf warn

统一换行符

如果想要统一换行符, 可以借助Prettier

sh
# 直接通过prettier运行转换操作, 点为要修改的文件路径,可以改成对应的目录
npx prettier --write --end-of-line lf .

在工程应用中, 更推荐的方式是使用Vscode插件 EditorConfig for VS Code , 并在根目录创建配置文件.editorconfig, 然后搭配EslintPrettier等工具, 完美解决换行符问题

想了解更多请参考作者的另一篇文章 代码风格工具集成

sh
# .editorconfig
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

Git Reset 的三个选项

  • mixed(默认值)

回退一个版本,且会将暂存区的内容和本地已提交的内容全部恢复到未暂存的状态,不影响原来本地文件及未提交的本地修改

sh
git reset (-–mixed) HEAD~1
  • soft

回退一个版本,不清空暂存区,将已提交的内容恢复到暂存区,不影响原来本地的文件(未提交的也不受影响)

sh
git reset -–soft HEAD~1
  • hard

回退一个版本,清空暂存区,将已提交的内容的版本恢复到本地,本地的文件也将被恢复的版本替换

sh
git reset -–hard HEAD~1

Git 恢复误删的分支

  • 使用 git log -g 找回之前提交的 commit_id

  • 使用 git branch recover_branch[新分支] commit_id 命令用这个 commit 创建一个分支

  • 切换到 recover_branch_abc 分支,检查文件是否存在

Git 图片提交失败

在某些情况下, Git提交成功并推送到远端后, 发现远端分支的图片并没有更新, 这种情况下需要使用-- force 或者 -- refresh选项来进行add

sh
git add --refresh .
# 或者
git add --force .

Git Tag 操作

Git tag 是给特定的提交打上标签,通常用于标记版本发布点。以下是常用的 tag 操作技巧:

创建标签

sh
# 创建轻量标签(推荐用于临时标记)
git tag <tagname>

# 创建带注释的标签(推荐用于版本发布)
git tag -a <tagname> -m "版本说明信息"

# 给特定的提交创建标签
git tag -a <tagname> <commit-id> -m "版本说明信息"

查看标签

sh
# 查看所有标签
git tag

# 查看标签详细信息
git show <tagname>

# 查看标签列表(按时间排序)
git tag --sort=-creatordate

# 查看标签列表(按版本号排序)
git tag --sort=version:refname

推送标签

sh
# 推送特定标签到远程仓库
git push origin <tagname>

# 推送所有标签到远程仓库
git push origin --tags

# 推送所有标签并覆盖远程标签
git push origin --tags --force

删除标签

sh
# 删除本地标签
git tag -d <tagname>

# 删除远程标签
git push origin --delete <tagname>

# 或者使用空标签覆盖远程标签
git push origin :refs/tags/<tagname>

检出标签

sh
# 检出标签到新分支(推荐方式)
git checkout -b <branch-name> <tagname>

# 直接检出标签(分离头指针状态)
git checkout <tagname>

标签管理最佳实践

版本标签命名规范

  • 使用语义化版本号:v1.0.0v2.1.3
  • 预发布版本:v1.0.0-alpha.1v1.0.0-beta.2
  • 发布候选版本:v1.0.0-rc.1
sh
# 示例:创建主版本标签
git tag -a v1.0.0 -m "第一个正式版本发布"

# 示例:创建预发布版本标签
git tag -a v1.1.0-beta.1 -m "Beta版本,新增用户管理功能"

# 示例:创建热修复版本标签
git tag -a v1.0.1 -m "修复登录验证bug"

删除远端库敏感文件

有时候不小心将一些敏感文件提交到远端库, 这时候需要删除远端库该敏感文件所有的记录

第一步、删除本地记录

首先在本地操作历史记录, 用于删除本地库中该文件所有记录

如果是文件夹, 还需要在git rm中添加-r参数

sh
git filter-branch --force --index-filter \
    "git rm --cached --ignore-unmatch 待删除的文件(相对项目的路径)" \
    --prune-empty --tag-name-filter cat \
    -- --all

第二步、将记录覆盖到远端

将上一步的操作覆盖到远端, 将远端库中的记录也全部删除

sh
# 覆盖所有的分支
git push origin --force --all
# 覆盖所有的tags
git push origin --force --tags

第三步、解除引用和垃圾回收

最后一步, 用于强制解除对本地存储库中的所有对象的引用和垃圾收集, 删除垃圾节省空间

sh
git for-each-ref --format='delete %(refname)' refs/original | git update-ref --stdin
git reflog expire --expire=now --all
git gc --prune=now

回滚本地分支

git reset (--mixed) [commit id]

回退到指定版本,且会将暂存区的内容和本地已提交的内容全部恢复到未暂存的状态,不影响原来本地文件(未提交的也不受影响) 该参数--mixedgit reset的默认参数 例如需要回退一个版本, 执行下面命令:

sh
# 回退一个版本
git reset HEAD~1

需要回退到某个 commitid(提交id通过git log查看), 如需要回滚到4a50c9f,则执行

sh
git reset 4a50c9f

git reset --soft [commit id]

回退到指定版本,不清空暂存区,将已提交的内容恢复到暂存区,不影响原来本地的文件(未提交的也不受影响)

git reset --hard [commit id]

回退到指定版本,清空暂存区,将已提交的内容的版本恢复到本地,本地的文件也将被恢复的版本替换

git revert [commit id]

生成一个新的 commit,将指定的 commit 内容从当前分支上撤除

常见用法

  • 撤销单个commit
sh
git revert commitId
  • 撤销多个不连续commit
sh
git revert commitId1 commitId2 commitId3
  • 撤销连续多个commit
sh
# 前开后闭区间, 不包含commitId1, 但包含commitId2
git revert commitId1..commitId2

git revert -m [parent Id][commit id]

当需要 revert 回滚两个分支合并后的一个公共提交, 此时需要加上-m选项来确认是第几个父 id(也就是确认回滚哪一条分支); 可以通过git show [提交id] 来查看有几个父 id

例子: 回滚到 cad132423 这个公共提交的第一个父节点 git revert -m 1 cad132423

回滚远端分支

案例: 需要回滚 master 上面的代码到 4a50c9f

第一种方法

主要使用Merge假合并策略, 以下是步骤:

  • 先回滚到需要移除的 comitid 的前一次正确 commitid
sh
git checkout -b remote-v1 4a50c9f
  • 合并策略为强行保留现在的分支(假合并) 合并中完全采用 remote-v1 的代码
sh
git merge -s ours master
  • 推送到远程分支
sh
# 也可以使用 git push origin HEAD:master
git push origin remote-v1:master

第二种方法

主要使用Revert撤销, 以下是步骤:

  • 先移除有代码错误的 comitid, 撤销一连串的 id 用(commit1..commit2](前开后闭区间, 不包含commit1, 但包含commit2), 参数--no-commit是用于后面手动提交
sh
# -n 是 --no-commit 的缩写
git revert -n f7742cd..551c408
  • 正常提交代码
sh
git commit -a -m 'This reverts commit 7e345c9 and 551c408'
git push origin HEAD:master

第三种方法(极不推荐) 危险

这种方式是强制操作, 忽略所有警告和报错, 强行推送并覆盖远端分支的代码

危险系数高, 稍微不慎, 会导致其他人的代码丢失, 以下是步骤:

  • 使用文章开头的方式回滚本地分支
  • 强行提交到远端分支
sh
git push origin master -f

参考资料