Note book & Life share

Git学习笔记(2)

字数统计: 8.4k阅读时长: 35 min
2019/12/27
Git这么好用的东西我不允许没有人用过!
本文接Git学习笔记(1),主要是分支/标签 的内容

本文接Git学习笔记(1),主要是分支/标签 的内容
参考的是廖雪峰的教程
补充文献:Git Rebase

分支管理

分支就是创建一个可合并的平行宇宙。或者说,火影忍者里的影分身,同时互不干涉地做事,然后合并~

创建与合并分支

Git鼓励大量使用分支:

查看分支:git branch

创建分支:git branch

切换分支:git checkout或者git switch

创建+切换分支:git checkout -b或者git switch -c

合并某分支到当前分支:git merge

删除分支:git branch -d

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

一开始的时候,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分支:

实战。

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

1
2
$ git checkout -b dev
Switched to a new branch 'dev'

git checkout命令加上-b参数表示创建并切换,相当于以下两条命令:

1
2
3
$ git branch dev
$ git checkout dev
Switched to branch 'dev'

然后,用git branch命令查看当前分支:

1
2
3
$ git branch
* dev
master

git branch命令会列出所有分支,当前分支前面会标一个*号。

然后,我们就可以在dev分支上正常提交,比如对readme.txt做个修改,加上一行:

1
Creating a new branch is quick.

然后提交:

1
2
3
4
$ git add readme.txt 
$ git commit -m "branch test"
[dev b17d20e] branch test
1 file changed, 1 insertion(+)

现在,dev分支的工作完成,我们就可以切换回master分支:

1
2
$ git checkout master
Switched to branch 'master'

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

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

1
2
3
4
5
$ git merge dev
Updating d46f35e..b17d20e
Fast-forward
readme.txt | 1 +
1 file changed, 1 insertion(+)

git merge命令用于合并指定分支到当前分支。合并后,再查看readme.txt的内容,就可以看到,和dev分支的最新提交是完全一样的。

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

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

合并完成后,就可以放心地删除dev分支了:

1
2
$ git branch -d dev
Deleted branch dev (was b17d20e).

删除后,查看branch,就只剩下master分支了:

1
2
$ git branch
* master

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

switch

我们注意到切换分支使用git checkout,而前面讲过的撤销修改则是git checkout --,同一个命令,有两种作用,确实有点令人迷惑。

实际上,切换分支这个动作,用switch更科学。因此,最新版本的Git提供了新的git switch命令来切换分支:

创建并切换到新的dev分支,可以使用:

1
$ git switch -c dev

直接切换到已有的master分支,可以使用:

1
$ git switch master

使用新的git switch命令,比git checkout要更容易理解。

解决冲突

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

解决冲突就是把Git合并失败的文件手动编辑为我们希望的内容,再提交。

git log --graph命令可以看到分支合并图。

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

准备新的feature1分支,继续我们的新分支开发:

1
2
$ git checkout -b feature1
Switched to a new branch 'feature1'

修改readme.txt最后一行,改为:

1
Creating a new branch is quick AND simple.

feature1分支上提交:

1
2
3
4
5
$ git add readme.txt

$ git commit -m "AND simple"
[feature1 14096d0] AND simple
1 file changed, 1 insertion(+), 1 deletion(-)

切换到master分支:

1
2
3
4
$ git switch master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)

Git还会自动提示我们当前master分支比远程的master分支要超前1个提交。

master分支上把readme.txt文件的最后一行改为:

1
Creating a new branch is quick & simple.

提交:

1
2
3
4
$ git add readme.txt 
$ git commit -m "& simple"
[master 5dc6824] & simple
1 file changed, 1 insertion(+), 1 deletion(-)

现在,master分支和feature1分支各自都分别有新的提交,变成了这样:

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

1
2
3
4
$ git merge feature1
Auto-merging readme.txt
CONFLICT (content): Merge conflict in readme.txt
Automatic merge failed; fix conflicts and then commit the result.

果然冲突了!Git告诉我们,readme.txt文件存在冲突,必须手动解决冲突后再提交。git status也可以告诉我们冲突的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.
(use "git push" to publish your local commits)

You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)

Unmerged paths:
(use "git add <file>..." to mark resolution)

both modified: readme.txt

no changes added to commit (use "git add" and/or "git commit -a")

我们可以直接查看readme.txt的内容:

1
2
3
4
5
6
7
8
9
10
cat readme.txt
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
<<<<<<< HEAD
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
>>>>>>> feature1

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

1
Creating a new branch is quick and simple.

再提交:

1
2
3
$ git add readme.txt 
$ git commit -m "conflict fixed"
[master cf810e4] conflict fixed

现在,master分支和feature1分支变成了下图所示:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git log --graph --pretty=oneline --abbrev-commit
* cf810e4 (HEAD -> master) conflict fixed
|\
| * 14096d0 (feature1) AND simple
* | 5dc6824 & simple
|/
* b17d20e branch test
* d46f35e (origin/master) remove test.txt
* b84166e add test.txt
* 519219b git tracks changes
* e43a48b understand how stage works
* 1094adb append GPL
* e475afc add distributed
* eaadf4e wrote a readme file

最后,删除feature1分支:

1
2
$ git branch -d feature1
Deleted branch feature1 (was 14096d0).

工作完成。

分支管理策略

Git分支十分强大,在团队开发中应该充分应用。

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

[参考链接][https://app.yinxiang.com/fx/5cb80672-82b7-4527-a790-6e216b8267d5]

通常,合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,删除分支后,会丢掉分支信息。

如果要强制禁用Fast forward模式,Git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息。

下面我们实战一下--no-ff方式的git merge

首先,仍然创建并切换dev分支:

1
2
$ git switch -c dev
Switched to a new branch 'dev'

修改readme.txt文件,并提交一个新的commit:

1
2
3
4
$ git add readme.txt 
$ git commit -m "add merge"
[dev f52c633] add merge
1 file changed, 1 insertion(+)

现在,我们切换回master

1
2
$ git switch master
Switched to branch 'master'

准备合并dev分支,请注意--no-ff参数,表示禁用Fast forward

1
2
3
4
$ git merge --no-ff -m "merge with no-ff" dev
Merge made by the 'recursive' strategy.
readme.txt | 1 +
1 file changed, 1 insertion(+)

因为本次合并要创建一个新的commit,所以加上-m参数,把commit描述写进去。

合并后,我们用git log看看分支历史:

1
2
3
4
5
6
7
$ git log --graph --pretty=oneline --abbrev-commit
* e1e9c68 (HEAD -> master) merge with no-ff
|\
| * f52c633 (dev) add merge
|/
* cf810e4 conflict fixed
...

可以看到,不使用Fast forward模式,merge后就像这样:

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

首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;

那在哪干活呢?干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在master分支发布1.0版本;

你和你的小伙伴们每个人都在dev分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。

所以,团队合作的分支看起来就像这样:

不用--no-ff,实际上只是将master的指针update成dev分支而已,用的还是dev的commit ID,而使用之后,则是重新commit了一哈,有了新的commit ID

Bug分支

修复bug时,我们会通过创建新的bug分支进行修复,然后合并,最后删除;

当手头工作没有完成时,先把工作现场git stash一下,然后去修复bug,修复后,再git stash pop,回到工作现场;

在master分支上修复的bug,想要合并到当前dev分支,可以用git cherry-pick命令,把bug提交的修改“复制”到当前分支,避免重复劳动。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
$ 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
$ git stash
Saved working directory and index state WIP on dev: f52c633 add merge

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

首先确定要在哪个分支上修复bug,假定需要在master分支上修复,就从master创建临时分支:

1
2
3
4
5
6
7
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
(use "git push" to publish your local 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
$ git add readme.txt 
$ git commit -m "fix bug 101"
[issue-101 4c805e2] fix bug 101
1 file changed, 1 insertion(+), 1 deletion(-)

修复完成后,切换到master分支,并完成合并,最后删除issue-101分支:

1
2
3
4
5
6
7
8
9
$ git switch master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
(use "git push" to publish your local 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(-)

太棒了,原计划两个小时的bug修复只花了5分钟!现在,是时候接着回到dev分支干活了!

1
2
3
4
5
6
$ git switch dev
Switched to branch 'dev'

$ git status
On branch dev
nothing to commit, working tree clean

工作区是干净的,刚才的工作现场存到哪去了?用git stash list命令看看:

1
2
$ git stash list
stash@{0}: WIP on dev: f52c633 add merge

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

一是用git stash apply恢复,但是恢复后,stash内容并不删除,你需要用git stash drop来删除;

另一种方式是用git stash pop,恢复的同时把stash内容也删了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ 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/stash@{0} (5d677e2ee266f39ea296182fb2354265b91b3b2a)

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

1
$ git stash list

你可以多次stash,恢复的时候,先用git stash list查看,然后恢复指定的stash,用命令:

1
$ git stash apply stash@{0}

在master分支上修复了bug后,我们要想一想,dev分支是早期从master分支分出来的,所以,这个bug其实在当前dev分支上也存在。

那怎么在dev分支上修复同样的bug?重复操作一次,提交不就行了?

有木有更简单的方法?

有!

同样的bug,要在dev上修复,我们只需要把4c805e2 fix bug 101这个提交所做的修改“复制”到dev分支。注意:我们只想复制4c805e2 fix bug 101这个提交所做的修改,并不是把整个master分支merge过来。

要注意:执行下列操作前,确保dev分支已add。

为了方便操作,Git专门提供了一个cherry-pick命令,让我们能复制一个特定的提交到当前分支:

1
2
3
4
5
6
$ git branch
* dev
master
$ git cherry-pick 4c805e2
[master 1d4b803] fix bug 101
1 file changed, 1 insertion(+), 1 deletion(-)

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

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

先说几个名词 未被追踪的文件:指的是新建的文件或文件夹且还没加入到暂存区(新建的还没有被git add 过得) 未加入到暂存区的文件:指的是已经被追踪过,但是没有加入到暂存区(已经执行过git add/commit的但是这次修改后还没有git add) 举例: readme.md 已经被git add/git commit过,但是呢 我这次只是修改了,而且没有修改完,不能commit

test 新建的文件夹,没有被git add/git commit过 有个急事需要处理,这时候我需要切换分支,去处理紧急任务,比如文中的举例去修改bug, 正确的步骤: git add test(让git去追踪这个新文件) git stash保留现场

如果我不执行这两个命令,那么我在修改BUG完成之后 git status,就会发现readme.md 没有添加到暂存区,同时又多了个test文件,但是自己的readme.md没有完成,万万不可以提交,这样导致bug的修改代码也提交不了。所以你需要 git stash,这样你在提交修改bug代码的时候 ,就不会看见readme.md 和 test。可以安心提交修改bug的代码。

git stash之前一定要用git status确认没有Untracked files

stash,就像是把当前的WIP压栈(这里WIP的含义就是所有修改了但未commit的数据)然后再一个一个取出来,默认的git stash pop是按照自栈顶开始的顺序依次出栈,也可以使用git stash pop stash@{num}的方式取出指定的WIP 我们可以用git stash list查看当前压栈了多少个WIP

注意:git stash不能将未被追踪的文件(untracked file)压栈,也就是从未被git add过的文件,也就是你在使用git status命令看到的提示Untracked files所列出的文件,所以在git stash之前一定要用git status确认没有Untracked files

分支强制删除

软件开发中,总有无穷无尽的新的功能要不断添加进来。

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

现在,你终于接到了一个新任务:开发代号为Vulcan的新功能,该功能计划用于下一代星际飞船。

于是准备开发:

1
2
$ git switch -c feature-vulcan
Switched to a new branch 'feature-vulcan'

5分钟后,开发完毕:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ 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 287773e] add feature vulcan
1 file changed, 2 insertions(+)
create mode 100644 vulcan.py

切回dev,准备合并:

1
$ git switch dev

一切顺利的话,feature分支和bug分支是类似的,合并,然后删除。

但是!

就在此时,接到上级命令,因经费不足,新功能必须取消!

虽然白干了,但是这个包含机密资料的分支还是必须就地销毁:

1
2
3
$ 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分支还没有被合并,如果删除,将丢失掉修改,如果要强行删除,需要使用大写的-D参数。。

现在我们强行删除:

1
2
$ git branch -D feature-vulcan
Deleted branch feature-vulcan (was 287773e).

终于删除成功(仅仅是删除分支,文件还是存在的)!

多人协作

  • 查看远程库信息,使用git remote -v
  • 本地新建的分支如果不推送到远程,对其他人就是不可见的;
  • 从本地推送分支,使用git push origin branch-name,如果推送失败,先用git pull抓取远程的新提交;
  • 在本地创建和远程分支对应的分支,使用git checkout -b branch-name origin/branch-name,本地和远程分支的名称最好一致;
  • 建立本地分支和远程分支的关联,使用git branch --set-upstream-to branch-name origin/branch-name;这条命令是在你本地已经建立了dev分支,但是还没有和远程dev分支关联的时候采用
  • 从远程抓取分支,使用git pull,如果有冲突,要先处理冲突。
  • 删除远程分支git push origin --delete dev

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

要查看远程库的信息,用git remote

1
2
$ git remote
origin

或者,用git remote -v显示更详细的信息:

1
2
3
$ git remote -v
origin git@github.com:michaelliao/learngit.git (fetch)
origin git@github.com:michaelliao/learngit.git (push)

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

推送分支

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

1
$ git push origin master

如果要推送其他分支,比如dev,就改成:

1
$ git push origin dev

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

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

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

抓取分支

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

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

1
2
3
4
5
6
7
$ git clone git@github.com:michaelliao/learngit.git
Cloning into 'learngit'...
remote: Counting objects: 40, done.
remote: Compressing objects: 100% (21/21), done.
remote: Total 40 (delta 14), reused 40 (delta 14), pack-reused 0
Receiving objects: 100% (40/40), done.
Resolving deltas: 100% (14/14), done.

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

1
2
$ git branch
* master

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

1
$ git checkout -b dev origin/dev

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ git add env.txt

$ git commit -m "add env"
[dev 7a5e5dd] add env
1 file changed, 1 insertion(+)
create mode 100644 env.txt

$ git push origin dev
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 308 bytes | 308.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
f52c633..7a5e5dd dev -> dev

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ cat env.txt
env

$ git add env.txt

$ git commit -m "add new env"
[dev 7bd91f1] add new env
1 file changed, 1 insertion(+)
create mode 100644 env.txt

$ git push origin dev
To github.com:michaelliao/learngit.git
! [rejected] dev -> dev (non-fast-forward)
error: failed to push some refs to 'git@github.com:michaelliao/learngit.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

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

1
2
3
4
5
6
7
8
9
10
$ 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

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

1
2
$ git branch --set-upstream-to=origin/dev dev
Branch 'dev' set up to track remote branch 'dev' from 'origin'.

再pull:

1
2
3
4
$ git pull
Auto-merging env.txt
CONFLICT (add/add): Merge conflict in env.txt
Automatic merge failed; fix conflicts and then commit the result.

这回git pull成功,但是合并有冲突,需要手动解决,解决的方法和分支管理中的解决冲突完全一样。解决后,提交,再push:

1
2
3
4
5
6
7
8
9
10
11
$ git commit -m "fix env conflict"
[dev 57c53ab] fix env conflict

$ git push origin dev
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), 621 bytes | 621.00 KiB/s, done.
Total 6 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
7a5e5dd..57c53ab dev -> dev

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

  1. 首先,可以试图用git push origin推送自己的修改;
  2. 如果推送失败,则因为远程分支比你的本地更新,需要先用git pull试图合并;
  3. 如果合并有冲突,则解决冲突,并在本地提交;
  4. 没有冲突或者解决掉冲突后,再用git push origin推送就能成功!

如果git pull提示no tracking information,则说明本地分支和远程分支的链接关系没有创建,用命令git branch --set-upstream-to <branch-name> origin/<branch-name>

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

Rebase

  • rebase操作可以把本地未push的分叉提交历史整理成直线;
  • rebase的目的是使得我们在查看历史提交的变化时更容易,因为分叉的提交需要三方对比。

在上一节我们看到了,多人在同一个分支上协作时,很容易出现冲突。即使没有冲突,后push的童鞋不得不先pull,在本地合并,然后才能push成功。

每次合并再push后,分支变成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ git log --graph --pretty=oneline --abbrev-commit
* d1be385 (HEAD -> master, origin/master) init hello
* e5e69f1 Merge branch 'dev'
|\
| * 57c53ab (origin/dev, dev) fix env conflict
| |\
| | * 7a5e5dd add env
| * | 7bd91f1 add new env
| |/
* | 12a631b merged bug fix 101
|\ \
| * | 4c805e2 fix bug 101
|/ /
* | e1e9c68 merge with no-ff
|\ \
| |/
| * f52c633 add merge
|/
* cf810e4 conflict fixed

总之看上去很乱,有强迫症的童鞋会问:为什么Git的提交历史不能是一条干净的直线?

其实是可以做到的!

Git有一种称为rebase的操作,有人把它翻译成“变基”。

假设你现在基于远程分支”origin”,创建一个叫”mywork”的分支(出处)。

1
$ git checkout -b mywork origin

现在我们在这个分支做一些修改,然后生成两个提交(commit).

1
2
3
4
5
$ vi file.txt
$ git commit
$ vi otherfile.txt
$ git commit
...

但是与此同时,有些人也在”origin”分支上做了一些修改并且做了提交了. 这就意味着”origin”和”mywork”这两个分支各自”前进”了,它们之间”分叉”了。

在这里,你可以用”pull”命令把”origin”分支上的修改拉下来并且和你的修改合并; 结果看起来就像一个新的”合并的提交”(merge commit):

但是,如果你想让”mywork”分支历史看起来像没有经过任何合并一样,你也许可以用 git rebase:

1
2
$ git checkout mywork
$ git rebase origin

这些命令会把你的”mywork”分支里的每个提交(commit)取消掉,并且把它们临时 保存为补丁(patch)(这些补丁放到”.git/rebase”目录中),然后把”mywork”分支更新 到最新的”origin”分支,最后把保存的这些补丁应用到”mywork”分支上。

当’mywork’分支更新之后,它会指向这些新创建的提交(commit),而那些老的提交会被丢弃。 如果运行垃圾收集命令(pruning garbage collection), 这些被丢弃的提交就会删除. (请查看 git gc)

现在我们可以看一下用合并(merge)和用rebase所产生的历史的区别:

在rebase的过程中,也许会出现冲突(conflict). 在这种情况,Git会停止rebase并会让你去解决 冲突;在解决完冲突后,用”git-add”命令去更新这些内容的索引(index), 然后,你无需执行 git-commit,只要执行:

1
$ git rebase --continue

这样git会继续应用(apply)余下的补丁。

在任何时候,你可以用--abort参数来终止rebase的行动,并且”mywork” 分支会回到rebase开始前的状态。

1
$ git rebase --abort

git的文档就清晰了。就使用过程来说: ===只对尚未推送或分享给别人的本地修改执行变基操作清理历史; ===从不对已推送至别处的提交执行变基操作

标签管理

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

Git的标签虽然是版本库的快照,但其实它就是指向某个commit的指针(跟分支很像对不对?但是分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。

Git有commit,为什么还要引入tag?

“请把上周一的那个版本打包发布,commit号是6a5819e…”

“一串乱七八糟的数字不好找!”

如果换一个办法:

“请把上周一的那个版本打包发布,版本号是v1.2”

“好的,按照tag v1.2查找commit就行!”

所以,tag就是一个让人容易记住的有意义的名字,它跟某个commit绑在一起。

创建标签

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

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

1
2
3
4
5
$ git branch
* dev
master
$ git checkout master
Switched to branch 'master'

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

1
$ git tag v1.0

可以用命令git tag查看所有标签:

1
2
$ git tag
v1.0

默认标签是打在最新提交的commit上的。有时候,如果忘了打标签,比如,现在已经是周五了,但应该在周一打的标签没有打,怎么办?

方法是找到历史提交的commit id,然后打上就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ git log --pretty=oneline --abbrev-commit
12a631b (HEAD -> master, tag: v1.0, origin/master) merged bug fix 101
4c805e2 fix bug 101
e1e9c68 merge with no-ff
f52c633 add merge
cf810e4 conflict fixed
5dc6824 & simple
14096d0 AND simple
b17d20e branch test
d46f35e remove test.txt
b84166e add test.txt
519219b git tracks changes
e43a48b understand how stage works
1094adb append GPL
e475afc add distributed
eaadf4e wrote a readme file

比方说要对add merge这次提交打标签,它对应的commit id是f52c633,敲入命令:

1
$ git tag v0.9 f52c633

再用命令git tag查看标签:

1
2
3
$ git tag
v0.9
v1.0

注意,标签不是按时间顺序列出,而是按字母排序的。可以用git show查看标签信息:

1
2
3
4
5
6
7
8
9
$ git show v0.9
commit f52c63349bc3c1593499807e5c8e972b82c8f286 (tag: v0.9)
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 21:56:54 2018 +0800

add merge

diff --git a/readme.txt b/readme.txt
...

可以看到,v0.9确实打在add merge这次提交上。

还可以创建带有说明的标签,用-a指定标签名,-m指定说明文字:

1
$ git tag -a v0.1 -m "version 0.1 released" 1094adb

用命令git show可以看到说明文字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ git show v0.1
tag v0.1
Tagger: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 22:48:43 2018 +0800

version 0.1 released

commit 1094adb7b9b3807259d8cb349e7df1d4d6477073 (tag: v0.1)
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 21:06:15 2018 +0800

append GPL

diff --git a/readme.txt b/readme.txt
...

注意:标签总是和某个commit挂钩。如果这个commit既出现在master分支,又出现在dev分支,那么在这两个分支上都可以看到这个标签。

操作标签

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

如果标签打错了,也可以删除:

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

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

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

1
2
3
4
$ git push origin v1.0
Total 0 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
* [new tag] v1.0 -> v1.0

或者,一次性推送全部尚未推送到远程的本地标签:

1
2
3
4
$ git push origin --tags
Total 0 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
* [new tag] v0.9 -> v0.9

如果标签已经推送到远程,要删除远程标签就麻烦一点,先从本地删除:

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

然后,从远程删除。删除命令也是push,但是格式如下:

1
2
3
$ git push origin :refs/tags/v0.9
To github.com:michaelliao/learngit.git
- [deleted] v0.9

要看看是否真的从远程库删除了标签,可以登陆GitHub查看。

国外网友制作的Git Cheat Sheet,建议打印出来备用:

Git Cheat Sheet

至此笔记结束,日后若有其他内容将会记录在GitIssue中。

CATALOG
  1. 1. 分支管理
    1. 1.1. 创建与合并分支
      1. 1.1.1. switch
    2. 1.2. 解决冲突
    3. 1.3. 分支管理策略
    4. 1.4. Bug分支
    5. 1.5. 分支强制删除
    6. 1.6. 多人协作
      1. 1.6.1. 推送分支
      2. 1.6.2. 抓取分支
    7. 1.7. Rebase
  2. 2. 标签管理
    1. 2.1. 创建标签
    2. 2.2. 操作标签