一个有趣的 Git 练习网站

前一阵子整理一个 GitHub 项目,整得焦头烂额。

一个项目我只上传了一部分文件上去,等到想要上传剩下的部分时,却告诉我无法完成提交。

因为对 Git 的操作还不是特别熟练,所以我最后只能采用了一个笨方法:下载 GitHub Destop 拖拽式上传。

为了以后不再出现这种尴尬的事情,我决定好好复习 Git 知识。

于是,我就找到了这个有趣的 Git 练习网站:Learn Git Branching

image-20210625154024547

  • Git For Branching 界面。还是非常美观的。

经过学习后,我发现我的问题可能是 2.8 节所指出的,远程服务器的拒绝。

以下是我学习过程整理的记录。

1. 主要内容

1.1 Git 的思路

Git Commit 的提交记录保存的是文件快照,类似复制粘贴。

通过 diffing 算法,将当前版本与上个版本比较,并将差异保存起来作为提交记录。

Git 保存了提交历史记录。

// 提交
git commit

1.2 Git Branch

Git Branch 的分支思路也非常轻量化,仅仅指向某个提交记录。

建议早建分支,多建分支。

image-20210623095552620

建完分支后,要切换分支。可以用 checkout 命令。当然,checkout -b 可以创建的同时切换。

image-20210623095638258

image-20210623095759276

// 创建分支
git branch newImage
git commit
// 切换分支
git checkout newImage
git commit
// 创建并切换分支
git checkout -b bugFix

1.3 Git Merge

Git Merge 用于合并两个分支,会产生一个特殊节点,包含两个父节点。

image-20210623101910070

image-20210623160859786

image-20210623162530728

// 当前分支与目标分支合并
git merge bugFix
git checkout bugFix
git merge main
// 由于 main 的一个父节点是 bugFix
// 换句话说,main 继承自 bugFix
// 所以 Git 什么也没做

1.4 Git Rebase

Git Rebase 也是一种合并分支的方法。取出一系列提交记录,复制并放在另一个地方。

image-20210623163141161

image-20210623163403457

image-20210623163655238

// 合并分支,得到线性提交序列
git rebase main
git checkout main
git rebase bugFix
// 由于bugFix 继承自 main,所以 Git 只是简单前移了 main

1.5 如何在提交树上移动

HEAD:你目前正在工作的提交记录,一般指向当前分支的最近一次提交记录。

查看 HEAD 指向可以用 cat .git/HEAD。

image-20210623170429494

image-20210623170450258

// 调整 HEAD 指向具体的提交记录,而不是分支
git checkout C1

1.6 相对引用

通过指定提交记录的哈希值移动 HEAD 不是很方便,因为哈希值都很长。

可以用 Log 查看提交记录的哈希值。

相对引用就是可以从一个容易记忆的地方移动 HEAD。^向上移动一个提交记录如main^~<num>向上移动多个提交记录,如~3

也可以用 HEAD 作为相对引用的参照。

使用相对引用最多的是移动分支,可以在 Branch 上加一个 -f 改变分支指向的提交节点。

image-20210623171401146

image-20210623171818211

image-20210623171827920

// 查看日志
git log
// 找到 main 的父节点
git checkout main^
// 找到 HEAD 的父节点
git checkout HEAD^
// 让 main 分支指向 HEAD 的第三级父提交节点
// 可以说,git branch -f 和 git branch 内容都不一样
// git branch 是创建分支,而 git branch -f 是改变分支指向提交节点
git branch -f main HEAD~3
// 注意,不能写 git branch -f HEAD HEAD^,因为没有这个分支
// 只能用 checkout 调整 HEAD
git checkout HEAD^

1.7 撤销变更

可以通过 Reset 和 Revert 撤销变更。

Reset 相当于撤销历史。通过它可以回退本地的提交节点。但是无法处理远程分支。

Revert 用于撤销远程提交。这个过程引入了新的提交节点,因为这个节点做的事情是撤销上个提交节点的内容,所以和上上个节点的内容是一样的。

image-20210623194350488

image-20210623194401889

image-20210623195541690

image-20210623195731444

// 回退本地提交。只针对 local。
git reset HEAD~1
// 撤销远程提交。只针对 pushed。
git revert HEAD

1.8 整理提交记录

整理提交记录在处理复杂请求时十分重要。

Cherry-pick 可以用于将指定提交节点复制到 HEAD 分支上。

也可以用交互式 Rebase,也就是带--interactive的 Rebase,简写为-i。会用一个交互式 vim 进行排序和复制。将指定数量的内容按照指定顺序放在 HEAD 分支上。

image-20210623200723295

image-20210623200739861

image-20210623201157152

// 将 C2 和 C4 复制到当前分支
git cherry-pick C2 C4
// 交互式 vim 进行复制
git rebase -i HEAD~4

1.9 本地栈式提交

image-20210623201615138

git checkout c1
git cherry-pick c4

1.10 提交技巧

如果相对之前的提交节点进行调整,可以用 Rebase -i 调整位置,将想要修改的记录挪到前面,然后用 Commit --amend 修改,最后用 Rebase -i 调整回原位,最后将 main 移动到修改的最前端即可。

如果用 Cherry-pick 就更简单。

image-20210623205317182

image-20210623205642249

git rebase -i HEAD~2
git commit --amend
git rebase -i HEAD~2
// 下面这条命令,不仅把 main 挪到了最前面,还将 HEAD 指向了 main,完美
git branch -f main caption

git checkout main
git cherry-pick newImage
git commit --amend
git cherry-pick caption

1.11 Tag

Tag 可以用于永久指向某一个提交节点。

image-20210623205844768

git tag v0 c1
git tag v1 c2
git checkout v1

1.12 Describe

Describe 用于找到离当前分支最近的锚点(也就是 Tag 指向的点)

默认的参数是 HEAD

image-20210623210601972

image-20210623210637652

git describe

1.13 多分支 Rebase

image-20210623211146782

git rebase bugFix
git rebase side
git rebase another
git rebase -i HEAD~7

1.14 选择父提交记录:

操作符^后面也可以接数字,但是并不是选择返回第几代,而是选择合并提交记录的某个父提交。默认是第一个,数字可以改变。

image-20210623211658247

image-20210623211713335

image-20210623211724880

image-20210623211743585

image-20210623211822359

image-20210623212735367

git checkout HEAD~^2~2

git branch -f bugWork HEAD~^2^

1.15 纠缠不清的分支

image-20210623212928477

2. 远程 Git 仓库

2.1 Clone

Clone 用于连接远程仓库,并获取一个远程分支 origin/main。

image-20210623214632355

2.2 远程仓库

远程分支命名都是<remote name>/<branch name>,例如origin/main,则远程仓库是origin,分支是main

远程分支和远程仓库相应的分支同步,而不会受到本地的 Commit 的影响。

image-20210623220221725

image-20210623220317033

image-20210623220537872

git checkout origin/main
git commit
// 提交后,HEAD 与分支进行了分离

git commit
git checkout o/main
git commit

2.3 Fetch

Fetch 用于从远程仓库获取数据。下载缺失的提交记录,并且更新远程分支指针 origin/main。

远程分支反映了远程仓库在你最后一次和它通信时的状态,而 Fetch 就是和远程仓库通信方式。通常通过 http:// 或 git:// 协议与远程仓库通信。

注意,Fetch 不会改变本地仓库状态,不会更新 main 分支,也不会修改磁盘文件!!并不是 Fetch 后本地仓库就和远程仓库同步了!!Fetch 可以理解为单纯的下载操作。

image-20210624112624473

image-20210624112701093

image-20210624112934807

image-20210624113049102

// 下载本地仓库缺失的提交记录,并更新远程分支指针,如 origin/main
git fetch

2.4 Pull

当远程分支有新的提交时,可以像合并本地分支一样来合并远程分支。如执行下列命令。

git cherry-pick origin/main
git rebase origin/main
git merge origin/main

先抓取更新,再合并到本地分支,这个流程十分常见。通过 Pull 可以完成这个流程。

Pull 相当于 Fetch 和 Merge 的结合。

image-20210624113404020

image-20210624113444078

image-20210624113520089

image-20210624113559743

// 下载更新,并合并本地分支与 origin/main 分支
git fetch
git merge origin/main
// 执行一样的操作
git pull

2.5 模拟团队合作

image-20210624113953830

git clone
git fakeTeamwork 2
git commit
git pull

2.6 Push

Push 将 HEAD 指向的分支上传到指定远程仓库,并再远程仓库上合并新提交记录。Push 不带参数时的行为和 push.default 的配置有关。

image-20210624114348373

image-20210624114402846

// 上传本地提交节点,更新远程节点和远程连接
git push

2.7 偏离的工作

Pull 和 Push 看起来没什么难度,但却非常让人困惑,这是源自于远程库提交历史的偏离。当本地提交不是基于远程最新提交节点时,Git 会强制要求本地先合并远程最新提交节点,然后再提交。这也是唯一的办法。

解决方法可以是 Fetch 和 Rebase,然后再 Push。用 Fetch 配合 Merge 也可以。

最简单的是 Pull --rebase,这是 Fetch 和 Rebase 的简写。

image-20210624114636193

image-20210624114722422

image-20210624114819941

image-20210624114959435

image-20210624115020985

image-20210624115212891

image-20210624115222399

image-20210624115357871

image-20210624115519282

image-20210624115641641

image-20210624120202386

// 首先下载远程节点,然后合并最新节点,然后再提交,就不会有问题
git fetch
git rebase origin/main
git push
// 用 merge 也是一样
git fetch
git merge origin/main
git push
// 用 Pull 是最简单的
git pull --rebase
git push
// 相同的效果
git pull
git push

git clone
git fakeTeamwork 1
git commit
// 为什么这里用 pull --rebase 而不用 pull?因为 pull 产生的是新提交记录
// 而 pull --rebase 是将原先的提交记录复制下来
git pull --rebase
git push

2.8 远程服务器的拒绝

有时候 main 被锁定了,就需要 Pull Request 流程来合并修改。如果直接 Push,就会收到警告。所以不能直接提交 main 分支,而是应该按照流程,新建一个分支,Push 该分支,并申请 Pull Request。如果忘记并直接提交给 main,就会卡住,且无法推送更新。

解决方法是新建一个分支,推送到远程服务器,然后 Reset 本地 main 分支,和远程服务器保持一致。否则下次 Pull,并且他人提交和你冲突时就会有问题。

image-20210624120446548

image-20210624122316544

git clone
// 创建并切换到分支 feature
git checkout -b feature
// 提交分支
git push
// 回撤
git branch -f main HEAD^

2.9 推送主分支

image-20210624150540495

image-20210624150601785

image-20210624151516999

// 将工作 rebase 到远程分支的最新提交记录
git pull --rebase
// 向远程仓库推送工作
git push
// 将 side1, side2, side3 放在一条分支上
git rebase side1 side2
git rebase side2 side3
// 将 main 分支放到头部,并将 HEAD 移到 main 分支上
git branch -f main HEAD
git checkout main
// 下载远程分支节点并提交
git pull --rebase
git push

2.10 合并远程仓库

为什么操作远程分支不喜欢用 Merge?Rebase 使提交树变得干净,所有提交都在一条线上。但是 Rebase 修改了提交树的历史。

image-20210624152110446

// 首先切换回 main,然后拉取远程仓库提交节点,保持最新
git checkout main
git pull
// 合并所有本地分支
git merge side1
git merge side2
git merge side3
// 提交
git push

2.11 远程追踪

main 和 origin/main 是关联的。Pull 时,记录会被先下载到 origin/main 上,然后再和本地的 main 合并。Push 时,工作从本地的 main 推送到远程仓库的 main 分支,并同时更新 origin/main。因为 main 分支制定了推送的目的地和拉取的合并目标。这是 Git 提供的属性。

可以类似的设置这种远程追踪属性。Checkout -b 或者 Branch -u。

通过这种远程追踪命令,就可以不移动本地的 main,直接用其他分支替代 main 了!

image-20210624152545575

image-20210624152630323

image-20210624152645925

image-20210624152711211

image-20210624152847071

image-20210624152901685

image-20210624153445502

// 第一种方法,创建新的分支,跟踪远程分支 origin/main,并获取 HEAD
git checkout -b totallyNotMain origin/main
// 第二种方法,命令分支跟踪远程分支。如果在 foo 分支上还可以省略 foo
git branch -u origin/main foo
git checkout -b side o/main
git commit
git pull --rebase
git push

2.12 Push 的参数

前面的远程追踪提到了,Push 未指定参数时是根据当前分支的属性确定远程仓库和要 Push 的目的地的。

指定参数中,包括<remote><place>

<place>参数为 main 时,同时指定了提交记录的来源和去向都是 main。如果想让来源和去向不一样,如本地的 foo 推送到远程的 bar,也是可以的。中间加一个冒号就可以了,<source>:<destination>

如果<destination>不存在,远程仓库会自己创建这个分支。

image-20210624153900023

image-20210624153914711

image-20210624153926528

image-20210624153936236

image-20210624154005059

image-20210625151106758

// 切换到本地仓库的 main 分支,获取所有提交
// 然后到远程仓库 origin 找到 main 分支,将远程仓库没有的记录都添加上去
git push origin main
// 向远程仓库 origin 分别提交 foo 和 main 分支
git push origin foo
git push origin main
// 向远程仓库 origin 的 main 分支分别提交本地的 foo 分支和 foo 分支的父节点
git push origin foo:main
git push origin foo^:main
git push origin main^:foo
git push origin foo:main

2.13 Fetch 参数

Fetch 的参数和 Push 是一样的!只不过方向反了而已。

image-20210625151643195

image-20210625151659137

image-20210625151750457

image-20210625151800527

image-20210625151818061

image-20210625151838464

image-20210625152204358

// 从远程仓库 origin 的 foo 分支上,获取本地不存在的提交
// 放到本地的 origin/foo 上
git fetch origin foo
// 课后挑战
git fetch origin foo:main
git fetch origin main^:foo
git checkout foo
git merge main

2.14 调整 <source>

在 Push 和 Fetch 的时候,可以将冒号前面的<source>留空。Push 的效果是删除分支!Fetch 的效果是创建新分支。

image-20210625152327753

image-20210625152354305

image-20210625152433954

image-20210625152444209

image-20210625152630360

// 删除远程仓库的 foo 分支和本地的 origin/foo 分支
git push origin :foo
// 新建本地的 bar 分支
git fetch origin :bar

2.15 Pull 的参数

Pull 就是 Fetch 和 Merge 的缩写。可以理解为用同样的参数执行 Fetch,然后再 Merge 所抓取到的提交记录。Merge 的对象是 HEAD 指向的提交节点或分支的节点。注意 Merge 之后的节点还是 HEAD 指向的分支。

image-20210625152747470

image-20210625152819997

image-20210625152848360

image-20210625152926159

image-20210625152948348

image-20210625153051363

git pull origin bar:foo
git pull origin main:side

image-20210625153311335

通关啦~~~

3. 其他 Git 操作

3.1 初始化

Init 可以进行初始化。

git init

3.2 添加文件

Add 可以添加文件。可以反复添加文件。

// 上传 foo 文件到本地仓库临时区
git add foo

3.3 查看工作区

Status 可以用于查看工作区状态。

// 查看工作区状态
git status

3.4 查看修改内容

Diff 可以查看修改内容。

// 查看修改内容
git diff

3.5 查看命令历史

Reflog 可以查看命令历史。

// 查看命令历史
git reflog