工具

Git 的使用——从入门到进阶

2019-03-28  本文已影响2人  ShannonChenCHN

注:本文首发于 https://github.com/ShannonChenCHN/GitTour

一、Git 简介

Git——最流行的分布式版本控制系统。

分布式(Git) VS. 集中式(CVS、 SVN)

CVS 和 SVN属于集中式版本控制系统,集中式版本控制系统最大的毛病就是必须要联网才能工作。

Git 属于分布式版本控制系统。
首先,分布式版本控制系统根本没有“中央服务器”,每个人的电脑上都是一个完整的版本库,这样,你工作的时候,就不需要联网了,因为版本库就在你自己的电脑上。因为每个人电脑上都有一个完整的版本库,所以非常适合多人协作。

和集中式版本控制系统相比,分布式版本控制系统的安全性要高很多,因为每个人电脑里都有完整的版本库,某一个人的电脑坏掉了不要紧,随便从其他人那里复制一个就可以了。

Git 还有一个优势就是极其强大的分支管理。

二、Git 的安装

1. 命令行工具

不同平台都有各自的安装教程(具体安装过程这里就不细说了):

2. 客户端

在 macOS 平台上,常见的有以下三种:

三、一些概念

1. 工作区和暂存区

我们把文件往Git版本库里添加的时候,是分两步执行的:

因为我们创建 Git 版本库时,Git自动为我们创建了唯一一个 master 分支,所以,现在,git commit就是往master分支上提交更改。

你可以简单理解为,需要提交的文件修改通通放到暂存区,然后,一次性提交暂存区的所有修改(没有被添加到暂存区的修改是不会被提交的)。

[图片上传失败...(image-3b029c-1553779982083)]
[图片上传失败...(image-d67cbc-1553779982083)]

2. 远程仓库(Remote repositories)

在使用 Git 进行多人协同工作时,一般会有一台电脑充当服务器的角色,每天24小时开机,其他每个人都从这个“服务器”仓库克隆一份到自己的电脑上,并且各自把各自的提交推送到服务器仓库里,也从服务器仓库中拉取别人的提交。

这个“服务器”仓库就是一个远程仓库。

推荐阅读:

3. SSH key

一般本地 Git 仓库和远程 Git 仓库之间的传输是通过 SSH 加密的,所以需要设置一下 SSH key。

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

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

4. GitHub

如果你先在本地创建了 Git 仓库,然后又在 GitHub 上创建了一个远程仓库,如何将本地的 Git 仓库跟 GitHub 上的 Git 仓库关联起来?

# push an existing repository from the command line

$ git remote add origin git@github.com:ShannonChenCHN/GitTour.git
$ git push -u origin master

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

比如,以后只要本地作了提交,就可以通过命令:

$ git push origin master

把本地 master 分支的最新修改推送至 GitHub 上的远程仓库了。

四、 分支(Branching)

分支特性能够让我们在不影响“主线开发进程”的情况下,去继续做一些其他开发任务,并且在最终可以将结果合并到“主开发进程”上。

Git 的分支功能是一个能够秒杀其他版本控制系统的 feature,其特点在于轻量、即时、快速。很多其他的 VCS 工具也可以实现分支开发,但是都是需要将整个项目代码重新拷贝一份,这样既低效又费时,而且还占用大量空间。

1. 分支的本质

分支的本质实际上是指针的控制,切换和合并分支实际上是调整相关指针的指向。

每次提交,Git 都把它们串成一条时间线,master 分支对应的 master 指针,指向最近的一次提交,而 HEAD 指针指向的是当前分支。

(1)提交:每次提交,当前分支都会向前移动一步,这样,随着你不断提交,当前分支的线也越来越长。

            HEAD
              ↓
            master
              ↓
----o----o----o
             
注:圆圈“o”代表一次 commit

(2)创建并切换到新分支:执行 checkout -b dev 时,Git 新建了一个指针叫 dev,指向 master 相同的提交,再把 HEAD 指向 dev,就表示当前分支在 dev 上。

            master
              ↓
----o----o----o
              ↑
             dev
              ↑
            HEAD

(3)切换分之后的提交:每新提交一次,dev 指针和 HEAD 指针往前移动一步,而 master 指针不变。

            master
              ↓
----o----o----o~~~~●
                   ↑
                  dev
                   ↑
                 HEAD

(4)合并分支:把 dev 合并到 master 上,其实就是直接把 master 指向 dev 的当前提交。

                master
                   ↓
----o----o----o~~~~●
                   ↑
                  dev
                   ↑
                 HEAD

(5)删除分支:删除 dev 分支就是把 dev 指针给删掉,删掉后,我们就剩下了一条 master 分支。

                master
                   ↓
----o----o----o~~~~●

(6)删除一个没有被合并的分支:如果要丢弃一个没有被合并过的分支,可以通过 git branch -D <branch-name> 强行删除,使用 git branch -d <branch-name> 无法删除一个没有被合并过的分支。

删除 dev 前:

            master
              ↓
----o----o----o~~~~●
                   ↑
                  dev
                   ↑
                 HEAD

切换到 master 直接删除 dev 分支后:

            HEAD
              ↓
            master
              ↓
----o----o----o

推荐阅读:

2. 分支策略

3. 分支管理

直接使用 git merge 合并分支时,默认情况下,Git 执行“快进式合并”(fast-farward merge),会直接将 master 分支指向 dev 分支。

$ git merge dev
Updating f1ed183..717b632
Fast-forward
 README.md | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 54 insertions(+), 8 deletions(-)

[图片上传失败...(image-6aa931-1553779982083)]

另一种合并分支的方式是 git merge --no-ff dev,使用 --no-ff 参数后,会执行正常合并,并在 master 分支上生成一个新节点,也就是生成一个新的 commit,这样从分支历史上就可以看出分支信息(加上 -m 参数,可以把 commit 注释同时写上去)。

$ git merge --no-ff -m "merge with no-ff" dev
Merge made by the 'recursive' strategy.
 README.md | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

[图片上传失败...(image-89061-1553779982083)]

小结:

合并分支时,加上 --no-ff 参数就可以用 recursive 模式合并,合并后的历史有分支,能看出来曾经做过合并,而 fast forward 合并就看不出来曾经做过合并(可以借助 git log --graph 命令)。

<div style="width: 50%; margin: 0 auto;">Hello</div>
<div align="center"><img src="https://github.com/ShannonChenCHN/GitTour/blob/master/img/4.png?raw=true"></div>
<p><center>合并前</center></p>

<div align="center"><img src="https://github.com/ShannonChenCHN/GitTour/blob/master/img/5.png?raw=true"></div>
<p><center>使用 git merge 合并</center></p>

<div align="center"><img src="https://github.com/ShannonChenCHN/GitTour/blob/master/img/6.png?raw=true"></div>
<p><center>使用 git merge --no-off 合并</center></p>

推荐阅读:

4. Rebase

在本地分支修改文件并且 commit 之后,如果远程相同分支的仓库也有修改,此时如果直接执行 git pull 就会出现提交历史分叉的情况(因为实际上本地和远程的是同一个分支,是不应该出现分叉的),为了让提交记录看起来更合理一些,我们可以使用 git rebase 来实现。

4.1 git rebase

第一种方式是,在 git fetch 或者 git pull 之后,手动执行 git rebase。

示例

初始状态:

* cce920e - Update README (3 days ago) <Shannon Chen>
* 717b632 - Update README for recursive merge (3 days ago) <Shannon Chen>

提交了两个 commit 之后:

* 6b3ae3f - (HEAD -> master) Update README (4 seconds ago) <Shannon Chen>
* b989631 - Update README for stash (4 seconds ago) <Shannon Chen>
* cce920e - Update README (3 days ago) <Shannon Chen>
* 717b632 - Update README for recursive merge (3 days ago) <Shannon Chen>

git pull 之后,我们的提交记录就会出现分叉的情况:

*   563a77b - (HEAD -> master) Merge branch 'master' of github.com:ShannonChenCHN/GitTour (51 seconds ago) <Shannon Chen>
|\
| * c5410d9 - (origin/master) Update LICENSE (2 minutes ago) <ShannonChen>
* | 3963470 - Update README (69 seconds ago) <Shannon Chen>
* | ddc3adf - Update README for stash (3 days ago) <Shannon Chen>
|/
* cce920e - Update README (3 days ago) <Shannon Chen>
* 717b632 - Update README for recursive merge (3 days ago) <Shannon Chen>

执行一下 git rebase 之后,提交记录就变成直线了:

* 6b3ae3f - (HEAD -> master) Update README (4 seconds ago) <Shannon Chen>
* b989631 - Update README for stash (4 seconds ago) <Shannon Chen>
* c5410d9 - (origin/master) Update LICENSE (9 minutes ago) <ShannonChen>
* cce920e - Update README (3 days ago) <Shannon Chen>
* 717b632 - Update README for recursive merge (3 days ago) <Shannon Chen>

4.2 git pull --rebase

另一种 Rebase 的快捷操作方式是在 git pull 时加上 --rebase 参数:

$ git pull --rebase origin rel/0.14
remote: Counting objects: 102, done
remote: Finding sources: 100% (53/53)
remote: Total 53 (delta 29), reused 35 (delta 29)
Unpacking objects: 100% (53/53), done.
From ssh://ssssss.xxxxxx.com:29418/Wireless/IOS
 * branch            rel/7.14   -> FETCH_HEAD
   a59d806..fbaa680  rel/7.14   -> origin/rel/0.14
First, rewinding head to replay your work on top of it...

4.3 git pull VS. git pull --rebase

git pull = git fetch + git merge against tracking upstream branch.

git pull --rebasegit fetch + git rebase against tracking upstream branch.

参考:

4.5 Cherry Pick

与 git merge 类似,git cherry pick 也是将一个分支上的 commit 合并到另一个分支上,不同的是 git cherry pick 仅仅是合并分支上的一个或一部分 commit 到另一个分支上,而不是所有 commit。

使用方法:

(1)切换到目标分支:

git checkout <target-branch>

(2)pick 指定的 commit

git cherry-pick <commit-hash>

参考:

五、解决冲突

当因为两个不同分支修改了相同文件而导致 Git 无法自动合并分支时,就必须首先解决冲突。解决冲突后,再提交,合并完成。

解决冲突就是把 Git 合并失败的文件手动编辑为我们希望的内容,再提交。Git 会用 <<<<<<<=======>>>>>>> 标记出不同分支的内容。

                    HEAD
                     ↓
                   master
                     ↓
----o----o----o--o---●
               \    /
                \  / 
                 o
                 ↑
              feature_01

六、储藏和清理(git stash)

有时,当你在项目的一部分上已经工作一段时间后,所有东西都进入了混乱的状态,而这时你想要切换到另一个分支做一点别的事情。 问题是,你不想仅仅因为过会儿回到这一点而为做了一半的工作创建一次提交。 这个时候 git stash 就派上用场了。

储藏会处理工作目录的脏的状态,也就是修改的跟踪文件与暂存改动,然后将未完成的修改保存到一个栈上,而你可以在任何时候重新应用这些改动。

6.1 储藏工作

在改动文件后,执行 git stash 即可 stash 修改的内容(包括已经被添加到暂存区的修改内容):

$ git stash 

查看 stash 列表:

$ git stash list

6.2 应用储藏

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

$ git stash apply stash@{0}
$ git stash drop stash@{0}

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

$ git stash pop stash@{0}

参考

七、查看修改记录

1. git log

查看文件修改记录,可以使用 git log <file>命令。

也通过 SourceTree 操作来查看:选中文件 -> 点击鼠标右键 -> log selected

2. git blame

查看文件修改记录,可以使用 git blame 命令。

参考:

八、打标签

像其他版本控制系统(VCS)一样,Git 可以给历史中的某一个提交打上标签,以示重要。 比较有代表性的是人们会使用这个功能来标记发布结点(v1.0 等等)。

git tag,查看所有 tag

git tag <name>,创建 tag,默认为 HEAD

git tag <name> [<commit-id>],基于某个 commit 创建 tag

git tag -a <name> -m <comment> [<commit-id>],创建带注释的 tag

git show <tagname>,查看某个 tag 的信息

git tag -d <tagname>,删除本地 tag

git push origin <tagname>,推送某个标签到远程

git push origin --tags,一次性推送全部尚未推送到远程的本地标签

删除远程的 tag:先通过 git tag -d <tagname> 从本地删除,然后再通过 git push origin :refs/tags/<tagname> 删除远程的 tag。

九、其他

1. 通过配置 .gitignore 来忽略某些不需要加入版本控制的文件

2. 搭建 Git 服务器

3. 配置别名

例如:

$ git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"

这样使用 git lg 命令就可以查看非常漂亮的 log 了。

参考

4. 生成 pacth 和应用 patch

应用场景:

参考

5. amend commit

修改已经提交的 commit,比如将新的修改合并到上一次的 commit 中,就可以用到这个命令。

参考

十、常用命令

1. 基本

2. 撤销

注:上面的 commit 撤销操作只对本地的 commit 起作用,如果要想撤销已经 push 的 commit,有两种方式,一种是直接 revert,生成一个新的提交记录,另一种是 reset 后再强制 push,推荐使用第一种方式。

3. 分支

4. 其他

十一、常见问题

详见版本控制:Git 和 SVN

十二、CheatSheet

参考

上一篇 下一篇

猜你喜欢

热点阅读