Git是一个分布式的版本控制系统。
注意上面的”分布式的“这个限定词,这一点是Git和CVS,VSN等其他版本控制系统最大的分别。
集中式版本控制系统最大的毛病就是必须联网才能够工作,也就是各个客户端必须连接到中央仓库才能够工作。
分布式的版本控制系统中,不必有中央仓库,每个人的电脑上都有一个完整的版本库。我们可以在自己的电脑上修改、提交。如果两个人需要交换修改的代码,则需要将自己的修改推送给对方。
在实际的应用中,为了方便一个团队之间交换代码修改,我们还是有一个中央仓库。注意这个仓库是提供便利的,并不是没有它就不行(虽然会很不方便)。
这个教程的主要目的是让大家了解使用Git进行版本管理的一些基本操作和逻辑,让大家对Git的使用有一个基本的了解。如果想要详细了解每个命令的用法,或者要了解一些“高级”的命令,建议大家还是去看看Git的手册。
2. 创建本地仓库
关于Git的安装,这里就不讲了,网上有很多文章,大家根据自己的系统,找一篇文章照着做就可以了。这里主要讲解Git的一些概念和操作。
前面已经说过,Git是一个分布式的版本控制系统,每个人的电脑上都有一个完整的版本库,我们把这个版本库称为本地仓库。所有其他的仓库,无论是同事电脑上的,还是中央服务器的,我们都称为远程仓库。
创建本地仓库有两种方式,第一种是我们从零开始创建,第二种是从远程仓库克隆一份。
我们可以把版本仓库理解为一个目录,其中保存了我们所有让git管理的文件。Git能够跟踪这些文件的修改、删除,能够追踪这些文件的历史,并且可以还原。
2.1 从头开始创建本地仓库
从头开始创建本地仓库非常简单。
- 选择你想要创建版本仓库的地方,创建一个空的目录。
- 进入该目录,在命令行中执行git init命令,把这个目录变成Git可以管理的版本仓库。
- 把文件添加到Git仓库中,一共需要三步:
- 把文件放到Git仓库的目录中,无论是新创建一个文件,还是从其他地方copy一个文件。也可以放在子目录中
- 运行
git add <filepath>
,告诉Git把文件添加到仓库中。 - 使用
git commit
命令,告诉Git把改变提交到仓库。
2.2 从远程仓库克隆
相比从头开始创建本地仓库,我们做得更多的是从远程仓库克隆一份。理论上来说远程仓库可以是任何一台电脑上的仓库,但是通常来说,我们会在github,或自己搭建的git服务器上创建中央仓库,团队中的所有成员都从该仓库上克隆,并在后续将自己的修改上传上去,或者从中央仓库读取其他同事的修改。因此我们这样只讲怎样中央仓库克隆到本地仓库。
- 创建本机的ssh-key,在类linux系统上,只需要运行ssh-keygen命令就可以了。
- 将ssh-key的公钥id_rsa.pub上传到github或者Git服务器上,具体的步骤要参考你们是要的Git服务器是哪一个。
- 执行命令
git clone <版本库的网址> [本地目录名]
来创建本地仓库。如果不指定目录,则会在当前目录下创建一个目录,名字与远程版本库的名字相同。
上面的命令中,版本库的网址中是可以带上协议、用户名等信息的。Git支持http(s),ssh,git,ftp(s),本地文件协议等不同的协议。
3 使用本地仓库进行版本管理
3.1 工作区和暂存区
为了能够更好的使用本地仓库进行版本管理,我们先了解一下工作区和暂存区的概念。
无论我们是从远程仓库克隆,还是使用git init
命令新建一个本地仓库,最终在本地仓库的目录中,都有一个隐藏的子目录.git
。
本地仓库目录是我们的工作区,我们要管理的文件都在这个目录中。但是.git
目录不是工作区,这个是版本库。我们不要手动去修改这个目录中的内容。
在这个版本库中,保存了很多的东西,其中最重要的是暂存区,以及分支。关于分支的概念,我们以后再解释。目前来说,我们也只有一个分支,也就是Git自动为我们创建的master分区。还有一个指向master分区的指针,叫做HEAD。
前面我们已经提到过,在添加一个文件到Git本地仓库中的时候,需要先执行git add
命令,然后执行git commit
命令。其中git add
命令就是将变化的代码从工作区拷贝到暂存区。 git commit
则是将代码从暂存区提交到版本库。一旦提交后,暂存区会被清空。
所以,我们其实可以执行多次git add
之后,执行一次git commit
,一次性的将多次修改的结果进行提交。
可以使用git status来查看有哪些修改没有add到暂存区,有哪些修改在暂存区中,没有提交到版本库。
3.2 提交版本和查看历史
我们修改了一个或多个文件之后,可以使用git add
和git commit
命令将修改提交到版本库。在使用git commit
命令时,可以使用-m选项来添加备注。该命令会得到类似下面的返回:
1 2 3
| $ git commit -m "Fixed the type mistake in readme.txt" [master 0db896a] Fixed the type mistake in readme.txt 1 file changed, 1 insertion(+), 1 deletion(-)
|
这个命令返回的信息中,包含了提交的分支,本次提交自动生成的版本号(commit id),输入的备注,以及修改内容的统计信息。 注意这里的版本号可能不是完整的版本号,而只是版本号的前几位。
我们可以看到,Git的版本号是一个用十六进制表示的随机数字,这是为了避免各自在本地数据库中提交时版本号的冲突。
Git会记录下我们所有的版本历史,可以使用git log
命令来查看。
3.3 版本回退
版本回退是版本管理系统最基本的功能。如果不能回退,要这系统何用。
要回退,首先需要知道回退到哪个版本。可以使用git log
命令来查看版本历史信息。然后使用git rest --hard <版本号>
来进行回退。这里的版本号不需要输全,只需要输入前面几位,能够唯一确认一个版本就可以了。
使用git log
的时候,我们看到的会是一堆密密麻麻的信息。如果我们使用图形化的Git工具,就能够很直观的看到Git是将所有的提交按照时间顺序串成了一条线,在这个时间线上每一个点就是一次提交,每一次提交都有很多的信息,比如提交人是谁,版本号是多少,备注信息是什么。
这下就体现了提交的时候使用备注的好处了,我们可以通过备注知道每一次修改的原因和内容,这样才知道需要回退到哪个版本。
如果我们从今天的版本回退到了昨天的版本,还能不能回到今天的版本呢?可以的,但是前提要是你还记得今天的版本的版本号。如果我们前面的窗口没有关闭,可以从git log
命令的输出中找到今天的版本号。如果已经关闭了,则可以重新使用git reflog
命令查看我们执行的每一条命令。这样就可以使用git rest
恢复到今天的版本了。
3.4 管理修改
前面讲过了工作区和暂存区。因此Git相比其他的版本管理系统多了一层。这一层有什么作用呢?
我们在提交的时候,提交的是暂存区中的内容,而不是工作区中的内容。因此,我们在修改的时候,可以多次将中间代码添加到暂存区。我们既不需要产生大量的中间版本号和提交记录,也可以保证不会因为后续的修改弄丢了前面的代码。我们可以大胆的试错,发现不合适了,很容易回滚到前一个版本。
我们可以使用git checkout -- <file>
来撤销工作区的修改。这个时候,如果暂存区中有还没有提交的修改,那么会使用暂存区中的内容覆盖工作区中的内容。如果暂存区中的修改都已经提交了,那么会用版本库中的内容覆盖工作区中的内容。 注意--
不能省略,省略了就变成切换分支的命令了。
当然,删除文件也是一种修改。一般我们把文件从Git的目录中删除之后,Git会检测到,并且使用git status
命令能够看到。这个时候,如果确实要删除文件,可以使用git rm <file>
来告诉暂存区我们要删除这文件,然后用git commit
命令提交修改,从版本库中删除文件。
如果是误删除的,可以使用git checkout -- <file>
命令从版本库中恢复文件。
4 分支管理
前面我们提到了,从git log
可以看到我们所有的提交的历史记录,并且按照先后顺序串成了一条线,这条线就是一个“分支”。只不过我们现在只有一个分支,就是创建本地仓库时,默认为我们创建一个master分支,通常称为主分支。
这个分支目前的情况如下面的示意图:
分支允许我们创建另外一条”路径“,同时管理两个版本的代码。例如,我们完成了1.0版本,进入2.0版本的开发。但是我们同时需要进行1.0版本的维护,修复bug。这个时候,我们就可以同时维护1.0分支和2.0分支。
4.1 创建与合并分支
分支就是提交记录组成的一个时间线,因此一个分支可以表示如下:
分支master指向该分支中的最后一次提交。然后Git还会用HEAD指向master,表明当前的分支是master分支。每次提交,master都会向前移动一步,一直指向最新的提交,master分支的时间线也越来越长。
如果我们需要创建一个新的分支,例如从当前最新提交创建一个dev分支,那么其实只是创建了一个名为dev的指针,指向最新的提交,同时将HEAD修改为指向dev。使用的命令是git branch dev
和 git checkout dev
,得到的结果如下图所示:
因此,在Git中创建分支非常的快,因为只是创建一个指针,然后修改做一个指针。
我们可以使用git branch
命令来查看当前的分支,该命令会列出所有的分支,并在当前使用的分支前使用*
来标注。
从现在开始,所有的提交操作都是针对dev分支了,因此,如果做了一次新的提交,dev会向前移动一步,但是master指针不变,如下图:
这个时候,要合并分支也很容易,只需要先使用git checkout master
命令切换到master分支,然后在master分支中执行git merge dev
来讲dev合并到当前分支就可以了。实际Git要做的只是将master指针指向最新的提交就可以了(这种合并叫做fast-forward,快速向前。当然并不是所有的合并都这么简单,我们后面会讲到)。如下图所示:
当一个分支完成了历史使命的时候,我们可以将其删除。例如,如果我们要删除dev分支,只需要执行git branch -d dev
就可以了。删除后就只剩下master分支了。
4.2 解决分支之间的冲突
前面介绍的合并分支是最简单的一种情况,当然现实世界通常不会这么简单。现实中常见的情况时在两个分支上我们都有提交,如下图所示:
这个时候,如果我们执行git merge dev
命令,就无法执行fast-forward合并,Git会试图把各自的修改合并起来。如果在dev分支上和在master分支上没有修改相同的文件,Git能够自动进行合并。如果有修改相同的文件,Git就处理不了,会告诉我们有冲突,需要我们手动解决冲突之后再提交。 使用git status
命令可以看到冲突的文件。
如果我们打开冲突的文件,会看到类似下面的内容:
1 2 3 4 5 6
| ...... <<<<<<< HEAD HEAD中的内容(当前分支中的内容) ========== dev分支中的内容。 >>>>>>> dev
|
对于每一个冲突,Git中会用<<<<<<<
,=======
,>>>>>>>
来标记不同分支中的内容。我们需要将这一段修改为合适的内容,并再次提交。如下图所示:
5 通过远程仓库合作
一个团队中有多个开发人员,他们一起协助来完成软件开发的工作。因此,不可能大家都只在自己的本地仓库中修修改改。通常来说,我们会配置一个”中央仓库“,大家都把自己的本地仓库中的代码要提交到中央仓库。
5.1 日常的工作流程
在软件开发过程中,我们首先是从中央仓库下载代码到本地仓库,这个操作在前面的第2.2节中已经描述过了。
有了本地仓库之后,日常的工作流程通常上:
- 从中央仓库拉取最新的代码。
- 根据开发任务,在工作区中修改和测试代码。
- 将代码提交到本地仓库。
- 重复2和3,直到任务开发和测试完成。
- 将代码推送到中央仓库。
5.2 从中央仓库拉取代码
从中央仓库拉取代码,使用的是git pull
命令。 git pull的时候,会试图自动合并本地与远程之间的冲突,如果无法自动合并,则需要手动解决冲突。解决的方式和我们前面第4.2节中说过的方法一样。
还有一种情况,是从中央仓库抓取一个新的分支。我们使用git clone
命令从中央仓库克隆的时候,获取的是master分支。这个时候,中央仓库就是我们的远程仓库,默认的名称是origin。我们可以使用git remote -v
来查看远程仓库的详细信息。
如果要从中央仓库获取一个新的分支,比如dev分支,则需要先执行git checkout -b <本地分支名称> <远程库名称>/<远程分支名称>
命令来获取新的分支。
5.3 推送分支到中央仓库
我们在本地做的修改,需要上传到中央仓库,使用的是git push <远程库名称> <分支名称>
命令。这个过程被称为推送。
如果从上次下载代码到本次推送之间,远程仓库上的代码没有变化,那么本次推送就没有问题。但是通常来说,在团队合作的时候,在这段时间内会有其他人推送了新的代码到中央仓库。这个时候使用git push
命令就会报错,并且会提示我们使用git pull
获取最新的代码,合并代码后(无论是自动还是手动)再推送。
6 其他
6.1 标签管理
在Git中,每一次提交都有一个版本号,但是这个版本号对人类很不友好。没有任何含义,并且又长又难记。我们在日常的交流中无法使用这个版本号。
这种情况下,引入了tag的功能。Tag就是一个标签,我们可以在重要的提交版本上添加一个tag,这个tag其实就是指向这一次提交的版本。我们可以给tag赋予有意义的名称,例如V1.0这样的,这样方便我们日常的交流,和将来的查找。
在Git中打标签非常简单,执行git tag <tagname>
就可以了。(我们暂时不讨论分支,后续的分支管理中在讨论。)
执行上面的命令的时候,默认是将标签打在分支的最后一次提交上。如果要将标签打在其他的提交上,可以使用git tag <tagname> <版本号>
的方式来执行命令。
可以使用git tag
命令来查看所有的标签,使用git show <tagname>
来查看指定标签的信息。
我们创建的标签都是在本地,如果需要将标签推送到远程仓库,则可以使用git push <远程仓库名> <标签名>
命令来推送。也可以一次性的将所有标签推送到远程,只需要使用命令git push <远程仓库名> --tags
命令。
标签是不能移动和修改的,但是可以删除。因此,如果标签打错了,修改的方式就是删除错误的标签,重新创建正确的标签。git tag -d <name>
就是用来删除指定名称的tag的命令。
如果标签已经被推送到了远程仓库,则需要先删除本地标签,然后使用如下形式的push命令来删除远程仓库中的标签。
git push <远程仓库名> :refs/tags/<标签名>
6.2 git stash操作
有些时候,我们在工作的中途,会接到一些紧急的任务,比如生产环境的bug修复。这个时候我们手头的工作才做了一半,既不能提交,也不能丢弃。但是我们又需要切换到另外的分支中来处理紧急任务,该怎么办?
这个时候就可以用到git stash操作了。
git stash命令会将工作区的修改“隐藏”起来。如下例所示:
1 2 3
| $git stash Saved working directory and index state WIP on master: 3793459 add new line in readme.txt HEAD is now at 3793459 add new line in readme.txt
|
这个命令会将工作区中的修改创建一个存根,并保存到堆栈中。我们可以使用git stash list
命令来查看堆栈中的存根。这个时候我们再用git status
命令来查看的话,就会发现在工作区中没有新的修改。这个时候我们就可以切换分支,完成紧急任务。
当紧急任务完成之后,我们在切换回原来的分支,这个时候可以使用git stash pop
,可以从堆栈中弹出修改并应用到工作区,这样我们就可以继续以前的工作了。
6.3 git配置
配置用户信息: 使用命令git config --global user.name="<用户名>"
来配置用户的名字。使用命令git config --global user.name="<邮箱地址>"
来配置用户的email地址。这两个命令配置的是本机的全局配置。
忽略特殊的文件:在有些时候,我们希望不要提交工作区中的某些文件,比如java编译时生成的class文件等。我们可以将不希望提交的文件的名称放在Git工作区的根目录下的.gitignore文件中,Git就会忽略这些文件。该文件中可以使用通配符。