Git

Git是一个开源的分布式版本控制系统。

历史

Git的作者是大名鼎鼎的Linus(Linux的创始人)。

Linux创始人Linus Torvalds访谈,Git的十年之旅 英文

为什么要创建Git?

阶段一:起初参与Linux开源项目的代码是由Linus本人通过diff和patch命令来手动为别人整合代码的,但是代码库之大很难继续通过手工方式管理了。

阶段二:为了解决上一个问题,开始使用版本控制工具BitKeeper,但注意它并非开源的

阶段三:开发Samba的Andrew试图破解BitKeeper的协议,被BitMover公司发现并收回Linux社区的免费使用权。

阶段四:工具没了,咋整?Linus花了两周时间自己用C写了一个分布式版本控制系统,这就是Git!一个月之内,Linux系统的源码已经由Git管理了!

原理

工作区&版本库

在初始化git版本库之后会生成一个隐藏的文件.git ,可以将该文件理解为git的版本库

项目文件夹属于工作区,在.git 文件夹里面还有很多文件,其中有一个index 文件就是暂存区(也可以叫做 stage)

每当编辑好一个或几个文件后,通过add操作把它加入到暂存区,然后接着修改其他文件,改好后放入暂存区,循环反复直到修改完毕,最后使用 commit 命令,将暂存区的内容永久保存到本地仓库。这个过程其实就是构建项目快照的过程,当我们提交时,git 会使用暂存区的这些信息生成tree对象,也就是项目快照,永久保存到数据库中,因此也可以说暂存区是用来构建项目快照的区域。

git还为我们自动生成了一个分支master(默认分支)以及指向该分支的指针head·。

数据存储

objects是用来存储git数据,具体可以分为blobtreecommit


blob用来存放项目文件的内容,但是不包括文件的路径、名字、格式等其它描述信息。项目的任意文件的任意版本都是以blob的形式存放的。

tree用来表示目录。项目就是一个目录,其中包括文件、子目录,因此 tree 中有 blob、子tree。

blobtree是使用 sha-1值引用的(将文件中的内容通过通过计算生成一个 40 位长度的hash值,其特点:由文件内容计算出的hash值hash值相同,文件内容相同)。

commit表示一次提交,有parent字段,用来引用父提交。指向了一个顶层 tree,表示了项目的快照,还有一些其它的信息,比如上一个提交,committer、author、message 等信息。


从顶层的 tree 纵览整个树状的结构,叶子结点就是blob,表示文件的内容,非叶子结点表示项目的目录,而顶层的 tree 对象就代表了当前项目的快照

分支

分支的目的是让我们可以并行的进行开发。

比如我们当前正在开发功能,但是需要修复一个紧急bug,我们不可能在这个项目正在修改的状态下修复 bug,因为这样会引入更多的bug。

分支的实现其实很简单,我们可以先看一下 .git/HEAD 文件,它保存了当前的分支,

1
2
~/work/communitycenter   features/0412 ●✚  cat .git/HEAD
ref: refs/heads/features/0412

Git 的分支本质上仅仅是指向提交对象的可变指针。 Git 的默认分支名字是master。 在多次提交操作之后,你其实已经有一个指向最后那个提交对象的 commit。 它会在每次的提交操作中自动向前移动。

Git 的 “master” 分支并不是一个特殊分支。 它就跟其它分支完全没有区别。 之所以几乎每一个仓库都有 master 分支,是因为 git init 命令默认创建它,并且大多数人都懒得去改动它。

常用命令

Config

修改分支之前应该需要修改好用户名和邮箱,这样才知道当前的修改属于谁。

1
2
git config user.email xxxx 设置邮箱
git config user.name yyyy 设置用户名

Merge & Rebase

在 git 中合并分支有两种选择:mergerebase,但是无论哪一种,都有可能产生冲突。

冲突的产生:两个已经提交的分支的相同文件相同位置的的不同操作进行了合并

由于并行开发,master分支和feature分支出现了并行情况,并且同时更改了相同文件的相同位置,造成了冲突。

1
2
3
4
5
6
7
8
<<<<<<<<HEAD
other code

========

your code

>>>>>>>>your branch name

Merge

Merge会把不同分支的commit合并(保留commit的时间线)起来,通过新增一个新的commit来实现冲突的解决。

在没有冲突的情况下,Merge合并情况如下,其中7为合并的commit

如果发生冲突的话,Merge命令会自动合并,否则需要人工解决这些冲突。

如果提示Automatic merge failed; fix conflicts and then commit the result.,说明自动合并失败。

此时可以通过git status来查看冲突情况,一般会有这样的提示both modified: xxxxx


merge是一种不修改分支历史提交记录的方式,这也是我们常用的方式。


Rebase

Rebase会把从Merge Base以来的所有提交以补丁的形式一个一个重新达到目标分支上。


Merge不同,Rebase会重新修改基于远程分支的commit,使得目标分支合并该分支的时候会直接Fast Forward,即不会产生任何冲突,提交历史是一条线,这对强迫症患者可谓是一大福音。

由于一条线提交历史也可以保证分支功能在master分支上是有序的


Rebase还可以用来修改历史commit,一般在写代码的时候经常会提交一些无效的commit,这些commit不利于代码review,从commit中不能获得此次的改动点。

1
2
3
4
5
6
7
8
9
commit af6922a5d3d31a4f3816a27a14476f62a533579e
Author: xxxx
Date: Fri Mar 16 10:17:50 2018 +0800
fix bug
...
commit 71966c45a79dcf7ae13842feea61d61ba77fa120
Author: xxxx
Date: Thu Mar 15 19:58:03 2018 +0800
fix bg

可使用Rebase命令如下,其中xxx代表base为xxx的commit。

1
git rebase -i <comment-id>

执行结果如下,其中可以通过修改pick来实现修改commit操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
pick e0f56d9 fix bug
s f3s0281 fix bug
s a310682 fix bug
s 1902931 fix bug

# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit

一般使用的姿势如下:

通过edit来修改目标commit的内容

通过squash来合并多个commit的内容


Branch、Checkout、Revert、Reset

创建分支的两种方式:

git branch new_branch => 创建新分支

git checkout -b new_branch => 创建并切换到新分支


Revert一般被称为反向提交。

1
git revert <comment-id>

什么是反向提交呢,就是旧版本添加了的内容,要在新版本中删除,旧版本中删除了的内容,要在新版本中添加。这在分支已经推送到远程仓库的情境下非常有用。

Revert也不会修改历史提交记录,实际的操作相当于是检出目标提交的项目快照到工作区与暂存区,然后用一个新的提交完成版本的回退


Revert很像,Reset用来在当前分支进行版本的“回退”,不同的是,Reset 是会修改历史提交记录的

Reset常用的选项有三个,分别是soft, mixed, hard,他们的作用域依次增大。

1
git reset --<option> <comment-id>

soft选项仅会修改分支指向,而不修改工作区与暂存区的内容,可以重新做一次提交,形成一个新的 commit,常用与撤销临时提交的场景。

soft可用于上面多个无效commit的合并(只使用于合并当前commit之前的连续commit)。

mixedsoft的作用域多了一个 暂存区,实际上mixed 选项与 soft 只差了一个 add 操作

hard选项会导致工作区内容“丢失”,因此在使用 hard 选项时,一定要确保知道自己在做什么,避免丢失分支内容。

如果真的误用了hard,造成了数据丢失怎么办?
如果真的误操作了,也不要慌,因为只要 git 一般不会主动删除本地仓库中的内容,根据你丢失的情况,可以进行找回,解决方法有两种:
一、使用 git reset --hard ORIG_HEAD立即恢复
二、使用 reflog命令查看之前分支的引用

Stash

stash将工作区与暂存区中的内容做一个提交并保存起来,然后使用reset hard选项恢复工作区与暂存区内容。我们可以随时使用 stash apply 将修改应用回来。

1
2
git stash
git stash apply

使用场景:有时,我们在一个分支上做了一些工作,修改了很多代码,而这时需要切换到另一个分支干点别的事。但又不想将只做了一半的工作提交。在曾经这样做过,将当前的修改做一次提交,message 填写 half of work,然后切换另一个分支去做工作,完成工作后,切换回来使用 reset —soft(重新提交commit)或者是 commit amend(修改commit)。

cherry-pick

cherry-pick用于把另一个本地分支的commit修改应用到当前分支。

1
git cherry-pick <comment-id>

如果在cherry-pick 的过程中出现了冲突?和rebase和merge一样,都需要手动解决冲突再提交。

set-upstream

set-upstream用于设置分支跟踪关系。

一般本地新建一个分支再往远程仓库推送的时候,需要指定远程仓库的目标分支。

1
2
git push --set-upstream origin <new_branch>
git branch --set-upstream-to=<target branch> <new_branch>

pull & fetch

pull=fetch+merge

pull是下拉远程分支并与本地分支合并。fetch只是下拉远程分支,并不会发生合并需要手动执行合并。

也可以采用rebase模式,

1
git pull --rebase

该命令做了以下内容:
一、把你 commit 到本地仓库的内容,取出来放到暂存区(stash)(这时你的工作区是干净的)
二、然后从远端拉取代码到本地,由于工作区是干净的,所以不会有冲突
三、从暂存区把你之前提交的内容取出来,跟拉下来的代码合并

rebase在拉代码前要确保你本地工作区是干净的,如果你本地修改的内容没完全 commit 或者 stash,就会 rebase 失败。

参考

https://blog.coding.net/blog/principle-of-Git
http://www.bootcss.com/p/git-guide/
https://www.cnblogs.com/dogdogwang/p/7072931.html