Git的常见使用(附带图文)--大猪的呕心沥血之作

551 阅读25分钟

1.Git简介和历史

Git 是一个工具,世界上目前最先进的分布式版本控制系统。Git有什么特点?简单来说就是:高端大气上档次!

1.1.那有朋友不禁疑问,版本控制系统?啥玩意儿?

举个👉,我们在大学书写毕业论文的时候,论文肯定经过很多次编写,今天写点做项目的实际中的问题,明天写点去图书馆查阅资料的文献,发现昨天项目中的总结不太好,想删除掉,但是又怕后面又用得上,只能再复制个word文档,继续编写,可是后天还要让班长或者辅导员帮助填写自己的党员资料,而且后天你总不能一天闲着,所以你用U盘copy一份给她,然后继续修改你的word文档,一天后老师给你修改好,通过email发给你了,此时,你必须想想,发给她之后到你收到她的文件期间,你作了哪些改动,得把你的改动和她的部分合并,真困难。于是你想,如果有一个软件,不但能自动帮我记录每次文件的改动,还可以让同事协作编辑,这样就不用自己管理一堆类似的文件了,也不需要把文件传来传去。如果想查看某次改动,只需要在软件里瞄一眼就可以,岂不是很方便?这样,你就结束了手动管理多个“版本”的史前时代,进入到版本控制的20世纪。

1.2.又有同学可能不明白,版本控制系统我理解了,但是啥叫分布式?

这就要提起Linux的创始人了,Linus这个非常优秀的程序员了,他一直痛恨CVS、SVN这些集中式的版本控制系统,不但速度慢,而且必须联网才能使用。那么集中式和分布式控制系统有什么区别呢?

集中式版本控制系统:必须联网,安全性低。

集中式版本控制系统:版本库是集中存放在中央服务器的,干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后再开始干活,干完活了,再把自己的活推送给中央服务器。中央服务器就好比一个图书馆,你要修改一本书,必须先从图书馆解出来,然后回到家自己改,等改完了,再放到图书馆。

分布式版本控制系统:不需要联网,安全性高,便于修改和分享,强大的分支管理。

分布式版本控制系统:根本没有“中央服务器”,因为每个人的电脑上都是一个完整的版本库,这样你工作的时候,就完全不需要联网了,既然每个人电脑上都有一个完整的版本库,那多个人如何协作呢?比方说你在自己电脑上改了文件A,你的同事也在他的电脑上修改了文件A,这时,你们俩之间只需要把各自的修改推送给对方,就可以互相看到对方的修改了。

小结:

1.集中式版本控制系统:必须联网,安全性低;
2.分布式版本控制系统:不需要联网,安全性高,便于修改和分享,强大的分支管理。

2.创建版本库分布式版本控制系统:不需要联网,安全性高,便于修改和分享,强大的分支管理。

什么是版本库呢?版本库又名仓库,英文名repository,你可以简单理解成一个目录,这个目录里面的所有文件都可以被Git管理起来,每个文件的修改、删除,Git都能够跟踪,以便任何时刻都可以追踪历史,或者在将来的某个时刻进行“还原”。

2.1.创建一个空目录

$ mkdir learngit // 创建一个文件夹名为learngit
$ cd learngit    // 进入到这个文件夹内
$ pwd            // 显示当前目录

2.2.通过git init命令把这个目录变成Git可以管理的仓库

$ git init

瞬间Git就把仓库给建好了,而且告诉你是一个空的仓库(empty Git repository)细心的同学可以发现当前目录下多了一个.git的目录。这个目录是Git用来跟踪管理版本库的,没事千万不要修改这个目录里面的东西,否则会把Git仓库破坏了。

2.3.把文件添加到版本库

$ touch readeMe.md   // 创建一个文件
$ git add readeMe.md // 用该命令告诉Git,把文件添加到仓库
$ git commit -m '写了一个readeMe的md文件' // 用该命令告诉Git,把文件提交到仓库

简单解释一下 git commit 命令,-m 后面输入的是本次提交的说明,最好输入一些有意义的内容,这样以后你就能从历史记录里面很方便地找到改动的记录。该命令执行成功过后会告诉你:

1 file changed:1个文件被改动了(我们新添加的readeMe.md文件)

1 insertion:插入了一行内容(readeMe.md有一行内容)

为什么Git添加文件需要add,commit两步呢?因为commit可以一次提交很多文件,所以你可以多次add不同的文件。

小结:

1.初始化一个Git仓库,使用 git init 命令;
2.添加文件到Git仓库,分为两步:
  2.1.使用命令 git add <file>,注意,可反复多次使用,添加多个文件;
  2.2.使用命令 git commit -m <message>,完成。

3.时光穿梭机

3.1.版本回退

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

我们来做几次修改和提交。

  • 1.写了一个readeMe的md文件
  • 2.add git简介
  • 3.add 补充git简介
$ git log  // 查看提交历史,以便确定要回退到哪个版本

git log 命令显示的是从最近到最远的提交日志,我们可以看到三次提交。如果嫌输出信息太多,看得眼花缭乱的,可以利用下面的命令:

$ git log --pretty=oneline

需要友情提示的是,你看到的一大串类似 35f2188 的是 commit id(版本号),它不是一个递增的数组,而是一个SHA1计算出来的一个非常大的数字,用十六进制表示,避免多人在同一个版本库中操作的时候出现冲突。

每提交一个新版本,实际上GIt就会把他们自动串成一条时间线。如果利用可视化工具查看Git提交历史,就可以更清楚地看到提交历史的时间线: 启动时光穿梭机,准备把readeMe.md回退到上一个版本(在Git中,用HEAD表示当前版本,上一个版本是HEAD^,上上一个版本是HEAD^^,往上100个版本可以写成HEAD~100)

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

再次利用 git log 来查看提交历史,发现最新的那个版本 add 补充git简介已经看不到了!好比你从21世纪坐时光穿梭机来到了19世纪,想再回去已经回不去了,肿么办?办法还是有的,只要上面的命令行窗口还没有关掉,你就可以找到那个提交的commit id,于是就可以指定回到未来的某个版本:

$ git reset --hard 35f2188  // 再次回到未来的某个版本

Git 的版本回退速度非常快,因为在Git内部有个指向当前版本的HEAD指针,当你回退到指定版本的时候,Git仅仅是把HEAD从指向 add 补充git简介:


改为指向add git简介:


然后顺便把工作区的文件更新了。所以你让HEAD指向哪个版本号,你就把当前版本定位在哪。

现在,你回退到了某个版本,关掉了电脑,第二天早上就后悔了,想恢复到新版本怎么办?找不到新版本的commit id怎么办?

在Git中,总是有后悔药可以吃的。当你用$ git reset --hard HEAD^回退到add git简介版本时,再想恢复到add 补充git简介,就必须找到add 补充git简介的commit id。Git提供了一个命令git reflog用来记录你的每一次命令:

$ git reflog  // 查看历史命令,以便要回到未来的哪个版本

终于舒了口气,从输出可知,add 补充git简介的commit id是35f2188,现在,你又可以乘坐时光机回到未来了。

$ git reset --hard 35f2188  // 再次回到未来的某个版本(上面已回到该版本)

注意点:git revert 和 git reset 的操作区别。git revert 不是回滚,而是往前进,只不过内容回到过去。
如果已经有A -> B -> C,想回到B:
方法一:reset到B,丢失C:A -> B。
方法二:再提交一个revert反向修改,变成B:A -> B -> C -> B。C还在,但是两个B是重复的
看你的需求,也许C就是瞎提交错了(比如把密码提交上去了),必须reset
如果C就是修改,现在又要改回来,将来可能再改成C,那你就revert。

小结:

1.HEAD指向的版本就是当前版本,因此,Git允许我们在版本的历史之间穿梭,使用命令git reset --hard commit_id;
2.穿梭前,用git log可以查看提交历史,以便确定要回退到哪个版本;
3.要重返未来,用git reflog查看命令历史,以便确定要回到未来的哪个版本。

3.2.工作区和暂存区

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

工作区(Working Directory)

就是你在电脑里能够看到的目录,比如我的git-learn文件夹就是一个工作区:

版本库(Repository)

在工作区中有一个隐藏目录.git,这个不算工作区,而是Git的版本库。Git的版本库里面存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master,以及指向master的指针叫HEAD。


前面讲了我们把文件往Git版本库里面添加的时候,是分两步进行的:

  • 第一步是用 git add 把文件添加进去,实际上就是把文件修改添加到暂存区;
  • 第二步是用 git commit 提交修改,实际上就是把暂存区所有的内容提交到当前分支。

可以简单理解为,需要提交的文件修改通通都放到了暂存区,然后一次性提交暂存区的所有修改。 Git很清楚地告诉我们,readeMe.md被修改了,而LICENSE还从来没有被添加过,所以它的状态是Untracked。现在使用添加命令再来查看状态:

现在,暂存区的状态就变成这样了:


所以,git add 命令实际上就是把要提交的所有修改放到暂存区(Stage),然后执行 git commit 就可以一次性把暂存区的所有修改一次性提交到分支上。

一旦提交后,如果你又没有对工作区做任何修改,那么工作区就是“干净”的。

现在版本库变成了这样,暂存区就没有任何内容了:

$git diff  // 是工作区(work dict)和暂存区(stage)的比较
$git diff --cached  // 是暂存区(stage)和分支(master)的比较

小结:

版本库(Repository):暂存区(Stage)+ 分支(master)。暂存区是Git非常重要的概念。

3.3.管理修改

现在,假定你已经完全掌握了暂存区的概念。下面,我们要讨论的就是,为什么Git比其他版本控制系统设计得优秀,因为Git跟踪并管理的是修改,而非文件。

你会问,什么是修改?比如你新增了一行,这就是一个修改,删除了一行,也是一个修改,更改了某些字符,也是一个修改,删了一些又加了一些,也是一个修改,甚至创建一个新文件,也算一个修改。

为什么说Git管理的是修改,而不是文件呢?我们还是做实验。第一步,对readMe.md做一个修改,第一次加一行内容:Git追踪改变,然后 git add readeMe.md。第二次修改这行内容:Git追踪文件的改变,然后直接 git commit -m 'git 追踪改变'。 咦,怎么第二次的修改没有被提交?

别激动,我们回顾一下操作过程:

第一次修改 -> git add -> 第二次修改 -> git commit

前面我们讲过了,Git管理的是修改,当你用 git add 命令后,在工作区的第一次修改被放到了暂存区,准备提交,但是,在工作区的第二次修改并没有放入到暂存区,所以,git commit 只负责把暂存区的修改提交了,也就是第一次的修改被提交了,第二次的修改不会被提交。

HEAD指向的是版本库中的当前(最新)版本 而后面的文件指的是当前工作区中的文件。

$ git diff HEAD -- readeMe.md  // 查看工作区中的文件与版本库中文件的差异 

提交后,可以用以上命令来查看工作区和版本库里面最新版本的区别。“+”代表工作区新增加的内容,“-”代表版本库新删除的内容。 可见,第二次修改确实没有被提交。

那么如何提交第二次的修改呢?你可以继续git add 再 git commit,也可以别着急提交第一次的修改,先 git add 第二次修改,再 git commit,就相当于把两次修改合并后一块提交了。 第一次修改 -> git add -> 第二次修改 -> git add -> git commit

小结:

总而言之:每次修改,都必须要先使用添加命令 git add ,接着使用提交命令 git commit 。

3.4.撤销修改

自然,你是不会犯错的。不过现在是凌晨1点,你正在赶一份工作报告,你在readeMe.md中添加了一行:

我愚蠢的老板仍然喜欢SVN

在你准备提交前,一杯浓茶起了作用,你猛然发现了愚蠢的老板可能会让你丢到这个月的奖金!既然错误发现的很及时,就可以很容易得纠正它。你可以手动删掉最后一行,手动把文件恢复到上一个版本的状态。还可以用下面的命令,如果用 git status 查看一下Git会告诉你:

$ git checkout --file  // 丢弃工作区的修改

文件果然又复原了。该命令的意思就是把readeMe.md文件在工作区的修改全部撤销,这里有两种情况:

  • 1.readMe.md自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
  • 2.readMe.md已经添加到暂存区后又作了修改,现在,撤销修改就回到添加到暂存区后的状态。

总之,就是让这个文件回到最近一次git commit或git add时候的状态。

现在假定是凌晨2点,你不但写了一些胡话,还git add 到了暂存区。庆幸的是,在commit之前,你发现了这个问题,用git status查看一下,修改只是添加到了暂存区,还没有提交: Git同样告诉我们:

$ git reset HEAD <file>  // 把暂存区的修改撤销掉(unstage),重新放回工作区

git reset 命令既可以回退版本,也可以把暂存区的修改回退到工作区。当我们用HEAD时,表示最新的版本。

再用git status查看一下,现在暂存区是干净的(Changes not staged for commit),工作区有修改: 还记得如何丢弃工作区的修改吗? 整个世界终于清静了!

Git有不少命令中有“-”,有时候是“-”,有时候是“--”?
linux的命令中,"-" 后面跟的是单个字母的参数,如-m, "--"后面跟的是多个字母的参数,如-- readMe.md

大猪我把这三个区域比作三个不同的时空,而且这三个时空彼此之间可以通过一定的指令来回穿梭(转化)关于转化的关系参考下图:

小结:

1.当你改乱了工作区某个文件的内容,想直接撤销工作区修改时,可以使用命令 git checkout -- <file> 
2.不但改乱了工作区某个文件的内容,还添加到了暂存区时,想撤销修改,操作如下:
    分两步:① 先 git reset HEAD <file> ,②接着使用命令 git checkout -- <file> 
3.不但改乱了工作区某个文件的内容,还添加到了暂存区时,并且还提交 git commit 了。
    想撤销本次提交,只能使用命令 git reset --hard commit-id ,直接回退到上一个版本;前提是没有推送到远程库。
4.不但改乱了工作区某个文件的内容,还添加到了暂存区时,并且还提交 git commit 了,想撤销本次提交(就算推送到远程仓库了)。
    分两步:① git reset --hard HEAD^ ,②接着使用命令 git push origin HEAD -f(全称是force) ,直接回退到上一个版本。
5.给当前分支重新命名:git branch -m <newname>;在指向任何分支时重命名分支:git branch -m <oldname> <newname>【-m 是 --move 的缩写】;推送本地分支并重置上游分支:git push origin -u <newname>;删除远程分支:git push origin --delete <oldname>

3.5.删除文件

在Git中,删除也是一个修改操作。我们先添加一个新文件test.md到Git并且提交: 一般情况下,你通常直接在文件管理中把没用的文件删了,或者rm命令删了:

$ rm test.md  // rm命令删除文件和手动删除一样

这个时候,Git知道你删除了文件,因此,工作区和版本库就不一致了,git status命令查看会立刻告诉你哪些文件被删除了: 现在你有两个选择,一是确实要从版本库中删除该文件,那就用命令git rm删掉,并且git commit :

$ git rm test.md  // 从分支中删除文件

现在,文件就从版本库中被删除了。另外一种情况是删错了,因为版本库里面还有呢,所以可以很轻松地把误删除的文件恢复到最新版本:git checkout -- test.md(如果执行完git commit -m "delete 测试md文件"后就不能用checkout恢复了,得用git reset --hard HEAD^) git checkout其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。

小结:

命令git rm用于删除一个文件。如果一个文件已经被提交到版本库,那么你永远不用担心误删, 但是要小心,你只能恢复文件到最新版本,你会丢失最近一次提交后你修改的内容。

4.分支管理

4.1.创建与合并分支

在版本回退里,你已经知道,,每次提交,Git都会把它们串成一条时间线,这条时间线就是一个分支。截止到目前,只有一条时间线,在Git里,这个分支叫做主分支,即master分支,HEAD严格来说不是指向提交,而是指向master,master才是指向提交的,所以,HEAD指向的就是当前分支。

流程: HEAD --> 主线分支 master --> 最新的提交 commit

一开始的时候,master分支是一条直线,Git用master指向最新的提交,再用HEAD指向master,就能确定当前分支,以及当前分支的提交点:

每次提交,master分支都会向前挪动异步,这样,随着你的不断提交,master分支的线也越来越长。

当我们创建一个新的分支,例如dev时,Git新建了一个指针叫dev,指向master相同的提交,再把HEAD指向dev,就表示当前分支在dev上:

你看,Git创建一个分支很快,因为除了增加一个dev指针,改变了HEAD指向,工作区的文件都没有任何变化!不过,从现在开始,对工作区的修改和提交就是针对dev分支了,比如新提交一次后,dev指针就往前移动一步,而master指针不变:

假如我们在dev上的工作完成了,就可以把dev合并发到master上。Git是如何合并的呢?最简单的办法就是直接把master指向dev分支的当前提交,这样就完成了合并:
所以Git合并分支也很快!就改改指针,工作区内容也不变!合并完分支后,甚至可以删除dev分支,删除dev分支就是把dev指针给删掉,删掉后,我们就剩下了一条master分支:
It is amazing! 简直了!下面开始实战。

这个还是之前的分支,master:

首先,我们创建dev分支,然后切换到dev分支上:

$ git checkout -b dev  // 创建dev分支并切换到该分支上,老版本的git采取的命令。
$ git switch -c dev  // switch命令是2.23版本以后有的,可以用git更新先 2.17.1之前用git update;2.17.1之后用git update-git-for-windows。
$ git branch  // 会列出所有分支,当前分支前面会标有一个*号

然后我们就可以在dev分支上正常提交,比如对readeMe.md文件做个修改,加上一行: 现在,dev分支的工作完成,我们就可以切换回到master分支:

$ git switch master  // 在当前分支上切换回到master分支

切回到master分支后,再查看一个readeMe.md文件,刚才添加的内容不见了!因为那个提交是在dev分支上进行的,而master分支此刻的提交点并没有改变:

现在,我们把dev分支的工作成果合并到master分支上:

$ git merge dev  // 把(dev)指定分支分支合并到(master)当前分支上

合并后,再查看readeMe.md文件的内容,就可以看到,和dev分支的最新提交是完全一样的。

注意到上面的Fast-forward信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master指向dev的当前提交,所以合并速度非常快。当然,也不是每次合并都能Fast-forward,我们后面会讲其他方式的合并。

最终的分支(两个分支上的代码是一模一样的): 合并完成后,我们就可以放心大胆地删除dev分支了:

$ git branch -d dev  // 删除dev分支

删除后,查看分支,现在只剩下master分支了: 因为创建、合并和删除分支非常快,所以Git鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在master分支上工作效果是一样的,但过程更安全。

小结:

1.查看分支:git branch
2.创建分支:git branch 分支名
3.切换分支:git checkout 分支名 | git switch 分支名
4.创建+切换分支:git checkout -b 分支名 | git switch -c 分支名
5.合并某分支到当前分支:git merge 分支名
6.本地删除分支:git branch -d 分支名
7.远程删除分支:git push origin --delete 分支名(简短语法:git push origin :分支名) 
8.同步分支:git fetch -p(prune的意思是精简,修剪。从远程仓库获取最新的代码更新,同时还会清理本地已删除的远程分支)

git log 命令支持的选项的一些其他常见使用命令:

-stat 显示每次更新的文件修改统计信息。
--shortstat 只显示 --stat 中最后的行数修改添加移除统计。
--name-only 仅在提交信息后显示已修改的文件清单。
--name-status 显示新增、修改、删除的文件清单。
--abbrev-commit 仅显示 SHA-1 的前几个字符,而非所有的 40 个字符。
--relative-date 使用较短的相对时间显示(比如,“2 weeks ago”)。
--graph 显示 ASCII 图形表示的分支合并历史。
--pretty 使用其他格式显示历史提交信息。可用的选项包括 oneline,short,full,fuller 和 format(后跟指定格式)。

4.2.分支管理策略

通常,合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,删除了分支后,会丢掉分支信息。
如果要强制禁用Fast forward模式,Git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息。
下面我们实战一下git merge 的 --no-ff 模式,即合并模式不是 fast forword (快进) 模式。
创建分支并提交和之前的流程一样,准备合并dev分支的时候,请注意--no-ff参数,表示禁用Fast forward:

$ git merge --no-ff -m 'merge 非no-ff模式' dev  // 合并dev分支到当前分支,禁用快进模式,会记录分支历史

因为本次合并要创建一个新的commit,所以加上-m参数,把commit描述写进去。 合并后,我们利用git log查看提交记录: 可以看到,不使用Fast forward模式,merge后就像这样:
上面的--graph显示 ASCII 图形表示的分支合并历史也证明了这一点。

分支策略

在实际开发中,我们应该按照几个基本原则进行分支管理:

  • 首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;那在哪干活呢?干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布的时候,dev稳定了,测试下来发现没有bug了,这个时候再把dev分支合并到master分支上,在master分支上发布1.0版本。
  • 你和你的小伙伴们每个人都美滋滋地在dev分支上干活,每个人都有自己的分支,时不时地王dev分支上合并就可以了。

所以,团队合作的分支看起来就像这样:
场景:你可以理解有a、b两个开发者,c是boss。a在michael分支开发,b在bob开发,今天boss给a、b同时分配了一个模块,要求今天完成,于是a勤劳奋进,在10点完成,提交了合并到dev上去了。b比较慢,但是也努力的在16点完成提交合并到dev上去了。图片上就是两个点,同时合并到dev上了,顺利完成boss交代任务啦!

小结:

1.Git分支十分强大,在团队中应该充分应用。
2.合并分支时,加上--no-ff参数就可以用普通模式合并(默认是快进)模式,合并后的历史有分支,能看出来曾经做过合并,而Fast forward合并就看不出来曾经做过合并。
3.命令 git log --graph --pretty=oneline --abbrev-commit  查看分支历史(分支合并图)。

4.3.Bug分支

在Git中,每个bug都可以通过一个新的临时分支来修改,修复好后,合并分支,然后将临时分支删除。

当你接到一个修复一个代号101的bug任务时,你准备创建一个分支issue-101来修复它,但是,等等,当前正在dev上进行的工作还没有提交呢,这可咋整。并不是你不想提交,而是你的工作还只进行到一半,这个功能还没有完全开发好,还没法提交,预计完成还需要1天时间。但是,测试提过来的bug必须要在2小时内修复完成,这该怎么办?

幸好,Git还提供了一个stash(保护现场)功能,可以把当前工作现场“存储”起来,等以后恢复现场后继续工作:

$ git stash  // 存储当前工作现场(保留一个现场)

现在,用git status查看工作区,就是干净的(除非有没有被Git管理的文件),因此可以放心地创建分支来修复bug。

首先需要确定是在哪个分支上修复bug的,假定需要在master分支上修复,就在对应的分支上创建临时分支。修复好bug后,再切换到master分支,并完成合并,最后删除issue-101分支: 太棒了,原计划两个小时的bug修复只花了5分钟!现在,是时候接着回到dev分支干活了!

利用git status查看文件状态,发现工作区是干净的,刚才的工作现场存到哪去了?用git stash list命令看看:

$ git stash list  // 列出现场

工作现场还在,Git把stash内容存在某个地方了,但是需要恢复一下,有两个办法:

  • 一是用git stash apply恢复,但是恢复后,stash内容并不删除,你需要用git stash drop来删除;
  • 另一种方式是用git stash pop,恢复的同时把stash内容也删了。
$ git stash pop  // 恢复的同时也把stash内容也删了(一次stash)
$ git stash apply stash@{索引值}  // 恢复到指定的stash

在master分支上修复了bug后,我们要想一想,dev分支是早期从master分支分出来的,所以,这个bug其实在当前dev分支上也存在。那怎么在dev分支上修复同样的bug?重复操作一次,提交不就行了?

有木有更简单的方法?有!

同样的bug,要在dev上修复,我们只需要把caabc39 fix bug 101这个提交所做的修改“复制”到dev分支。注意:我们只想复制caabc39 fix bug 101这个提交所做的修改,并不是把整个master分支merge过来。 为了方便操作,Git专门提供了一个cherry-pick命令,让我们能复制一个特定的提交到当前分支:

$ git cherry-pick  // 复制一个特定的提交到当前分支

Git自动给dev分支做了一次提交,注意这次提交的commit是bbc3834,它并不同于master的caabc39,因为这两个commit只是改动相同,但确实是两个不同的commit。用git cherry-pick,我们就不需要在dev分支上手动再把修bug的过程重复一遍。

有些聪明的童鞋会想了,既然可以在master分支上修复bug后,在dev分支上可以“重放”这个修复过程,那么直接在dev分支上修复bug,然后在master分支上“重放”行不行?当然可以,不过你仍然需要git stash命令保存现场,才能从dev分支切换到master分支。

小结:

1.修复bug时,我们会创建新的临时分支进行修复,修复完和然后合并,最后删除;
2.当手头工作没有完成时,先把工作现场贮藏一下通过命令 git stash,然后去修复bug,修复后,再 git stash pop,回到工作现场;
3.在master分支上修复的bug,想要合并到当前dev分支,可以用 git cherry-pick <commit-id>命令,把bug提交的修改“复制”到当前分支,避免重复劳动。

4.4.Feature分支

在软件开发中,总有无穷无尽的新功能要不断添加进来。添加一个新功能时,你肯定不会希望把主分支上的代码搞乱了,所以,每天就一个新功能,最好新建一个feature分支,在上面开发,完成后,合并,最后,删除该feature分支。

现在,你终于接到了一个新任务:开发代号为Vulcan(瓦肯星(Vulcan)是《星际迷航》中星际联邦中最重要的智慧种族之一——瓦肯人的母星。)的新功能,该功能计划用于下一代星际飞船。于是准备开发: 好了功能完成了,现在切回到master分支,准备合并,一些顺利的话,合并然后删除,但是!就在此刻,接到上级命令,因经费不足,新功能必须取消!虽然白干了,但是这个包含机密资料的分支还是必须就地销毁: 销毁失败。Git友情提醒,feature-vulcan分支还没有被合并,如果删除,将丢失掉修改,如果要强行删除,需要使用大写的—D参数。好吧,现在我们只能强行删除了:

$ git branch -D 分支名  // 强行删除一个分支

终于删除成功!

小结:

1.开发一个新功能,最后新建一个feature分支;
2.如果要丢弃一个没有被合并过的分支,可以通过 git branch -D 分支名 强行删除

删除分支 git branch -d与git branch -D的区别

  • git branch -d 会在删除前检查merge状态(其与上游分支或者与head)。
  • git branch -D 是git branch --delete --force的简写,它会直接删除。

5.远程仓库

5.1.远程仓库简介

到目前为止,我们已经掌握了如何在Git仓库里对一个文件进行时光穿梭,你再也不用担心文件备份或者丢失的问题了。本章开始介绍Git的杀手级功能之一:远程仓库。

Git是分布式版本控制系统,同一个Git仓库,可以分布到不同的机器上。怎么分布呢?最早,肯定只有一台机器有一个原始的版本库,此后,别的机器也可以“克隆”这个原始的版本库,而且每台机器的版本库其实都是一样的,并没有主次之分。

你肯定会想,至少需要两台电脑才可以玩远程不是?但是我只有一台电脑,怎么玩?其实一台电脑上也是可以克隆多个版本库的,只要不在同一个目录下。不过,现实生活中是不会有人这么傻的在一台电脑上搞几个远程库玩,因为一台电脑上搞几个远程库完全没有意义,而且硬盘挂了会导致所有库都挂掉。

实际情况往往是这样,找一台电脑充当服务器的角色,每天24小时开机,其他每个人都从这个“服务器”仓库克隆一份到自己的电脑上,并且各自把各自的提交推送到服务器仓库里,也从服务器仓库中拉取别人的提交。

完全可以自己搭建一台运行Git的服务器,不过现阶段,为了学Git先搭个服务器绝对是小题大作。好在这个世界上有个叫GitHub的神奇的网站,从名字就可以看出,这个网站就是提供Git仓库托管服务的,所以,只要注册一个GitHub账号,就可以免费获得Git远程仓库。

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

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

你需要把邮件地址换成你自己的邮件地址,然后一路回车,使用默认值即可,由于这个Key也不是用于军事目的,所以也无需设置密码。

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

第2步:登陆GitHub,打开“Account settings”,“SSH Keys”页面,然后,点“Add SSH Key”,填上任意Title,在Key文本框里粘贴id_rsa.pub文件的内容,点“Add Key”,你就可以看到已经添加的Key了。

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

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

最后友情提示,在GitHub上免费托管的Git仓库,任何人都可以看到喔(但只有你自己才能改)。所以,不要把敏感信息放进去。

小结:

“有了远程仓库,妈妈再也不用担心我的硬盘了。”——Git点读机

5.2.本地关联远程仓库

现在的情景是,你已经在本地创建了一个Git仓库后,又想在Github创建一个Git仓库,并且让这两个参加进行远程同步,这样,GitHub上的仓库既可以作为备份,又可以让其他人通过该仓库来协作,真是一举多得。

首先,登陆GitHub,然后,在右上角找到“your Repositories”按钮,创建一个新的仓库,如下: 在Repository name填入git-learn,其他保持默认设置,点击“Create repository”按钮,就成功地创建了一个新的Git仓库: 目前,在GitHub上的这个git-learn仓库还是空的,GitHub告诉我们,可以从这个仓库克隆出新的仓库,也可以把一个已有的本地仓库与之关联,然后,把本地仓库的内容推送到GitHub仓库。

现在,我们根据GitHub的提示,在本地的git-learn仓库下运行命令:

$ git remote add origin git@github.com:shiwanjun1994/git-learn.git

添加后,远程库的名字就是origin,这是Git默认的叫法,也可以改成别的,但是origin这个名字一块就知道是远程库。

下一步,就可以把本地库所有的内容推送到远程库上:

$ git push -u origin master

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

由于远程是空的,我们第一次推送master分支时,加上了-u参数,Git不但会把本地的master分支上的内容推送到远程新的master分支,还会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时就可以简化命令。 推送成功后,刷新GitHub页面,可以看到远程库的内容和本地一模一样: 从现在起,只要本地作了提交,就可以通过命令:

$ git push origin master

把本地master分支的最新修改推送至GitHub,现在,你就拥有了真正的分布式版本库!

SSH警告

当你第一次使用Git的clone或者push命令连接GitHub时,会得到一个警告:

The authenticity of host 'github.com (xx.xx.xx.xx)' can't be established.
RSA key fingerprint is xx.xx.xx.xx.xx.
Are you sure you want to continue connecting (yes/no)?

这是因为Git使用SSH连接,而SSH连接在第一次验证GitHub服务器的Key时,需要你确认GitHub的Key的指纹信息是否真的来自GitHub的服务器,输入yes回车即可。

Git会输出一个警告,告诉你已经把GitHub的Key添加到本机的一个信任列表里了:

Warning: Permanently added 'github.com' (RSA) to the list of known hosts.

这个警告只会出现一次,后面的操作就不会有任何警告了。

如果你实在担心有人冒充GitHub服务器,输入yes前可以对照GitHub的RSA Key的指纹信息是否与SSH连接给出的一致。

小结:

1.要关联一个远程库,使用命令 git remote add origin git@github.com:Owner-name/Repo-name.gitOwner-name 表示 github的用户名,Repo-name 表示 仓库名);本地仓库名和远程仓库名要一致;
2.关联后,使用命令 git push -u origin master  第一次推送master分支的所有内容;
3.此后,每次本地提交后,只要有必要,你想往远程仓库提交代码,就可以使用命令 git push origin master  推送最新修改。

分布式版本系统的最大好处之一是在本地工作完全不需要考虑远程库的存在,也就是有没有联网都可以正常工作,而SVN在没有联网的时候是拒绝干活的!当有网络的时候,再把本地提交推送一下就完成了同步,真是太方便了!

5.3.从远程库克隆

上个小节我们讲了先有本地库,后有远程库的时候,如何关联远程库。现在,假设我们从零开发,那么最好的方式是先创建远程库,然后,从远程库克隆。

首先,登录我们自己的GitHub,比如你看到大猪的这个仓库很不错,想着下载下来学习: 点击进入这个仓库,跟着下面步骤下载这个仓库里面的代码: 利用以下命令:

$ git clone git@github.com:shiwanjun1994/javascript-learn.git  // 克隆一个本地库

git clone的时候出现了这个问题,关闭防火墙就可以了:

Please make sure you have the correct access rights and the repository exists.

然后进入javascript-learn这个文件目录看看,你下载的文件都有了: 如果有多个人协作开发,那么每个人各自从远程克隆一份就可以了。

你也许还注意到,GitHub给出的地址不止一个,还可以用https开头的这样的地址。实际上,Git支持多种协议,默认的git://使用ssh,但也可以使用https等其他协议。

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

小结:

1.要克隆一个仓库,首先必须知道仓库的地址,然后使用git clone命令克隆;
2.Git支持多种协议,包括https,但ssh协议速度最快。

6.多人协作

当你从远程仓库克隆时,实际上Git就把本地的master分支和远程的master分支对应起来了,并且,远程仓库的默认名称就是origin。

$ git remote  // 查看远程仓库的信息
$ git remote -v  // 更详细地显示远程仓库的信息

上面显示了可以抓取和推送的origin的地址,如果没有推送权限,就看不到push的地址。

6.1.推送分支

就是把该分支上的所有本地提交推送至远程仓库,推送时,要指定本地分支,这样,Git就会把该分支推送到远程库对应的远程分支上:

$ git push origin master  // 把本地master分支上的所有修改推送到远程库master上(第二次修改)

但是,并不是一定要把本地分支往远程推送,那么,哪些分支需要推送,哪些不需要呢?

  • master分支是主分支,因此要时刻与远程同步;
  • dev分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;
  • bug分支只用于在本地修复bug,就没必要推到远程了,除非老板要看看你每周到底修复了几个bug;
  • feature分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。

总之,就是在Git中,分支完全可以在本地自己藏着玩,是否推送,视你的心情而定!

6.2.抓取分支

多人协作时,大家都会往master和dev分支上推送各自的修改。

现在,模拟一个你的小伙伴,可以在另一台电脑(注意要把SSH Key添加到GitHub)或者同一台电脑的另一个目录下克隆。

当你的小伙伴从远程库clone时,默认情况下,你的小伙伴只能看到本地的master分支。不信可以用git branch命令看看。

现在,你的小伙伴要在dev分支上开发,就必须创建远程origin的dev分支(前提是GitHub要存在远程分支dev)到本地,于是他用这个命令创建本地dev分支:

$ git switch -c dev origin/dev  // 创建本地dev分支和远程仓库的dev分支

如果拉取远程库的代码出现这个问题:

$ git pull
There is no tracking information for the current branch.
Please specify which branch you want to merge with.
See git-pull(1) for details.

    git pull <remote> <branch>

If you wish to set tracking information for this branch you can do so with:

    git branch --set-upstream-to=origin/<branch> dev
  • There is no tracking information for the current branch 表示:没有当前分支的跟踪信息。
  • Please specify which branch you want to merge with 表示:请指定要与哪个分支合并。

说明没有指定本地dev分支与远程origin/dev分支的链接,根据提示,设置dev和origin/dev的链接:

$ git branch --set-upstream-to=origin/dev dev

因此,多人协作的工作模式通常是这样:

1.首先,可以试图用git push origin 分支名 推送自己的修改;
2.如果推送失败,则因为远程分支比你的本地更新,需要先用git pull试图合并;
3.如果合并有冲突,则解决冲突,并在本地提交;
4.没有冲突或者解决掉冲突后,再用git push origin 分支名 推送就能成功!
5.如果git pull提示no tracking information,则说明本地分支和远程分支的链接关系没有创建,用命令git branch --set-upstream-to 分支名 origin/分支名。

这就是多人协作的工作模式,一旦熟悉了,就非常简单。

小结:

1.查看远程库信息,使用 git remote -v;
2.本地新建的分支如果不推送到远程,对其他人就是不可见的;
3.从本地推送分支,使用 git push origin 分支名,如果推送失败,先用 git pull 抓取远程分支的新提交;
4.在本地创建和远程分支对应的分支,使用 git switch -c 分支名 origin/分支名,本地和远程分支的名称要一致;
5.建立本地分支和远程分支的关联,使用 git branch --set-upstream 分支名 origin/分支名
6.从远程抓取分支,使用 git pull,如果有冲突,要先处理冲突。

7.Fork别人的项目

fork 就是复制代码库。fork一个代码库之后,你就可以任意地尝试去修改代码,而不会影响到源项目。fork通常用在你打算修改别人的项目,或者使用别人的项目作为实现自己想法的开端。

7.1.修改别人的项目

修改别人项目的一个很好的例子就是修复 bug。当你发现一个 bug 时可以通过 issue 来记录,但是更好的方式是:

  • 1.拷贝那个项目
  • 2.将发现的 bug 进行修复
  • 3.提交一个 pull request 给项目的拥有者

如果项目的拥有者认同你做出的修改,他们可能会将你修复的内容加入原来的项目

7.2.使用别人的项目作为实现自己想法的开端

开源的核心是我们通过分享代码来创建更好,更可靠的软件。

通过 fork 其他人的项目来创建一个公开的项目时,你需要确保包含一个许可证文件,来说明你希望项目与其他人共享的方式。

关于开源的更多信息,特别是如何创建并维护一个开源项目,我们创建了开源指南,在创建和维护你的开源项目的仓库时,良好的实践这些建议,将会帮助你创建一个健康的开源社区。

廖老师的Git学习的官方仓库michaelliao/learngit、你在GitHub上克隆的仓库shiwanjun1994/learngit,以及你自己克隆到本地电脑的仓库,他们的关系就像下图显示的那样:
如果你想修复廖老师的learngit的一个bug,或者新增一个功能,立刻就可以开始干活,干完后,往自己的仓库推送。

fork一个仓库: 点击即可,然后刷新,进入到自己的GitHub主页,发现多了以下一个仓库: clone完代码后,参照作者要求格式在本地做好修改后,推送至远程(你自己的远程仓库)。 在对应的文件夹下写点自己的Git学习心得: 最后推送至远程仓库:

如果你希望廖雪峰老师能够接受你的修改,你就可以在GitHub上发起一个 pull request。当然,对方是否接受你的pull request就是另外一回事了。 让你留下点什么评论,感觉没啥好写的,你可以写感谢廖老师之类的话: 可以看到现在,2020年9月24日17:33:35,9月份总共有6个(我之前是5个request): 好了现在等着廖老师是否接受了: 有的话,上面的提交信息就会变成:add 大猪的Git学习心得。

过了3天,廖老师接受了:

疑惑解释

有三个仓库,local(本地的),origin(远程你自己的)和upstream(远程别人的)。

  • local 和 origin 要同步,就是采用之前讲的 git pull 和 git push;
  • local 和 upstream 要同步,就要设置上游(git remote add upstream <upstream地址>),之后 git pull upstream master 拉下来同步;
  • origin 和 upstream 要同步,可以选择去 github 操作,去 origin 仓库向 upstream 发起 pull request,但是 head 设置为 upstream,base 设置为 origin,这相当于反向 pull request,可以让你 fork 的仓库与原仓库保持同步。

小结:

1.GitHub上,可以任意Fork开源仓库;
2.自己拥有Fork后的仓库的读写权限;
3.可以推送pull request给官方仓库来贡献代码。

好了,至此大猪的Git教程就做到这里了。如果你学了Git后,工作效率大增,有更多的空闲时间健身看电影,那我的教学目标就达到了。谢谢观看!

常见场景
1、git 恢复误删的远程分支
第一步:查看分支记录
git reflog --date=iso // 引用log,记录HEAD在各个分支上的移动轨迹。选项 `--date=iso`,表示以标准时间格式展示。
第二步:切分支
git switch -c <branch-name> <SHA> // 请将 `<branch-name>` 替换为您要恢复的分支名称,将 `<SHA>` 替换为您在第一步中找到的 SHA 标识符,也就是 commitid。
第三步:推分支
git push -f origin recovery_branch_name

读后感觉有收获,可以支付宝请大猪来杯咖啡: