优秀的编程知识分享平台

网站首页 > 技术文章 正文

深入浅出 Git(深入浅出 高境界)

nanyue 2024-08-03 17:56:56 技术文章 5 ℃

git 初体验

使用git前需设置用户名和Email,这些信息会出现在提交记录中以标识作者。

git config --global user.name "Ye Hanlin"
git config --global user.email "yehanlin@scratchlab.com"

这些信息将以如下形式出现在提交记录中:

commit 270d71a66ec534ea4d567132b8d1ff9eb857a399
Author: Ye Hanlin <yehanlin@scratchlab.com>
Date: Sat Nov 6 16:18:17 2021 +0800

add a in alphabet.txt

下面的步骤演示了使用git的流程。

1. 创建proja版本库

mkdir github
git init --bare github/proja.git

2. 模拟两个用户进行开发,两人各自从repo克隆proja版本库

mkdir user1
cd user1
git clone /home/yehanlin/github/proja.git

mkdir user2
cd user2
git clone /home/yehanlin/github/proja.git/

3. user1创建num.txt并向该文件写入1

echo 1 > num.txt

4. user1将num.txt添加到暂存区以备提交

git add num.txt

5. user1提交num.txt

git commit -m “user add 1 in num.txt”

6. 重复3、4、5步创建alphabet.txt、color.txt

echo a > alphabet.txt
git add alphabet.txt
git commit -m “user1 add a in alphabet.txt”

echo red > color.txt
git add color.txt
git commit -m “user1 add red in color.txt”

7. user1推送

git push

8. user2拉回提交

git pull

9. 使用git log可查看提交日志

git log
commit 700e481d0f58aafe01c92583dfd31b5e5cfe47a6 (HEAD -> master, origin/master)
Author: Ye Hanlin <yehanlin@scratchlab.com>
Date: Sun Nov 14 17:02:40 2021 +0800

user1 add red in color.txt

commit a99c124d0bef020f14adf0c94c2ca9376f60a1f6
Author: Ye Hanlin <yehanlin@scratchlab.com>
Date: Sun Nov 14 17:02:21 2021 +0800

user1 add a in alphabet.txt

commit 1d7925b777604f73d4227249e2aad68b9d6bb774
Author: Ye Hanlin <yehanlin@scratchlab.com>
Date: Sun Nov 14 17:01:50 2021 +0800

user1 add 1 in num.txt

与cvs、svn等集中式版本控制系统相比,git在使用时有如下不同:

  • git引入了暂存区,加入暂存区的内容才可提交。当用户修改了很多,需分多次提交时(例如:用户修复了5个bug,需分5次提交),可分批将需提交内容加入暂存区,然后提交,类似于svn的add命令
  • git是分布式版本控制系统。用户commit后,提交进入到本地版本库,push到远程版本库后,其他用户才可看到。git鼓励用户频繁提交以保留历史;少push,以免影响其他用户(例如:用户可频繁提交,然后在下班时进行push)。使用svn等集中式版本控制系统时,频繁提交会引发冲突

工作区

在user1目录下查看proja的组成,命令及结果如下:

tree -a -L 1 proja/
proja/
├── alphabet.txt
├── color.txt
├── .git
└── num.txt

proja目录为工作区,用户操作的目录与文件均位于该目录;proja/.git为暂存区与本地版本库。

工作区是文件系统中的目录树,用户在该目录树下进行开发。

tree proja/
proja/
├── alphabet.txt
├── color.txt
└── num.txt

版本库

.git目录为暂存区与版本库。git版本库类似单链表,示意图如下:

使用git log命令可查看该链表:

git log --graph --pretty=raw
* commit 700e481d0f58aafe01c92583dfd31b5e5cfe47a6
| tree d7be21b272e8793bf891ed72d57ac8898844993b
| parent a99c124d0bef020f14adf0c94c2ca9376f60a1f6
| author Ye Hanlin <yehanlin@scratchlab.com> 1636880560 +0800
| committer Ye Hanlin <yehanlin@scratchlab.com> 1636880560 +0800
|
| user1 add red in color.txt
|
* commit a99c124d0bef020f14adf0c94c2ca9376f60a1f6
| tree 32016b977690a9799a41775cabaa9c9d3f051d4e
| parent 1d7925b777604f73d4227249e2aad68b9d6bb774
| author Ye Hanlin <yehanlin@scratchlab.com> 1636880541 +0800
| committer Ye Hanlin <yehanlin@scratchlab.com> 1636880541 +0800
|
| user1 add a in alphabet.txt
|
* commit 1d7925b777604f73d4227249e2aad68b9d6bb774
tree 96f36c5a16702b8314e9b2d9c89406b12d7a0930
author Ye Hanlin <yehanlin@scratchlab.com> 1636880510 +0800
committer Ye Hanlin <yehanlin@scratchlab.com> 1636880510 +0800

user1 add 1 in num.txt

图中,提交、目录树、文件均以对象的形式存在,可以将对象理解为将内存中的数据结构序列化而生成的文件。git中有四类对象,分别为commit、tree、blob、tag,其中blob用于存储文件。对象由根据其内容、时间戳、作者等计算出的哈希值唯一标识,类似于文件的md5值,该哈希值称为对象id。使用git cat-file命令可查看对象:

git cat-file -t <对象id> # 查看对象类型
git cat-file -p <对象id> # 查看对象内容

链表的每个节点代表一个commit对象,该对象包含作者信息、提交时间、提交说明、父提交、目录树等信息。

git cat-file -p 700e481d0f58aafe01c92583dfd31b5e5cfe47a6
tree d7be21b272e8793bf891ed72d57ac8898844993b
parent a99c124d0bef020f14adf0c94c2ca9376f60a1f6
author Ye Hanlin <yehanlin@scratchlab.com> 1636880560 +0800
committer Ye Hanlin <yehanlin@scratchlab.com> 1636880560 +0800

user1 add red in color.txt

目录树是一个tree对象,记录了该提交对应的目录树快照。git只关心文件数据整体的变化(大多数版本控制系统则只关心文件内容的具体差异),也就是说每次提交,git将记录一次完整的目录树。

git cat-file -p d7be21b272e8793bf891ed72d57ac8898844993b
100644 blob 78981922613b2afb6025042ff6bd878ac1994e85 alphabet.txt
100644 blob a9d1386a1d99728de010ad4044fb984886b57f33 color.txt
100644 blob d00491fd7e5bb6fa28c517a0bb32b8b506539d4d num.txt

目录树中的文件以blob对象的形式存储,目录树只持有blob的id、文件名、权限等信息。

git cat-file -p a9d1386a1d99728de010ad4044fb984886b57f33
red

git存储的是完整的对象(进行了压缩优化)而非差异,这叫做松散对象。例如,num.txt在第一次提交时,内容如下:

1

在第二次提交时,内容如下:

1
2

两次提交的目录树均存储了num.txt的完整内容,而非想象中的第二次提交仅存储差异:

2

这样做,很浪费磁盘空间,但是速度很快。git提供了一些版本库管理命令,对这些对象进行优化、清理,用户可自行执行。

git提交操作相当于表头插入。

暂存区

暂存区与tree对象类似,是一个目录树对象。tree对象是只读的,一旦创建,无法修改;暂存区需频繁读写,git针对此进行了设计,因此暂存区与tree对象在实现上有很大不同。

使用git ls-files查看暂存区:

git ls-files
alphabet.txt
color.txt
num.txt

git ls-files 有如下几个选项:

  • -c:显示暂存区(默认)文件
  • -d:显示存在于暂存区,但已从工作区删除的文件
  • -m:显示存在于暂存区,但已在工作区删除或修改的文件
  • -s:显示暂存区中的对象信息

使用git add向暂存区添加文件:

echo big > size.txt
git add size.txt

git ls-files
alphabet.txt
color.txt
num.txt
size.txt

使用git rm从工作区和暂存区删除文件:

git rm num.txt

使用git restore --staged撤销对暂存区中的更改(添加、删除、修改):

echo rabbit > animal.txt
git add animal.txt
git restore --staged animal.txt # 撤销添加到暂存区中的文件

git rm color.txt
git restore --staged color.txt # 撤销添加到暂存区中的删除

echo 2 >> num.txt
git add num.txt
git restore --staged num.txt # 撤销添加到暂存区中的修改

使用git restore利用暂存区撤销对工作区的更改(删除、修改):

rm color.txt
git restore color.txt # 撤销删除工作区中的文件

echo 2 >> num.txt
git restore num.txt # 撤销对工作区的修改

向暂存区添加、修改文件会生成blob对象。这些blob对象是全量存储的,记录了文件的全部内容。因此,暂存区会产生大量的临时对象,git有超时机制,会定期清理没有被引用的对象。

git diff

git中工作区、暂存区、版本库各存储了版本库的一份副本。执行如下命令后,三份副本各不相同:

echo audi > car.txt
git add car.txt
echo byd >> car.txt

使用git diff --staged查看版本库与暂存区的差异:

git diff --staged
diff --git a/car.txt b/car.txt
new file mode 100644
index 0000000..51a0cb3
--- /dev/null
+++ b/car.txt
@@ -0,0 +1 @@
+audi

使用git diff查看暂存区与工作区的差异:

git diff
diff --git a/car.txt b/car.txt
index 51a0cb3..9b74cca 100644
--- a/car.txt
+++ b/car.txt
@@ -1 +1,2 @@
audi
+byd

分支

在版本库示意图中,如果给表头起个名字(例如:dev、stable等),就可以用名字指代这个提交链,这个名字就叫做分支。git中,分支以文本文件的形式存在,文件名为分支名,文件内容为最新commit id。当执行提交时,该文件的内容自动更新为最新commit id。

git中,分支文件位于.git/refs/heads目录下:

tree .git/refs/heads/
.git/refs/heads/
└── master

cat .git/refs/heads/master
700e481d0f58aafe01c92583dfd31b5e5cfe47a6 # 最新commit id

使用git branch对管理分支:

操作

命令

说明

创建

git branch <branchname> <commit id>

基于commit创建分支

删除

git branch -d <branchname>

删除分支

查看

git branch

查看本地分支

基于上一次提交(使用git log查看)创建dev分支:

git branch dev a99c124d0bef020f14adf0c94c2ca9376f60a1f6

git branch
dev
* master

tree .git/refs/heads/
.git/refs/heads/
├── dev
└── master

cat .git/refs/heads/dev
a99c124d0bef020f14adf0c94c2ca9376f60a1f6

使用git branch查看分支时,master前面有一个*,表示当前分支(版本库中有很多分支,正在使用的那个为当前分支)。git中,当前分支由.git/HEAD表示,该文件为文本文件,内容为分支名。

cat .git/HEAD
ref: refs/heads/master

使用git switch可切换分支,命令格式如下:

git switch <branchname>

切换分支的本质是修改HEAD的内容,HEAD的内容可以是分支的名字,也可以是某个commit id。切换到某个commit id时,需提供-d选项:

git switch -d d8b8607a4e5ea9cfacf90bef08714f2e0745697f

此时,HEAD处于分离头指针状态,无法提交。

下面,切换到dev分支:

git switch dev
Switched to branch 'dev'

git branch
* dev
master

cat .git/HEAD
ref: refs/heads/dev

git switch对工作区/暂存区的影响遵循保留用户修改的原则,即切换分支时,尽量保留用户所做修改。下表列出了执行git switch时,几种典型的工作区/暂存区情况及执行结果:

情形

结果

工作区/暂存区无修改

switch成功,使用新分支的目录树覆盖暂存区/工作区

新增了文件

switch成功,新文件保留在工作区/缓存区

删除了目标分支中不存在的文件(1. 文件名不同2. 文件名相同但文件内容不同)

switch成功

删除了目标分支中存在的文件(文件名与文件内容完全相同)

switch成功,文件被从工作区/暂存区删除

修改了目标分支中不存在的文件(1. 文件名不同2. 文件名相同但文件内容不同)

switch失败,提示提交或保存进度

修改了目标分支中存在的文件(文件名与文件内容完全相同)

switch成功,修改保留在工作区/缓存区

分支的内容除在提交时自动变化外,也可通过git reset进行修改。git reset可使分支指向任一commit,并使用该commit的目录树重置工作区/暂存区。git reset通常用于恢复历史版本,git reset命令格式如下:

git reset [--soft|--mixed|--hard] <commit>

当使用--soft选项时,仅仅将当前分支指向commit,不做重置工作区/暂存区的操作。--soft通常用于丢弃提交(用户修改保留在工作区/暂存区中),当用户对提交本身不满意时,可使用该选项。

git reset --soft <commit>

例如,当用户提交后,发现提交说明中有错别字,可利用--soft丢弃该提交,重新提交。

git reset --soft a99c124d0bef020f14adf0c94c2ca9376f60a1f6

git commit -m "user1 add red in color.txt ^^^"
[master d5964e7] user1 add red in color.txt ^^^
1 file changed, 1 insertion(+)
create mode 100644 color.txt

当使用--mixed选项(mixed是默认选项,可省略)时,除将当前分支指向commit外,还使用该commit的目录树重置暂存区。--mixed通常用于丢弃某commit之后所有的提交记录,重新提交(用户修改保留在工作区),当用户对提交包含的文件不满意时(例如:对bug1的修复提交到了bug2的修复提交中),可使用该选项。

git reset <commit>

当使用--hard选项时,除将当前分支指向commit外,还使用该commit的目录树重置暂存区与工作区。可用于丢弃某<commit>之后所有的修改。

git reset对工作区/暂存区的影响遵循只要用户修改影响了重置,就丢弃用户修改的原则。下表列出了执行git reset时,几种典型的工作区/暂存区情况及执行结果:

情形

结果

工作区与暂存区无修改

reset成功,使用新分支的目录树覆盖暂存区/工作区

工作区新增了文件

reset成功,新文件在工作区保留

工作区与暂存区新增了文件

reset成功,新文件从暂存区与工作区删除

工作区删除了文件

reset成功,若目标commit的目录树中有该文件,该文件被恢复到暂存区/工作区

工作区与缓存区删除了某文件

修改了文件

reset成功,修改丢失

远程版本库

git允许一个版本库与任意多个远程版本库交互(从远程版本库获取数据、向远程版本库推送数据)。使用git remote命令管理远程版本库:

操作

命令

说明

新增

git remote add <reponame> <repopath>

创建远程版本库

删除

git remote rm <reponame>

删除远程版本库

修改

git remote set-url <reponame> <repopath>

git remote rname <reponame> <reponame_new>

修改远程版本库

查看

git remote -v

查看远程版本库

新建两个版本库:

mkdir gitlab
git init --bare gitlab/proja.git

mkdir gitee
mkdir gitee --bare/proja.git

为user1添加两个远程版本库:

git remote add gitlab /home/yehanlin/gitlab/proja.git
git remote add gitee /home/yehanlin/gitee/proja.git

git remote -v
gitee /home/yehanlin/gitee/proja.git (fetch)
gitee /home/yehanlin/gitee/proja.git (push)
gitlab /home/yehanlin/gitlab/proja.git (fetch)
gitlab /home/yehanlin/gitlab/proja.git (push)
origin /home/yehanlin/github/proja.git/ (fetch)
origin /home/yehanlin/github/proja.git/ (push)

每一个远程版本库对应.git/config文件中的一个remote小节。user1的初始版本库从github/proja.git克隆而来,该版本库自动命名为origin。

[remote "origin"]
url = /home/yehanlin/github/proja.git/
fetch = +refs/heads/*:refs/remotes/origin/*
[remote "gitlab"]
url = /home/yehanlin/gitlab/proja.git
fetch = +refs/heads/*:refs/remotes/gitlab/*
[remote "gitee"]
url = /home/yehanlin/gitee/proja.git
fetch = +refs/heads/*:refs/remotes/gitee/*

通常远程版本库的url既是pull url也是push url,可以单独为push设置url(不可为pull单独提供url)。

git remote set-url --push gitee /home/yehanlin/github/proja.git

git remote -v
gitee /home/yehanlin/gitee/proja.git (fetch)
gitee /home/yehanlin/github/proja.git (push)
...

为pull与push设置两个url通常适用于以下情形:某项目有官方库,由核心人员维护。当用户想为该项目贡献代码时,首先克隆一个自己专属的副本库。用户从官方库pull代码;向副本库push代码,然后通过pull requst的方式向官方库提交代码。

remote的fetch参数是一个引用表达式,对于gitlab来说,表示远程版本库gitlab中的分支拷贝到本地版本库的.git/refs/remotes/gitlab/目录下,分支名字不变。这种关系是可配置的,但通常不会有人自找麻烦去修改fetch参数。

git push

使用git push向远程版本库推送数据,命令格式如下:

git push <reponame> <branchname>

branchname是一个引用表达式,可以实现将本地x分支推送到远程y分支的功能。但通常是同名分支推送,因此,该引用表达式可简化为分支名。

在本地版本库创建dev分支,并推送到gitlab远程版本库:

git branch dev
git push gitlab dev

gitlab远程版本库本来没有任何分支,通过git push的方式在gitlab远程版本库创建了dev分支:

cd /home/yehanlin/gitlab/proja.git
git branch
dev

使用-d选项可删除远程版本库中的分支:

git push gitlab -d dev
To /home/yehanlin/gitlab/proja.git
- [deleted] dev

git pull

使用git pull从远程版本库获取数据,获取的数据包括对象、分支等。pull由两个步骤组成:

git pull = git fetch + git merge

fetch是指从远程版本库获取数据;merge是指将从远程版本库合并到的本地版本库。

切换到dev分支,然后执行git pull,我们预期从gitlab远程版本库获取数据:

git switch dev

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=<remote>/<branch> dev

结果,pull失败,提示用户缺少分支跟踪。

分支追踪是指本地版本库的某个分支与哪个远程版本库的哪个分支是关联的。使用git push在远程版本库创建分支时,并没有创建分支追踪。

使用git branch命令管理分支追踪:

操作

命令

说明

设置

git branch -t <remotebranchname> <branchname>


设置分支跟踪

取消

git branch --unset-upstream <branchname>


取消分支追踪

查看

git branch -vv

查看分支追踪

设置本地dev分支追踪远程版本库gitlab的dev分支:

git branch -u gitlab/dev dev
Branch 'dev' set up to track remote branch 'dev' from 'gitlab'.

git branch -vv
* dev a99c124 [gitlab/dev] user1 add a in alphabet.txt
master 315c595 [origin/master] Merge branch 'master' of /home/yehanlin/github/proja

每个分支跟踪对应.git/config中的一个branch小节:

[remote "origin"]
url = /home/yehanlin/github/proja.git/
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
[remote "gitlab"]
url = /home/yehanlin/gitlab/proja.git
fetch = +refs/heads/*:refs/remotes/gitlab/*
[remote "gitee"]
url = /home/yehanlin/gitee/proja.git
fetch = +refs/heads/*:refs/remotes/gitee/*
[branch "dev"]
remote = gitlab
merge = refs/heads/dev

此时,执行git pull就可以成功了:

git pull
Already up to date.

执行pull时,根据当前分支dev在.git/config中查找分支跟踪,然后从remote参数指定的远程版本库中拉取数据,然后合并到merge参数指定的分支中。

用户可以指定本地版本库的x分支追踪远程版本库的y分支,但通常是同名分支追踪。

fetch是指从远程版本库下载对象和引用等,命令格式如下:

git fetch <reponame>

若没有指定远程版本库,根据当前分支的分支追踪确定远程版本库。

远程分支存储在./git/refs/remotes/<reponame>/目录下。使用git branch -r查看远程分支:

git branch -r
gitlab/dev
origin/master

执行git status时,会提示本地分支与远程分支的差距。此时,比较的就是本地分支与remotes下的分支,而非真正远程版本库中的分支。

git status
On branch dev
Your branch is ahead of 'gitlab/dev' by 1 commit.
...

git push时自动更新remotes下的引用,无需再次fetch:

git push
Enumerating objects: 5, done.
...

git status
On branch dev
Your branch is up to date with 'gitlab/dev'.
...

远程分支在remotes目录下,需要将远程分支与本地分支merge,用户才可使用。

merge操作合并的是两个分支。merge将commit对应的目录树和当前分支的目录树进行合并,合并后的提交以当前分支的提交作为第一个父提交,以commit为第二个父提交。

merge的本质是将两颗目录树及其中的文件合并,如果能自动合并则自动合并;如果不能自动合并,则将两个文件的内容合在一起,以冲突的形式标出,让用户自己决策保留哪些部分。理论上,任意两颗目录树均可合并。

git使用如下方式标识冲突:

<<<<<<<
内容1
=======
内容2
>>>>>>>

内容1为本地分支的内容;内容2为远程分支的内容。用户需解决冲突,然后提交,新提交有两个父提交。merge后会覆盖工作区与暂存区。

merge对工作区/暂存区的影响遵循保留修改的原则,即merge时,尽量保留用户当前修改。下表列出了执行merge时,几种典型的工作区/暂存区情况及执行结果:

情形

结果

新增了文件

merge成功,新文件保留在工作区/缓存区

从工作区删除了文件

merge成功,恢复被删除的文件

从工作区和暂存区删除了文件

merge失败,提示先提交再合并

修改了工作区中的文件

merge失败,提示先提交再合并

修改了工作区和暂存区中的文件

merge失败,提示先提交再合并

通常merge由pull自动调用,也可手动执行git merge操作,命令格式如下:

git merge <commit1> <commit2> <commit3>...

该命令将commit1、commit2、commit3对应的分支合并到当前分支,通常只需一个分支。

echo "3" >> num.txt
git add num.txt
git commit -m "user1 add 3 in num.text on dev"
[dev 3e40d0d] user1 add in num.text on dev
1 file changed, 1 insertion(+)

git switch master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.

git merge dev
Merge made by the 'recursive' strategy.
num.txt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)

tag

tag也叫里程碑,本质是命名的提交。比如,某次提交后,用户将程序打包发布,可将此次提交命名为v1.0.0。

tag分为轻量级tag和普通tag。创建轻量级tag方法如下:

git tag <tagname> <commit>

创建tag v1.0.0:

git tag v1.0.0 66beaeef0c40704284ba4bd9d93345a385a027a1

轻量级tag是位于.git/refs/tags目录下的文件,文件名为tag的名字,内容为commit id。

tree .git/refs/tags
.git/refs/tags
└── v1.0.0

cat .git/refs/tags/v1.0.0
66beaeef0c40704284ba4bd9d93345a385a027a1

创建tag的方法如下:

git tag -m <message> <tagname> <commit>

创建tag v2.0.0:

git tag -m "version 2.0.0" v2.0.0 bc61c1fb3fe88207877e1a699b2721a0ec5273e2

创建tag时,会创建一个tag对象,该对象包含commit id、tag作者、时间、message等信息。然后在.git/refs/tags目录下创建一文件文件,文件名为tag的名字,内容为新建tag对象的id。

tree .git/refs/tags
.git/refs/tags
├── v1.0.0
└── v2.0.0

cat .git/refs/tags/v2.0.0
bff3af08213eb11a1195e2faa3bd76d4b685c114

git cat-file -t bff3af08213eb11a1195e2faa3bd76d4b685c114
tag

git cat-file -p bff3af08213eb11a1195e2faa3bd76d4b685c114
object bc61c1fb3fe88207877e1a699b2721a0ec5273e2
type commit
tag v2.0.0
tagger Ye Hanlin <yehanlin@scratchlab.com> 1636880560 +0800

version 2.0.0

使用git tag -d tagname可删除tag:

git tag -d v1.0.0
Deleted tag 'v1.0.0' (was 66beaee)

使用git tag可查看tag:

git tag
v1.0.0
v2.0.0

执行git push时,默认不会将tag推送给远程版本库,需要显示推送。

git push <reponame> <tagname>

执行git pull时,默认会获取tag。tag并没有像远程分支那样存放在.git/refs/remotes目录下,而是与本地tag一样直接放在了.git/refs/tags目录下。使用--no-tags选项,可避免获取tag。

若本地版本库和远程版本库均有名为x的tag,则pull时,会忽略远程版本库中的x tag。

对象管理

版本库长期运行后,会产生许多垃圾,例如:

  • 暂存区产生的临时对象
  • 由于重置而丢弃的提交及相关对象
  • 由于对象全量储存而浪费的磁盘空间

使用git gc命令可对版本库进行一系列优化,例如:

  • 删除未被关联且过期的对象
  • 将松散对象打包,采用增量存储方式,降低版本库大小
  • 将分支、tag等文件打包成一个文件


觉得不错,就请您转发

最近发表
标签列表