简介

Wiki:

Git is a distributed version-control system for tracking changes in source code during software development.

Git 是一个常见的版本控制工具,在软件开发中普遍使用,也用来分工协作。

此文章适用于对Git有所了解,但(多次) 希望入门 Git 却一直无法入门的人 (比如我),主要从理解上解决不知道为什么就不能切换分支,不能merge,不能push等等问题。此外,文章不对命令的各种参数进行详细介绍(需要翻翻手册就好啦~

推荐阅读(可以先把他们过一遍,可能就不用看这篇了:)

常用 Git 命令清单 作者:阮一峰

Learn Git Branching 关卡式的有趣教程,对 Git 的分支与版本有一个形象的展示,了解命令之间的不同

启动了Git的文件夹中包括三个空间:**工作区 [Workspace]暂存区 [Index / Stage]本地仓库 [Repository]**。三个空间相互独立,通过 Git 命令进行文件在不同区之间的转化控制。

工作区即直接打开文件的地方。对文件的修改均在工作区中进行。通过将工作区的文件添加到区域从而产生备份[历史]。而可以根据暂存区或本地仓库中重置工作区的文件从而实现版本控制的效果 [即回退]。

此外,Git 的主要功能还有远程协作,因此还有一个**远程仓库 [Remote]**。

图源:常用 Git 命令清单 [侵删]

image-20200217170241279

下面根据 暂存区 [Index / Stage]本地仓库 [Repository]远程仓库 [Remote] 的主要操作进行介绍。

暂存区

和暂存区相关的主要操作即为 git add, git commit以及撤销回退操作。

如上图所示,git add将修改提交到暂存区后,git commit将暂存区中的修改提交到本地仓库。

为什么需要有暂存区?知乎有一个问题:《为什么要先 git add 才能 git commit ?》而我个人的理解是,暂存区是本地仓库之前的一个暂时备份。在一些情况下,我们希望可以保存当前的修改,若之后的修改不满意可以回退,又认为当前的修改还不够完善,不应该作为一次版本提交,此时可以使用暂存区暂存。

需要注意的是,暂存区和工作区是基于一个 commit 存在的。在当前的 commit 与暂存区或工作区中的内容不同时,无法切换到其他的 commit。为什么这里说的是 commit 而不是分支呢?因为分支实际上是通过指针实现的。当两个分支指向同一个 commit 时,可以带着当前的暂存区和工作区切换分支。

下面通过图解介绍一些常用的命令:

提交命令

首先,我们通过 git init 初始化 git,然后新建了a.txt 文件,并写入 “Test 1”,此时状态为:

image-20200217204702014

命令 git add .之后:

image-20200217204652107

命令 git commit -m "a"之后:

image-20200217204637553

可以简单知道, git add .将修改从工作区放入暂存区,git commit -m "a"将修改从暂存区放入本地仓库。

下面测试一些函数:我们在 a.txt 文件中添加 “Test 2”,执行 git add .,再在 a.txt 文件中添加 “Test 3”,此时的状态为:

image-20200217204606402

查看命令

使用命令 git status: 可以看到,有修改需要commit,也有修改需要 add

image-20200217203501110

命令 git diff: 显示了暂存区与工作区的区别

image-20200217203720725

命令 git diff HEAD: 显示了工作区与当前的 commit 的区别 [HEAD 表示的是当前所在的commit]

image-20200217203811422

撤销命令

命令 git checkout .:恢复暂存区的所有文件到工作区 (当然也可以只恢复一个文件

image-20200217204617610

命令 git reset :重置暂存区的指定文件,与上一次commit保持一致,但工作区不变。注意现在直接看文件时看不到重置的,因为看的是工作区的内容。也可以同时修改暂存区和工作区[–hard]。

image-20200217205406324

到这里暂存区的概念大概就懂了:)剩下的就是背命令了

切换分支

接下来是第一个坑:无法切换分支。更准确的说法是:当工作区或暂存区有修改时,无法切换 commit。

继续我们的实验:此时,新建并切换到一个新的分支 git checkout -b other 。这里是可以成功的:我们并没有切换 commit ,而是多了一个指向当前 commit 的指针:other。暂存区和工作区的内容也没有改变。

image-20200217210709830

通过 git add .git commit -m "a" 新建一个提交后,Master 和 other 分支会指向不同的 commit :

image-20200217211027399

接着修改 a.txt,尝试切换为 master 分支,失败:因为需要切换 commit

image-20200217211151753

通过 git add .git commit -m "a"将修改提交。可以顺利切换到 master 分支。此时执行 merge 操作:git merge other 由于 master 分支指向的 commit 的是 other 分支所指向的 commit 父节点,因此 merge 操作简单的将 master 分支的指针移动到 other 分支所指向的 commit 父节点。也就是说:他们又指向了同一个 commit 。此时即使修改工作区或暂存区的内容,也可以直接切换到 other 分支。[关于 merge ,下面会说到]

当暂存区有修改又希望切换分支时,可以使用 git stash 系列命令,将当前的工作隐藏起来,这里不展开。

关于暂存区暂时就到这,接下来是更奇怪的本地仓库

本地仓库

本地仓库中的版本是根据 commit 存储的,而不是根据分支。事实上,大多数命令中出现分支的名字都是用于指定一个 commit,可以用其他指定 commit 的方法替代。指定 commit 的方法有很多:

  1. 分支:最常用的显然是分支的名称。分支表示的是一个指向 commit 的指针,分支很容易被人为移动,并且当有新的提交时,它也会移动。因此,分支很容易被改变。
  2. 标签:由于分支容易改变,有时候我们希望可以给一次commit 打上永久的标识,这就是标签,用 git tag 等命令可以创建和查看、删除标签。
  3. 相对位置:可以在一个指定的 commit 后添加符号表示相对它的位置。有两个表示相对位置的符号 ‘^’, ‘‘。其中,’‘ 表示父节点,后面可以加数字表示往上寻找的第几个父节点,比如 ‘~2’ 表示爷爷节点。’^’也表示父节点,后面加数字表示多个父节点中的第几个父亲,如在 merge 后,’^2’ 表示不是原本分支的另一个分支(我也不知道还有什么情况有多个父节点)。在使用相对位置时, head 表示当前所在的 commit 。
  4. commit 序号:每创建一个commit ,Git都会自动生成一个唯一的序号指定这个commit。不过 Git自动生成的序号都是一段很长的编码,难以记录。如果一定要使用,只需要用前几个字符即可代表一个commit

下面的图解用 Learn Git Branching 提供的沙盒生成,解释参考该教程,侵删。图解中,一个圆点表示一个commit

合并分支

合并分支 (commit) 主要有两种方式:merge,rebase

Git merge

merge合并两个分支时会产生一个新的提交记录,它有两个父节点。当merge的分支是当前分支的子节点时,只是简单的移动当前分支。

还是实例:在 master 分支下git merge other ,会形成图二的 commit [HEAD^ 表示的是 C3 这个commit,HEAD^2 表示的是C2]。切换到 other 分支,git merge master ,形成图三,other 的指针移动,没有产生新的 commit。

image-20200217223739515

Git rebase

Rebase 实际上是取出一系列的提交记录,“复制”到需要的分支上。Rebase 的优势是可以创造线性的提交历史。

在 master 分支下git rebase other ,即将 master 分支上进行的变动复制到 other 分支上,从而使 master 分支合并 other 分支的修改。切换到 other 分支,git rebase master ,形成图三,other 的指针移动,没有产生新的 commit。这里 other 分支原来的 commit 已经没有指针指向它,但在一定时间内他还是存在的,可以通过 commit 序号指定。

image-20200218091005159

Git merge 与 Git rebase 的比较

参考:简书:闲谈 git merge 与 git rebase 的区别

  • merge 是一个合并操作,会将两个分支的修改合并在一起,提交合并中修改的内容。merge 的提交历史忠实地记录了实际发生过什么,关注点在真实的提交历史上面。

  • rebase 并没有进行合并操作,只是提取了当前分支的修改,将其复制在了目标分支的提交后面。rebase 的提交历史反映了项目过程中发生了什么,关注点在开发过程上面。

merge 与 rebase 都是非常强大的合并命令,没有优劣之分,使用哪一个应由项目和团队的开发需求决定

冲突处理

git merge

遇见冲突后会直接停止,等待手动解决冲突并重新提交 commit ,才能再次 merge。merge中遇到的冲突会在文件中标记出来。整个流程为:

  1. 假设我们在 master 分支上执行了 git merge other,此时,提示出现冲突无法自动合并
  2. 当前暂存区和工作区未成功合并的文件已被标记 “<<<<<<< HEAD \n … \n ======= \n … \n>>>>>>> “, 还未生成新的 commit ,命令行中的分支名变为 “ other| MERGING” (这里突然不想 merge 了怎么办?用 git reset [–hard] 将暂存区与工作区恢复为 commit 的内容。)
  3. 按照需要修改完未成功合并的文件后
  4. 进行一次 git add , git commit 操作,此时生成的 commit 即为合并后的 commit。也就是说,一般不需要在进行 merge。这里 Git 的判定成功合并是出现了git add , git commit的操作,而无关修改了多少(完全可以带着奇怪的标识符完成 merge (然后编译出错(

git rebase

遇见冲突后会暂停当前操作,开发者可以选择手动解决冲突,然后 git rebase --continue 继续,或者 --skip 跳过(注意此操作中当前分支的修改会直接覆盖目标分支的冲突部分),或者 --abort 直接停止该次 rebase 操作。整个流程为:假设我们在 master 分支上执行了 git rebase other,此时,提示出现冲突无法自动合并:

  1. 当前暂存区和工作区未成功合并的文件已被标记, 命令行中的分支名变为 “ other|REBASE x/x” (当前所处的commit 为 other 分支所在的 commit
  2. 按照需要修改完未成功合并的文件后,进行一次 git add , git commit 操作,即可用 git rebase --continue 继续合并。
  3. git rebase --skip 跳过合并,则 other 分支的修改会覆盖 master 分支中冲突部分。
  4. git rebase --abort 停止合并,返回 master 分支指向的commit

移动 commit

上面的两个操作主要是应用在分支间的操作,而下面的操作则是对commit 进行操作

Git Cherry-pick

git cherry-pick [commit]将提交复制到当前所在的位置下面。如在master下进行命令:git cherry-pick C2 C4 (这里C2 C4 表示 commit 的序号)。Git Cherry-pick 也可以完成分支合并的工作,但它的主要特点是可以提取特定的 commit ,而不是整个分支。

image-20200218103903136

Git rebase -i

-i --interactive 的缩写,也就是交互式 rebase。Git rebase -i [commit] 可以对从指定 commit 到当前commit 直接的 commit 进行排序,合并或删除等。在 Git 中,会通过 vim 打开一个文件,可以在文件中进行调整。Git rebase -i 的主要功能在于调整一个分支上的 commit 。(具体就不介绍了(x

image-20200218104658494

撤销命令

这里关注的是撤销一个commit ,与前面撤销暂存区和工作区的修改不同。主要有 git resetgit revert

Git reset

git reset [commit] 通过把分支回退到指定 commit 来实现撤销改动。如git reset head^ [图二]

Git revert

git revert [commit] 通过新建一个 commit,这个 commit 包含了可以撤销前一个 commit 提交的更改。如git revert head [图三]

image-20200218111408784

远程仓库

远程仓库实际上只是你的仓库在另个一台计算机上的拷贝。你可以通过因特网与这台计算机通信 。a)远程仓库是一个强大的备份。有了远程仓库以后,即使丢失了本地所有数据, 你仍可以通过远程仓库拿回你丢失的数据。b)还有就是, 远程让代码社交化了,可以更方便的进行协作。

最简单的连接本地仓库和远程仓库的命令是:git clone ,在本地创建一个远程仓库的拷贝。当然也有其他的方式,这里不详细介绍。

在本地仓库中,分为本地分支指针和远程分支指针,两者可以相互关联,绑定的指针不需要同名,也可以指向不同的 commit。远程分支通过 pull,push等命令进行更新。

Git fetch

git fetch 将本地仓库中的远程分支更新成了远程仓库相应分支最新的状态,完成了:

  • 从远程仓库下载本地仓库中缺失的提交记录
  • 更新远程分支指针(如 origin/master)

如:进行命令 git fetch 之后(虚线表示远程仓库中的内容)

image-20200218113837994

git fetch 并不会改变本地的状态。不会更新本地的 master 分支,不会改变当前所在的分支,也不会修改你磁盘上的文件。可以将 git fetch 的理解为单纯的下载操作。

Git pull

git fetch 之后,本地仓库就有了远程分支,这时候需要把本地分支与远程分支合并,自然就用到了 merge 或 rebase 命令。由于 fetch 之后经常会进行这两个操作,因此有了一个更简便的命令: git pull

  • git pull = git fetch + git merge
  • git pull --rebase = git fetch + git rebase

应该不难理解:

git pull = git fetch + git merge origin/master

image-20200218123344897

git pull --rebase = git fetch + git rebase

image-20200218123443546

Git push

git push 负责将本地变更上传到指定的远程仓库,并在远程仓库上合并你的新提交记录。

当希望上次的本地分支是基于远程分支的旧版进行,与远程仓库最新的代码不匹配时,push 请求会被拒绝。Git 要求需要push 的分支先合并远程最新的代码,也就是使工作基于最新的远程分支,才能进行push。

在下面中进行 git push,系统回复失败:因为最新提交的 C3 基于远程分支中的 C1。而远程仓库中该分支已经更新到 C2 了。

image-20200218124632347

此时,需要用 git pull [--rebase] 命令将本地分支与最新的远程分支合并,在进行 git push

git pull, git push

image-20200218125219913

git pull --rebase, git push

image-20200218125447896

远程仓库主要用这几个命令就可以啦

先写到这里(

参考资料: