Git 概念与原理
掌握 Git 的使用有助于我们打开代码世界的大门。
学习 Git 的难点在于概念理解,Git 的概念是由一套完整的思维逻辑所构成。
问题
- 如何理解、掌握
rebase
指令的使用? - 为什么要 commit 后再 push 这么啰嗦,而不能直接提交到中央仓库?
- reset 这个指令为什么这么神奇,好多看起来并不相似的操作却都要用到它?它到底是用来干什么的?
- revert 和 rebase 都可以撤销历史提交?它们的区别在哪?什么,你说 reset 也行?
- 如何修改历史提交中的错误?
- 误删 branch 怎么办?
- merge 和 rebase 的区别?
- reset 的几种实用用法?
- 查找特定的提交记录
- 什么是
SHA-1
?
Git 基本
版本控制系统
概念:
- 版本控制系统:Version Control System - VCS
- 中央式版本控制系统:Centralized VCS
- 分布式版本控制系统:Distributed VCS
中央式:
- 工作模型:
- 有一个中央服务器存储所有版本数据
- 开发者从中央服务器获取最新代码
- 开发者修改后直接提交到中央服务器
- 优点:
- 管理简单,权限控制方便
- 代码都在服务器上,安全性好
- 学习成本低,容易上手
- 缺点:
- 必须联网才能工作
- 中央服务器出现故障,所有人都无法工作
- 服务器压力大,数据备份困难
分布式:
- 工作模型:
- 每个开发者都有完整的本地仓库
- 可以在本地提交代码,形成版本历史
- 可以从其他开发者处获取代码
- 可以推送代码到远程仓库
- 开发者之间可以建立对等连接
- 优点:
- 可在本地操作,速度快,无需联网
- 可分步提交代码,便于 review 和回溯
- 缺点:
- 初次 clone 项目较慢
- 本地存储占用较大
仓库体积问题:
- 一般项目主要是文本代码,体积小且易压缩,推荐 DVCS(Git)
- 游戏项目包含大量媒体文件难压缩,故多用 CVCS(SVN)
Git 安装
- win:下载安装
- mac:用 Homebrew
Git 配置(WIP)
Git 基本命令
利用 GitHub 创建项目,完成提交推送操作:
# 克隆仓库到本地
git clone [远程地址]
# 查看仓库日志
git log
# 查看仓库状态
git status
# 添加到版本控制
git add [文件名]
# 提交改动
git commit
# 推送改动
git push
假装同事
再 clone
一次远程仓库到本地,当做你的模拟同事的本地仓库(Git 的管理是目录级别, 而不是设备级别)。
如果跟原仓库在同级目录,可以在 clone
命令加个参数:
git clone https://github.com/[accoutn-name]/[repo-name].git [自定义仓库名称]
拉取改动:
# 拉取改动到本地(影响本地文件)
git pull
# 抓取提交记录(不影响本地文件)
git fetch
push 冲突
冲突处理:
- 用
pull
把远程仓库上的新内容取回到本地和本地合并 - 把合并后的本地仓库向远程仓库推送
冲突处理的过程,也称为“合并”的过程,对应的 Git 指令是 merge
。
git pull
指令内部实现是把远程仓库用 git fetch
取下来后再进行 merge
操作。
Git 进阶
HEAD、master 与 branch
引用(reference):
- commit 的快捷方式:Git 提供了“引用”机制:使用固定的字符串作为引用,指向某个
commit
,作为操作commit
的快捷方式 - 引用的本质就是一个个的字符串,可以是 commit 的 SHA-1 码,也可以是一个 branch(ref: refs/heads/feature3)
HEAD:当前 commit 的引用(可以理解为 Git 中一个独特的引用)。
branch:也是一种引用。
master:默认 branch。
branch 命令:
# 创建分支
git branch [分支名称]
# 切换分支
git checkout [分支名称]
# 创建并切换分支
git checkout -b [分支名称]
# 删除分支
git branch -d [分支名称]
# 强制删除分支
git branch -D [分支名称]
git clone 的本质:git checkout master
push 的本质
push
是把当前的分支上传到远程仓库,并把这个branch
的路径上的所有commit
也一并上传push
的时候,如果当前分支是一个本地创建的分支,需要指定远程仓库名和分支名,用git push origin branch_name
的格式,而不能只用git push
;或者可以通过git config
修改push.default
来改变push
时的行为逻辑push
执行之后会上传当前分支,但不会上传HEAD
;远程仓库的HEAD
是永远指向默认分支(即master
)
merge:合并 commits
merge
的含义:从两个commit
分叉的位置起,把目标commit
的内容应用到当前commit
(HEAD 所指向的commit
),并生成一个新的commit
merge
的适用场景:1)单独开发的 branch 用完了以后,合并回原先的 branch;2)git pull 的内部自动操作merge
的三种特殊情况:- 冲突(Conflict):原因:当前分支和目标分支修改了同一部分内容,Git 无法确定应该如何合并;处理:解决冲突后手动
commit
- HEAD 领先于目标
commit
:Git 什么也不做,空操作 - HEAD 落后于目标
commit
:快速移动(fast-forward)
- 冲突(Conflict):原因:当前分支和目标分支修改了同一部分内容,Git 无法确定应该如何合并;处理:解决冲突后手动
Feature Branching:最流行的工作流
Feature Branching 工作流:
- 任何新的功能(feature)或 bug 修复全都新建一个 branch 来写
- branch 写完后,合并到 master,然后删掉这个 branch
关于 Pull Request:
- 不是 Git 的特性,而是一些 Git 仓库服务提供方(如 GitHub)所提供的一种便捷功能
关于 add
使用 add
可以把改动的东西放进暂存区。
add
添加的是文件改动,而不是文件名。
# 把所有改动放进暂存区
git add .
# 提交改动
git commit
实际上,VSCode, IntelliJIDEA 等 IDE 工具不需要 add,都是直接 commit。
看看我都改了什么
查看历史记录:
git log
查看详细历史记录:
git log -p
查看简要统计:
git log --stat
查看具体的 commit:show
看当前 commit:
git show
看任意一个 commit:
# git show [引用]
git show 5e68b0d8
看指定 commit 中的指定文件:
# git show [引用] 文件名
git show 5e68b0d8 list.txt
看未提交的内容:diff
对比暂存区和上一条提交:
git diff --staged
提示:
--staged
有一个等价的选项叫做 --cached
。这里所谓的「等价」,是真真正正的等价,它们的意思完全相同。
比对工作目录和暂存区:
git diff
比对工作目录和上一条提交:
git diff HEAD
总结:
- 查看历史中的多个 commit:
log
- 查看具体某个 commit:
show
- 查看未提交的内容:
diff
.gitignore —— 排除不想被管理的文件和目录
在 Git 中有一个特殊的文本文件:.gitignore。这个文本文件记录了所有你希望被 Git 忽略的目录和文件。
.gitkeep —— 提交空目录
Git 不能提交空目录,如果非要提交,可以在空目录中放一个 .gitkeep
文件。
Git 高级
不喜欢 merge 的分叉?用 rebase
如果你不喜欢 merge
后的分叉历史,可以用 rebase
来保持提交历史的线性。
rebase
(变基):给 commit
重新设置基础点(也就是父 commit) —— 即把你指定的 commit 以及它所在的 commit 串,以指定的目标 commit 为基础,依次重新提交一次。
我的理解:把一个分支上的一段 commit 串摘下来(剔除),嫁接到另一个分支上(移动)
(这里应该有图示)
示例:
合并操作(merge):
git merge branch1
变基操作(rebase):
git checkout branch1
git rebase master
# 还要切回 master 再 merge 一下,把 master 移到最新的 commit
git checkout master
git merge branch1
为什么还要切回 master 再 merge
一下?
- 因为如果在 master 执行
rebase,会导致
master 上最新commit
被剔除掉,而如果远程仓已经这几个最新commit
的话,就会导致无法push
- 为了避免和远程仓库发生冲突,一般不要从 master 向其它分支执行
rebase,而如果是非
master 分支,则可以直接rebase
刚刚提交的代码,发现写错了怎么办?
一个地方写错,然后本地 commit 了,但还没 push 到远程仓库
解决:
# 替换上一次提交
git commit --amend
"amend" 是“修正”的意思。在提交时,如果加上 --amend
参数,Git 不会在当前 commit
上增加 commit
,而是会把当前 commit
里的内容和暂存区(stageing area)里的内容合并起来后创建一个新的 commit
,用这个新的 commit
把当前 commit
替换掉。所以 commit --amend
做的事就是它的字面意思:对最新一条 commit
进行修正。
思考:已经提交到远程仓库就不适用了。
写错的不是最新的提交,而是倒数第二个?
如果不是最新的 commit
写错,就不能用 commit --amend
来修复了,而是要用 rebase
。不过需要给 rebase
也加一个参数:-i
。
rebase -i
= rebase --interactive
= 交互式 rebase
所谓“交互式 rebase”,就是在 rebase
的操作执行之前,你可以指定要 rebase
的 commit
链中的每一个 commit
是否需要进一步修改。
那么你就可以利用这个特点,进行一次“原地 rebase“。
操作:
- 执行
git rebase -i 目标 commit
- 在编辑界面中指定需要操作的 commit 以及操作类型
- 操作完成之后用
git rebase --continue
来继续 rebase 过程
# 查看下提交记录
git log
# 开启交互式 rebase 过程(跳到一个新界面)
git rebase -i HEAD^^
# 修改写错的 commit
git commit --amend
# 继续 rebase 过程
git rebase --continue
提示:Git 偏移符号:
^
:在 commit 后面加一个或 n 个^
,表示可以把 HEAD 所指向的commit
往回偏移 n 个~
:同理,向前偏移 n 个
思考:这样做有什么好处?复杂度提升,只是为了可以减少一个新 commit?