Git 的正确使用姿势与最佳实践
本文并不会具体介绍 git 的很多命令,侧重于深入理解 git 的基本原理,侧重于原理,带你剖析
.git
文件夹下的东西到底是什么
Git 是什么
基本原理
- 每一个库都存有完整的提交历史,可以直接在本地进行代码提交
- 每次提交记录的都是完整的文件快照,而不是记录增量
- 通过
push
等操作来完成和远端代码的同步
优点
- 分布式开发,每一个库都是完整的提交流失,支持本地提交,强调个体
- 分支管理功能强大,方便团队管理,多人协同开发
- 校验和机制保证完整性,一般只添加数据,很少执行删除操作,不容器导致代码丢失
缺点
- 相对 SVN 来说更复杂,学习成本更高
- 对于大文件的支持不是很好(git-lfs 工具可以弥补这个功能)
Git 的出现就是为了当初为了维护 Linux 开发出现的,和 Linux 是同一个创始人
Git 的发展历史
-
Github 全球最大的代码托管平台
-
GitLab 全球最大的开源代码托管平台,项目的所有代码都是凯源的,便于自己在服务器上部署 GitLab
-
Gerrit 由谷歌开发的一个代码托管平台,Android 就托管在这个平台上,对于多仓库有更好的支持
-
Gitee 国内的代码托管平台
Git 基本使用方式
常见问题
- :tada: 为什么明明配置了 Git 配置,但是依然没有办法拉取代码
没有配置密钥,但最初遇到这个问题是在 push 到远端的时候一直无响应,后来发现要配置一下密钥,但是使用 http 方式的话就不需要啦.如果使用的是 ssh 方式无论是拉取还是 push 都要配置密钥
生成/添加SSH公钥 - Gitee.com
Github 也是同样的道理
还有一种可能就是仓库本身设置了权限,总结一下就是,还是权限出了问题,
- :cactus: 为什么明明
fetch
了远端分支,但是我看本地当前的分支历史还是没有变化
fetch
只会更新本地记录的 origin
分支,也就是更新本地记录的远端分支到远端仓库的代码
git remote -v # 查看远程分支
初始化
mkdir demo
cd demo
git init
git init -h
--template <template-directory>
directory from which templates will be used
--bare create a bare repository
--shared[=<permissions>]
specify that the git repository is to be shared amongst several users
-q, --quiet be quiet
--separate-git-dir <gitdir>
separate git dir from working tree
-b, --initial-branch <name>
override the name of the initial branch
--object-format <hash>
specify the hash algorithm to use
条三个比较重要的
--template
参数指定 模版文件,git将会根据这个模版文件创建一个仓库--bare
创建一个裸仓库,不包含工作区-b
参数 初始化分支main
或者master
,当然也可以配置全局默认值
其实有一个比较重要的概念就是 裸仓库
, 其实就是只存在仓库记录本身,不存在实际的工作区. 重要用途就是作为中心仓库供其他人推拉代码的. 这里其实牵扯到 Git 的存储原理,其实简单说就是 Git 会保存每一次提交的文件. 所以,单单只有一个 .git
文件夹也是可以恢复整个仓库的. 那么对于中心仓库来说,也仅仅存储 .git
文件夹下面的东西就好了,没必要再单独的保存一个特定分支的文件.因为每一个提交和分支都已经保存在了 .git
中,下面会介绍 Git 的详细原理
浅谈Git的内部原理
Git 配置
Git 有三个级别的配置
graph TD;
A(全局 global)
B(系统 system)
C(本地 local)
A -->B
B --> C
~/.gitconfig
System
/etc/gitconfig
Local
.git/config
每一个级别的配置可能重复,但是低级别的配置会覆盖高级别的配置
基本配置
- 用户名配置
git config --global user.name "xiaoming"
git config --global user.email "xiaoming@gmail.com"
instead of
配置
git config --global url.git@github.com:.insteadOf https://github.com/
ssh
协议换 http
协议 或者其他协议的更换,注意前后顺序,是前面的替换后面的
Git
命令别名配置
git config --global alias.cin "commit --amend --no-edit"
使用 cin
来替换 commit --amend --no-edit
可以直接使用 git cin
Remote 配置
git remote add origin_ssh git@github.com:Tom-debug110/demo.git
git remote add origin_http https://github.com/Tom-debug110/demo.git
配置好以后,可以打开 .git/config
看到一些东西
cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = https://github.com/Tom-debug110/demo.git
fetch = +refs/heads/*:refs/remotes/origin/*
[remote "origin_ssh"]
url = git@github.com:Tom-debug110/demo.git
fetch = +refs/heads/*:refs/remotes/origin_ssh/*
尤其是后面几行,就是 [remote]
相关的配置,甚至我们可以手动更改里面的配置项
其他配置
git remote rename <old> <new> #更改远端名称
git remote remove <name> #移除某一个远端
git remote -v #输出当前所有远端信息
能否设置同一个远端不同的 push
和 fetch
源
上面通过 git remote add
默认情况下, fetch
push
是同一个源,但是是可以设置不同源的
git remote add origin git@github.com:Tom/project.git
git remote set-url --add --push origin git@github.com:Other/project.git
通过 git remote -v
即可看到效果
接着来看看 .git/config
文件发生了什么变化
尤其是最后一条,相比于其他的 remote
多了一个 pushurl
http 和 ssh
访问远端仓库一般有两种协议,一种是 http
,另外一个就是ssh
无论是从安全性还是访问速度来看,都推荐使用 ssh
的方式来访问仓库,但是一定不能忘记配置公钥到服务器上,而且推荐使用 ed25519
具体方法
生成/添加SSH公钥 - Gitee.com
然后在 Github
Gitee
平台中的个人账户设置中配置即可,如果要配置多个不同的密钥,参考以下:
Git配置多个SSH-Key - Gitee.com
Git Add
这里关于暂存区和工作区的介绍不再赘述
执行git add
前后的区别
<font color=pink>注意看线框中的部分</font>
多出来的这个东西,应该连起来才有意义,可以使用
git cat-file -p e69de29bb.....5319
输出的就是刚才我们README.md
中添加的内容
上面截图的时候忘记使用
vim
打开README.md
文档添加内容啦,如果有读者读到,还请自行添加一些内容到README.md
文档,否则将没有内容输出
Hello World
Hello World
经过 git add .
操作,已经把文件添加到了暂存区
Git Commit
git commit -m 'add files'
执行命令后,发现
相比于 git add .
之后多了两个这些东西(暂时叫东西吧)
下面来看一下都是何方圣神
git cat-file -p 4690 #其实不输入完整也是可以的哈
# 输出
100644 blob 8254f875c6f8d41f9137917a46804bc5eb9e781f README.md
后面不就是我们的文件名吗,其实,上面的意思表示这个玩意是一个目录树类型的 object
再看一下另外一个
git cat-file -p b4e2af
#输出
tree 4690e50c687a1e77c71bb5461ee3e4357df8d20c
author mixinju <mixinju@outlook.com> 1676031166 +0800
committer mixinju <mixinju@outlook.com> 1676031166 +0800
add file
这不就是刚才的提交信息吗. 先看第一行, tree指明了目录树,后面的一串是不是很眼熟呢?不就是指明了目录树的校验和值吗
第二行指明了作者,第三行指明了提交者,大部分情况下作者就是提交者,但也有特殊情况吧
git log
# 输出
commit b4e2af8233708a5dcb763f4dde699208d773af40 (HEAD -> main)
Author: mixinju <mixinju@outlook.com>
Date: Fri Feb 10 20:12:46 2023 +0800
add file
(END)
出现的b4e2af8233708a5dcb763f4dde699208d773af40
不就是.git/object/
下其中一个吗
:tangerine: **Git 中的
Object
**
commit
tree
blob
在 Git 里面都统称为Object
,除此之外还有一个tag
的Object
Blob
存储文件的内容Tree
存储文件的目录信息Commit
存储提交信息,一个commit
可以对应唯一版本的代码除此之外,还有一个叫做
Tag
的Object
,下面说到Git tag
的时候会介绍到
:apple: 如何把这三个信息串联在一起呢?
-
通过
commit
找到tree
信息,每一个commit
对应着一个tree
的 id -
通过
tree
存储的信息,获取到对应的 目录树的信息(tree 中可能存有多个 blob) -
从
tree
中获取blob
信息
➜ demo git:(main) tree .git/objects
.git/objects
├── 46
│ └── 90e50c687a1e77c71bb5461ee3e4357df8d20c
├── 82
│ └── 54f875c6f8d41f9137917a46804bc5eb9e781f
├── b4
│ └── e2af8233708a5dcb763f4dde699208d773af40
├── info
└── pack
5 directories, 3 files
➜ demo git:(main) git cat-file -p b4e2
tree 4690e50c687a1e77c71bb5461ee3e4357df8d20c
author mixinju <mixinju@outlook.com> 1676031166 +0800
committer mixinju <mixinju@outlook.com> 1676031166 +0800
add file
➜ demo git:(main) git cat-file -p 4690
100644 blob 8254f875c6f8d41f9137917a46804bc5eb9e781f README.md
➜ demo git:(main) git cat-file -p 8254
Hello World
Hello World
➜ demo git:(main)
:warning: 一个 tree
Objects
可能含有多个 blob
比如下面:
Git Refs
Branch
先简单说一下分支吧
git branch dev1 # 基于当前分支创建一个 dev1 的分支,但是不切换过去,还需要手动切换
git branch -a # 列出远端和本地的所有分支
git branch -d dev1 # 删除 dev1 分支
git branch -m dev1 dev2 # 重命名dev1 分支为 dev2
git checkout -b dev2 # 创建一个 dev2 分支并切换过去
git switch -c dev2 # 创建一个 dev2 分支并且切换过去
git checkout dev2 # 从当前分支切换到 dev2 分支
git switch dev2 # 从当前分支切换到 dev2 分支
对于分支切换和创建都建议使用
git switch
命令
Ref
接下来看一下 .git/refs
文件夹里面的内容
tree .git/refs
.git/refs
├── heads
│ └── main
└── tags
然后查看一下这个 main
里面保存着什么
cat .git/refs/heads/main
b4e2af8233708a5dcb763f4dde699208d773af40
接着继续查看这个 校验和表示的内容
cat-file -p b4e2af8233708
#输出
tree 4690e50c687a1e77c71bb5461ee3e4357df8d20c
author mixinju <mixinju@outlook.com> 1676031166 +0800
committer mixinju <mixinju@outlook.com> 1676031166 +0800
add file
会发现,这不就是刚才的那个提交吗?
现在新建一个分支( branch )来看看
git switch -c test
Switched to a new branch 'test'
tree .git/refs
.git/refs
├── heads
│ ├── main
│ └── test
└── tags
2 directories, 2 files
会发现在 .git/refs/heads/
目录下多了一个 test
条目,接下来看一下这个 test
里面存储的什么
.git/refs/heads/test
b4e2af8233708a5dcb763f4dde699208d773af40
会发现和上面的 main
是相同的校验值,也就是同一个 commit
,因为我们的 test
分支就是从 main
分支切换过来的,而且目前还没有在 test
分支上做任何的事情,自然就和 .main
分支一样,指向了同一个分支
Tag
标签一般表示一个稳定版本,指向的 commit
一般不会变更
git tag v1.0.0. # 创建一个 tag
来查看一下 .git/refs
中的变化
tag v1.0.0
tree .git/refs
.git/refs
├── heads
│ ├── main
│ └── test
└── tags
└── v1.0.0
2 directories, 3 files
可以看到 .git/refs/tags
下多出来的一个文件(校验值)
cat .git/refs/tags/v1.0.0
b4e2af8233708a5dcb763f4dde699208d773af40
还是 main
分支上的最后一个 commit
Annotation Tag
这个是附注标签,可以给 tag
添加一些额外的信息
git tag -a v2.0.0 -m "add feature1"
使用 -m
选项来添加附注,继续查看 .git/refs/tags
下变化
git tag -a v2.0.0 -m "add feature1"
tree .git/refs/tags
.git/refs/tags
├── v1.0.0
└── v2.0.0
0 directories, 2 files
cat .git/refs/tags/v2.0.0
b63b9bc0d5c2aa210791266f20f5386eb50178de
这个时候我们会发现,此时的这个校验值已经不再是最新的那个 commit
啦. 查看 .git/objects
会发现新增了一个文件(夹)
使用 git cat-file
查看这个 id
这个和前面提到的那个 tag
Object
就对应上了
总结一下
-
refs
文件存储的内容就是对应的commit id
可以把ref
当作一个指针,指向对应的。commit
来表示当前ref
对应的版本 -
refs/heads
前缀表示的是分支(branch), 除此之外还有其他种类的ref
,比如refs/tags
前缀表示的是标签
追溯历史代码
:baby_bottle: 获取当前版本代码
可以通过 ref
指向的 commit
可以获取唯一的代码版本
:game_die: 获取历史版本代码
commit
里面存有 parent commit
字段,通过 commit
的串联获取历史版本代码
我们上面没有看到这个字段是因为没有进行连续的多个提交,就是没有产生迭代,自然也就没有
parent
只说啦
echo "Hello world KKKKKKKKKKK" > README.md
git add .
git commit -m 'add kkkkk'
查看 .git/objects
下的文件
新增了三个 object
其实这三个 object
分别是一个 commit
存储提交信息,一个 blob
存储文件信息,一个 tree
存储目录树信息
然后查看 commit
,到底哪一个是 commit
呢,使用 git log
来查看一下
按
q
退出即可
查看这个 commit
对应的信息
注意看,这里就出现了 parent
字段,而且的确是上一次 commit
的 id
,同时,main
分支也更新到了最新的 commit
,在 .git/refs/heads
下,查看 main
分支当前的指向
修改历史版本
- :lantern:
commit --amend
通过这个命令来修改最近一次提交的 commit
信息,<font color=pink>修改之后的 commit id 会发生变化</font>,具体的不再通过查看 id
的形式演示了,就给一个图示吧
graph TD;
A((commit1))
B((commit2))
C((commit3))
D(commit4 add file)
E(修改commit4 提交信息)
F((commit5))
A -->B
B -->C
C -.废弃.->D
C -->E
E -->F
其中的 commit4
也被称为悬空的 Object
可以使用 git fsck --lost-found
来查找这个悬空的 commit
git fsck --lost-found
Checking object directories: 100% (256/256), done.
dangling commit f209eb9c50dbb6efe0d5c545dd741f565ab130fd
- :ear_of_rice: rebase
先学习一下使用吧,下面都是比较权威、清晰的讲解,建议先看文档,再看视频.
Git - 变基 (git-scm.com)
git rebase: 人生无法重来,但代码可以!
建议实际操作一下,就会对这两个玩意有更清楚的认识啦
总结
git rebase
是非常强大的命令,要搞懂它的基本使用,注意一点就是不要对还有他人引用进行 rebase
操作
git rebase的时候捅娄子了,怎么办?在线等…… - 掘金 (juejin.cn)