版本控制工具Git详解

2021-05-14 17:37:31 浏览数 (1)

一、Git和SVN的区别?

这是一个学Git无法绕开的话题,也是面试的常见题,我猜很多人的回答都是百度上直接背的,有了解过SVN底层的实现原理吗?

SVN是一种集中式版本控制工具,SVN架构如图:

A、B、C三个开发者如果需要提交自己的代码到远程仓库,必须联网(上传),上传之后SVN仓库内部做了什么?

假设用户A提交代码,会将用户A改动过的A.java提交给SVN仓库,仓库中记录的仅仅是变化(增量),对于B.java,C.java等没有h执行操作的文件,则没有增量。

那么引申出两个问题

1、假设无法联网怎么办?

2、假设SVN仓库硬盘坏了怎么办?

Git是一个分布式的版本控制工具,其架构如图所示:

 local computer1、local computer2本质上就是一个本地仓库,所以Git无需联网就可以在本地开发实现提交等操作,Server、local computer1、computer2的内容是一样的。并且与SVN不同的是local computer1和local computer2直接可以进行通信。

Git版本控制的底层原理:

对于修改过的文件,如图中的A.java,会在新版本中保存修改过后的文件副本,可以理解为是一个Snapshot(注意:并不是增量文件)。对于没有修改过的文件,则在新版本中保存的是旧版本的引用。

二、工作区与暂存区

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

工作区(Working Directory):就是你在电脑里能看到的目录。

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

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

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

第一步是用git add把文件添加进去,实际上就是把文件修改添加到暂存区;

第二步是用git commit提交更改,实际上就是把暂存区的所有内容提交到当前分支。

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

你可以简单理解为,需要提交的文件修改通通放到暂存区,然后,一次性提交暂存区的所有修改

俗话说,实践出真知。现在,我们再练习一遍,先对readme.txt做个修改,比如加上一行内容:

代码语言:javascript复制
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.

然后,在工作区新增一个LICENSE文本文件(内容随便写)。

先用git status查看一下状态:

代码语言:javascript复制
$ git status
On branch master
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

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	LICENSE

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

Git非常清楚地告诉我们,readme.txt被修改了,而LICENSE还从来没有被添加过,所以它的状态是Untracked

现在,使用两次命令git add,把readme.txtLICENSE都添加后,用git status再查看一下:

代码语言:javascript复制
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	new file:   LICENSE
	modified:   readme.txt

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

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

代码语言:javascript复制
$ git commit -m "understand how stage works"
[master e43a48b] understand how stage works
 2 files changed, 2 insertions( )
 create mode 100644 LICENSE

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

代码语言:javascript复制
$ git status
On branch master
nothing to commit, working tree clean

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

本文第二章节以上部分参考自《工作区和暂存区——廖雪峰的官方网站》 

git中包含四种状态:Untracked、Unmodified、modified   [可以通过git status查看当前状态]

1、Untracked一般出现在新建文件时候,表示新建的文件处于未被追踪的状态

2、将新建文件add之后,此时新建文件处于暂存区(Stage),状态为Staged状态

3、当用户commit提交暂存区域的文件到分支时,所有文件将处于Unmodified状态,此时Stage(暂存区是完全干净的)

4、当用户对文件进行修改后,被修改的文件将处于Modified状态,等待add 【重复2-4步骤】

三、Git最常用的几个命令

  • remote
  • fetch/pull/push
  • reset
  • checkout
  • log
  • merge

当然还包括我们上一章节所说的git add <file>    和  git commit -m '信息'    和 git status  命令,下面我们将一个个介绍。

3.1  git remote(建立远端连接)

因为我们会存在远端的分支,比如使用gitlab举例来说。

假设我们本地已有一个项目,但是GitLab/GitHub上没有,我现在想将该项目推送上去,该怎么做呢?

1、这是一个比较笨的方法,在远端建立一个项目,从远端仓库clone到本地,将本地代码拷贝到该项目,再分别执行三个命令add->commit->push。

2、这是一个比较明智的做法,步骤如下:

(1)将需要提交项目的目录的项目初始化,打开Git Bash,比如我要将我的目录/User/itcats_cn/git_test项目提交到Gitlab,就cd到git_test,输入git init命令,初始化该项目为git管理的项目,在执行完该命令后,输入ls -a,应该可以看到.git的隐藏文件。

(2)在本地git add <file> 和 git commit -m 'message' 

(3)在Gitlab中创建项目,项目名需要与第(2)步骤中提交的文件<file>名称一致

(4)复制gitlab中的git协议的地址,如git@192.168.0.11:web/test.git

(5)执行命令  git remote add origin git@192.168.0.11:web/test.git  【含义是"add" 本地项目与远端建立联系】

(6)执行命令  git config --list  发现remote.origin.url=git@192.168.0.11:web/test.git,已经关联上了

输入git remote -v命令,返回的是一个origin集合,说明用户可以对远端的仓库执行fetch和push操作

代码语言:javascript复制
origin	git@192.168.0.11:web/test.git (fetch)
origin	git@192.168.0.11:web/test.git (push)

3.2  git fetch(版本更新)

从远程仓库获取最新到本地,不会自动merge,Git中从远程的分支获取最新的版本到本地方式如下:

方式一

(1)查看远程仓库

代码语言:javascript复制
$ git remote -v
eoecn   https://github.com/eoecn/android-app.git (fetch)
eoecn   https://github.com/eoecn/android-app.git (push)
origin  https://github.com/com360/android-app.git (fetch)
origin  https://github.com/com360/android-app.git (push)
su@SUCHANGLI /e/eoe_client/android-app (master)

从上面的结果可以看出,远程仓库有两个,一个是eoecn,一个是origin (2)从远程获取最新版本到本地

代码语言:javascript复制
$ git fetch origin master
From https://github.com/com360/android-app
 * branch            master     -> FETCH_HEAD
su@SUCHANGLI /e/eoe_client/android-app (master)

$ git fetch origin master 这句的意思是:从远程的origin仓库的master分支下载代码到本地的origin master

(3)比较本地的仓库和远程参考的区别

代码语言:javascript复制
$ git log -p master.. origin/master
su@SUCHANGLI /e/eoe_client/android-app (master)

我的本地参考代码和远程代码相同,所以是Already up-to-date

以上的方式有点不好理解,大家可以使用下面的方式,并且很安全

方式二

(1)查看远程分支,和上面的第一步相同 (2)从远程获取最新版本到本地

代码语言:javascript复制
$ git fetch origin master:temp
From https://github.com/com360/android-app
 * [new branch]      master     -> temp
su@SUCHANGLI /e/eoe_client/android-app (master)

git fetch origin master:temp 这句命令的意思是:从远程的origin仓库的master分支下载到本地并新建一个分支temp

(3)比较本地的仓库和远程参考的区别

代码语言:javascript复制
$ git diff temp
su@SUCHANGLI /e/eoe_client/android-app (master)

命令的意思是:比较master分支和temp分支的不同 由于我的没有区别就没有显示其他信息 (4)合并temp分支到master分支

代码语言:javascript复制
$ git merge temp
Already up-to-date.
su@SUCHANGLI /e/eoe_client/android-app (master)

由于没有区别,所以显示Already up-to-date. 合并的时候可能会出现冲突,有时间了再把如何处理冲突写一篇博客补充上。 (5)如果不想要temp分支了,可以删除此分支

代码语言:javascript复制
$ git branch -d temp
Deleted branch temp (was d6d48cc).
su@SUCHANGLI /e/eoe_client/android-app (master)

如果该分支没有合并到主分支会报错,可以用以下命令强制删除git branch -D <分支名>

总结:方式二更好理解,更安全,对于pull也可以更新代码到本地,相当于fetch merge,多人写作的话不够安全。

git fetch章节参考自:《git fetch的简单用法》

3.3  git pull(版本更新合并)

git pull origin master

3.4  git push(版本推送)

git add .

git commit -m 'update .gitignore'

git push origin master  或者  git push -u origin dev

3.5  git reset(版本回退)

可以通过git log命令查看历史的commit记录,通过选择复制commit后的SHA1 id进行指定的版本回退,如:

代码语言:javascript复制
localhost:git_test fatah$ git log
commit 617866c48b75d62b8b33a29fc21eb6d32cff3eb1 (HEAD -> master)
Author: itcats_cn <xxx@qq.com>
Date:   Tue Aug 6 23:00:27 2019 -0400

    Add new file

commit 8ea66b2158d45ddd459fc6b538669be5b546bbb0
Author: itcats_cn <xxx@qq.com>
Date:   Wed Aug 7 10:20:37 2019  0800

    init2

目前版本为617866开头的commit id版本号,如果想回退版本,可以通过指定版本号进行回退

代码语言:javascript复制
git reset --hear '8ea66b2158d45ddd459fc6b538669be5b546bbb0'

如果嫌输出信息太多,看得眼花缭乱的,可以试试加上--pretty=oneline参数:

代码语言:javascript复制
localhost:git_test fatah$ git log --pretty=oneline
881667e679a88b557b1014d33e6a46a3c1d0a442 (HEAD -> master, origin/master) add hahaha
617866c48b75d62b8b33a29fc21eb6d32cff3eb1 Add new file

Git的commit id(版本号)和SVN不一样,Git的commit id不是1,2,3……递增的数字,而是一个SHA1计算出来的一个非常大的数字,用十六进制表示,而且你看到的commit id和我的肯定不一样,以你自己的为准。为什么commit id需要用这么一大串数字表示呢?因为Git是分布式的版本控制系统,后面我们还要研究多人在同一个版本库里工作,如果大家都用1,2,3……作为版本号,那肯定就冲突了。

Git必须知道当前版本是哪个版本,在Git中,用HEAD表示当前版本,上一个版本就是HEAD^,上上一个版本就是HEAD^^,当然往上100个版本写100个^比较容易数不过来,所以写成HEAD~100。

3.6 checkout

作用:

1、切换到新的分支: 一般来说master分支上都是最稳定的版本,我们日常开发的时候在分支开发,开发完毕之后再合并到master中,具体操作如下。

代码语言:javascript复制
$ git checkout -b dev-0807-work
 Switched to a new branch 'dev-0807-work'

$ git checkout -b master
 Switched to a new branch 'master'

2、撤销更改 :只能撤销更改过的文件,不能撤销新增的文件或删除的文件

比如我们修改了3.txt的内容

代码语言:javascript复制
$ git status
On branch master
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:   3.txt

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

现在我想撤销3.txt的更改,操作如下:

代码语言:javascript复制
$ git checkout 3.txt
Updated 1 path from the index

此时3.txt的内容就成功被撤销,这样不会影响之前的版本,恢复成非常"干净"的状态

3.7  merge

一般我们开发环境都在分支上开发,分支开发完毕时候测试通过则发布,并且在master进行merge合并并发布,这样才能保证master上是最"clean"的状态,但是一般我们将分支合并到Master都是在图形化界面发起的,下面我将展示GitLab中的 Pull Request。

这种merge方法方便code-review,查看改动之处并让参与者、审阅者也可以方便在下面书写评论。 

在确认没有任何问题后,再点击绿色的按钮"Merge',就可以将我们的工作分支合并到master之中。

合并之后,在本地机器执行  git fetch 命令,从远端拉取最新的代码[此时本机处于dev-08-07-work分支]

本机切换到master分支,执行命令 git checkout master

代码语言:javascript复制
localhost:git_test fatah$ git checkout master
Switched to branch 'master'
Your branch is behind 'origin/master' by 2 commits, and can be fast-forwarded.
  (use "git pull" to update your local branch)

提示远端的master有两处commit,先pull到本机的orgin/master

代码语言:javascript复制
localhost:git_test fatah$ git pull origin master
Enter passphrase for key '/Users/fatah/.ssh/id_rsa': 
From 192.168.1.103:web/test
 * branch            master     -> FETCH_HEAD
Updating 881667e..b117d8f
Fast-forward
 a.txt | 3   -
 1 file changed, 2 insertions( ), 1 deletion(-)

整个过程我们发现,本地的master并没有push到远端仓库,而是使用分支push到远端仓库中,再使用Request Merge在远端将分支与Master进行合并,在code-view没有问题之后,在远端仓库执行Merge操作。由于本地在fetch最新代码的时候并不会直接合并,所以在本机切换为master分支的时候会提示pull一下,因为有两处commit,在pull成功之后本机master便拥有了最新的master代码。

在merge的时候如何解决冲突呢?

3.7.1  merge解决冲突

模拟场景:

(1)比如修改gitlab中的a.txt文件,本地也修改git管理目录下的a.txt文件

(2)本地进行add 和 commit操作

(3)执行git pull origin master时候,发现冲突文件,因为远端和本地两处修改位置一样,到底选用哪一行?

(4)删除冲突部分,其中<<<<HEAD到=====之间的内容为本地仓库内容,后面的内容为远端仓库的内容,两处冲突地方选用哪个可以自己权衡,再修改完之后,输入git status,发现a.txt变为了unmodified状态,重新执行add -> commit -> push操作,发现完成解决冲突。

本文来源itcats_cn,由javajgs_com转载发布,观点不代表Java架构师必看的立场,转载请标明来源出处

0 人点赞