[TOC]

0x00 三步骤四个区五状态

描述:下面所有讨论我们都假设只使用一个分支,也就是主分支master的情况,虽然这种作法并不符合git规范,但是现实情况中绝大部分用户是直接在master分支上进行工作的,所以在这里我们不去引入更加复杂的各种分支的情况,也不涉及标签tag的操作,只讲在最简单的主分支上如何回退。

Git 是一个分布式的版本控制工具,因此远程和本地可以视为两个独立的 Git 仓库,下图是一张经典的 Git 中的数据流与存储级别的介绍,其中储存级别主要包含几部分:

  • 上图是一张经典的 Git 中的数据流与存储级别的介绍,其中储存级别主要包含几部分:
    WeiyiGeek.工作流程

    WeiyiGeek.工作流程

注意:

  • 工作区有一个隐藏目录.git,这个不算工作区而是Git的版本库(Repository):。

正常情况下我们的工作流就是3个步骤

1
2
3
git add .  #把所有文件从工作区放入暂存区
git commit -m "comment" #把所有文件从暂存区提交进本地仓库;(提交后工作区和暂存区都为空)
git push #把所有文件从本地仓库推送进远程仓库。


四个区和其他版本控制管理工具SVN的一个不同之处,多引入了一个暂存区(Stage)的概念

  • 工作区(Working Area) :
    • 就是你在电脑里能看到的目录,比如我的learngit文件夹就是一个工作区
  • 暂存区(Stage) :

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

  • 远程仓库(Remote Repository)



五种状态,以上4个区进入每一个区成功之后会产生一个状态,再加上最初始的一个状态,一共是5种状态

1
2
3
4
5
未修改(Origin) #默认
已修改(Modified) #常见
已暂存(Staged)
已提交(Committed)
已推送(Pushed)


检查状态与对比

如何查看我们进行到上面哪一个区,哪一个状态?
答:git status告诉我们,将要被提交的修改包括readme.txt,下一步就可以放心地提交了;

1
2
3
4
5
6
7
8
9
$ git status #查看当前状态
$ git add .
$ git commit -m "add distributed"
# [master ea34578] add distributed
# 1 file changed, 1 insertion(+), 1 deletion(-)
$ git status #提交后,我们再用git status命令看看仓库的当前状态:
# On branch master
# nothing to commit (working directory clean)
#Git告诉我们当前没有需要提交的修改,而且,工作目录是干净(working directory clean)的。

WeiyiGeek.git status

WeiyiGeek.git status


采用git diff可以查看不同区里面的提交的数据,告诉我们更改了什么内容,检查修改的二级命令都相同都是diff,只是参数有所不同

1
2
3
4
5
6
#已修改,未暂存(ctrl+s保存)
git diff
#已暂存,未提交 (git add .)
git diff --cached #看到暂存区和本地仓库之间的差异
#已提交,未推送 (git commit)
git diff master origin/master #master就是你的本地仓库,而origin/master就是你的远程仓库,master是主分支的意思,而后者代表了远程仓库主分支

WeiyiGeek.git diff

WeiyiGeek.git diff

总结:

  • 要随时掌握工作区的状态,使用git status命令。git status告诉你有文件被修改过,用git diff可以查看修改内容。


0x01 撤销与丢弃

撤销修改

了解清楚如何检查各种修改之后,我们开始尝试各种撤销操作。

1
2
3
4
5
6
7
#(1)已修改,未暂存:只是在编辑器里修改了文件,但还没有执行git add .
#一对反义词 git add .的反义词是git checkout .。做完修改之后,
git checkout . #在执行完git checkout .之后,修改已被撤销,git diff没有任何内容了。
#或者
git reset --hard
# 如果你想向前走一步,让修改进入暂存区,就执行git add .
# 如果你想向后退一步,撤销刚才的修改,就执行git checkout .
1
2
3
4
5
6
#(2)已暂存,未提交:已经执行了git add .,但还没有执行git commit -m "comment"
git reset #只是把修改退回到了git add .之前的状态,也就是说文件本身还处于已修改未暂存状态,你如果想退回未修改状态,还需要执行git checkout .
git checkout .
#或者
git reset --hard
#以上两个步骤都可以用同一个命令git reset --hard来完成
1
2
#(3)已提交,未推送 : 执行了git add .,又执行了git commit
git reset --hard origin/master #origin/master代表远程仓库,既然你已经污染了你的本地仓库,那么就从远程仓库把代码取回来吧。
1
2
3
#(4)已推送: 你既git add了又git commit了,并且还git push了这时你的代码已经进入远程仓库
git reset --hard HEAD^ #由于你的本地仓库和远程仓库是等价的,你只需要先恢复本地仓库,再强制push到远程仓库就好了:
git push -f

_总结_:

  • 以上4种状态的撤销我们都用到了同一个命令git reset –hard,前2种状态的用法甚至完全一样,所以只要掌握了git reset –hard这个命令的用法,从此你再也不用担心提交错误了。


管理修改

为什么Git比其他版本控制系统设计得优秀,因为Git跟踪并管理的是修改,而非文件。
你会问,什么是修改?
答:比如你新增了一行,这就是一个修改,删除了一行,也是一个修改,更改了某些字符,也是一个修改,删了一些又加了一些,也是一个修改,甚至创建一个新文件,也算一个修改。

示例:

1
2
3
4
5
6
7
8
$ echo "change 1 id" > readme.txt #第一次修改
$ git add readme.txt #然后添加到暂存区
$ git status #查看工作区状态
# modified: readme.txt
$ echo "change 2 id" >> readme.txt #第2次修改
$ git commit -m "git tracks changes" #提交后,再看看状态
$ git status
# modified: readme.txt

咦,怎么第二次的修改没有被提交?
答:我们回顾一下操作过程:第一次修改 -> git add -> 第二次修改 -> git commit,Git管理的是修改,当你用git add命令后,在工作区的第一次修改被放入暂存区,准备提交,但是在工作区的第二次修改并没有放入暂存区,所以git commit只负责把暂存区的修改提交了,也就是第一次的修改被提交了,第二次的修改不会被提交

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

1
2
#每次修改,如果不add到暂存区,那就不会加入到commit中提交到本地仓库
第一次修改 -> git add -> 第二次修改 -> git add -> git commit -> git status #这时候工作区便为空了


丢弃修改

比如:您在您的项目中修改了文件并添加文件到了暂存区,却发现错误需要进行更正;
既然错误发现得很及时,就可以很容易地纠正它,你可以删掉最后一行手动把文件恢复到上一个版本的状态。

1
2
3
4
5
6
#你也可以丢弃工作区的修改
git checkout -- file

$ git checkout -- readme.txt #把readme.txt文件在工作区的修改全部撤销,这里有两种情况:
* 一种是readme.txt自修改后还没有被放到暂存区,现在撤销修改就回到和版本库一模一样的状态;
* 一种是readme.txt已经添加到暂存区后,又作了修改,现在撤销修改就回到添加到暂存区后的状态(即没有更改)。

总之,就是让这个文件回到最近一次git commit或git add时的状态。
git checkout -- file命令中的--很重要,没有–,就变成了“切换到另一个分支”的命令,我们在后面的分支管理中会再次遇到git checkout命令

1
2
3
4
#用命令git reset HEAD file也可以把暂存区的修改撤销掉(unstage),重新放回工作区
$ git reset HEAD readme.txt
Unstaged changes after reset:
M readme.txt

_总结_:

  • 场景1:当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令git checkout – file。
  • 场景2:当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步,第一步用命令git reset HEAD file,就回到了场景1,第二步按场景1操作。
  • 场景3:已经提交了不合适的修改到版本库时,想要撤销本次提交,参考版本回退一节,不过前提是没有推送到远程库。


时光机穿梭

描述:当然了在实际工作中,我们脑子里怎么可能记得一个几千行的文件每次都改了什么内容,不然要版本控制系统干什么。
版本控制系统肯定有某个命令可以告诉我们历史记录即提交说明执行git log你看到的一大串类似3628164...882e1e0的是commit id(版本号),和SVN不一样Git的commit id不是1,2,3……递增的数字,而是一个SHA1计算出来的一个非常大的数字用十六进制表示

1
2
3
$git log --pretty=oneline
c75b5b8afcff97b3c37c4d57d574b0bc868f6f2f (HEAD -> master, origin/master, origin/HEAD) git study
c1ee2c4ec8f7d6a8e9b44f91b5fa59ea3aec13d8 all

为什么commit id需要用这么一大串数字表示呢?
答:因为Git是分布式的版本控制系统,后面我们还要研究多人在同一个版本库里工作,如果大家都用1,2,3……作为版本号,那肯定就冲突了。

每提交一个新版本,实际上Git就会把它们自动串成一条时间线。如果使用可视化工具查看Git历史,就可以更清楚地看到提交历史的时间线;

如果准备把readme.txt回退到上一个版本,也就是“distributed”的那个版本,怎么做呢?
首先,Git必须知道当前版本是哪个版本,在Git中用HEAD表示当前版本,也就是最新的提交3628164…882e1e0(注意我的提交ID和你的肯定不一样),上一个版本就是HEAD^,上上一个版本就是HEAD^^,当然往上100个版本写100个^比较容易数不过来所以写成HEAD~100。
现在,我们要把当前版本“append GPL”回退到上一个版本“distributed”,就可以使用git reset命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#(1)查看需要回退的版本 
git log HEAD^ #上一个
git log HEAD^^ #上上个
git log HEAD^^^ #上三个版本等同于
git log HEAD~3
# commit ddbfbfbf4951747c80e87d980e439ccbd73ab245
# Author: WeiyiGeek
# Date: Mon Jul 15 21:23:05 2019 +0800
# Initial commit


#(2)回退到上一个版本或者回退到指定版本
git reset --hard HEAD^ #同一提交的所有内容都会变回上一个Commit时的状态,这时候回到了以前 (可使用 HEAD~9)
HEAD is now at ea34578 add distributed # distributed 英 [dɪˈstrɪbjuːtɪd] 分布式

还可以继续回退到上一个版本wrote a readme file,不过且慢,然我们用git log再看看现在版本库的状态:
WeiyiGeek.gitlog回退版本查看

WeiyiGeek.gitlog回退版本查看


最新的那个版本append GPL已经看不到了!好比你从21世纪坐时光穿梭机来到了19世纪,想再回去已经回不去了,肿么办?
答:在命令窗口没有关闭的情况下,往上查找 append GPL的commit id是3628164,于是就可以指定回到未来的某个版本

1
2
3
#(3)又回到了未来版本上
$ git reset --hard 3628164 #版本号没必要写全,前7位就可以了,Git会自动去找
# HEAD is now at 3628164 append GPL

Git的版本回退速度非常快,因为Git在内部有个指向当前版本的HEAD指针,当你回退版本的时候,Git仅仅是把HEAD从指向append GPL,改为指向add distributed,然后顺便把工作区的文件更新了;所以你让HEAD指向哪个版本号,你就把当前版本定位在哪

WeiyiGeek.HEAD版本回退

WeiyiGeek.HEAD版本回退


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

答:当你用$git reset --hard HEAD^回退到add distributed版本时,再想恢复到append GPL,就必须找到append GPL的commit id。

1
2
3
4
5
$ git reflog
ea34578 [email protected]{0}: reset: moving to HEAD^
3628164 [email protected]{1}: commit: append GPL #第二行显示append GPL的commit id是3628164,现在你又可以乘坐时光机回到未来了。
ea34578 [email protected]{2}: commit: add distributed
cb926e7 [email protected]{3}: commit (initial): wrote a readme file

_总结_:

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


删除文件

在Git中,删除也是一个修改操作,我们实战一下,先添加一个新文件test.txt到Git并且提交:

1
2
3
4
5
6
7
8
$ git add test.txt
$ git commit -m "add test.txt"
[master 94cdc44] add test.txt
#一般情况下,你通常直接在文件管理器中把没用的文件删了,或者用rm命令删了
$ rm test.txt
#Git知道你删除了文件,因此工作区和版本库就不一致了,git status命令会立刻告诉你哪些文件被删除了:
$ git status
# deleted: test.txt

现在你有两个选择,一是确实要从版本库中删除该文件,那就用命令git rm删掉,并且git commit,现在文件就从版本库中被删除了。

1
2
3
4
$ git rm test.txt  
rm 'test.txt'
$ git rm --cached test.txt #只删除暂存区里的文件
$ git commit -m "remove test.txt"

另一种情况是删错了,因为版本库里还有呢,所以可以很轻松地把误删的文件恢复到最新版本:

1
2
#git checkout其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”
$ git checkout -- test.txt

利用文本过滤方式,这些脚本会在文件签出前(”smudge”)和提交到暂存区前(”clean”)被调用。这些过滤器能够做各种有趣的事。

WeiyiGeek.checkout与add

WeiyiGeek.checkout与add


0x02 分支管理

描述:分支就是科幻电影里面的平行宇宙,当你正在电脑前努力学习Git的时候,另一个你正在另一个平行宇宙里努力学习SVN。

如果两个平行宇宙互不干扰,那对现在的你也没啥影响。不过在某个时间点,两个平行宇宙合并了,结果你既学会了Git又学会了SVN!

WeiyiGeek.漫画图

WeiyiGeek.漫画图

分支在实际中有什么用呢?
假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。

现在有了分支你就创建了一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样既安全,又不影响别人工作。

Git的分支是与众不同的,无论创建、切换和删除分支,Git在1秒钟之内就能完成!无论你的版本库是1个文件还是1万个文件。

专有名词:

  • 跟踪分支:tracking branch
  • 远程跟踪分支: remote tracking branch


创建与合并分支

在版本回退里,每次提交,Git都把它们串成一条时间线,这条时间线就是一个分支。只有一条时间线,在Git里,这个分支叫主分支即master分支

HEAD严格来说不是指向提交,而是指向master才是指向提交的,所以HEAD指向的地址就是当前分支。

  • (1) 一开始的时候 master分支是一条线,Git用master指向最新的提交,再用HEAD指向master,就能确定当前分支以及当前分支的提交点,每次提交,master分支都会向前移动一步,这样,随着你不断提交,master分支的线也越来越长

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

    • Git创建一个分支很快,因为除了增加一个dev指针,改改HEAD的指向,工作区的文件都没有任何变化!
WeiyiGeek.创建分支

WeiyiGeek.创建分支

  • (3) 不过从现在开始,对工作区的修改和提交就是针对dev分支了,比如新提交一次后,dev指针往前移动一步,而master指针不变:
  • (4) 假如我们在dev上的工作完成了,就可以把dev合并到master上。
    • Git怎么合并呢?最简单的方法,就是直接把master指向dev的当前提交,就完成了合并
WeiyiGeek.分支合并

WeiyiGeek.分支合并

  • (5) 所以Git合并分支也很快!就改改指针,工作区内容也不变!
    • 合并完分支后,甚至可以删除dev分支。删除dev分支就是把dev指针给删掉,删掉后,我们就剩下了一条master分支
WeiyiGeek.主分支

WeiyiGeek.主分支

开始实战演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#1.首先,我们创建dev分支,然后切换到dev分支(Branch)
git checkout -b dev #相当于以下两条命令:
$ git branch dev #创建分支
$ git checkout dev #切换分支
#Switched to a new branch 'dev'


#2.查看当前分支指向情况
git branch
* dev
master


#3.然后我们就可以在dev分支上正常提交,比如对readme.txt做个修改然后提交
echo "qweqwe" > readme.txt
git add .
git commit -m "branch test"
# [dev 50aad8f] branch test


#4.现在dev分支的工作完成,我们就可以切换回master分支,查看一个readme.txt文件,刚才添加的内容不见了!
#因为那个提交是在dev分支上,而master分支此刻的提交点并没有变:
$ git checkout master #或者将master换成 -
#Switched to branch 'master'


#5.现在我们把dev分支的工作成果合并到master分支上,合并后再查看readme.txt的内容,就可以看到,和dev分支的最新提交是完全一样的。
git merge dev
# Updating 890adee..50aad8f
# Fast-forward #Git告诉我们这次合并是“快进模式”,也就是直接把master指向dev的当前提交,所以合并速度非常快
# readme.txt | 1 + #Non-fast-forword merge,三方合并把master和分支合并到第三方
# 1 file changed, 1 insertion(+)
# create mode 100644 readme.txt


#6.合并完成后,就可以放心地删除dev分支了,删除后查看branch,就只剩下master分支了:
[email protected]:/mnt/e/githubProject/test$ git branch -D dev
Deleted branch dev (was 50aad8f).
[email protected]:/mnt/e/githubProject/test$ git branch
* master

当然也不是每次合并都能Fast-forward,我们后面会讲其他方式的合并。

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


冲突解决

人生不如意之事十之八九,合并分支往往也不是一帆风顺的。

实际案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#1.准备新的feature1分支,继续我们的新分支开发,修改readme.txt最后一行
$ git checkout -b feature1
$ echo "Creating a new branch is quick AND simple." >> readme.txt

#2.在feature1分支上提交并且切换到master分支
$ git add readme.txt
$ git commit -m "feature1 AND simple"
$ git checkout master #Git还会自动提示我们当前master分支比远程的master分支要超前1个提交。

#3.这时在master分支上把readme.txt文件的最后一行改为下面,即master与feature1分支都更改readme.txt文件
$ echo "Creating a new branch is quick & simple." >> readme.txt
$ git add readme.txt
$ git commit -m "Master & simple"

现在,master分支和feature1分支各自都分别有新的提交,变成了这样:
WeiyiGeek.无法快速合并

WeiyiGeek.无法快速合并

这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,我们试试看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#4.尝试合并feature1
$ git merge feature1
Auto-merging readme.txt #Git告诉我们,readme.txt文件存在冲突,必须手动解决冲突后再提交。
CONFLICT (content): Merge conflict in readme.txt
Automatic merge failed; fix conflicts and then commit the result.
$ git status #可以告诉我们冲突的文件


#5.可以直接查看readme.txt的内容修改后进行提交
Git tracks changes of files.
\<<<<<<< HEAD #Master分支
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
\>>>>>>> feature1 #建立的分支
#Git用<<<<<<<,=======,>>>>>>>标记出不同分支的内容,我们修改如下后保存:
# Creating a new branch is quick AND simple.
$ git add readme.txt
$ git commit -m "conflict fixed"

#6.最后,删除feature1分支工作完成。
$ git branch -d feature1
Deleted branch feature1 (was 75a857c).

现在,master分支和feature1分支变成了下图所示:
WeiyiGeek.主从分支更改同一文件合并冲突解决

WeiyiGeek.主从分支更改同一文件合并冲突解决

用带参数的git log也可以看到分支的合并情况:

1
2
3
4
5
6
7
8
9
$ git log --graph --pretty=oneline --abbrev-commit
* 59bc1cb conflict fixed //冲突解决
|\
| * 75a857c AND simple
* | 400b400 & simple
|/
* fec145a branch test
...
$ git log --oneline --decorate --graph

总结:

  • 当Git无法自动合并分支时,就必须首先解决冲突。解决冲突后,再提交,合并完成。


分支管理策略

描述:通常合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,删除分支后,会丢掉分支信息。如果要强制禁用Fast forward模式,Git就会在merge时生成一个新的commit,这样从分支历史上就可以看出分支信息。

实际案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ git checkout -b dev

#修改readme.txt文件,并提交一个新的commit
$ git add readme.txt
$ git commit -m "add merge"

#现在我们切换回master
$ git checkout master

#准备合并dev分支,请注意--no-ff参数,表示禁用Fast forward(快进)
#因为本次合并要创建一个新的commit,所以加上-m参数,把commit描述写进去。
$ git merge --no-ff -m "merge with no-ff" dev
Merge made by the 'recursive' strategy.
readme.txt | 1 +
1 file changed, 1 insertion(+)


#合并后,我们用git log看看分支历史:
$ git log --graph --pretty=oneline --abbrev-commit # (abbr. 缩写 abbreviation )
* 7825a50 merge with no-ff
|\
| * 6224937 add merge
|/
* 59bc1cb conflict fixed
...

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

  • 首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;
  • 那在哪干活呢?干活都在dev分支上,也就是说dev分支是不稳定的,到某个时候,比如1.0版本发布时再把dev分支合并到master上,再到master分支发布1.0版本;每个人都有自己的分支,时不时地往dev分支上合并就可以了。

WeiyiGeek.git团队合作

_总结_:

  • Git分支十分强大,在团队开发中应该充分应用。
  • 合并分支时加上--no-ff参数就可以用普通模式合并合并后的历史有分支能看出来曾经做过合并,而fast forward合并就看不出来曾经做过合并。


缺陷BUG分支

软件开发中bug就像家常便饭一样。有了bug就需要修复在Git中,由于分支是如此的强大,所以每个bug都可以通过一个新的临时分支来修复,修复后合并分支,然后将临时分支删除。

当你接到一个修复一个代号101的bug的任务时很自然地,你想创建一个分支issue-101来修复它,但是等等,当前正在dev上进行的工作还没有提交:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git status
# On branch dev
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: hello.py
#
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: readme.txt
#

并不是你不想提交,而是工作只进行到一半,还没法提交,预计完成还需1天时间。但是,必须在两个小时内修复该bug,怎么办?

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

1
2
3
$ git stash
Saved working directory and index state WIP on dev: 6224937 add merge
HEAD is now at 6224937 add merge

现在,用git status查看工作区,就是干净的(除非有没有被Git管理的文件),因此可以放心地创建分支来修复bug。
首先确定要在哪个分支上修复bug,假定需要在master分支上修复,就从master创建临时分支:
1
2
3
4
5
6
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.

$ git checkout -b issue-101 #临时分支
Switched to a new branch 'issue-101'

现在修复bug,需要把“Git is free software …”改为“Git is a free software …”,然后提交:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ git add readme.txt
$ git commit -m "fix bug 101"
[issue-101 cc17032] fix bug 101
1 file changed, 1 insertion(+), 1 deletion(-)

#修复完成后,切换到master分支,并完成合并,最后删除issue-101分支:
$ git checkout master
# Switched to branch 'master'
# Your branch is ahead of 'origin/master' by 2 commits.

$ git merge --no-ff -m "merged bug fix 101" issue-101
# Merge made by the 'recursive' strategy.
# readme.txt | 2 +-
# 1 file changed, 1 insertion(+), 1 deletion(-)

$ git branch -d issue-101
# Deleted branch issue-101 (was cc17032).

现在,是时候接着回到dev分支干活了!

1
2
3
4
5
6
7
8
9
10
$ git checkout dev
Switched to branch 'dev'

$ git status
# On branch dev
# nothing to commit (working directory clean)

#查看前面保存的工作现场
$ git stash list
[email protected]{0}: WIP on dev: 6224937 add merge

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

  • (1)用git stash apply恢复但是恢复后,stash内容并不删除,你需要用git stash drop来删除;
  • (2)用git stash pop,恢复的同时把stash内容也删了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ git stash pop
# On branch dev
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: hello.py
#
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: readme.txt
#
# Dropped refs/[email protected]{0} (f624f8e5f082f2df2bed8a4e09c12fd2943bdd40)

$ git stash list #再用git stash list查看,就看不到任何stash内容了

总结:

  • 修复bug时,我们会通过创建新的bug分支进行修复,然后合并,最后删除;
  • 当手头工作没有完成时,先把工作现场git stash一下然后去修复bug,修复后,再git stash pop回到工作现场。


未合并分支

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

现在你终于接到了一个新任务,开发代号为Vulcan的新功能,于是准备开发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ git checkout -b feature-vulcan
$ git add vulcan.py
$ git status
# On branch feature-vulcan
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: vulcan.py

$ git commit -m "add feature vulcan"
# [feature-vulcan 756d4af] add feature vulcan
# 1 file changed, 2 insertions(+)
# create mode 100644 vulcan.py

#切回dev,准备合并;一切顺利的话,feature分支和bug分支是类似的,合并然后删除。
$ git checkout dev
#在此时,接到上级命令,因经费不足,新功能必须取消!虽然白干了但是这个分支还是必须就地销毁:
$ git branch -d feature-vulcan
error: The branch 'feature-vulcan' is not fully merged. #销毁失败
If you are sure you want to delete it, run 'git branch -D feature-vulcan'.
#Git友情提醒,feature-vulcan分支还没有被合并,如果删除,将丢失掉修改,如果要强行删除,需要使用命令git branch -D feature-vulcan。
$ git branch -D feature-vulcan #强行删除


0x03 远程分支关联

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

1
2
3
4
5
6
$ git remote #或者用git remote -v显示更详细的信息:
origin
$ git remote -v #显示了可以抓取和推送的origin的地址。
#如果没有推送权限,就看不到push的地址。
origin [email protected]:michaelliao/learngit.git (fetch)
origin [email protected]:michaelliao/learngit.git (push)

推送分支,就是把该分支上的所有本地提交推送到远程库。推送时要指定本地分支,这样,Git就会把该分支推送到远程库对应的远程分支上:
1
2
3
$ git push origin master
#如果要推送其他分支,比如dev就改成:
$ git push origin dev #远程origin 本地其他分支dev

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

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


抓取分支

多人协作时,大家都会往master和dev分支上推送各自的修改。现在模拟一个你的小伙伴,可以在另一台电脑(注意要把SSH Key添加到GitHub)或者同一台电脑的另一个目录下克隆:

1
2
$ git clone [email protected]:michaelliao/learngit.git
Cloning into 'learngit'...

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

1
2
$ git branch
* master

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

1
2
# 创建远程分支到本地
$ git checkout -b dev origin/dev

现在他就可以在dev上继续修改,然后时不时地把dev分支push到远程:

1
2
3
4
5
6
$ git commit -m "add /usr/bin/env"
[dev 291bea8] add /usr/bin/env
1 file changed, 1 insertion(+)

$ git push origin dev #注意这里需要将本地的dev推送到远程的origin/dev中
fc38031..291bea8

你的小伙伴已经向origin/dev分支推送了他的提交,而碰巧你也对同样的文件作了修改,并试图推送:

1
2
3
4
5
6
7
8
9
10
11
$ git add hello.py
$ git commit -m "add coding: utf-8"
[dev bd6ae48] add coding: utf-8
1 file changed, 1 insertion(+)


$ git push origin dev
To [email protected]:michaelliao/learngit.git
! [rejected] dev -> dev (non-fast-forward)
error: failed to push some refs to
hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')

推送失败,因为你的小伙伴的最新提交和你试图推送的提交有冲突,解决办法也很简单Git已经提示我们,先用git pull把最新的提交从origin/dev抓下来,然后,在本地合并,解决冲突再推送:

但是 git pull 也失败了,原因是没有指定本地dev分支与远程origin/dev分支的关联,根据提示设置dev和origin/dev的链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ git branch --set-upstream dev origin/dev  #设置本地dev分支与远程origin/dev分支进行关联;
Branch dev set up to track remote branch dev from origin.

#然后再进行pull:这回git pull成功,但是合并有冲突,需要手动解决,解决的方法和分支管理中的解决冲突完全一样。
$ git pull
Auto-merging hello.py
CONFLICT (content): Merge conflict in hello.py
Automatic merge failed; fix conflicts and then commit the result.
 

#解决后提交,再push:
$ git commit -m "merge & fix hello.py"
[dev adca45d] merge & fix hello.py

$ git push origin dev
291bea8..adca45d dev -> dev

因此多人协作的工作模式通常是这样,一旦熟悉了就非常简单:

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


总结:

  • 查看远程库信息,使用git remote -v;
  • 本地新建的分支如果不推送到远程,对其他人就是不可见的;
  • 从本地推送分支,使用git push origin branch-name,如果推送失败先用git pull抓取远程的新提交;
  • 在本地创建和远程分支对应的分支,使用git checkout -b branch-name origin/branch-name,本地和远程分支的名称最好一致;
  • 建立本地分支和远程分支的关联,使用git branch –set-upstream branch-name origin/branch-name;
  • 从远程抓取分支,使用git pull,如果有冲突,要先处理冲突,最后在git push origin branch-name推送


分支之间commit

实际案例:

1
2
#把A分支的某一个commit放到B分支上
git checkout <branch-name> && git cherry-pick <commit-id>


0x04 标签管理

描述:发布一个版本时我们通常先在版本库中打一个标签(tag),这样就唯一确定了打标签时刻的版本。将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来,所以标签也是版本库的一个快照

Git的标签虽然是版本库的快照,但其实它就是指向某个commit的指针(跟分支很像对不对?但是分支可以移动,标签不能移动),所以创建和删除标签都是瞬间完成的,所以tag就是一个让人容易记住的有意义的名字,它跟某个commitID绑在一起。

Git有commit,为什么还要引入tag?
“请把上周一的那个版本打包发布,commit号是6a5819e…” / “一串乱七八糟的数字不好找!”
如果换一个办法:“请把上周一的那个版本打包发布,版本号是v1.2” / “好的,按照tag v1.2查找commit就行!”


创建标签

在Git中打标签非常简单,首先切换到需要打标签的分支上:

1
2
3
4
5
6
$ git branch
* dev
master

$ git checkout master
Switched to branch 'master'

然后敲命令git tag <name>就可以打一个新标签:

1
2
3
4
5
$ git tag v1.0  #默认标签是打在最新提交的commit上的。

#可以用命令git tag查看所有标签:
$ git tag
v1.0

有时候如果忘了打标签,比如现在已经是周五了但应该在周一打的标签没有打怎么办?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#方法是找到历史提交的commit id,然后打上就可以了:
$ git log --pretty=oneline --abbrev-commit
6a5819e merged bug fix 101
cc17032 fix bug 101
7825a50 merge with no-ff
6224937 add merge
59bc1cb conflict fixed
400b400 & simple
75a857c AND simple
fec145a branch test
d17efd8 remove test.txt
 
#比方说要对add merge这次提交打标签,它对应的commit id是6224937,敲入命令:
$ git tag v0.9 6224937

#再用命令git tag查看标签:#注意,标签不是按时间顺序列出,而是按字母排序的。
$ git tag
v0.9
v1.0

#可以用git show <tagname>查看标签信息:
$ git show v0.9 #可以看到v0.9确实打在add merge这次提交上。
commit 622493706ab447b6bb37e4e2a2f276a20fed2ab4
Author: Michael Liao <[email protected]>
Date:   Thu Aug 22 11:22:08 2013 +0800
 
    add merge

#还可以创建带有说明的标签,用-a指定标签名,-m指定说明文字:
$ git tag -a v0.1 -m "version 0.1 released" 3628164
 
#用命令git show <tagname>可以看到说明文字:
$ git show v0.1
tag v0.1
Tagger: Michael Liao <[email protected]>
Date:   Mon Aug 26 07:28:11 2013 +0800
 
version 0.1 released
 
commit 3628164fb26d48395383f8f31179f24e0882e1e0
Author: Michael Liao <[email protected]>
Date:   Tue Aug 20 15:11:49 2013 +0800
 
    append GPL

签名采用PGP签名,因此,必须首先安装gpg(GnuPG),如果没有找到gpg,或者没有gpg密钥对就会报错:

1
2
3
gpg: signing failed: secret key not available
error: gpg failed to sign the data
error: unable to sign the tag

用PGP签名的标签是不可伪造的,因为可以验证PGP签名,通用git tag 可以通过-s用私钥签名一个标签:
1
$ git tag -s v0.2 -m "signed version 0.2 released" fec145a

用命令git show 可以看到PGP签名信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ git show v0.2
tag v0.2
Tagger: Michael Liao <[email protected]>
Date:   Mon Aug 26 07:28:33 2013 +0800
 
signed version 0.2 released
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.12 (Darwin)
 
iQEcBAABAgAGBQJSGpMhAAoJEPUxHyDAhBpT4QQIAKeHfR3bo...
-----END PGP SIGNATURE-----
 
commit fec145accd63cdc9ed95a2f557ea0658a2a6537f
Author: Michael Liao <[email protected]>
Date:   Thu Aug 22 10:37:30 2013 +0800
 
    branch test

总结:

  • 命令git tag 用于新建一个标签,默认为HEAD,也可以指定一个commit id,git tag可以查看所有标签。
  • git tag -a -m “blablabla…”可以指定标签信息;
  • git tag -s -m “blablabla…”可以用PGP签名标签;


操作标签

如果标签打错了也可以删除指定的标签,因为创建的标签都只存储在本地,不会自动推送到远程;所以打错的标签可以在本地安全删除。

1
2
$ git tag -d v0.1
Deleted tag 'v0.1' (was e078af9)

如果要推送某个标签到远程,使用命令git push origin <tagname>

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git push origin v1.0
Total 0 (delta 0), reused 0 (delta 0)
To [email protected]:michaelliao/learngit.git
 * [new tag]         v1.0 -> v1.0
 
#或者,一次性推送全部尚未推送到远程的本地标签:
$ git push origin --tags
Counting objects: 1, done.
Writing objects: 100% (1/1), 554 bytes, done.
Total 1 (delta 0), reused 0 (delta 0)
To [email protected]:michaelliao/learngit.git
 * [new tag]         v0.2 -> v0.2
 * [new tag]         v0.9 -> v0.9

如果标签已经推送到远程,要删除远程标签就麻烦一点,操作完成后要想看看是否真的从远程库删除了标签,可以登陆GitHub查看

1
2
3
4
5
6
7
8
#先从本地删除:
$ git tag -d v0.9
Deleted tag 'v0.9' (was 6224937)

#然后,从远程删除-删除命令也是push,但是格式如下:
$ git push origin :refs/tags/v0.9
To [email protected]:michaelliao/learngit.git
 - [deleted]         v0.9

总结:

  • 命令git push origin 可以推送一个本地标签;
  • 命令git push origin –tags 可以推送全部未推送过的本地标签;
  • 命令git tag -d 可以删除一个本地标签;
  • 命令git push origin :refs/tags/可以删除一个远程标签。


0x05 总结

.git 仓库元数据
每一个 git 的代码仓库目录下,都会有一个 .git 的文件夹,其中包含的重要文件包含以下:
文件/文件夹 含义

  • config* 配置文件
  • description 描述仅供 Git Web 程序使用
  • HEAD 当前被检出的分支
  • index 暂存区信息
  • hooks/ 客户端或服务端的钩子脚本(hook scripts)
  • info/ 全局性排除(global exclude)文件,不希望被记录在 .gitignore 文件中的忽略模式(ignored patterns)
  • objects/ 所有数据内容
  • refs/ 数据(分支)的提交对象的指针