Git快速上手指南
Git入门
什么是Git
Git是一种分布式版本控制系统。假设有一个文件夹,随之时间的变化文件夹中文件的内容随时间变化,无论是是删除或增加文件夹的内容还是修改文件夹中的某个文件的内容,都能称为一个“版本”,而Git可以记录每个版本之间的区别并快速回滚到某个版本。简单来说,Git就像是是游戏的进度存档,可以快速回档
Git可以清楚地记录每个文件是谁在什么时候加进来的,什么时候修改的,修改的内容有哪些
无论做什么工作,如果有Git帮你保留这些历史记录和证据,那么发生意外情况的时候你就能知道是从什么时候开始有问题,以及该找谁负责了
Git的优点
速度块,占用体积小
如果你也有按照时间顺序备份文件的习惯,或许一个两个小文件还好,但是一旦涉及到大型文件夹,一次备份就会占用大量的空间,并且没有办法记录各个版本之间的差异
分布式系统
Git通常会有一服共同的服务器,所有文件都保存在这台服务器之上,即使在没有网络的环境下,在个人电脑上依然可以使用Git进行版本控制,待网络恢复后,再将个人电脑的数据同步(Push)到这台服务器上去。而个人电脑的数量可以有很多,每个人都可以将自己的修改后的文件同步到这台服务器上,大大提高了工作效率。这台服务器最著名的就有免费的Github,或者国内的Gitee,还可以是自建的Gitlab
Git的缺点
易学难精
Git的指令有非常多,但常用的却不多。约20%的指令就可以胜任80%的工作。但好在有许多实用的图形界面供新手学习使用(如SourceTree)
二进制文件的无奈
Git只关心文件的内容,每次版本的更新都会逐行对比文本文件(如md、txt)的差异并予以记录。但像是Photoshop的psd、Office的docx、ppt等文件是二进制文件,并不能像常规文本一样一行行查询,也就无法精确地查看不同版本的差异。但总体来说Git还是可以帮的上忙的,版本回溯的功能依然可以使用,至少当文件不小心被覆盖或者被删除的时候依然可以找回旧版本的文件
环境安装
Windows
Git官方下载地址
里面提供的32位和64位、安装版和便携版共4种组合供大家选择,选择适合的版本就好
Mac
Git官方下载地址
在macOS上安装Git有几个选项:Homebrew、MacPorts、Xcode、Binary installer、Building from Source、Installing git-gui
Linux
在Linux上安装是最简单的,只需要在终端中输入apt-get命令安装即可
1 | $ apt-get install git |
测试Git是否成功安装
打开终端,输入命令
1 | $ git -v |
如果返回版本号则代表Git已经正确安装
图像界面工具
一开始接触Git,特别是没有Linux基础的人比较习惯图形界面工具,对终端及Git指令操作相对不熟悉,可以使用GUI工具辅助学习使用
在Git官方网站中提供了多款GUI工具,有的是商业付费软件,有的是免费软件
其中SourceTree和Github Desktop这款两工具使用于Windows和macOS,并且功能都较为完善,也可以免费使用
如果在Linux上使用,SourceTree没有Linux的版本,但是可以使用gitk,可以使用下面命令安装
1 | $ sudo apt-get install gitk |
设置Git
用户设置
使用Git首先先要设置用户的邮箱和用户名
1 | $ git config --global user.name "Akimio" |
完成后可以输入以下命令检查当前配
1 | $ git config --list |
单个项目用户设置
前面在设置用户时多加了一个--global
的参数,其含义是进行全局(Global)设置,为默认的使用的账户,但要是在某一项目使用不同的作者则可以在该目录的终端中进行设置
1 | $ git config --local user.name "Akimio" |
其他方便设置
更换编辑器
Git默认使用Vim编辑器,这里以更换为Emacs编辑器为例
1 | $ git config --global core.editor emacs |
除Emacs之外,Git还可以使用Sublime Text、Atom、VS Code等比较现代的文字编辑器,只需了解如果从终端启用这些编辑器,然后就可以利用同样的方法进行替换
设置缩写
虽然Git的命令并不长,但是有时候懒得打太多字,或者是经常打错,就可以设置缩写
1 | $ git config --global alias.co checkout |
设置之后输入git co
就和输入git checkout
的效果一致,其他同理
Git中有些命令需要填入一些参数,同样也可以通过Alisa设置缩写
1 | $ git config --global alias.l "log --oneline -- graph" |
设置之后输入git l
就和输入git log --oneline -- graph
的效果一致
甚至还可以设置得更复杂一些
1 | $ git config --global alias.ls 'log -- graph --pretty=format:"%h <%an> %ar %s"' |
除了在终端中修改,也可以直接在配置文件中修改,在~/.gitconfig
中可以查看之前的修改:
1 | co = checkout br = branch |
你也可以仿照这种格式直接修改
虽然只是少打了几个字母,但是长期积累,减少的输入量还是十分惊人的
开始使用Git
初始化仓库(Repository)
如果是第一次使用Git,那么就从建立一个全新的文件夹开始吧
1 | cd /tep # 切换至绝对路径/tmp,Windows用户可以直接进入文件夹中右键空白处选择打开终端 |
把文件交给Git管控
创建文件交给Git
创建文件
现在在这个test文件夹中只有一个.git
的隐藏文件夹,并没有其他的文件,所以我们创建一个文件
1 | $ echo "Hello,Git" > welcome.html # 将Hello,Git添加到welcome.html中,因为我们文件夹中并没有welcome.html,所以系统会为我们创建一个welcome.html |
输入以下命令查看Git状态
1 | $ git status |
在终端就会显示(井号后面为含义):
1 | On branch master # 在master分支(branch)中 |
把文件交给Git
使用的命令是git add
后面加上空格和文件名:
1 | $ git add welcome.html |
再次查看Git状态:
1 | On branch master |
从以上的信息可以看出,welcome.html
的状态已经从untracked
变成new file
,表明该文件已经被提交到缓存区中
如果觉得一个一个提交git太麻烦,也可以尝试使用以下命令
1 | $ git add *.html # *代表通配符,意思是提交所有后缀名为html的文件 |
git add后又对文件内容进行改动
设想一下这种情况:
- 新增了一个文件abc.txt
- 执行git add abc.txt命令,将文件提交到缓存区,但是并没有提交(commit)
- 编辑abc.txt文件
此时查看一下Git状态
1 | $ git status |
可以发现,abc.txt
变成了两个。因为步骤2中把txt提交到缓存区后,步骤3又编辑了该文件,对于Git来说,编辑的新内容并没有提交到缓存区中,所以缓存区的内容还是步骤2中加进来的那个文件。如果你确定步骤3的这个改动是你想要的,就可以再次执行git add abc.txt
,将abc.txt添加到缓存区中
把暂存区内容提交到仓库存档
如果仅仅是通过gti add
命令把异动文件添加到缓存区,还不算是完成整个流程,还需要将缓存区的文件提交到仓库中永久保存:
1 | $ git commit -m"第一次提交" |
这里双引号中的内容代表提交的说明,最主要的目的就是告诉自建和别人这次改动做了什么,下面右键建议:
- 中文、英文都可以
- 用简洁的语言表达
- 尽量不要用Fix bug等模糊的表述,因为没人知道你修复了什么BUG
在提交(commit)后会显示有什么做了修改
1 | [master (root-commit) 5df35a7] 第一次提交 |
这里很清楚地可以看到新增了abc.txt
、welcome.html
两个文件
查看记录
使用Git命令查看记录
因为目前只提交(Commit)了一次,不好比较,所以我们再创建一个新的文件提交一次
1 | $ echo "test2.1" > test2.txt |
然后输入以下命令查看提交记录
1 | $ git log |
越在上面说明内容约新,通过这个命令大致就可以看出是谁在什么时候提交的内容,提交了什么内容,其中第一次提交
和第二次提交
都是commit的说明,所以大家要清楚的写下自己修改了说明内容,方便日后查询记录。其中a291b22f6fed953f2cd869fa6baacf301934a027
是一个哈希值,其重复的概率非常低,每一次提交(Commit)都有这么一个哈希值,可以当作提交(Commit)的唯一身份识别码
使用Git命令查看记录时常见问题
查找某个人或某些人的提交记录
例如查找一位叫Akimio的提交记录可以使用以下命令:
1 | $ git log --oneline --author="Akimio" |
如果需要查找Akimio和zuilang的提交记录,就可以使用:
1 | $ git log --oneline --author="Akimio\|zuilang" |
其中\
是转义字符,如果输入git log --oneline --author="Akimio|zuilang"
,则会查找一个名字为Akimio|zuilang
的提交记录
通过提交说明查找提交记录
使用--grep
参数可以在所以提交的记录中查找关键字,例如搜索fix bugs
1 | $ git log --oneline --grep="fix bugs" |
在提交文件中找到Ruby
使用-S
参数可以在所以提交的记录中进行搜索,找到那些符合特定条件的内容
1 | $ git log --oneline -S "Ruby" |
查找某一时间段内的提交记录
在查看历史记录时,可以搭配--since
和--until
这两个参数使用
查找今天早上9点到下午6点所以的提交记录:
1 | $ git log --oneline --since="9am" --until="6pm" |
还可以搭配--after
这个参数
查找自2023年6月往后每天早上9点到下午6点所以的提交记录:
1 | $ git log --oneline --since="9am" --until="6pm" --after="2023-06" |
在Git中删除文件或更改文件名
无论删除还是更改文件名,对Git来说都是一种改动
删除文件
直接删除
可以使用系统命令或者是直接在资源管理器等图形化界面工具直接删除文件:
1 | $ rm welcome.html |
此时在文件夹中已经找不到welcome.html
这个文件了
查看一下Git的状态
1 | $ git status |
可以看到welcome.html
这个文件当前状态是deleted
,如果你确定是你想做的,就可以把改动添加到缓冲区:
1 | On branch master |
welcome.html
当前状态是deleted
而且已经被添加到缓冲区,接下来就可以提交(Commit)了:
1 | $ git commit -m "删除welcome.html" |
然后可以查看一下提交记录
1 | $ git log |
使用Git删除
输入命令:
1 | $ git rm test2.txt |
这时候查看状态:
1 | $ git status |
他就直接在缓冲区,就不再需要git add
了,可以少一个步骤
加上–cached参数
无论是执行rm
还是git rm
命令都会真的把这个文件从文件夹中删除,如果只是像让这个文件不再被Git控制则可以加上--cached
这个参数
1 | $ git rm abc.txt --cached |
在文件夹中查看,abc.txt
依然在文件夹中
此时查看Git状态
1 | $ git status |
可以看到文件abc.txt
的状态是deleted
且文件夹中有一个文件abc.txt
未被追踪,由于之前删除test2.txt
未提交,所以我们看到test2.txt
的状态是deleted
更改文件名
更改文件名和删除文件类似,这里不再赘述其中过程,只给出命令
直接改名
1 | $ mv welcome.html hello.html # 把welcome.htm改成hello.html |
使用Git改名
1 | $ git mv welcome.html hello.html # 把welcome.htm改成hello.html |
文件的名称并不重要
Git是根据文件的内容来计算“SHA-1”的值,所以文件的名称并不重要,因此在更改文件名时,Git并不好为此做出有个新的Blob对像,而是指向原来的那个Blob对线,但因为文件名变了,所以Git会为此做出一个新的Tree对象
修改Commit记录
改动最后一次的Commit信息
只需要增加参数--amend
即可
1 | $ git commit --amend -m "新的Commit信息" |
改动更早的Commit信息
参数--amend
只能出来最后一次的Commit,想要改动更早的记录需要使用到命令:Rebase
,详细使用方法可以参考后面的内容。
追加文件到最后一次Commit
刚刚提交了Commit但是突然发现有一个文件忘记加上了,虽然可以为了这个文件单独加送一次Commit,但是看上去没那么优雅,可以采用下面两种方式来完成:
- 先使用
git reset
删除最后一次Commit,将文件加入后再重新提交Commit - 使用
--amend
参数进行Commit
这里先介绍第二种方式,第一种方式会在后续章节讲解
例如,这里有一个名为cinderella.html
的文件要把它加入最近一次的Commit。先使用git status
命令查看cinderella.html
的状态是Untracked
,先将其加入缓冲区:git add cinderella.html
,然后再提交Commit:git commit --amend --no-edit
其中--no-edit
参数的意思是不编辑Commit信息,这样就把文件添加到最近一次的Commit了
新增文件夹
刚刚添加了一个新的的文件夹image
,但是查看Git状态却提示没有任何改动,这是因为Git在计算时是根据”文件内容“进行计算的,所以新增一个空文件夹是无法直接添加到git中的
解决方法其实也很简单,可以在文件夹中添加一个.kepp
或.gitkeep
的空文件让Git监测到这个文件夹,可以通过以下命令创建空文件:
1 | $ touch image/.keep |
然后再依次提交到缓冲区、提交Commit就好了
有些文件夹不想放在Git中
有些比较机密的文件不想放在Git文件夹中一起备份或者是一些程序编译产生的中间文件抑或是缓存文件
添加.gitignore
使用命令在Git命令下创建一个.gitignore
文件
1 | $ touch .gitignore |
然后编辑文件内容
1 | # 文件名.gitingore |
如果不清楚自己所用的工具或程序语语言通常会忽略哪些文件,可以在这里查看:传送门
忽略这个忽略
虽然.gitignore
列出了一些忽略规则,但其实还是可以忽略这些忽略规则的,只需要在将文件提交到缓冲区时添加一个参数:
1 | $ git add -f 文件名称 |
忽略失效
添加忽略规则后发现Git依然在追踪一些应被忽视的文件,是因为这些文件在创建.gitignore
的时候就已经存在了,如果依然想要这些文件套用忽视规则可以使用以下命令:
1 | $ git rm --cached |
清除忽略的文件
如果像清除那些已经被忽略的文件,可以使用git clean
命令配合-X
参数:
1 | $ git clean -fX |
其中那个额外加上的参数-f
是指强制删除
查看特定文件的Commit记录
使用git log
就可以查看整个Git项目所有的Commit,如果想查看某个文件所有的Commit可以使用-p
参数:
1 | $ git log -p welcome.html # 查看welcome.html所有的Commit |
+++
表示某次commit中新增加的行;---
表示删除的行
查看某个文件中某行代码是谁写的
如果想查看某个文件中某行的代码是是谁写的,可以使用git blame
:
1 | $ git blame index.html |
然后git就可以详细地列出index.html这个文件每一行代码是谁在什么时候写的
如果文件很大,可以使用-L
这个参数:
1 | $ git blame -L 5,10 index.html # 这样Git只会列出5-10行的信息 |
恢复删除的文件
无论是有意还是无意将文件删除,Git都能将文件恢复,为了掩饰,我这里用rm
删除所有的后缀为html
文件:
1 | $ rm *.html |
再使用git status
查看当前Git状态,可以看到删除的html文件的状态是deleted
。假设我想恢复其中的a.html
,可以使用以下命令:
1 | $ git checkout a.html |
然后a.html
就恢复了;如果想恢复所有的文件,可以使用:
1 | $ git checkout . |
然后就可以恢复所有删除的文件了
将提交的Commit拆掉重做
拆掉重做
这种情况很常见,虽然使用的命令git reset
很简单,但是不少人误解了Reset的意义,所以导致很多学习Git的人卡在这里
退一步海阔天空
先查看以下Git的状态:
1 | $ git status |
拆掉最后一次Commit分为“相对”和“绝对”的做法,下面是“相对”的做法:
1 | $ git reset e12d8ef^ |
^
代表的是“前一次”,所以e12d8ef^
指的就是e12d8ef的前一次Commit也就是85e7e30
这个Commit。如果是前两次就是e12d8ef^^
,以此类推,但是如果要退回到前五此一般不会用e12d8ef^^^^^
而是用e12d8ef~5
因为正好HEAD与master当前都是指向e12d8ef这个Commit,而且一般这些哈希值不好记,所以会用以下这两个命令替代:
1 | $ git reset master^ |
以上是“相对的”方式,如果你很清楚要回到那个Commit,可以直接指明:
1 | $ git reset 85e7e30 |
然后就可以直接切换到85e7e30这个Commit的状态,因为85e7e30
刚好就是e12d8ef
的前一次Commit,所以也能达到“拆掉最后一次的Commit”这个效果
Reset模式
git reset
这个命令可以搭配三个参数使用,分别是—mixd
、—soft
、—hard
。搭配不同的参数,执行结果会有些许差别
mixed模式
—mixd
是默认参数,如果没有另外加参数,git reset
将会使用—mixd
模式
在—mixd
模式下,会把缓冲区的文件删除,但不会影响工作目录的文件。也就是说,Commit拆出来的文件会留在工作目录,但不会留在暂缓区
soft模式
在这种模式下,其工作目录与缓冲区的文件都不会被删除,所以看起来就只有HEAD的移动而已。因此,Commit拆出来的文件会直接放在缓冲区
HARD模式
在这种模式下,无论是工作目录还是缓冲区的文件,都会直接被删除
三种模式的比较
模式 | mixed | soft | hard |
---|---|---|---|
工作目录 | 不变 | 不变 | 被删除 |
缓冲区 | 被删除 | 不变 | 被删除 |
模式 | mixed | soft | hard |
---|---|---|---|
Commit拆出来的文件 | 放回工作目录 | 放回缓冲区 | 直接删除 |
恢复不小心被hard reset的文件
退回到Reset前
还是使用之前的例子:
1 | $ git status |
这里一共有6个Commit,我们假设倒退两步:
1 | $ git reset HEAD~2 |
然后再次查看Git状态:
1 | $ git status |
会发现最后两次Commit都不见了,但最后两次的Commit其实是依旧存在的,只是没有列出,假设我想要恢复到执行执行git reset HEAD~2
命令之前的状态,只需要执行这个命令:
1 | $ git reset e12d8ef --hard # e12d8ef为执行reset前的那个Commit的SHA-1值 |
这里使用了--hard
这个参数可以强迫放弃Reset之后改动的文件
使用Reflog
如果一开始没有记录Commit的SHA-1值也没有关系,可以使用下面的方法恢复
方法演示前,我们先reset一下我们的git仓库:
1 | $ git reset HEAD~2 --hard # 与上面的例子不同的是这里使用了hard模式进行reset |
这样不仅Commit不见了,文件也消失了。然后可以使用Reflog命令来查看一下记录:
1 | $ git reflog |
当HEAD移动时(如切换分支或者Reset都会造成HEAD移动),Git就会在Reflog中留下一条记录。从上面的三条记录中,可以大致猜出最近3次HEAD的移动,最后一次的动作就是Reset。如果想要取消这次Reset,只需要Reset到它Reset前的那个Commit即可:
1 | $ git reset e12d8ef --hard |
HEAD是什么
HEAD简介
HEAD是一个指标,指向了某个分支,通常可以把他当作当前所在分支
来看待。在.git
目录中有一个名为HEAD
的文件,其中记录的就是HEAD的内容,可以来看看他长什么样:
1 | $ cat .git/HEAD |
从这个文件就看也看出,HEAD当前正指向master分支,可以在深入看一下master:
1 | $ cat ./refs/heads/master |
可以看到所谓的master分支其实也只是一个40个字节的文件罢了
切换分支
假设当前项目一共有三个分支,当前在master分支上:
1 | $ git branch |
接下来尝试切换到dev
分支上:
1 | $ git checkout dev |
这时再看一下HEAD文件:
1 | $ cat .git/HEAD |
就可以看到HEAD现在指向dev
分支了,我们再尝试切换到main
分支:
1 | $ git checkout main |
这时再确认一下HEAD:
1 | $ cat .git/HEAD |
它又指向了main
分支。也就是说,HEAD通常会指向当前所在的分支。不过HEAD也不一定总所指向某个分支,当HEAD没有指向某个分支时便会造成detached HEAD
状态
版本分支
使用版本分支的原因
在开发的过程中,一路往前Commit迭代版本似乎也并没有问题,但是当越来越多的伙伴加入同一个项目中的时候,就不能像前面那么随意的Commit了。这是分支的作用就派上用场了。例如,想要新增功能或者修复Bug,都可以新创建一个分支,在新分支上做完并确认没有问题之后再合并回到原本的分支,这样就不会影响正在运行的产品线
开始使用分支
查看分支
1 | $ git branch |
可以看到当前项目有两个分支,并且当前在master
前面有个*
,说明当前在这个分支上
新增分支
假设我需要新增一个名为dev
的分支,只需要输入下面这段命令
1 | $ git branch de |
然后在查看一下当前项目的分支情况
1 | $ git branch |
可以看到多出了一个分支,但当前的项目依旧在master
分支上
更改分支名称
假设我们需要将分支dev
修改成test
:
1 | $ git branch -m dev test |
再查看一下分支:
1 | $ git branch |
删除分支
假设test
已经不再需要了,就可以使用下面的命令删除:
1 | $ git branch -d test |
如果test
分支中还有内容未被合并,Git就会给出提示,并且test
分支也不会被删除,如果想要强制删除,可以使用下面的命令:
1 | $ git branch -D test |
需要注意的是,“当前所在分支”是无法被删除的,想要删除该分支,必须先要切换到其他的分支上
切换分支
假设当前在master
上,现在需要切换到main
分支去
1 | $ git checkout main |
这个命令只能切换到已经存在的分支上,如果需要创建一个分支并切换过去可以使用下面这个命令:
1 | $ git checkout -b test2 |
合并分支
假设我们新创建了一个dev
分支,并且在这个分支上提交了两次Commit,现在需要将这两次提交的内容合并回主线分支master
,我们需要先回到主线分支master
,在将dev
分支的内容合并回主线分支:
1 | $ git checkout master |
然后就可以看到哪些文件有改动
A合并B与B合并A
假设有两个分支dev1
和dev2
都来自master
的同一个Commit。现在这两个分支都提交了不同的Commit,现在需要将这两个分支合并。
假设目前HEAD
在dev1
分支上,先执行合并dev2
:
1 | $ git merge dev2 |
然后Git会创建一个额外的Commit,这个Commit同时指向了dev1
和dev2
两个分支,然后HEAD
会随dev1
向前指向这个新的Commit,而dev2
分支会停留在原地
值得注意的是,如果dev1
和dev2
修改同一文件,合并时就会有冲突,需要打开这个文件手动修改冲突位置
恢复不小心被删除的分支
恢复已被删除但还未合并过的分支
如果某个还未合并过的分支被用-D
强制删除了:
1 | $ git branch -D dev |
找到这个分支最新Commit的HASH值,也就是b174a5a
,只要创建一个新的分支从这个Commit开始就好了:
1 | $ git branch new_dev b174a5a |
然后检查一下分支,切换过去,查看了一下文件列表就会发现文件都回来了
1 | $ git branch |
删除分支时没有记录HASH值
上面介绍了如果记录了删除分支时没有记录HASH值的方法,如果没有记录删除分支时的HASH值还可以通过git reflog
命令通过查看HEAD移动推断出是哪个Commit。值得注意的是Reflog默认保留30天,所以30天内删除的分支还是可以找回来的
使用Rebase合并分支
前面一节介绍了 git merge
命令来合并分支,现在介绍另一种方式
假设目前的状态还是有两个分支基于 master
的分支 dev1
和 dev2
并分别提交了自己的Commit,现在在 dev1
分支上,现在需要合并分支:
1 | $ git rebase dev2 |
dev1
和 dev2
原本都是基于 master
分支的。执行这段 命令后,就将 dev1
的参考基准指向了 dev2
。
假设 dev1
在 master
分支的基础上提交了3个Commit,dev2
在 master
分支的基础上提交了5个Commit,再执行了上述命令后,dev1
分支就领先了 master
3+5共8个Commit,前5个Commit依旧是 dev2
原本提交的Commit,后三个Commit来自于 之前的 dev1
,但是Commit的HASH值于 dev1
原本提交的Commit的HASH值不同,效果上相当于在 dev2
上额外提交了三个Commit,并将 dev1
和 HEAD
移动到最新的Commit上
合并时发生冲突
发生冲突
只要不同的分支检测到同一文件同一行受到改动时就会出现冲突,假设在 dev1
分支上改动了 index.html
文件内容:
1 |
|
然后在 dev2
分支上也改动了 index.html
:
1 |
|
这时不管是使用Merge和Rebase合并都会出现冲天
1 | $ git merge dev2 |
这时Git发现那个 index.html
出现问题了,这时查看一下当前的状态
1 | $ git status |
这里有两点需要说明:
- 对于
dev1
分支来说,dev2-1.html
和dev2-2.html
都是加入的新文件,但已被放入暂存区 - 因为在
dev1
和dev2
都对index.html
的 同一行进行了改动,所以Git将其标记为both modified
的状态
解决冲突
Merge
查看 index.html
的文件内容:
1 |
|
可以看到有两个分支的内容,现在决定使用dev1的内容,最后内容如下:
1 |
|
改完后记得将内容提交回暂存区,并提交Commit:
1 | $ git add index.html |
Rebase
使用Rebase进行合并:
1 | git rebase dev2 |
如果遇到冲突,合并就会暂停,并且HEAD不会指向任何一个Commit,现在需要修复冲突的文件,然后提交文件到暂存区后继续合并:
1 | $ git add index.html |
至此,就完成了Rebase
如果冲突的文件不是文本文件
因为上述的冲突文件 index.html
文件是文本文件,所以Git可以标记出冲突的位置,但如果冲突的文件是二进制文件又该如何解决?
假设 dev1
和 dev2
都同时加入了一张叫 background.png
的图片,则合并时的冲突信息为:
1 | $ git merge dev2 |
如果决定采用 HEAD
的 background.jpg
的话:
1 | $ git checkout --our background.jpg |
如果决定采用 dev2
的 background.jpg
的话:
1 | $ git checkout --theirs background.jpg |
决定之后再像前面一样把内容加入到暂存区并提交Commit:
1 | $ git add background.jpg |
从过去的某一Commit创建新分支
回到过去重新开始
先通过git记录找到需要回去的Commit,切换过去并创建分支:
1 | $ git log |
然后就在 657fce7
这个Commit上创建了一个名为 past_commit
的分支
一步到位
如果你实现就知道是Commit的HASH值就可以用下面的命令一步到位:
1 | $ git branch past_commit 657fce7 |
修改历史记录
修改历史Commit信息
在之前历史信息,在5.6节讲过可以使用--amend
参数来修改最后Commit的信息,但仅限于最后一次,如果要改动其他更早的形象,就需要使用其他方式
之间介绍过通过git rebase
来合并分支,其实git rebase
是一个强大的互动模式,接下来的几节内容都是介绍怎么使用这种模式改动历史记录
启动互动模式
1 | $ git rebase -i bb0c9c2 |
这段命令种的-i
参数是指进入Rebase指令的“互动模式”,后面的bb0c9c2
指的是从现在这个Commit回到bb0c9c2
这个Commit,可以修改中间若干个Commit的信息
此时会弹出一个Vim编辑器,中间有若干个Commit的形象,前面的pick
表示“保留这次Commit,不做改动”,找到需要改动的Commit,把前面的pick
改成reword
,表示要改动这几次Commit的信息,保存修改后会立即弹出另一个Vim,在这里就可以修改之前的Commit信息了
修改Commit信息后的其他影响
虽然只是修改了Commit信息,但只要调出Git日志就可以发现,修改过信息Commit的HASH值都改变了,已经是一个全新的Commit对象了
取消这次Rebase
如果想放弃这次对Commit的修改,只需要使用之前介绍过的ORIG_HEAD
就好:
1 | $ git reset ORIG_HEAD --hard |
将多个Commit合并
有时候Cimmit太过“琐碎”,例如:
1 | $ git log --oneline |
5d52302
和507780e
两个Commit上各添加了一个文件(分别是cat1.html
和cat2.html
)类似的c90b00f
和37646cd
也各添加了两个Commit,也可也使用git rebase
将这几个Commit合并为一起,可以让Commit看起来更加简洁:
1 | $ git rebase -i 5944ffd |
将需要合并的Commit的pick
修改成squash
1 | pick d943a01 add database settings |
上面的改动代表了下面这两件事:
add dog 2
会与上一个前一个Commit合并,即3c90b00f
与37646cd
合并add 2 cats
、add cat 2
、add cat 1
这三个Commit会合并
简单来说,squash
会和前一个Commit合并
保存Vim的编辑后,会弹出另一个Vim编辑窗口,重新编辑一下新合并的Commit信息
将一个Commit拆成多个Commit
与上节相反,有时觉得单词Commit的文件太多了,可能就会想这拆解得更细点,下面是当前的历史记录:
1 | $ git log --oneline |
0d40e75
这个Commit提交了两个文件
还是使用rebase
:
1 | $ git rebase -i 5944ffd |
我们需要把0d40e75
拆成多个Commit,只需要把pick
改成edit
然后Rebase在执行到0d40e75
这个Commit后会在暂停下来,因为需要将当前这个Commit拆成两个Commit,所以先要回到上一个Commit:
1 | $ git reset HEAD^ |
然后就可以看到原本0d40e75
添加的两个文件cat3.html
和cat4.html
都放在了工作目录中,并且处于Untracked未追踪
的状态,所以现在只需要分别提交Commit即可:
1 | $ git add cat3.html |
添加完后只需要让Rebase继续执行即可:
1 | $ git rebase --continue |
插入Commit
有些时候可能需要在某些Commit中间插入其他的Commit,这个技巧其实和上一个拆分Commit的方法很像。假设当前的历史记录如下:
1 | $ git log --oneline |
然后Rebase:
1 | $ git rebase -i 5944ffd |
假设需要在add 2 cats
和add dog 1
之间插入其他的Commit,则将add 2 cats
的pick
改成edit
,保存后等待Rebase运行到0d40e75
后插入Commit:
1 | $ touch bird1.html |
然后继续刚刚中断的Rebase:
1 | $ git rebase --continue |
调整Commit顺序
假设当前Commit记录如下:
1 | $ git log --oneline |
我们需要把所有cat
的记录都移动到dog
之后,还是使用Rebase:
1 | $ git rebase -i 5944ffd |
然后修改Commit顺序,在Rebase状态下越新的Commit记录越在下方
1 | pick d943a01 add database settings |
保存离开后就调整好Commit的顺序了
删除Commit
假设当前Commit记录如下:
1 | $ git log --oneline |
我们需要把所有dog
的Commit删除,还是使用Rebase:
1 | $ git rebase -i 5944ffd |
然后把dog
前面的pick
修改成drop
或者也可也直接删掉
1 | pick d943a01 add database settings |
保存离开后就已经删除dog
了
标签
开始使用标签
标签是什么
在Git中,标签(Tag)是一个指向某个Commit的指示标,这看起来好像与分支(Branch)有些类似
什么时候使用标签
通常在软件开发时会完成特定的“里程碑”,如软件版本号1.0.0
或beta-release
之类的,在这种时候就很适合用标签做标记
标签的分类
标签有轻量标签(lightweight tag)
和附注标签(annotated tag)
两种标签。无论是哪一种标签,都是可以把它当作贴纸贴在某一个Commit上
轻量标签(lightweight tag)
轻量标签的使用方法想当简单,只需要指定要贴上去的那个Commit即可。
假设当前Commit记录如下:
1 | $ git log --oneline |
如果需要在add lion and tiger
这个Commit上贴一个big_cats
的标签,可以使用如下命令
1 | $ git tag big_cats 7b6d9d0 |
这样标签就贴好了,再次查看一下记录:
1 | $ git log --oneline |
可以看到big_cats
这个标签指向了7b6d9d0
这个Commit
附注标签(annotated tag)
如果需要为add lion and tiger
这个Commit添加一个附注标签,则可以使用下面这段命令:
1 | $ git tag big_cats 7b6d9d0 -a -m "Big Cats are coming" |
其中参数-a
代表让Git创建一个附注标签,后面的参数-m
类似于提交Commit时的-m
,如果没有-m
则会弹出一个Vim编辑器去修改信息
两种标签的区别
在Git官方文档中对这两种标签也有相应说明,Git官方推荐将轻量标签用于个人使用或是暂时标记;而附注标签主要用作软件版本号。
此外两种标签的信息也不同,可以通过以下命令查看信息:
1 | $ git show big_cats |
可以发现附注标签比轻量标签多一些信息,可以清楚地看到是谁在什么时候贴了这张标签
删除标签
不管是哪一种标签,其本质都类似于一张帖子,撕掉一张贴纸并不会删除Commit或者文档,只需要一个参数-d
:
1 | $ git tag -d big_cats |
标签与分支有什么区别
标签和分支的区别是,分支会随着Commit移动,但标签不会。在之前的章节介绍过,当Git往前推进一个Commit时,它所在的分支会跟着向前移动;但标签一旦贴上去,无论Commit怎么前进,标签都不会移动。因此分支可以看成是可以移动的标签
其他一些冷知识
临时切换到其他任务
Commit当前进度
假设当前在dev
分支开发项目,但是main
分支出现问题,需要修复,可以先在dev
分支上提交一个Commit然后切换回main
分支:
1 | $ git add . |
当main
分支的问题修复完后,需要继续回到dev
分支继续开发未完成的项目:
1 | $ git checkout dev |
然后即可以看到之前未完成的文件了
使用Stash
保存Stash
刚刚这种情况除了提交Commit还可以用Stash。
先查看一下当前的状态:
1 | $ git status |
就可看到哪些文件有修改
然后利用stash
将修改的地方暂存起来:
1 | $ git stash |
然后可以查看一下刚刚保存起来的文件:
1 | $ git stash list |
最前面的stash@(0)
就是这个Stash的代名词,你也可以像其中添加多个Stash;后面的WIP
就是Work In Progress
的缩写,即工作进行中。
然后就可以切换回主分支修复问题了。
应用并删除Stash
问题修复完成后,现在需要继续进行刚刚的工作:
1 | $ git stash pop stash@(0) |
需要注意一下三点:
- 使用
pop
指令相当于把Stash拿出来并套在了当前的分支上,所以需要考虑是否切换回原本的dev
分支。 - 如果
pop
没有指定哪一个Stash,则会选择数值最小的Stash - 套用Stash成功后,那个被套用的Stash就会自动删除
删除Stash
如果某些Stash确定不需要了,可以使用drop
1 | $ git stash drop stash@(1) |
应用Stash
把Stash捡回来,除了pop
还有一个就是apply
:
1 | $ git stash apply stash@(2) |
apply
和pop
类似,还是把Stash套在当前的分支上,不同的是,apply
并不会删除已经被套用的Stash。
可以把pop
理解成apply
+drop
使用Github
Github概述
Github是什么
Github是一个商业网站,是当前全球最大的的Git服务器,在这里可以和许多国内外开发者交朋友,既可以为他人项目贡献自己的力量,也可也为自己的项目寻求他人帮助。
同时,他也是开发者最好的履历展示平台,你曾经做过哪些专案和贡献,做出什么的贡献都一目了然,想要造假也非常难
Github和Git的区别
Github是网站,而Git是工具。Github本体是一个基于Ruby on Rails开发的Git服务器
将内容Push到Github上
在Github上创建新仓库
想要将自己的文件上传到Github中,先需要创建一个自己的仓库存放自己的文件,打开Github官网,点击右上角的+
选择New repository
,接着就是填写仓库名称,有两点需要说明:
- 仓库名称可以任意填写,但不能重复
- 仓库可以选择
公开(Public)
或者私人(Private)
创建完成后就会跳转到刚刚创建的空仓库以及Gitub给出的提示。假设我们需要开启一个新的项目,首先需要创建一个README.md
文件用于Github
项目的默认说明页面,后缀名md
代表了采用MarkDown语法,感兴趣的朋友可以阅读我这一篇博客快速上手MarkDown入门教程
1 | $ echo "# My First Github Repository" > README.md |
然后就是初始化Git并提交Commit:
1 | $ git init |
接下来就是链接我们在Github的远程仓库
1 | $ git remote add origin git@github.com:Akimio521/MyGit |
下面是几个参数说明:
git remote
主要进行与远端有关的操作add
代表添加一个远端节点origin
是一个代名词,指代后面的服务器地址,按照惯例,远端节点默认使用origin
这个名称,如果是从服务器Clone下来的仓库,其默认的名称是origin
。当然,要是不喜欢的话也可也改成其他的名字Akimio521/MyGit
是我刚刚在Github上新创建的仓库,其中Akimio521
是我Github的名称,MyGit
是我的仓库名
接下来就准备把刚刚提交的Commit推送(Push)到Github中:
1 | $ git push -u origin master |
简答的Push命令其实主要做了下面几件事:
- 把
master
分支的内容推送(Push)到origin这个远程节点(服务器)上 - 如果在
origin``master
这个分支,就会新建一个名为master
的分支的仓库中不存在 - 如果在
origin
的仓库中存在master
这个分支,就会将master
的分支标签移动到最新的Commit上 -u
就是upstream
,关于这个参数的含义会在后续说明
如果你理解了上面这个命令,可以尝试将dev
这个分支推送到MyGitlab
这个远程节点上:
1 | git push MyGitlab dev |
upstream是什么意思
简单来说,upstream就是设置一个默认的推送位置,只要第一次使用了git push -u origin master
后,之后想要将master
推送到origin
节点只需要使用git push
这个命令
如果想要不同的分支名
之前介绍的命令git push origin master
其实也是一个缩写的命令,完整的写法是git push origin master:master
,含义为将本地的master
分支推送到origin
中的master
分支,如果想要推送到Github的main
分支,就可以用下面的命令:
1 | $ git push origin master:main |
Pull下载更新
在上一节介绍了如何将本地更新的内容推送(Push)到远程节点;这一节将介绍如何将远程的更新拉取(Pull)回本地。但在介绍Pull
前,还需要了解一下Petch
什么是Petch
假设目前本地仓库是目前最新的Commit的是8c3a0a5
,远程的仓库已领先一个Commit85e848b
执行git fetch
后就可以看到Commit85e848b
的内容被拉取下来了,但多了两个分支,origin/master
和origin/HEAED
,这两个分支都指向85e848b
这个Commit,而master
和HEAD
都指向原本的8c3a0a5
这个Commit。
但无论是master
还是origin/master
都是从同一个分支发展的,,所以可以将origin/master
合并到master
上:
1 | $ git merge origin/master |
Pull指令
看完上面对git fetch
的解释,那么理解git pull
就很简单了:
1 | $ git pull = git fetch + git merge |
Pull+Rebase
现在知道了Pull = Fetch + Merge
,在之前的章节提到,合并不仅只有Merge
这一个方法,还可以使用Rebase
,在git pull
也可以使用Reabse
这个方法合并:
1 | $ git pull -rebase |
Push失败
在多人开发时这个问题挺常见的,原因就是当前仓库的版本领先于本地的内容,所以需要先更新本地内容再推送:
1 | $ git pull -rebase |
提交PullRequest
什么是PR
在Github上有许多有趣的开源项目,许多热心的人会前来帮忙,但是一般项目的原作者不会直接开放原仓库的权限给一个陌生人,因此引申出PR这个机制:
- 先复制(Fork)原项目的仓库到自己的账号下
- 因为已经复制到自己的账号下,所以拥有完整的权限,可以对仓库内容进行修改
- 修改完成后,将自己账号下的仓库推送(Push)上去
- 发个通知给原作者,让原作者知道你做了修改
- 如果原作者觉得你的改动可以,就会合并(Merge)到原仓库中
跟上项目进度
如果在提交PR前,有其他人也抢先一步提交了PR并且原作者也接受了,那么该仓库的进度就会领先于自己账号下的仓库进度,想要提交PR的话就先要让自己的仓库跟上进度,目前Github上没有提供相应的功能,但是这里有两个方法达成这个目的
强制同步
如果是Fork别人的仓库,在Fork过来的仓库中会有一个Sync Fork
的按钮,这个按钮是强制同步,原理就算删掉仓库再重写Fork一次,这样保证的是版本是最新版,但是会丢失原本仓库中自己提交的内容
跟上游同步
这个算是比较正统的做法,将作者的项目设为上游项目,Fetch下来后再手动合并
假设我有一个仓库是Fork其他人的,并且我将其Pull到本地,现在需要添加远端节点:
1 | $ git remote add dummy-kao https://github.com/kaochenlong/dummy-kao |
这样就新增了一个名为dummy-kao
的远程节点指向作者的原仓库,然后获取原仓库最新的内容:
1 | $ git fetch dummy-kao |
然后将最新的版本合并进自己的版本(Merge和Rebase都行)
1 | $ git merge dummy-kao/master |
这样,本机进度就和原项目的进度一致了,如果你希望Github上Fork的那个仓库也保持最新的进度话只需要推送上去就行了:
1 | $ git push origin master |
尾声
至此,Git大部分内容已经介绍完毕,剩下的内容需要在实践中亲自探索了。若文章中有疏漏,欢迎指出。
- 感谢您的赞赏。