Git教程——从入门到入土
本教程参考廖雪峰的Git教程
Git是一个分布式版本控制系统
Git安装
安装过程请自行百度👀
安装之后要在命令行中设置自己的用户名和邮箱,需要在命令行中输入如下命令:
git config --global user.name "Your Name"
git config --global user.email "email@example.com"
版本库(repository)
版本库:你可以简单理解成一个目录,这个目录里面的所有文件都可以被Git管理起来,每个文件的修改、删除,Git都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。
创建版本库很简单,即用git打开一个空文件夹,输入:
git init //初始化一个仓库
要想将文件添加到仓库中,需要分两步
第一步先把文件放到仓库目录或子目录中,然后输入如下命令(例如文件名为:readme.txt):
git add readme.txt //将readme.txt文件添加到仓库中
第二步需要用git commit提交到仓库
git commit -m "wrote a readme file" //将修改提交到仓库并说明
其中- m "xxx"是对本次提交的说明,等同于注释,有助于别人对你提交的理解。
版本控制
查看当前仓库的状态(例如什么文件被修改了):
git status
若想查看哪个文件具体修改了什么,可以用如下命令:
git diff readme.txt //查看readme.txt文件具体修改了什么内容
版本回退
版本退回有如下几个命令用于查看历史:
git log//1.
git log --pretty=oneline//2.
git reflog//3.
- 查看详细历史提交记录(包括commit_id、作者、日期、提交注释),确定要退回哪个版本,每个版本都有一个commit_id
- 可以让每个版本只输出一行,简化输出信息(只显示commit_id和提交注释)
- 查看历史命令
版本退回命令:
git reset --hard HEAD^//1.
- 退回到当前版本的上一个版本,如果想退回上上一个版本就是... HEAD^,前一百个版本可以用HEAD~100表示。--hard参数暂不讲解。这里的HEAD也可以替换为想要退回版本的commit_id,由于commit_id很长,只用写前几位能区别于别的版本就行。
例:
1094adb7b9b3807259d8cb349e7df1d4d6477073 (HEAD -> master) append GPL
e475afc93c209a690c39c13a46716e8fa000c366 add distributed
eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0 wrote a readme file
这是我当前有的版本,我想回到add distributed版本,我可以再commit_id处填写"HEAD^"或者"e4751fc"
带有(HEAD -> master)的即为当前版本
当退回某一个版本之后再用git log就不能看到原来版本的commit_id了,此时可以用git reflog来查看历史命令,找到commit_id
工作区和暂存区
工作区就是我们创建的仓库文件夹。工作区有一个隐藏文件夹是.git,这里是版本库。版本库里面有一个stage(或index)叫暂存区,还有一个git自动创建的master分支,以及指向master分支的指针HEAD 。
当我们git add时是把文件提交到stage中,git commit时是把stage中的所有修改一次性提交到分支中

当你commit过之后,没有对工作区的文件进行修改,那么版本库就变成这样:

管理修改
Git管理的是修改而非文件
当你对文件readme.txt进行如下操作时:第一次修改readme.txt->git add->第二次修改readme.txt->git commit。会发现master中的readme.txt文件是第一次修改过的而非第二此修改过的。
如果想让master中的readme.txt文件是第二次修改的:第一次修改readme.txt->git add(有没有都行)->第二次修改readme.txt->git add->git commit
撤销修改
例如撤销对readme.txt文件的修改:
当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时
git checkout -- readme.txt//1.
- 把readme.txt文件在工作区的修改全部撤销
当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改
在commit之前可以先用git status查看哪个文件被修改了,撤销可用如下命令:
git reset HEAD readme.txt
已经提交了不合适的修改到版本库时,想要撤销本次提交
直接使用版本退回就好了呀
删除文件
例如删除工作区中的readme.txt文件:
git rm test.txt//删除
git commit//提交
在提交之前如果发现删错了可以用:
git checkout -- readme.txt
远程仓库
git是分布式版本控制,同一个git仓库可以分布到不同的机器上面。而拿一个机器来当作服务器,全天24小时开机,其他人久都可以从这台服务器上把仓库克隆到自己的电脑上了,并且可以把各自的提交推送到服务器仓库中,也可以从服务器仓库拉取别人的提交。
现在我们不需要自己搭建服务器了,我们可以使用GitHub或者码云,但是在国内GitHub速度比较慢。。。下面先以GitHub为例讲解远程仓库。(码云的用法会在后面说到)
-
创建SSH Key:在用户主目录下看看有没有(.ssh)目录,如果有再看看这个目录下有没有(id_rsa)和(id_rsa.pub)这两个文件,如果都有则可以直接跳到下一步。如果没有,则在用户主目录下用git bash here(Windows)或者用shell(Linux)打开,输入如下命令创建SSH Key:
ssh-keygen -t rsa -C "youremail@example.com"这里要把邮箱换成自己的邮箱,然后一路回车,使用默认值即可,由于这个Key也不是用于军事目的,所以也无需设置密码。
如果一切顺利的话,可以在用户主目录里找到(.ssh)目录,里面有(id_rsa)和(id_rsa.pub)两个文件,这两个就是SSH Key的秘钥对,(id_rsa)是私钥,不能泄露出去,(id_rsa.pub)是公钥,可以放心地告诉任何人。
-
登陆GitHub,打开“Account settings”,“SSH Keys”页面,然后,点“New SSH Key”,填上任意Title,在Key文本框里粘贴(id_rsa.pub)文件的内容,然后就可以在SSH keys界面看到你添加的密钥。

SSH Key用于绑定将你的电脑和GitHub绑定,GitHub是开源仓库,你在上面提交的代码可以被别人看到。
添加远程仓库
先在GitHub创建一个仓库:

(这是仓库创建好的样子)我创建了一个名叫test的仓库,刚刚创建的仓库是空的。现在GitHub告诉我们可以从这个仓库克隆出新的仓库,也可以把一个已有的仓库与之关联,然后把本地仓库的内容推送到GitHub仓库。
我这里是在本地也新建了一个叫test的仓库,并且在仓库根目录创建了一个(README.md)的文件。之后根据前面讲过的命令,或者GitHub上面提示的命令将本地test仓库与GitHub上的远程仓库相关联,需要在新建的空文件夹下依次运行如下命令:
git init
git add README.md
git commit -m "wrote a readme file" //1.
git remote add origin https://github.com/ZhangZef/test.git //2.
git push -u origin master //3.
- 这里双引号里面为提交注解,前面讲过
- 这里要把网址换成自己的GitHub仓库地址,提示页面中给出了。origin就是远程仓库的名字,在git中默认这样叫,也可以改成别的,不过不推荐,修改不利于以后和别人合作或者自己忘了。
- git push是把当前分支master推送到远程仓库。由于远程库是空的,我们第一次推送时加上-u参数,git不但会把本地的master分支内容推送到远程新的master分支,还会把本地master分支和远程master分支关联起来,在以后的推送和拉取时可以简化命令。
- 如果已有本地仓库可直接执行后两个命令。
这是推送成功后的样子:

以后只要本地提交过,直接运行:(git push origin master)命令就可以提交
从远程仓库克隆
将远程仓库克隆到本地可以用如下命令:
git clone git@github.com:ZhangZef/test.git
注意要把仓库地址换成自己的地址
分支管理
创建并合并分支
在git中,master是主分支。master是指向提交的分支,而HEAD是指向当前分支的指针。
一开始,master分支是一条线,git用master指向最新提交,再用HEAD指向master,就能确定当前分支,以及当前分支的提交点,每次提交,master就向前移动一步。

当我们新建了一个分支dev(自己命名)的时候,git就会新建一个叫dev的指针,指向master相同的提交。再把HEAD指向dev就表示当前分支在dev:

从现在开始,你对工作区的修改和提交就是针对dev分支的了,每提交一次,dev分支就向前挪一步:

我们要想把dev合并到主分支master上,就是把master指针指向dev的位置就可以了:

这时候我们可以把dev分支给删去,他不会修改工作区的任何内容,只会将dev指针删掉:

下面来实际进行上述操作,创建并切换分支用如下命令:
git checkout -b dev //1.
git branch dev //2.
git checkout dev //3.
git branch //4.
- git checkout命令加上-b参数表示创建并切换,dev处可换成你起的分支名
- git branch + name表示创建分支
- 切换到分支
- 查看当前分支,名字前面带*号的表示当前HEAD指向的分支
我们创建一个新分支dev,并转向dev分支,并在dev分支中做修改然后提交:

之后我们将dev的改变合并到master分支上,先将转向master分支,之后用如下命令:
git merge dev

注意到上面的Fast-forward信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master指向dev的当前提交,所以合并速度非常快。当然,也不是每次合并都能Fast-forward,我们后面会讲其他方式的合并。
之后我们就可以将dev删除了,之后运行branch命令就看到只有master分支了:
git branch -d dev

还有switch命令:
git switch -c dev //1.
git switch master //2.
- 这个也是创先并转向新分支
- 这个是转向分支,和checkout用法类似
小结
git branch //1.
git branch <name> //2.
git checkout <name> //3.
git switch <name> //4.
git checkout -b <name> //5.
git switch -c <name> //6.
git merge <name> //7.
git branch -d <name> //8.
- 查看分支
- 创建分支
- 转向分支
- 转向分支
- 创建并转向分支
- 创建并转向分支
- 合并分支
- 删除分支
解决冲突
当新建的分支和原来的分支同时向前推进,导致生成了分叉,这时候就发生了冲突,这节要来解决冲突。
我们先新建一个分支dev,并在dev分支修改文件并提交,这样dev分支就比master分支超前了一个提交:

之后我们再切换回master分支,系统就提示我们当前分支比master分支超前了一个分支(这里他显示的超前两次,是因为我的master分支修改过之后没有对远程仓库提交,它显示的‘origin/master’意思是远程仓库的master分支):

之后我们再在master修改一次并提交:

会发现合并的时候git会提示我们发生冲突(conflict),这时分支的状态是这样的:

我们可以用(git status)来查看哪些文件发生了冲突:

这时git提示我们README.md文件发生了冲突,要我们手动解决冲突,之后我们打开README.md文件查看:

会发现两个分支中的提交分别在文件中提示出来,我们手动修改后再提交即可:

这是修改过的文件,我们将修改过的文件提交后可以用如下命令查看分支合并状态:
git log --graph --pretty=oneline --abbrev-commit

可以看到我们的分支已经合并完成,这时可以将dev分支删除。
小结
git无法自动合并冲突分支,合并后必须手动解决冲突,再提交,用如下命令可以查看分支合并图:
git log --graph
分支管理策略
在一般情况,合并分支时git会使用Fast forward模式进行合并。在这种模式下删除分支后会丢掉分支信息。
fast forward
那么什么是fast forward模式呢?假设现在有两个分支,一个是主分支master,另一个是dev分支。
当我们在dev分支做了一个提交为“new 444444”:
当HEAD指向dev时我们可以看到“new 444444”这次提交,当我们将HEAD指向master时则看不到这个“new 444444”提交:
这次我们使用fast forward模式进行合并,合并之后的分支图如图所示:
可以看出git将“new 444444”这次提交和master分支强行糅合到了一块儿。
接下来我们来看关闭 fast forward模式的合并是什么样子的,这次我先删除了dev分支,又创建了新的dev分支,以避免和前面的混淆,其他操作类似,这次在dev的提交换成了“new 666666”:可看出这次合并之后git并没有将“new 666666”这次提交和master分支强行糅合在一起。
可用两张图总结一下,第一张为fast forward合并,第二张为非fast forward合并:


看到廖雪峰老师对fast forward这里讲解的不是很清楚,我对这里也有点迷,就动手实践了一下,唠叨唠叨。关闭fast forward可以避免丢失信息,对以后的代码分析很有用(看网上说的👀),不过fast forward模式确实将一部分信息给抹去了,比如上面第一个图,我们就看不出曾经有过dev的痕迹,而第二张图则可以看出原来有某个分支提交了这个功能。
分支策略
在实际开发中,我们应该按照几个基本原则进行分支管理:
首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;
那在哪干活呢?干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在master分支发布1.0版本;
你和你的小伙伴们每个人都在dev分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。
所以,团队合作的分支看起来就像这样:

如图michael在他自己的分支上工作,bob在他自己的分支上工作,他们每次完成工作都把自己的分支合并到dev分支,当dev分支上的项目稳定下来,则将dev合并到master上。
Bug分支
当你正在dev分支中做某项工作时,突然通知让你去修复master分支中的一个非常紧急bug-001,但是你手头的工作一时半会儿也做不完。这时候要先保护工作现场,就像调用函数时,系统会先保护工作现场,将各寄存器中的值压入栈中,再去调用别的函数。
git也提供了一个保护工作现场的功能:
git stash
可以将当前未提交的修改先存起来,以便bug修复完成后恢复现场。恢复现场的命令如下:
git stash list //1.
git stash apply //2.
git stash pop //3.
- 这个命令用于查看当前有哪些被保存起来的工作现场,这个list其实是一个栈结构,即后进先出。
- 第二条和第三条命令都是恢复工作现场,但是apply不会删除list中的记录,想要删除还要用(git stash drop),而pop是应用并删除。
master分支中存在bug,那么说明dev分支中一定也存在bug。那么怎么方便快捷地修复dev上的bug呢?
要先再master分支中查看那个bug分支的commit_id(例如是aadbd3d......,取前几位就行),然后转到转到dev分支使用如下命令:
git cherry-pick aadbd3e
这样就可以将那次bug修复直接合并到dev分支中,不过还需要手动解决冲突并提交。
Feature分支
当新开发功能时,肯定不能从master分支中修改。这时候要从dev分支新建一个feature分支来开发新功能,但是当你开发完新功能提交过还未合并时又被要求取消这个功能,这时候用(git branch -d + 分支名)是删除不了的,因为分支还未合并。git会提示你要用(-D)来删除(例如分支名为feature):
git branch -D feature
多人协作
当你从远程仓库克隆时,实际上Git自动把本地的master分支和远程的master分支对应起来了,并且,远程仓库的默认名称是origin。查看远程仓库的信息(第一个是查看信息,第二个是查看详细信息,remote中文翻译是偏僻的、偏远的😁):
git remote
git remote -v

推送分支
推送分支,就是把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样,Git就会把该分支推送到远程库对应的远程分支上(比如说要推送master分支,origin是远程仓库的意思):
git push origin master
但是,并不是一定要把本地分支往远程推送,那么,哪些分支需要推送,哪些不需要呢?
- master分支是主分支,因此要时刻与远程同步;
- dev分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;
- bug分支只用于在本地修复bug,就没必要推到远程了,除非老板要看看你每周到底修复了几个bug;
- feature分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。
抓取分支
想要抓取分支,先要获取你的工程在GitHub上的地址,先在GitHub中打开你的工程,之后(这个按钮是将地址信息复制到粘贴板):

之后使用(git clone)命令,例如上图中的工程:
git clone git@github.com:ZhangZef/test.git
但是在GitHub上我的工程有两个分支,一个是master分支,一个是dev分支,当你clone之后使用(git branch)命令时会发现只有一个master分支,这时候想将dev分支也抓取下来要用如下命令:
git checkout -b dev origin/dev
后面带上(origin/dev)意思是将本地仓的dev分支和远程仓库的dev分支相关联,以后推送拉取的时候git会自动寻找远程仓库和当前推送拉取分支相关联的分支,如果远程仓库没有关联分支:
- 拉取时则会拉取失败
- 而推送时没有关联分支且无同名分支则会自动在远程仓库创建新分支,但是创建的新分支并不会和本地仓库的分支相关联,还要手动将远程仓库的分支和本地分支相关联
- 而推送时没有关联分支且有同名分支则不会发生任何事,没有推送上去,也没有创建新分支
要想关联远程仓库和本地仓库的分支可用如下命令(例如要关联分支是dev):

git branch --set-upstream-to=origin/dev dev
当然你也可以指定将本地dev和远程仓库的其他分支相关联,修改origin后面的分支名字即可,但是并不推荐
解决推送冲突
当你的小伙伴向远程仓库的dev推送分支推送之后,你再向远程仓库推送时git会提示你产生冲突,并且提示你要用(git pull)命令:
此时就使用(git pull)将远程仓库的分支抓取下来,手动解决冲突再合并(拉取失败时记得关联远程仓库的分支和本地仓库分支)。
Rebase
当解决冲突时,后push的人要先pull,再add...commit...push,这样分支图就会变得很杂乱。利用git的rebase(变基)操作可以将解决冲突时的分支图整理成一条直线。(不太理解rebase的操作,回来再写吧,先跳过去)
标签管理
标签用于标记某个commit,方便使用。
创建标签
git tag <name> //1.
git tag <name> <commit_id> //2.
git tag //3.
git show <tagname> //4.
git tag -a <tagname> -m "......" //5.
- 创建一个标签,默认指向当前HEAD指向的提交
- 指定某一个commit创建标签,用于指定以前的提交未添加标签
- 查看所有标签
- 显示某个标签(标签指向的commit)的信息
- 可以指定某个标签的注解
操作标签
git tag -d <tagname> //1.
git push origin <tagname> //2.
git push origin --tags //3.
git push origin :refs/tags/<tagname> //4.
- 删除本地的某个标签
- 将某个标签推送到远程仓库
- 一次性推送全部尚未推送到远程的本地标签
- 删除远程标签,要先删除本地的标签,再使用这个命令删除远程的标签
使用GitHub
想要修改别人的文件,就要先fork到自己的仓库,然后才能进行修改提交。当你想要别人能接受你的提交,你就可以在GitHub上发起一个pull request,之后要看别人接受不接受了。
使用码云
由于GitHub是国外的网站,国内用户访问时速度有时候令人抓狂。这时候你可以使用国内的git托管服务码云
不过码云的免费git仓库有5人的成员上限。
添加SSH Key的步骤就略过了。
在码云上的git操作和GitHub类似,就不赘述了。

当HEAD指向dev时我们可以看到“new 444444”这次提交,当我们将HEAD指向master时则看不到这个“new 444444”提交:
可以看出git将“new 444444”这次提交和master分支强行糅合到了一块儿。
浙公网安备 33010602011771号