5770 words
29 minutes
Git 高级操作指南

Git 高级操作指南#

第一章:处理 Git 分支分叉(Divergent Branches)#

引言#

在使用 Git 进行版本控制时,分支分叉(Divergent Branches)是每个开发者都会遇到的常见问题。当你的本地分支与远程分支各自拥有不同的提交历史时,Git 无法执行快速前推(Fast-Forward),这就形成了分叉状态。本文将深入分析分支分叉的成因,并提供三种实用的解决方案。

问题根源:什么是分支分叉#

分支分叉的本质是:同一个分支引用在本地和远程指向了不同的提交序列。造成分叉的原因通常有以下几种:

  1. 多人协作:其他团队成员向远程分支推送了提交
  2. 多设备使用:你在另一台设备上提交了代码,然后尝试从当前设备推送
  3. 长时间未同步:本地分支与远程分支分离太久

以下图示展示了分支分叉的典型场景:

A---B---C (本地分支 feature)
/
D---E---F---G (远程分支 origin/feature)

在这个例子中:

  • 远程分支从提交 D 开始,经过 E、F、G
  • 本地分支从同一个起点 D 开始,但沿着不同的路径发展出了 A、B、C 三个提交
  • 此时本地和远程的提交历史已经分叉,无法进行快速前推

分叉的直观判断:当你执行 git status 时,会看到类似以下的提示:

On branch feature
Your branch and 'origin/feature' have diverged.
and have 3 and 1 different commits each, respectively.

三种解决方案详解#

方案一:Merge(合并)#

Merge 是最直观的解决方案,它将两个分支的提交历史合并在一起,创建一个新的合并提交。

配置与执行命令

Terminal window
# 设置 pull 行为为合并(默认行为)
git config pull.rebase false
# 执行合并
git pull --no-rebase

原理图示

合并前:

A---B---C (本地分支 feature)
/
D---E---F---G (远程分支 origin/feature)

合并后:

A---B---C
/ \
D---E---F---G---H (合并提交 M)

特点

  • 保留完整的提交历史
  • 创建一个合并提交(M),记录合并信息
  • 不会改变已有的提交 hash
  • 适用于需要保留完整历史的项目

适用场景

  • 团队协作时
  • 需要保留完整历史记录
  • 已发布的分支
  • 不介意产生额外合并提交

方案二:Rebase(变基)#

Rebase 将本地分支的提交「重放」到目标分支的顶部,从而创造出一条线性历史。

配置与执行命令

Terminal window
# 设置 pull 行为为变基
git config pull.rebase true
# 执行变基
git pull --rebase

原理图示

变基前:

A---B---C (本地分支 feature)
/
D---E---F---G (远程分支 origin/feature)

变基后:

A'--B'--C' (本地分支 feature)
/
D---E---F---G (远程分支 origin/feature)

重要警告

注意:Rebase 会改变提交历史!
这意味着原有的 A、B、C 提交会被重新创建为 A'、B'、C',
它们的 hash 值将与原来完全不同。

特点

  • 创造线性、整洁的提交历史
  • 不产生额外的合并提交
  • 会改变提交 hash
  • 危险:不要对已发布的分支进行变基

适用场景

  • 个人开发分支
  • 未推送的提交
  • 清理本地提交历史(如 squash)
  • 渴望整洁的线性历史

方案三:Fast-Forward Only#

如果你希望保持分支历史的纯净,只接受能够快速前推的更新,可以配置 Git 仅允许 fast-forward 合并。

配置与执行命令

Terminal window
# 设置仅允许 fast-forward 合并
git config pull.ff only
# 执行拉取
git pull

当存在分叉时,此命令会失败并报错:

fatal: Not possible to fast-forward, aborting.

特点

  • 强制保持线性历史
  • 不会产生合并提交
  • 不会产生变基提交
  • 分叉时直接拒绝操作
  • 适用于严格的分支管理策略

适用场景

  • 要求绝对线性历史的团队
  • 使用 git flow 的项目
  • 需要避免所有合并记录的场景

如何选择#

面对分支分叉,选择正确的处理方式至关重要。以下是决策流程:

开始
分叉的分支是否已推送/共享?────是──→ 使用 Merge
是否需要保留完整历史记录?────是──→ 使用 Merge
是否在个人分支上?───────────是──→ 使用 Rebase
使用 Merge

对比表格

特性MergeRebaseFast-Forward Only
提交历史保留完整历史线性历史严格线性
新提交合并提交重放提交不允许
改变历史
复杂性
适用场景团队协作个人清理严格管理

第二章:Git 初始化与远程仓库连接#

git init:创建本地仓库#

git init 是开始使用 Git 的第一步,它在当前目录创建一个新的 Git 仓库。

基本用法

Terminal window
# 在当前目录初始化仓库
git init

执行后会看到以下输出:

Initialized empty Git repository in /path/to/project/.git/

初始化裸仓库

Terminal window
# 创建裸仓库(通常用于服务器)
git init --bare

裸仓库与普通仓库的区别:

  • 裸仓库没有工作目录,不允许直接编辑和提交
  • 专为作为远程共享仓库而设计
  • 通常以 .git 结尾命名(如 project.git/

带描述的初始化

Terminal window
git init -b main

指定初始分支名称,而不是使用默认的 master

.gitignore:排除不需要跟踪的文件#

.gitignore 文件用于告诉 Git 忽略特定的文件和目录,不将它们纳入版本控制。

基本语法

# 注释行,以 # 开头
file.txt # 忽略特定文件
*.log # 使用通配符忽略所有 .log 文件
folder/ # 忽略特定目录

忽略规则示例

# 依赖目录
node_modules/
vendor/
# 构建产物
dist/
build/
*.o
*.a
# 日志文件
*.log
# 环境配置文件(但保留示例)
.env
!.env.example
# IDE 配置
.vscode/
.idea/
# 操作系统文件
.DS_Store
Thumbs.db

否定匹配! 前缀表示取反,即使前面的规则匹配,也要包含该文件。

# 忽略所有 .log 文件
*.log
# 但保留 error.log
!error.log

GitHub 官方忽略模板

GitHub 提供了针对各种语言的 .gitignore 模板,可通过以下地址访问: https://github.com/github/gitignore

HTTPS vs SSH:选择合适的连接方式#

连接远程仓库有两种主要方式:HTTPS 和 SSH。

对比表格

特性HTTPSSSH
端口44322
认证用户名密码/tokenSSH 密钥
每次操作需认证是(可配置缓存)
防火墙友好通常是
配置复杂度
密钥类型ED25519、RSA 等

HTTPS 适用场景

  • 临时访问
  • 防火墙限制严格的网络
  • 不希望配置 SSH 密钥

SSH 适用场景

  • 频繁的推送拉取操作
  • 需要自动化脚本
  • 更安全的认证方式

SSH 配置步骤

  1. 检查是否已有 SSH 密钥:
Terminal window
ls -la ~/.ssh
  1. 生成新的 ED25519 密钥(推荐):
Terminal window
ssh-keygen -t ed25519 -C "your_email@example.com"
  1. 系统会提示输入密钥保存位置和密码,直接回车使用默认值:
Generating public/private ed25519 key pair.
Enter file in which to save the key (/c/Users/username/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
  1. 将公钥添加到 GitHub/GitLab:
Terminal window
cat ~/.ssh/id_ed25519.pub

复制输出内容,添加到 GitHub 的 Settings > SSH Keys 中。

  1. 测试连接:
Terminal window
ssh -T git@github.com

成功会看到:Hi username! You've successfully authenticated.

完整工作流程#

以下是创建一个新项目并推送到远程仓库的完整流程:

第一步:创建项目目录并进入

Terminal window
mkdir my-project && cd my-project

第二步:初始化 Git 仓库

Terminal window
git init

第三步:创建初始文件并提交

Terminal window
git add .
git commit -m "Initial commit"

第四步:添加远程仓库

Terminal window
git remote add origin git@github.com:username/repo.git

第五步:推送并设置上游分支

Terminal window
git push -u origin main

-u 参数设置上游跟踪,后续可以直接使用 git pushgit pull

远程仓库管理命令#

查看远程仓库

Terminal window
# 查看已配置的远程仓库
git remote -v
# 示例输出:
# origin git@github.com:username/repo.git (fetch)
# origin git@github.com:username/repo.git (push)

添加远程仓库

Terminal window
# 添加新的远程仓库(通常用于 fork 的项目)
git remote add upstream git@github.com:original-owner/repo.git

修改远程仓库 URL

Terminal window
# 修改已存在的远程仓库 URL
git remote set-url origin new-url

获取远程数据

Terminal window
# 仅获取数据,不合并
git fetch origin
# 获取所有远程仓库的数据
git fetch --all

拉取数据

Terminal window
# 从上游分支拉取并合并
git pull origin main
# 使用变基方式拉取
git pull --rebase origin main

分支基础操作#

查看分支

Terminal window
# 列出本地分支
git branch
# 列出所有分支(包括远程)
git branch -a
# 查看分支追踪关系
git branch -vv

创建与切换分支

Terminal window
# 创建新分支
git branch feature/new-feature
# 切换到新分支
git checkout feature/new-feature
# 创建并切换(简写)
git checkout -b feature/new-feature
# 现代 Git 语法
git switch -c feature/new-feature

推送分支到远程

Terminal window
# 推送并设置上游追踪
git push -u origin feature/new-feature
# 后续可以直接 push
git push

删除分支

Terminal window
# 删除本地分支(已合并)
git branch -d feature/old-feature
# 强制删除本地分支
git branch -D feature/old-feature
# 删除远程分支
git push origin --delete feature/old-feature

常见问题排查#

问题一:fatal: not a git repository

fatal: not a git repository (or any of the parent directories): .git

原因:当前目录不是 Git 仓库。

解决方案

Terminal window
# 在项目目录中初始化
git init
# 或者切换到正确的仓库目录
cd path/to/repo

问题二:error: src refspec main does not match any

error: src refspec main does not match any.

原因:本地分支不存在,或者还没有任何提交。

解决方案

Terminal window
# 添加文件并创建初始提交
git add .
git commit -m "Initial commit"
# 然后再推送
git push -u origin main

问题三:fatal: remote origin already exists

fatal: remote origin already exists.

原因:远程仓库 origin 已存在。

解决方案

Terminal window
# 方法一:修改现有 URL
git remote set-url origin new-url
# 方法二:删除后重新添加
git remote remove origin
git remote add origin new-url

问题四:Permission denied (publickey)

Permission denied (publickey).
fatal: Could not read from remote repository.

原因:SSH 密钥未配置或未添加到 GitHub。

解决方案

Terminal window
# 1. 检查 SSH 密钥是否存在
ls -la ~/.ssh
# 2. 生成新密钥(如不存在)
ssh-keygen -t ed25519 -C "your_email@example.com"
# 3. 添加公钥到 GitHub
cat ~/.ssh/id_ed25519.pub
# 复制输出到 GitHub > Settings > SSH Keys
# 4. 测试连接
ssh -T git@github.com

问题五:fatal: Authentication failed

fatal: Authentication failed for 'https://github.com/...'

原因:HTTPS 认证失败,凭证不正确。

解决方案

Terminal window
# 方法一:使用 token 作为密码
# GitHub Settings > Developer settings > Personal access tokens
# 方法二:配置 Git 凭据缓存
git config --global credential.helper cache
# 方法三:使用 SSH 方式(避免每次输入)
git remote set-url origin git@github.com:username/repo.git

推荐配置#

基础配置

Terminal window
# 设置用户名
git config --global user.name "Your Name"
# 设置邮箱
git config --global user.email "your@email.com"
# 设置默认分支名(新项目)
git config --global init.defaultBranch main

Pull 行为配置

Terminal window
# 默认使用 merge 方式(而非 rebase)
git config --global pull.rebase false
# 或者默认使用 rebase
git config --global pull.rebase true
# 仅允许 fast-forward
git config --global pull.ff only

别名配置

Terminal window
# 简洁的日志查看
git config --global alias.lg "log --oneline --graph --decorate --all"
# 简化 status
git config --global alias.st "status -sb"
# 撤销最后一次提交(保留更改)
git config --global alias.undo "reset --soft HEAD~1"

第三章:commit 后 pull 的分叉问题#

问题场景#

日常开发中,你是否遇到过这样的场景:本地 commit 之后,兴致勃勃地准备 push,却发现远程已经有了新的提交,于是顺手执行了 git pull。这一拉不要紧,Git 提示你:

Merge made by the 'ort' strategy.
Your branch and origin/main have diverged,
and have 1 and 1 different commits each.

明明只是做了正常的开发工作,为什么 Git 突然变得”分叉”了?

分叉是如何产生的#

问题的本质#

分叉(divergence)发生在两条独立的历史线同时推进时。当你执行 git commit 创建新提交时,这条历史线是基于你当时的本地分支状态。与此同时,如果远程分支在其他地方有新提交,这两条历史线就走向了不同的方向。

commit 之后 pull 产生分叉的典型场景:
时间线:
┌─────────────────────────────────────────────────────────────┐
│ │
│ 你(本地) 同事(远程) │
│ │ │ │
│ ├─ A ← 你做了 commit │ │
│ │ ├─ B ← 同事先 push 了 │
│ │ │ │
│ └─ 此时 pull 会怎样? │ │
│ │ │
└─────────────────────────────────────────────────────────────┘

为什么 Git 无法自动合并#

当你执行 git commit 时,Git 创建了一个新提交对象,它的父提交指向你之前的 HEAD。这个提交对象有一个唯一的 SHA-1 哈希值,基于当时的文件状态和历史。

当你随后执行 git pull 时,Git 发现:

  • 你的本地分支有一个新提交(你知道的那个)
  • 远程分支也有一个新提交(你不知道的)
  • 这两个提交没有直接的父子关系

Git 不知道如何将两条独立的历史合并在一起,因此它需要你的介入来做出选择。

关键误解:pull 不会推送暂存区#

这里有一个常见的误解需要澄清:

git pull 是从远程获取并合并到本地,它永远不会推送任何内容

无论你执行多少次 git pull,本地暂存区的内容都不会被推送到远程。如果你有暂存的文件,需要:

Terminal window
git add . # 添加到暂存区
git commit -m "xxx" # 创建提交
git push # 推送到远程

merge vs rebase:处理方式对比#

当你遇到分叉时,git pull 实际上在后台执行了 fetch + merge 或 fetch + rebase。Git 提供了两种主要的处理策略。

方式一:git pull —no-rebase(Merge)#

Merge 方式会创建一个合并提交(merge commit),将两条历史线合并在一起。

Terminal window
# 默认的 pull 行为(取决于配置)
git pull
# 或者显式指定 merge 方式
git pull --no-rebase

结果图示

A (你的提交)
/
...-M (远程提交 B) ← merge commit 有两个父提交
\
B' (合并后的状态)

Merge 的特点

特性说明
保留完整历史所有提交,包括合并过程都被保留
不重写提交原有提交的 SHA-1 哈希值不变
产生额外提交会创建一个 merge commit
历史复杂度分支图会出现”分叉-合并”的形状

适用场景

  • 共享分支(main、develop)需要保留完整历史
  • 需要清晰看到团队的协作轨迹
  • 已推送的提交不希望被修改

方式二:git pull —rebase(Rebase)#

Rebase 方式会将你的提交重写到远程分支最新提交之上,形成线性历史。

Terminal window
# 执行 rebase 方式的 pull
git pull --rebase

结果图示

A' (重放后的提交,全新 hash)
/
...-B (远程提交)

Rebase 的特点

特性说明
线性历史历史呈一条直线,简洁清晰
无额外提交不产生 merge commit
重写提交原有提交的 SHA-1 会改变
冲突处理需要逐个提交解决冲突

重要警告

不要对已推送的提交执行 rebase!

Rebase 会重写提交历史。如果其他人基于你原来的提交工作,这会造成严重的问题。

适用场景

  • 个人开发分支,保持历史整洁
  • 本地未推送的提交
  • 需要线性历史便于排查问题时

实际效果对比#

假设你基于 commit B 创建了一个功能分支,然后你做了 commit A:

初始状态:
...-B (main 分支最新提交)
└─ A (你的功能分支上的提交)

与此同时,main 分支上有人推送了新的 commit C:

远程更新后:
...-B-C (main 分支)
└─ A (你的功能分支上的提交,还不知道 C 的存在)

使用 Merge 合并后

A
/ \
...-B-C---M (merge commit)

使用 Rebase 合并后

...-B-C-A' (A 被重放到 C 之上,变成全新的提交 A')

如何避免分叉#

最佳实践:先 fetch,后决策#

与其等分叉发生了再处理,不如在 commit 之前就了解远程状态:

Terminal window
# 1. 先获取远程最新状态(不合并)
git fetch origin
# 2. 查看远程分支的状态
git log --oneline origin/main -5
# 3. 如果远程有更新,再决定如何处理
# 个人分支:用 rebase
git pull --rebase
# 共享分支:用 merge
git pull --no-rebase

配置默认 pull 行为#

根据你的工作场景,配置合适的默认行为:

Terminal window
# 对于大多数仓库,默认使用 merge
git config --global pull.rebase false
# 对于个人项目较多的仓库,可能更喜欢 rebase
git config --global pull.rebase true

在 commit 前检查远程状态#

养成在 commit 之前检查远程状态的习惯:

Terminal window
# 查看当前分支与远程分支的差异
git status
# 如果显示 "Your branch is ahead of origin/main by X commits"
# 说明你本地的提交还没有同步到远程

使用 pull —ff-only 强制快进#

如果你确定本地没有独立的工作,只想同步远程更新:

Terminal window
git fetch origin
git pull --ff-only

如果无法快进(例如存在分叉),这个命令会失败并提示你,而不是创建 merge commit 或 rebase。

如果已经分叉了怎么办#

情况一:刚发生分叉,本地没有其他提交#

如果分叉刚刚发生,你还没有基于这个状态做更多开发,最简单的方式是:

方案 A:Reset 到远程状态,重新来过

Terminal window
# 先查看分叉状态
git status
# 放弃本地提交(谨慎操作!)
git reset --hard origin/main
# 然后重新做你的修改
git add .
git commit -m "你的提交"

方案 B:用 rebase 挽救本地提交

Terminal window
# fetch 最新状态
git fetch origin
# 尝试 rebase
git rebase origin/main
# 如果有冲突,解决后继续
git add .
git rebase --continue
# 或者放弃 rebase,恢复原状
git rebase --abort

情况二:分叉后已经做了多个提交#

如果你已经在一个分叉的状态下做了多个提交,处理起来需要更谨慎:

保留历史用 merge

Terminal window
git fetch origin
git merge origin/main
# 解决可能的冲突
git add .
git commit -m "Merge remote changes"
git push

整理历史用 rebase(仅限未推送的提交)

Terminal window
git fetch origin
git rebase origin/main
# 逐个解决冲突
# ...
git push --force-with-lease

注意:--force-with-lease--force 更安全,它会检查是否有其他人也在推送,避免覆盖别人的工作。

情况三:不确定如何处理,先备份#

在尝试任何修复之前,先备份当前状态:

Terminal window
# 创建备份分支
git branch backup/my-feature-$(date +%Y%m%d%H%M%S)
# 然后再尝试修复

实操流程总结#

日常开发推荐流程#

1. 开始新任务前
git fetch origin # 获取最新状态
git pull origin main # 同步 main 分支
git checkout -b feature/xxx # 创建功能分支
2. 开发过程中
git add .
git commit -m "feat: 完成功能"
git fetch origin # 再次 fetch 检查远程是否有更新
3. commit 之前发现远程有更新
git pull --rebase # 个人分支用 rebase
git push
4. commit 之后发现分叉
git fetch origin
git pull --rebase # 或 --no-rebase,看需求
git push

分叉处理决策树#

发现分叉
├── 这是个人开发分支?
│ ├── 是 → git pull --rebase → git push
│ └── 否 → git pull --no-rebase → git push
├── 已推送的提交需要保留完整历史?
│ └── 是 → git pull --no-rebase → git push
└── 想保持历史整洁且只有本地提交?
└── 是 → git pull --rebase → git push

常见问题解答#

Q: 为什么我用 git pull 报错了?#

可能的原因:

  1. 分支存在分叉,Git 不知道如何合并
  2. 存在未解决的冲突
  3. 凭证过期(对于 HTTPS 连接)

Q: merge 和 rebase 哪个更好?#

没有绝对的好坏,只有适用场景:

  • 公共分支用 merge:保留完整历史,便于追溯
  • 个人分支用 rebase:保持历史整洁

Q: 误操作导致分叉,能恢复吗?#

大多数情况下可以:

Terminal window
# 查看 reflog 找到之前的 HEAD 状态
git reflog
# 恢复到某个之前的状态
git reset --hard HEAD@{5}

Q: 如何避免强制 push?#

配置保护规则:

Terminal window
# 在 Git 配置中禁止非快进的推送
git config --global push.default simple

在 GitHub/GitLab 等平台也可以在仓库设置中强制启用分支保护。

Q: rebase —continue 和 —skip 区别?#

命令使用场景
--continue解决了冲突后,继续变基过程
--skip忽略当前冲突提交,继续下一个
--abort取消整个变基,恢复原状态
Terminal window
# 解决冲突后
git add .
git rebase --continue
# 跳过当前提交(如果冲突无法解决或不需要)
git rebase --skip
# 取消变基,恢复原状态
git rebase --abort

Q: force-with-lease 和 force 区别?#

命令安全性
git push --force危险:强制覆盖远程,可能丢失他人提交
git push --force-with-lease安全:只有在你有最新远程引用时才覆盖

推荐:始终使用 --force-with-lease,它会在有他人新提交时拒绝推送。

Q: 长裤提交历史太多,导致全量clone时的文件太大,拉取失败,如何操作?#

执行 git clone 时,Git 默认会拉取整个仓库的快照,包括所有分支、标签以及完整的历史提交记录。

  1. 历史包袱:即使当前仓库很小,但如果历史提交中曾经存在过大文件,或者积累了成千上万次的提交,Git 都需要下载并重建这些历史数据。

  2. 压缩与解包:Git 在传输时会对数据进行打包压缩,客户端下载后需要消耗大量内存和 CPU 进行解包(index-pack)和校验。这个过程中,内存占用可能是下载包大小的 3-5 倍,极易触发超时或内存溢出。

解决方法:浅克隆(Shallow Clone) 如果只需要最新的代码,不需要查看历史提交记录,可以使用浅克隆。这会把下载的数据量降到最低:

Terminal window
# 只克隆最近一次提交,不拉取任何历史记录
git clone --depth=1 https://github.com/用户名/仓库名.git

总结#

分支分叉处理核心要点

  1. Merge 适合团队协作,保留完整历史,会产生合并提交
  2. Rebase 创造线性历史,但会改变提交 hash,绝不能对已发布分支使用
  3. Fast-Forward Only 强制线性历史,分叉时直接拒绝
  4. 选择依据:分支是否已共享、是否需要保留历史、是否在个人分支上

初始化与远程连接核心要点

  1. 使用 git init 创建仓库,--bare 用于服务器
  2. .gitignore 规则遵循特定语法,善用 GitHub 官方模板
  3. HTTPS 适合临时访问,SSH 适合频繁操作
  4. 完整流程:initaddcommitremote addpush
  5. 使用别名提升效率,配置凭据缓存避免重复输入密码

commit 后 pull 分叉核心要点

  1. 预防为主:频繁 pull 或 fetch,避免长时间分叉
  2. 共享分支用 merge:保留完整历史,不改变提交 hash
  3. 个人分支用 rebase:追求线性历史,但避免对已推送分支使用
  4. force-with-lease:强制推送时使用,更安全的选项
  5. 有冲突不慌张:解决冲突后 add + commit/continue 即可

掌握这三大主题,将帮助你更好地管理 Git 仓库,从容应对日常开发中的版本控制挑战。

Git 高级操作指南
https://sgjki547.top/posts/git-devops/
Author
SGJki
Published at
2026-04-07
License
CC BY-NC-SA 4.0