什么是git
Git是一个分布式的版本控制软件,最初是由Linus Torvalds发起的开源项目。没错,就是那个写了Linux内核源码并发起开源项目的男人。2005年,为了管理Linux内核的源代码,开发并开源了git。
从2002年开始,Linus Torvalds决定用BitKeeper来作为Linux内核主要的版本控制系统。但是由于BitKeeper是专用软件,而Linux社区主张应该使用开放源代码的软件来作为Linux内核的版本控制系统。但是当时现存的开源软件多多少少都存在一些问题,于是Linus Torvalds决定自行开发一个版本控制系统来替代BitKeeper,然后仅用十天的时间就编写出git第一个版本。
为什么需要git
上面简单介绍了一下git诞生的历史,接下来简单说一下为什么需要git这样一个软件。前面也有提到,git的出现是为了作为Linux内核主要的版本控制系统。想象这样一个场景,老板叫你写一个方案,因为老板不满意,于是你在最初版本上修改了多个版本。当你给老板一个最新的方案的时候,他又突然觉得上一个方案更好。为此,你不得不将每一次改动都存为一个新的文件。同时在这个过程中,你可能需要将一个方案修改成多个版本,也可能将几个修改后的方案做整合。甚至,如果你有同事需要和你共同在你的方案基础上进行修改,当他把文件再传给你时,你就必须要检查他作了那些改动,你作了哪些改动,是否有冲突,并让这些改动有机地合并。就这样,你的文件夹下就会出现一大堆文件,当你想要找某个版本的方案时,头都大了。
所以我们需要这样一个时间机器,保存着我们各个版本的文件,记录着每次修改的内容,谁修改的,什么时候修改的以及修改的内容。同时能够更方便地允许多人合作,以及将不同版本的内容进行合并。
git的原理
其实在git出现之前,就已经有了满足上面需求的版本控制系统。例如CVS及SVN这类集中式的版本控制系统。这一类版本控制系统的出现就是为了方便软件的开发,为了让不同主机上的不同用户能够协同工作。作为集中式版本控制系统,整个版本库是集中存放在中央服务器的,意味着每次你要对内容进行修改,你需要先从中央服务器中取出当前的最新版本,然后进行一番修改后,再将自己的成果上传到中央服务器。
这样的集中式版本控制系统会出现一些问题,比如当中央服务器宕机时,所有人都无法获取到最新的版本内容,又或者如果中央服务器中版本信息丢失的话将是不可逆的。同时由于所有版本信息只存在于中央服务器中,所以需要请求相关信息的时候必须保证网络畅通,如果网络出现问题,则无法访问到中央服务器,进而开发者的工作可能就无法进行。
于是针对集中式的版本控制系统,分布式版本控制系统很好地解决了上面的一些问题,在集中式的版本控制系统中,版本库只存在中央服务器中。而分布式版本控制系统让每个开发者都有了一个本地仓库,保存着当前所有的版本信息。因为每个开发者都有一个版本库,所以不论谁的电脑崩了,版本库丢失了,都可以从别人那里复制一个最新的版本库。同时分布式版本控制系统大大提高了开发者的开发效率,例如上面出现的网络问题完全不会影响开发者的开发工作,每个开发者可以在本地进行开发工作,当最后需要进行版本的合并时再同步自己修改后的最新版本。
对于版本库里有关版本信息的存储,一般有两种方案:
-
全量方案:每一个修改后的版本数据都会完整保存,使用时可以直接取走。
-
增量方案:保存一个初始版本,然后保存每一次修改之间不同的内容,使用时需要通过合并来获取所需版本。
方案对比
优点 | 缺点 | |
---|---|---|
全量方案 | 简单、快速 | 重复存储、浪费内存 |
增量方案 | 存储空间小 | 需要运算、速度更慢、性能更低 |
因为git主要用于代码管理,存储的都是代码文件,所以文件很小,不会占用太多的内存空间。同时由于增量方案有个最致命的缺点,就是如果初始版本没有了,会导致后续所有版本的丢失。因此git最终采用了全量存储的方式来作为版本库的存储。
git如何追踪文件的更改?
在git仓库里,文件目录就是一个树结构。文件目录保存的是里面各个文件的SHA-1值,该哈希值作为文件名。因此如果文件内容一样,哈希值也一样,所以文件库里,内容相同的不同版本的文件也只会保存一份。反过来,如果有任何文件发生变动,文件的SHA-1值就会改动,进而表示文件发生了修改。因此可以将git目录下的「树节点」想成是文件夹;把「叶子节点」想成是文件。
每个时刻,每个最新修改后的版本会有自己专属的「快照」,也就是上面提到的包含SHA-1值的树结构,在git中每次生成新的版本将会用到commit命令来创建这种「快照」。它除了会记录前边提到的关于目录和文件的tree,还会记录提交人信息和上一次提交的SHA1。
使用git
安装git
-
Windows可以在官网直接下载git bash
-
Ubuntu可以通过
sudo apt-get install git
直接安装Git -
Mac先安装Homebrew,然后通过Homebrew
brew install git
通过安装Git
初始配置
- 该配置是为了告诉你合作的开发者们你是谁
git config --global user.name “<Your Name>”
git config --global user.email “<Your Email>”
- 查看所有的配置
git config --list
- 查看配置手册
git help config
git config --help
配置信息都将被存在~/.gitconfig
文件中
git仓库
git init
:在当前目录下创建一个新的git仓库,接下来所有的版本信息都将存储在隐藏的.git文件夹中。下图为.git目录下的文件:
当你新建了一个仓库,你可以对当前仓库进行配置,设置姓名和邮箱,配置仅仅针对当前项目有效,类似于局部变量覆盖了全局变量。最终修改会存入config文件中。如果不进行配置,则会默认使用上面设置的全局配置。
git config --local user.name “<Your Name>”
git config --local user.email “<Your Email>”
.git文件中另一个非常重要的文件就是HEAD,存储的是当前位置的指针,指向当前工作区的分支。HEAD其实就是一个指向当前的最新的版本的指针,存储的其实就是当前版本目录的SHA-1值。
注意:由于.git文件里面保存了所有版本文件修改的敏感信息 ,所以最好不要上传。
git分区
下面介绍一下git的分区,一般来说,对于当前的git仓库,有三个分区,分别为工作区、暂存区和版本库。
工作区就是你当前工作的目录,你所要修改的文件的最新版本就是在你的工作区。版本库就是你所在的建好的git仓库,存储了所有版本信息。而在工作区和版本库之间还有一个缓存区,那就是暂存区。很多才接触git的人搞不明白为什么一定要有一个暂存区,我在工作区修改,修改后直接上传到本地仓库不就好了吗?
其实像我们写文档时,写几个字就保存一下是一个好的习惯。所以我们需要在修改文件时及时地将修改保存下来,且我们希望更新能够及时地保存最新版本到版本库。但是由于每次提交最新版本的动作是原子的,而我们会在每次小更改后就保存到版本库。如果这样每次一个小的修改都作为新的版本加入到版本库的话,则会造成许多无用的日志信息。因此引入了一个中间状态,就是暂存区。
所以在git仓库里整个大致的工作流程在工作区写代码,然后放到暂存区,最后将该版本放到版本库时就会将暂存区的内容存到一个新的版本结点,然后再放到版本库里,使版本树增加新的版本节点
下面是一些在本地关于工作区、暂存区、版本库三个工作区之间的命令:
git add <file_name>
:将工作区发生修改的文件添加到暂存区
git add .
:将当前目录下所有发生修改待加入暂存区的文件加入暂存区
git commit -m "Your Message"
:将提交到暂存区的内容提交,生成一个新的版本节点并保存到版本库中,并留下message
git status
:查看仓库状态,会显示工作区里以修改未加入暂存区的文件或加入暂存区未commit的文件
git diff <file_name>
:查看当前工作区的文件相对于之前提交到暂存区又修改了哪些内容
git log
:查看当前分支的所有版本,所有提交记录,其中每一条记录包括提交人相关信息、时间以及message
git log --pretty=oneline
:可以让日志信息显示在一行
注意:git log只会显示当前分支从初始到当前HEAD指针的所有版本节点信息
上面提到每一次commit都会生成一个新的版本节点,然后HEAD将指向新的节点,所以如果我们需要回滚到之前的版本,可用下述命令
git reset --hard HEAD^
或 git reset --hard HEAD~
:将代码库回滚到上一个版本
git reset --hard HEAD^^
:往上回滚两次,以此类推
git reset --hard HEAD~100
:往上回滚100个版本
每次回滚不会删除其他的版本节点,回滚到某一版本之后,当前目录的所有文件将会变成该版本文件。那么如果我们需要回滚到指定的版本怎么办呢?
git reflog
:查看HEAD指针的移动历史(包括被回滚的版本),以及每个版本的id,其实版本号就是当前版本文件信息的哈希值。所以我们可以通过该命令查看到我们想要回滚到的版本的哈希值,取前7位即版本号。
git reset --hard 版本号
:回滚到某一特定版本
git restore <file_name>
:将文件尚未加入暂存区的修改全部撤销,恢复到暂存区里保存的内容。如果暂存区还没有,就回滚到当前HEAD所在版本原始内容。
这句命令的作用其实就是将暂存区的文件取出来并覆盖当前工作区的文件,不论当前工作区的文件是否更改,都将最终变为之前上传到暂存区的版本。
所以如果你当前的所有修改还未加入暂存区,但是想舍弃当前所有修改,并恢复到该版本最初的样子,你就可以输入git restore .
git restore --staged <file_name>
:将放入暂存区的文件从暂存区里取出来。
然后如果这时候再git restore
当前文件,那么就会将文件变成最初的样子。
远程仓库/云端仓库
这里以GitHub为例,介绍一下如何将本地仓库和云端远程仓库关联起来。当你在云端新建了一个远程仓库,你会得到你仓库的地址https://github.com/XXX/XXX,如果你采用SSH连接,你会得到git@github.com:XXX/XXXX.git这样的地址。
git remote add origin git@github.com:xxx/xxx.git
:将本地仓库关联到远程仓库
git push -u
(第一次需要-u以后不需要):将当前分支推送到远程仓库,-u其实就是将本地仓库与云端仓库链接,链接后就不需要使用该参数了。
git push origin branch_name
:将本地的某个分支推送到远程仓库,origin是云端主分支默认的名字
git push -u origin master
:一般情况下可以将上两条命令合并起来,当前仓库一般默认在主分支master上,下面会介绍关于分支的相关概念
git clone git@github.com:xxx/XXX.git
:将远程仓库XXX下载到当前目录下
git分支
不同的开发者如果修改了不同的文件,最终合并为新版本的时候不会发生冲突。但是如果同时修改了同一个文件的相同内容,在合并时必然会发生冲突。为了解决合并冲突 的问题,于是就有了分支的概念,当多个开发者同时工作的时候,可以在本来的版本控制流上产生多个分支,当各自处理完后,再将自己的分支与主分支合并。例如需要为一个软件编写不同的功能时,由于是不同的开发者进行代码的编写,所以就需要产生多个分支各自工作。由于不同开发者的分支链条会越来越长,最终开发完成时就需要将其版本进行合并,并保留其中各自版本信息。所以git提供了分支以及合并等相关功能。
git branch <branch_name>
:创建新分支
git checkout -b <branch_name>
:创建并切换到branch_name这个分支上
git branch
:查看所有分支和当前所处分支,*表示当前所在分支
git checkout <branch_name>
:切换到branch_name这个分支
git branch -d <branch_name>
:删除本地仓库里的branch_name分支
git merge <branch_name>
:将分支branch_name合并到当前分支上
关于git里的分支合并,一共有两种方式,第一种就是上面提到的merge,还有一种叫rebase
该命令的原理如下:
merge意为合并,我们假设从master分支中分出了dev1和dev2分支,当最后需要合并时需要找到dev1和dev2分支的最新版本进行合并,然后创建一个新的commit,生成我们的终版节点。
rebase意为变基,首先找到dev1和dev2分支最近共同祖先节点,然后先将其中一条dev1的版本节点缓存到一个目录下,然后把dev1在共同节点之后的提交版本放到dev2分支上,最后将dev2的提交放在后面。
merge操作会生成一个新的节点,之前提交分开显示。而rebase操作不会生成新的节点,是将两个分支融合成一个线性的操作。
本地分支和远程分支
git push --set-upstream origin <branch_name>
:设置本地的branch_name分支对应远程仓库的branch_name分支
git push -d origin <branch_name>
:删除远程仓库的branch_name分支
git pull
:将远程仓库的当前分支与本地仓库的当前分支合并
git pull origin <branch_name>
:将远程仓库的branch_name分支与本地仓库的当前分支合并
git branch --set-upstream-to=origin/<branch_name1> <branch_name2>
:将远程的branch_name1分支与本地的branch_name2分支对应
git checkout -t origin/<branch_name>
将远程的branch_name分支拉取到本地
暂存栈stash
当你在当前工作目录下修改到一半,但是突然要开一个新的分支处理其他问题时。而当前修改还不是很完整,所以不想将当前所有修改commit,你可以将修改到一半时的工作区和暂存区的所有内容存到栈里,所有操作只与本地有关。
git stash
:将工作区和暂存区中尚未提交的所有修改都存入栈中
git stash apply
:将栈顶存储的修改恢复到当前分支,但不删除栈顶元素
git stash drop
:删除栈顶存储的修改
git stash pop
:将栈顶存储的修改恢复到当前分支,同时删除栈顶元素
git stash list
:查看栈中所有元素