git

所有的版本控制系统,其实只能跟踪文本文件的改动,比如TXT文件,网页,所有的程序代码等等,Git也不例外。版本控制系统可以告诉你每次的改动,比如在第5行加了一个单词“Linux”,在第8行删了一个单词“Windows”。而图片、视频这些二进制文件,虽然也能由版本控制系统管理,但没法跟踪文件的变化,只能把二进制文件每次改动串起来,也就是只知道图片从100KB改成了120KB,但到底改了啥,版本控制系统不知道,也没法知道。

不幸的是,Microsoft的Word格式是二进制格式,因此,版本控制系统是没法跟踪Word文件的改动的,前面我们举的例子只是为了演示,如果要真正使用版本控制系统,就要以纯文本方式编写文件。

使用统一UTF-8编码,所有语言使用同一编码,既没有冲突,又被所有平台支持。

使用Windows的童鞋要特别注意:千万不要使用Windows自带的记事本编辑任何文本文件。原因是Microsoft开发记事本的团队使用了一个非常弱智的行为来保存UTF-8编码的文件,他们自作聪明地在每个文件开头添加了0xefbbbf(十六进制)的字符,你会遇到很多不可思议的问题,比如,网页第一行可能会显示一个“?”,明明正确的程序一编译就报语法错误,等等,都是由记事本的弱智行为带来的。建议你下载[Visual Studio Code](https://code.visualstudio.com/)代替记事本,不但功能强大,而且免费!

版本管理的核心:Git 的版本控制管理的是一系列提交(commits),每个提交形成一个快照(snapshot),记录了项目在某个时间点的状态。这些提交通过父子关系构成了一个链条,形成了分支。

HEAD 的作用HEAD 指向的是当前检出的分支的最新提交。也就是说,Git 知道你当前工作在哪个版本上是通过 HEAD 指针实现的。当你进行 commitcheckoutreset 等操作时,HEAD 的位置会改变,以反映你当前的工作状态。

举例

本地仓库

git init : 初始化仓库

git add */文件名 : 把文件添加到仓库

Unix的哲学是“没有消息就是好消息”,说明添加成功。

git commit -m "第一次提交" : 把文件提交到仓库

虽然Git告诉我们readme.txt被修改了,但如果能看看具体修改了什么内容,自然是很好的。比如你休假两周从国外回来,第一天上班时,已经记不清上次怎么修改的readme.txt

git diff 文件名 : 查看文件修改了什么

git diff HEAD -- readme.txt命令可以查看工作区和版本库里面最新版本的区别

知道了对readme.txt作了什么修改后,再把它提交到仓库就放心多了。

像这样,你不断对文件进行修改,然后不断提交修改到版本库里,就好比玩RPG游戏时,每通过一关就会自动把游戏状态存盘,如果某一关没过去,你还可以选择读取前一关的状态。有些时候,在打Boss之前,你会手动存盘,以便万一打Boss失败了,可以从最近的地方重新开始。Git也是一样,每当你觉得文件修改到一定程度的时候,就可以“保存一个快照”,这个快照在Git中被称为commit。一旦你把文件改乱了,或者误删了文件,还可以从最近的一个commit恢复,然后继续工作,而不是把几个月的工作成果全部丢失。

git log : 显示从最近到最远的提交日志

如果嫌输出信息太多,看得眼花缭乱的,可以试试加上--pretty=oneline参数

git log --pretty=oneline

如何进行版本回退?

在Git中,用HEAD表示当前版本。上一个版本就是HEAD^,上上一个版本就是HEAD^^,当然往上100个版本写100个^比较容易数不过来,所以写成HEAD~100

git reset --hard HEAD^	回退到上一个版本

--hard参数有啥意义?--hard会回退到上个版本的已提交状态,而--soft会回退到上个版本的未提交状态,--mixed会回退到上个版本已添加但未提交的状态。现在,先放心使用--hard

当我们回退了版本以后,想再回到最新的版本,就需要最新版本的版本号了。怎么办呢?

git reflog : 用来记录你的每一条命令

版本号没必要写全,前几位就可以了,Git会自动去找。当然也不能只写前一两位,因为Git可能会找到多个版本号,就无法确定是哪一个了。

Git和其他版本控制系统如SVN的一个不同之处就是有暂存区的概念。

工作区:就是电脑上能看到的本地目录

版本库:工作区的一个.git文件夹

版本库中有很多东西,其中最重要的就是被称为stage(或index)的暂存区,还有git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD。

repo

当我们使用git add将文件添加进来的时候,实际上就是把文件修改添加到暂存区。

当我们进行git commit提交修改的时候,实际上就是把暂存区中的所有内容提交到当前分支。

可以简单的理解为,需要提交的文件修改都放到暂存区(git add),然后一次性提交暂存区的所有修改(git commit)。

如果你没有对工作区进行任何修改,那么工作区是干净的。

这里我们要提到一点,为什么git会比其他版本管理工具要优秀呢?

那是因为,git跟踪并管理的是修改,并非文件。

当我们进行以下操作时(实验:为了理解修改):第一次修改 -> git add -> 第二次修改 -> git commit

第一次修改将被提交到分支,但是,第二次修改并不会被提交到分支。因为第二次修改并没有被放到暂存区。

那么如何提交第二次修改呢?

可以继续git add再git commit。也可以在将第一次修改提交前,将第一次修改和第二次修改一块放到暂存区,然后一块提交。

那么如何撤销修改呢?

git checkout -- file可以丢弃工作区的修改。

放弃对工作区的修改有两种情况:第一种,修改后还没有被放到暂存区(没有add,只对文件进行了修改),这时候撤销修改就会回到和版本库一样的状态,也就是说,这个修改直接被忽略了。第二种,已经将文件添加到了暂存区(进行了add),又做出了修改,这时候撤销修改的话,文件会回到添加到暂存区的状态。

删除文件呢(这里指的是commit后的文件,因为commit之前删除不会导致工作区和版本库不同)?

删除文件有两种情况

第一种,自己主动删除。一般情况下,你通常直接在文件管理器中把没用的文件删了,或者用rm命令删了 rm test.txt .这个时候,git知道你删除了文件,因此工作区和版本库不一致。一般需要先从版本库中删除文件(git rm 文件),然后git commit -m "remove 文件"提交这个删除操作

第二种,误删。这时候使用(git checkout -- test.txt)恢复误删文件。其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。

需要注意的是,从来没有被添加到版本库就被删除的文件(没有commit的),是无法恢复的!

远程仓库

添加远程仓库

第1步:创建SSH Key。在用户主目录下,看看有没有.ssh目录,如果有,再看看这个目录下有没有id_rsaid_rsa.pub这两个文件,如果已经有了,可直接跳到下一步。如果没有,打开Shell(Windows下打开Git Bash),创建SSH Key:

ssh-keygen -t rsa -C "youremail@example.com"

如果一切顺利的话,可以在用户主目录里找到.ssh目录,里面有id_rsaid_rsa.pub两个文件,这两个就是SSH Key的秘钥对,id_rsa是私钥,不能泄露出去,id_rsa.pub是公钥,可以放心地告诉任何人。

第2步:登陆GitHub,打开“Account settings”,“SSH Keys”页面:

然后,点“Add SSH Key”,填上任意Title,在Key文本框里粘贴id_rsa.pub文件的内容

为什么GitHub需要SSH Key呢?因为GitHub需要识别出你推送的提交确实是你推送的,而不是别人冒充的,而Git支持SSH协议,所以,GitHub只要知道了你的公钥,就可以确认只有你自己才能推送。

当然,GitHub允许你添加多个Key。假定你有若干电脑,你一会儿在公司提交,一会儿在家里提交,只要把每台电脑的Key都添加到GitHub,就可以在每台电脑上往GitHub推送了。

在github上创建一个仓库后。

先关联仓库

git remote add origin git@github.com:example/example.git

这里的origin是一个常用的默认习惯命名。当然,也可以起其他的名字。

将本地仓库推从到远程仓库

git push -u origin master

把本地库的内容推送到远程,用git push命令,实际上是把当前分支master推送到远程。

为什么第一次推送要加 -u

由于远程库是空的,我们第一次推送master分支时,加上了-u参数,Git不但会把本地的master分支内容推送的远程新的master分支,还会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时就可以简化命令

第一次提交以后,就可以用

git push origin master

来进行推送了

如果,远程仓库添加错了,怎么办?

使用git remote -v查看远程库信息

根据具体的名字来删除远程仓库

git remote rm origin

此处的“删除”其实是解除了本地和远程的绑定关系,并不是物理上删除了远程库。远程库本身并没有任何改动。要真正删除远程库,需要登录到GitHub,在后台页面找到删除按钮再删除。

从远程库克隆

直接使用

git clone git@github.com:example/example.git

将远程仓库克隆到本地。

值得注意的是,GitHub给出的地址不止一个,还可以用https://github.com/michaelliao/gitskills.git这样的地址。实际上,Git支持多种协议,默认的git://使用ssh,但也可以使用https等其他协议。

使用https除了速度慢以外,还有个最大的麻烦是每次推送都必须输入口令,但是在某些只开放http端口的公司内部就无法使用ssh协议而只能用https

分支管理
创建和合并分支

根本原理是什么样呢?

在版本回退里,我们已经知道,在每次进行提交之后,git都会把它们串成一条时间线,这条时间线就是一个分支。这条分支就是我们所说的主分支,也就是master分支。严格来说,HEAD并不是指向提交的,而是指向master的,master才指向提交。

HEAD指向master1 drawio

当我们每进行一次提交,master就会向前移动一步,随着不断地提交,master分支的线越来越长。

当我们创建了一个新的分支dev后,git会新建一个指针,这个指针会指向master当前的提交,然后再把HEAD指向dev。

dev指向master当前 drawio

注意:在进行上述操作时,工作区没有任何变化

从现在开始,工作区的修改和提交就是针对dev分支的了。如果进行了新的提交。那么会出现HEAD指向的dev往后移动一步,而master不变。

HEAD指向dev向后移动一步 drawio

当我们在dev上的工作完成以后,就可以把dev合并到master上去了。怎么合并呢?就是让master直接指向dev的当前提交。

master指向dev当前提交 drawio

git合并是非常快的。本质上就是修改指针。

合并完成后,dev分支的作用就没有了。可以删除dev分支。

具体操作

git checkout -b dev	:	创建分支并切换
git switch -c dev	:	创建分支并切换

git branch dev		:创建分支
git checkout dev	:切换分支
git switch dev		:切换分支

git branch			:查看当前分支

git switch master	:切换回master分支
发现在dev分支上添加的内容不见了。不要着急,现在将dev分支合并到master分支上去。
git merge dev		:将dev分支合并到当前的分支
注意:如果同一个文件来说,内容不一样,进行合并会覆盖掉master中的原有内容
解决冲突

当分支冲突时,也就是说,当我们修改过dev分支后,没有在master分支上提交。然后在master上进行了修改之后,进行了合并。这时候,两个分支的文件内容会冲突。

分支冲突 drawio

文件存在冲突,必须手动解决冲突后再提交。git status也可以告诉我们冲突的文件。

Git用<<<<<<<=======>>>>>>>标记出不同分支的内容,我们修改如下后保存。

再进行提交,就变成正常了。

冲突解决 drawio

分支管理策略(能看到分支合并历史)

当我们进行实际开发的时候,master一般作为发布版本的主分支。一般不在上面进行开发。在那干活呢?在副分支上干活,dev上。如果是多人共同合作的话,还会有很多很多分支。每个人都有自己的分支。

git merge --no-ff -m "merge with no-ff" dev

branches

合并分支时,加上--no-ff参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而fast forward合并就看不出来曾经做过合并(git默认是fast forward合并分支)

bug分支

现在有一个bug任务比较紧急。目前来看,需要创建一个分支来修复它,但是dev分支上的工作还没有提交。dev上的工作至少还需要两天才能完成,但是bug比较急,需要5分钟之内解决。这时候该怎么办?

git提供一个stash功能,可以把工作现场储藏起来,等以后恢复现场后继续工作。

git stash

用git status查看工作区,是干净的。

这时候就可以想办法修复bug了。首先要确认在哪个分支上修改bug,如果是master,就从master上创建分支。在新创建的分支上修复好bug以后,合并到master分支,删除新分支。

这时候要继续进行dev分支上的工作了,恢复工作内容一般有两个办法。

先查看工作现场在哪

git stash list

第一种,使用

git stash apply

恢复以后,stash内容不会删除,需要手动使用git stash drop删除。

第二种,使用

git stash pop

恢复的同时会把stash也删除了。

在master上修复bug以后,又出现了一个新问题,master上有的问题,dev分支上应该也有。这时候难道需要在dev上进行一遍相同的操作?

大可不必。git提供一个

git cherry-pick	版本号(提交的时候出现)

切换到dev分支,执行上面的命令。git会自动给dev分支做一次提交,相当于复制了修复bug的分支在dev提交。

开发一个新功能,最好新建一个分支;

如果要丢弃一个没有被合并过的分支,可以通过

git branch -D <name>`

强行删除。

多人协作