Git教程

Git简介

Git 是一种分布式版本控制系统,它可以不受网络连接的限制,加上其它众多优点,目前已经成为程序开发人员做项目版本管理时的首选,非开发人员也可以用 Git 来做自己的文档版本管理工具。

SVN与Git最主要的区别

SVN是集中式版本控制系统,版本库是集中放在中央服务器的,而干活的时候,用的都是自己的电脑,所以首先要从中央服务器哪里得到最新的版本,然后干活,干完后,需要把自己做完的活推送到中央服务器。集中式版本控制系统是必须联网才能工作,如果在局域网还可以,带宽够大,速度够快,如果在互联网下,如果网速慢的话,就纳闷了。

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

工作原理 / 流程:

image.png

  • Workspace:工作区(working directory)
    • 就是你在电脑上看到的目录里的文件(.git隐藏目录版本库除外)
  • Index / Stage:暂存区
  • Repository:仓库区(或本地仓库/版本库)
    • 工作区有一个隐藏目录.git,这个不属于工作区,这是版本库。其中版本库里面存了很多东西,其中最重要的就是stage(暂存区),还有Git为我们自动创建了第一个分支master,以及指向master的一个指针HEAD。
  • Remote:远程仓库

在掌握具体命令前,先理解下HEAD。

HEAD,它始终指向当前所处分支的最新的提交点。你所处的分支变化了,或者产生了新的提交点,HEAD就会跟着改变。

安装后需设置用户信息

第一次使用git的时候需要设置。因为Git是分布式版本控制系统,所以需要填写用户名和邮箱作为一个标识。

1
2
3
4
# 设置用户名:
git config --global user.name "<用户名>"
# 设置邮箱:
git config --global user.email "<邮箱地址>"

注:--global参数,表示你这台机器上所有的Git仓库都会使用这个配置,当然你也可以对某个仓库指定的不同的用户名和邮箱。

基础操作

1
2
3
4
5
6
# 初始化一个git仓库
git init
# 提交文件(添加到暂存区)
git add readme.md
# 把暂存区中的文件提交到本地仓库的当前分支上,并添加描述信息
git commit -m "<提交的描述信息>"

版本回退

git reset --hard HEAD^那么如果要回退到上上个版本只需把HEAD^ 改成 HEAD^^ 以此类推。那如果要回退到前100个版本的话,使用上面的方法肯定不方便,我们可以使用下面的简便命令操作:git reset --hard HEAD~100即可

1
2
# 回退到上个版本(如果工作区有内容修改的话也会被覆盖,即该命令会将工作区变成和上个版本一模一样的代码,没提交的代码会丢失)
git reset --hard HEAD^

现在我们已经还原到上个版本了,但是怎么变回去呢?

1
2
3
4
# 使用reflog查看还原前的版本号
git reflog
# 然后通过命令进行恢复
git reset --hard 44c44fc

Git撤销修改

在工作区已经修改了文件,但是希望恢复到以前的版本。

1
2
3
# 使用git checkout -- <fileName> 可以丢弃工作区的修改
# 注意:命令git checkout -- readme.md 中的 -- 很重要,如果没有 -- 的话,那么命令就变成创建分支了。
git checkout -- readme.md

上面的命令的作用是把readme.md文件在工作区做的修改全部撤销

这里有2种情况,如下:

  • 1、readme.md自动修改后,还没有放到暂存区,使用撤销修改就会到和版本库一模一样的状态
  • 2、另外一种是readme.md已经放入暂存区了,接着又作了修改,撤销修改就回到添加暂存区后的状态

远程仓库

1
2
3
4
5
# 添加远程仓库
git remote add origin https://github.com/Coding-Coder/testGit.git
# 把本地仓库分支提交到远程仓库
# 加了参数-u:会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时就可以简化命令,以后即可直接用git push 代替git push origin master
git push -u origin master

创建与合并分支

1
2
3
4
5
6
7
8
9
10
11
# 创建并切换到指定的分支,保留所有的提交记录
# –b参数表示创建并切换,等同于 "git branch <分支名>""git checkout <分支名>" 两个命令合并
git checkout -b dev
# 列出本地的所有分支,当前所在分支以 "*" 标出
git branch
# 切换到已存在的指定分支
git checkout master
# 合并指定分支到当前分支上(在master分支上合并dev分支的内容)
git merge dev
# 删除分支
git branch -d dev

解决冲突

Git用<<<<<<<=======>>>>>>>标记出不同分支的内容,其中<<<<<<< HEAD是指主分支修改的内容,>>>>>>> fenzhi1 是指fenzhi1上修改的内容

1
2
3
4
5
<<<<<< HEAD
999999
======
888888
>>>>>> fenzhi1

手动解决冲突后,然后执行2个命令进行提交:git add .&git commit -m "解决冲突"

1
2
# 以图片且单行的形式查看提交记录
git log --pretty=oneline --graph

分支管理策略

通常合并分支时,git一般使用"Fast forward"模式,在这种模式下,删除分支后,会丢掉分支信息,现在我们来使用带参数–no-ff禁用"Fast forward"模式。首先我们来做demo演示下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 创建并切换到新分支fenzhi2
git checkout -b fenzhi2
# 修改readme.md内容。
手动修改内容
# 添加到暂存区
git add .
# 提交到本地仓库
git commit -m "fenzhi2添加内容aaaaa"
# 切换回主分支(master)。
git checkout master
# 合并dev分支,使用命令 git merge –no-ff -m “注释” fenzhi2
# --no-ff参数:表示禁用fast forward
git merge --no-ff -m "merge with no-ff" fenzhi2
# 删除fenzhi2
git branch -d fenzhi2
# 查看历史记录
# -–graph:图形展示
# -–pretty=oneline:单行显示,减少数据
# -–abbrev-commit:头部数据减少(commit id变短)
git log --pretty=oneline --graph --abbrev-commit

可以看到被删除的分支的提交记录还存在。

分支策略:首先master主分支应该是非常稳定的,也就是用来发布新版本,一般情况下不允许在上面干活,干活一般情况下在新建的dev分支上干活,干完后,比如上要发布,或者说dev分支代码稳定后可以合并到主分支master上来。

bug分支(使用stash进行隐藏工作现场)

在开发中,会经常碰到bug问题,那么有了bug就需要修复,在Git中,分支是很强大的,每个bug都可以通过一个临时分支来修复,修复完成后,合并分支,然后将临时的分支删除掉。

比如我在开发中接到一个404 bug时候,我们可以创建一个404分支来修复它,但是,当前的dev分支上的工作还没有提交。比如如下:

1
2
3
4
5
# 当前工作区已经有修改了
# 切换分支
git checkout dev
# 修改readme.md的内容
手动修改

并不是我不想提交,而是工作进行到一半时候,我们还无法提交,比如我这个分支dev要2天完成,但是我issue-404 bug需要5个小时内完成。怎么办呢?还好,Git还提供了一个stash功能,可以把当前工作现场**”隐藏起来”**,等以后恢复现场后继续工作。如下:

1
2
3
4
# 将当前的工作现场隐藏起来
git stash
# 查看状态,是干净的(之前修改的代码也看不到了,被隐藏了,已经恢复的和分支上的代码一样了)
git status

所以现在我可以通过创建issue-404分支来修复bug了。

首先我们要确定在那个分支上修复bug,比如我现在是在主分支master上来修复的,现在我要在master分支上创建一个临时分支,演示如下:

1
2
3
4
5
6
7
8
9
# 如果要在master分支上创建分支,需要先切换到master分支
git checkout master
# 在当前分支的基础上创建并切换到新分支issue-404
git checkout -b issue-404
# 解决bug
一顿操作
# 提交代码
git add .
git commit -m "fix bug 404"

修复完成后,切换到master分支上,并完成合并,最后删除issue-404分支。演示如下:

1
2
3
4
5
6
# 切换回master分支
git checkout master
# 合并代码
git merge --no-ff -m "merge fix bug 404" issue-404
# 最后删除临时分支issue-404
git branch -d issue-404

最后,我们回到dev分支上干活。

1
2
3
4
# 切回到dev分支
git checkout dev
# 查看状态,目前还是干净的
git status

工作现场还在,Git把stash内容存在某个地方了,但是需要恢复一下,可以使用如下2个方法:

  • 1、git stash apply恢复,恢复后,stash内容并不删除,你需要使用命令git stash drop来删除。
  • 2、另一种方式是使用git stash pop,恢复的同时把stash内容也删除了。
    演示如下:
    1
    2
    3
    4
    # 查看stash列表内容
    git stash list
    # 恢复工作现场
    git stash pop

多人协作

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

  • 1、要查看远程库的信息 使用git remote
  • 2、要查看远程库的详细信息 使用git remote –v
    1
    2
    3
    4
    # 详细信息里包含有2条信息
    $ git remote -v
    origin https://github.com/Coding-Coder/testGit.git (fetch)
    origin https://github.com/Coding-Coder/testGit.git (push)

    1. 推送(push)分支:

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

使用命令git push origin master,如果我们现在要推送到其他分支,比如dev分支上,我们还是那个命令git push origin dev

2.抓取(fetch)分支:

多人协作时,大家都会往master分支上推送各自的修改。现在我们可以模拟另外一个同事,可以在另一台电脑上(注意要把SSH key添加到github上)或者同一台电脑上另外一个目录克隆,新建一个目录名字叫testGit2

但是我首先要把dev分支也要推送到远程去,如下

1
git push origin dev

接着进入testgit2目录,进行克隆远程的库到本地来,如下:

1
git clone https://github.com/Coding-Coder/testGit.git

现在我们的小伙伴要在dev分支上做开发,就必须把远程的origin的dev分支拉取到本地来,于是可以使用命令创建本地dev分支:

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

现在小伙伴们就可以在dev分支上做开发了,开发完成后把dev分支推送到远程库时。如下:

1
2
3
4
5
6
7
# 小伙伴在dev进行开发
一顿操作
# 提交代码
git add .
git commit -m "增加内容bbbbbb"
# 推送到远程
git push orgin dev 或者 git push

小伙伴们已经向origin/dev分支上推送了提交,而我在我的目录文件下也对同样的文件同个地方作了修改,也试图推送到远程库时,如下:

1
2
3
4
5
6
7
# 我在dev进行开发
一顿操作
# 提交代码
git add .
git commit -m "增加内容cccccc"
# 推送到远程仓库(推送时会发生错误,不同的人推送同样的文件,修改同个文件同个地方报错)
git push orgin dev

根据git的提示,先用git pull把最新的提交从origin/dev抓下来,然后在本地合并,解决冲突,再推送

1
git pull

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

1
2
3
4
5
6
7
8
9
10
# 将本地的dev与远程origin/dev分支进行链接
git branch --set-upstream-to=origin/dev dev
# 再次尝试拉取
git pull
# 发现可以拉取了,但是存在冲突,需要手动解决冲突
# 手动解决完冲突后进行代码提交
git add .
git commit -m "合并并解决冲突"
# 最后推送到远程仓库(可以不用git push origin dev,因为之前已经将本地dev与远程origin dev进行链接了)
git push

因此:多人协作工作模式一般是这样的:首先,可以试图用git push origin branch-name推送自己的修改.如果推送失败,则因为远程分支比你的本地更新早,需要先用git pull试图合并。如果合并有冲突,则需要解决冲突,并在本地提交。再用git push origin branch-name推送。

合并(merge)和变基(rebase)

到底什么时候使用 merge 操作,什么时候使用 rebase 操作呢?

使用 merge 操作(保留合并的痕迹)

支持使用 merge 的开发者,他们认为仓库的提交历史就是记录实际发生过什么,它是针对于历史的一个文档,本身其实是有价值的,我们不应该随意修改。我们改变历史的话,就相当于使用“谎言”来掩盖实际发生过的事情,而这些痕迹是应该被保留的。可能,这样并不是很好。

使用 rebase 操作(不保留合并的痕迹)

支持使用 rebase 的开发者,他们认为提交历史是项目过程中发生过的事情,需要项目的主干非常的干净。而使用 merge 操作会生成一个 merge 的 commit 对象,让提交历史多了一些非常多余的内容。

当我们后期,使用 log 命令参看提交历史的话,会发现主干的提交历史非常的尴尬。比如,同样的修改内容重复提交了两次,这显然是分支合并导致的问题。

两者的使用原则

总的原则就是,只对尚未推送或分享给其他人的本地修改执行变基操作清理历史,从不对已经推送到仓库的提交记录执行变基操作,这样,你才可能享受到两种方式带来的便利。

附录

记住这些常用命令即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 工作区 -> 暂存区
$ git add <file/dir>
# 暂存区 -> 本地仓库
$ git commit -m "some info"
# 本地仓库 -> 远程仓库
$ git push origin master # 本地master分支推送到远程origin仓库
# 工作区 <- 暂存区
$ git checkout -- <file> # 暂存区文件内容覆盖工作区文件内容

# 暂存区 <- 本地仓库
$ git reset HEAD <file> # 本地仓库文件内容覆盖暂存区文件内容

# 本地仓库 <- 远程仓库
$ git clone <git_url> # 克隆远程仓库
$ git fetch upstream master # 拉取远程代码到本地但不应用在当前分支
$ git pull upstream master # 拉取远程代码到本地但应用在当前分支
$ git pull --rebase upstream master # 如果平时使用rebase合并代码则加上
# 工作区 <- 本地仓库
$ git reset <commit ID> # 本地仓库覆盖到工作区(保存回退文件内容修改)
$ git reset --mixed <commit ID> # 本地仓库覆盖到工作区(保存回退文件内容修改)
$ git reset --soft <commit ID> # 本地仓库覆盖到工作区(保留修改并加到暂存区)
$ git reset --hard <commit ID> # 本地仓库覆盖到工作区(不保留修改直接删除掉)

Git命令速查表

image.png

Git工作流程图

image.png