Git Guide
Git 是一个分布式版本控制系统,对比 SVN 这类集中式版本控制系统,分布式版本控制系统可以完全去中心化工作,无需与远程中央服务器通信,在本地即可进行全部版本控制操作,即便是离线。
Git 使用指针管理分支,分支是指针指向某次提交,而 SVN 中的分支则是目录的拷贝,这使得 Git 拥有强大的分支管理能力,分支的切换、合并和删除等操作更迅速和灵活。Git 的提交采用快照机制来存储文件的状态,快照存储相对于 SVN 的差异存储,不需要重新计算差异或补丁,这意味着在提交、分支切换或版本回滚等操作时,效率更高,操作也更为高效和可靠。
基本原理
三大分区
Git 项目一共有三大分区:
1 | * Workspace/Working Tree/Working Directory:工作区 |
- 工作区
工作区是当前正在编辑和修改的项目目录,它包含了实际的项目文件。
暂存区是一个中间区域(在 .git/index 文件中),用于暂时存放想要提交到版本库的更改,在 commit 之前,可以选择将工作区中的文件添加到暂存区。
- 暂存区
暂存区设计目的是提供更灵活的版本控制和工作流程。通过暂存区,开发者可以实现选择修改、组织修改、撤销修改。选择要提交的特定文件或文件某次特定修改,将多个相关的修改组织在一起提交,无需频繁地提交,另外,暂存区内的修改可随时撤销,而不会产生提交历史记录。
- 版本库
版本库是 Git 的核心部分(在 .git 目录下),它存储了项目的完整历史记录。版本库包含了所有的提交记录、分支、标签等信息。每次提交更改时,Git 会将暂存区的内容保存为一个新的提交,并更新版本库。
这三大分区也对应着三种状态,分别是 untrack 和 modified、staged、committed。Git 基本的工作流程是,在工作区中修改文件,git add
将想要下次提交的更改选择性地暂存,git commit
提交更改,找到暂存区的文件,将快照永久性存储到 Git 目录。git add
让文件进入 staged,git commit
让文件进入 committed。
Git 对象
Git Object 位于 .git/objects 目录下,一共有三种对象类型:
1 | * blob object: 数据对象 |
每个 Git Object 都有一个唯一的 object id (由 SHA-1 生成,40 位,分为 blob id、tree id、commit id),这也是 object 在 Git 仓库中的唯一身份证,所有类型的 object 文件名都以 object id 命名。
- Blob Object
blob object 存储的是文件的具体内容,对文件内容及 mate 信息做 SHA-1 计算,得到一个 blob id,通过这个 id 就能找到文件内容。
当对工作区修改或新增的文件执行 git add
时,文件内容被写入到对象库中的一个新的 blob object 中,而暂存区的目录树被更新,该对象的 id 被记录在暂存区的文件索引中。
以如下 Git 项目为例,Git 项目目录结构、暂存区内容以及 .git/objects/
目录结构分别如下:
1 | tree . |
1 | git ls-files --stage |
1 | tree .git/objects/ |
cat
object 文件返回结果是乱码,因为 Git 将信息压缩成二进制文件,需要通过 git cat-file [-t] [-p]
查看,-t
可以查看 object 的类型,-p
可以查看 object 储存的具体内容:
1 | cat .git/objects/6d/d90d24d319b452859920bf74120405fcdaa017 # 乱码 xK□□OR0f022□□ |
1 | git cat-file -t 9d07 # blob |
- Tree Object 和 Commit Object
tree object 存储的内容是一个目录的快照,包括文件/文件夹的权限、类型、内容对应的 SHA-1 值(blob id)、名称(从左往右),commit object 存储的内容是这个 commit 对应的快照是哪一个,以及作者、提交时间、提交信息。
当对暂存区的文件执行 git commit
时,暂存区的内容会在 Git 仓库中生成新的 tree object,再生成新的 commit object 指向这个 tree object,然后再将指针(HEAD、Branch Reference)指向新的 commit object 上:
1 | git cat-file -t 8d02 |
注:040000 tree ...
又对其他 object 进行引用,所有的 tree object 就是一棵树结构(目录树)。
1 | git cat-file -t 31c8 |
注:如果有父提交,还会有 parent xxx 信息,Git 也是通过这个来确定 commit 的分支归属的。
HEAD
、分支(分支也是一个引用,指向一个 commit)、Tag
、FETCH_HEAD
都是指针/引用(refs),内容都是一个 commit id,指向对应的 commit。它们被保存在 .git/refs/**/*
、.git/HEAD
、.git/FETCH_HEAD
文件中。
HEAD
指针保存在 .git/HEAD
文件中,默认指向当前分支的最新提交,通过 git checkout commitId
可进入“头部/头指针(HEAD)分离状态(detache HEAD state)”,即 HEAD 不再指向分支引用,而是直接指向某个 commit。
1 | git rev-parse HEAD # 查看当前 HEAD 的位置 |
FETCH_HEAD
是一个特殊的指针,它指向最近一次使用 git fetch
命令从远程仓库获取的提交。它通常用于在合并远程分支之前查看或操作获取的提交。
安装
Linux 上使用 apt(或 yum) 安装 sudo apt install git
,Windows 上可以从 Git 官网下载安装程序,Mac OS 上,Xcode 默认集成了 Git,如果没有安装 Xcode,可通过 Homebrew 单独安装。
另外,Windows 中要想在 CMD 中执行 Git,需要将 Git 添加到环境变量 path 中。
1 | C:\Program Files\Git\bin |
1 | git help [-a|--all] [-g|--guide] [-i|--info|-m|--man|-w|--web] [COMMAND|GUIDE] # git help help 查看 help 命令如何使用 |
注:Windows 版本的 Git 自带 git-gui,包括 gitk,为 Git 提供可视化界面。
配置
Git 共有三级配置,优先级依次是本地配置 > 全局配置 > 系统配置。系统配置在 /etc/gitconfig
文件中(Windows 在 /mingw64/etc/gitconfig
。Windows Git Bash 中的根目录是 Git 的安装目录),全局配置在 ~/.gitconfig
文件中,本地配置在本地仓库的 .git/config
文件中。cat ~/.gitconfig
如下:
1 | [user] |
注:mingw(Minimalist GNU on Windows) 是一款 Windows 上的 GNU 工具集(含 vim、ssh client…),mingw64 是其 64 位版。Windows 版 Git 自带 mingw。
查看配置信息
1 | git config --system --list # 查看系统配置 |
设置配置信息
最常见的自定义配置是用户信息(用户名和邮件地址,commit 时用)、Commit Message Template、Credential Helper。
- 用户信息
用户信息用于 commit 时标示用户身份,git log 的提交日志中可以查看,如果没有配置,在 MacOS 下会使用操作系统用户名。
1 | git config --global user.name "Tracy" # 姓名 |
注:–global 参数是全局参数,也就是这些命令在这台电脑的所有 Git 仓库下都有用。
- 提交模版
Commit Message Template
1 | git config --global commit.template /d/commit-template |
- 换行
由于各操作系统文本文件所使用的换行符不一样,UNIX/Linux/OS X 使用的是 LF
,Windows/Dos 使用的 CRLF
。Git 默认提供了一个“换行符自动转换”功能。
1 | git config --global core.autocrlf xx # true、input(推荐使用)、false |
true
表示开启自动转换,迁入时将文件换行风格转换成 Unix 风格,迁出时根据本地系统确定是否转换成 CRLF,input
表示迁入的时候将换行风格转换成 Unix 风格,迁出时不做处理,false
表示迁入迁出都不对换行风格进行处理。
- 其他
除了上述常用配置,还有其他的配置,比如颜色、别名。
1 | # 显示颜色 |
注,可直接编辑配置文件来设置配置信息。
1 | git config --system -e # 系统配置 |
.gitignore
.gitignore 配置文件用于忽略文件,不被添加到版本库中,其配置语法如下。
1 | 以斜杠 / 开头表示目录 |
1 | # 忽略所有 txt 结尾的文件 |
注:Git 对于 .gitignore 配置文件是按行从上到下进行规则匹配的,如果前面的规则匹配的范围更大,则后面的规则将不会生效。
.gitattributes
- merge.ours.driver
merge.ours.driver 配置在有冲突时使用 ours(当前分支) 合并策略,常用来,比如根据不同分支做不同持续集成的 .gitlab-ci.yml。
1 | git config --global merge.ours.driver true |
配置 .gitattributes 文件如下
1 | .gitlab-ci.yml merge=ours |
注:merge.ours.driver
默认就是 true,不用设置,只需要配置 .gitattributes 文件即可。
版本管理
1 | # 初始化仓库 |
通过哈希值指定提交记录不太方便(尽管只需要 4 位,但是也需要通过 log 查询出具体 commit id),所以 Git 引入了相对引用,^
表示向上移动 1 个提交记录,~<num>
表示向上移动多个提交记录。比如,HEAD
(即 HEAD~0
) 表示当前版本,HEAD^
是上一个版本,HEAD^^
是上上一个版本,依此类推,HEAD~100
表示往上第 100
个版本,master^
表示 master 分支的父提交。
- reset
git reset
三种模式 --mixed
、--soft
、--hard
最主要区别是对目标提交到当前提交的差异以及工作区和暂存区未提交部分处理方式不同。--hard
下工作区和暂存区未提交会被清除,差异也全部清除,--soft
下工作区和暂存区保留,差异会存入暂存区,--mixed
下工作区保留,差异和暂存区都会存入工作区。--hard
参数具破坏性的,它会永久删除工作区和暂存区中未提交的更改(已提交的可通过 git reflog
找回,但未提交的不能),务必小心,所以 --hard
常用来切换版本,--mixed
和 --soft
用来修改提交。
注意:如果本地分支已经被提交到了远程仓库,在使用了 git reset
后本地库 HEAD
指向的版本比远程库的要旧,push
时会报错 Updates were rejected because the tip of your current branch is behind
,这时候需要使用 git push -f
强制推上去。
- revert
git revert
也能做到版本回滚,但是原理与 git reset
完全不一样,git reset
是在各个版本中切换,而 git revert
会生成新的版本。
reset 前:
reset 后(目标版本之后的版本不见了):
rever 前:
revert 后(生成了一个新的版本):
分支管理
分支是用来进行并行作业的。git init
会默认创建 master
主分支。git commit
每次提交,Git 都会自动把它们串成一条时间线,这条时间线就是一个分支。如果只有一个分支,那么也只有一条时间线。
1 | # 查看分支(当前分支前面标有 × 号) |
头部分离
git checkout
除了常见的创建/切换分支、切换标签、恢复文件外,还能切换提交,通过 git checkout commitId
或 git checkout --detach branchName
进入”头部/头指针(HEAD)分离状态(detached HEAD state)“,该状态下 HEAD 不再指向任何分支,而是一个提交。
detached HEAD 不是分支,只是一种临时状态,常用于查看、修改、调试特定的提交,或者基于该 commit,创建新的分支,当然也可以回滚提交(不推荐,detached HEAD 不是具体分支,不能 push,还需要创建新分支来承载)。在 detached HEAD 上进行提交时,这些提交只是临时的,但如果切换到其他分支,这些提交可能会被丢失,因此,在 detached HEAD 状态下进行的工作完成后,需将其合并到一个具名分支上,以确保提交的持久性。
以以下场景为例,在执行完 git checkout 版本二 commitId
后,HEAD
引用指向了版本二,但是当前分支的引用仍然指向版本三,这个仓库目前处于一个头部分离状态。
执行前:
执行后:
远程仓库
1 | # 拉取远程仓库提交,但不合并(git fetch 的目的是 git merge FETCH_HEAD 合并,或者 git checkout FETCH_HEAD 切换到 FETCH_HEAD 查看) |
注:不带任何参数的 git push,默认只推送当前分支,这叫做 simple 方式。此外,还有一种 matching 方式,会推送所有有对应的远程分支的本地分支。Git 2.0 版本之前,默认采用 matching 方法,现在改为默认采用 simple 方式。如果要修改这个设置,可以采用 git config 命令。
1 | # matching |
标签管理
Git 中的标签和分支有点类似,都是引用或者说指针,不过标签的位置是固定的,在给指定提交打好标签以后,它就固定于此位置,而分支的位置是会不断变动的,随着分支的向前推移或者向后回滚,都在不断变化。分支和标签的用处也不一样,分支用于并行作业,而标签用于处理发布。
Git 标签分为两种类型:轻量型(lightweight)和附注型(annotated)。轻量标签是指向特定提交对象的引用,而附注标签则是仓库中的一个独立对象,它有自身的校验和信息,包含着标签的名字,电子邮件地址和日期,以及标签说明,标签本身也允许使用 GNU Privacy Guard (GPG) 来签署或验证。建议使用附注标签,以便保留相关信息。标签名应采用统一的格式,v${MajorVersion}.${MinorVersion}.${FixVersion}-${TypeLabel}
,其中 TypeLabel (alpha、 beta…) 可选。
1 | # 查看标签 |
存储管理
Stash 可用在一些特殊的工作场景中,比如,需要临时修复 Bug,可以把当前工作现场储藏起来,等 Bug 修复后恢复现场后继续工作。在 Git Flow 下,Stash 基本用不到。
1 | # 保存存储 |
远程仓库
远程中心化仓库可用来备份存储和共享协作。
登录
- SSH 登录
使用 SSH 登录 GitHub 流程以下:
1 | cd ~/.ssh # 查看是否已经生成 ssh 密钥 |
ssh-keygen 生成的 rsa 默认命名是 id_rsa (私钥) 和 id_rsa.pub (公钥),如果在键入上述命令回车之后,重新输入了命名,那此时生成的两个文件就是 [命名] 和 [命名].pub
- Git Credential
除了使用 SSH 实现免密登录,还可通过 Git Credential 保存登录名和密码(默认所有都不缓存,每一次连接都会询问你的用户名和密码),每次自动输入用户名和密码,实现免密登录,Git 使用 credential.help 来存储本地凭证,其所支持的选项如下。
1 | cache: cache 模式会将凭证存放在内存中一段时间,密码不会被存储在磁盘中,并且在 15 分钟后从内存中清除 |
可通过 Git 的帮助文档来查看我们系统支持哪种 helper。
1 | # Linux 下 |
1 | git config --list | grep credential # 查看默认配置 |
Mac 默认 credential helper 是 osxkeychain,Windows 默认是 manager,而 Linux 默认不存储,需手动设置,以 store 模式为例。
1 | git config --global credential.helper store |
这样,会生成 ~/.git-credentials
文件,用户信息会以明文保存在里面,https://{userName}:{password}@github.com
,比如 https://Tracy-xu:395083226%40gh@github.com
。
注:要操作一个远程仓库,首先要有 Git 服务器登录权限,然后要有项目权限(项目的所有者,或者 Collaborators – 协作者)。
基本命令
1 | git clone [remote url] [local url] # 克隆远程仓库(在 GitHub 可以使用 HTTPS 和 SSH 协议) |
搭建 Git 服务器
SSH 作为 Git 所支持通信协议其中之一,如果想通过 SSH 来使用 Git,则需要安装 SSH Server。Linux 一般都自带 SSH,而 Windows 配置起来麻烦,所以这里以 Linux 为例。
当然,在实际使用中不单单只会用到一个简单的非常底层的 Git 服务器,还有其他很多功能会使用到(比如,提供图形化界面的用户管理、SSH 管理、Log、Issue、Pull Request,甚至 Wiki 和持续集成),所以一般会使用 GitHub、GitLab、GitBucket 基于 Git 实现的 Git 仓库托管系统。
- 安装 Git
1 | sudo apt-get install git # CentOS 用 yum 带 -y 参数安装(-y 参数不用一步步询问) |
- 创建一个 Git 用户,用来运行 Git 服务
1 | sudo adduser git |
- 设置证书登录
收集所有需要登录的用户的公钥,就是他们自己的 id_rsa.pub 文件,把所有公钥导入到 /home/git/.ssh/authorized_keys
文件里,一行一个(参考 Linux 相关章节。在没有像 GitHub、GitLab 这样的平台来管理 authorized_keys,管理员手动管理 authorized_keys 很麻烦)。
1 | # 验证证书登录是否设置成功(如果让输入密码则没有成功,如果不让输入则设置成功) |
注:~/.ssh
目录在用户目录下(不同用户 ~
下输入 pwd
,结果会是 /root
或者 /home/xxx
),修改 ~/.ssh/authorized_keys
只对当前用户生效。另外,如果不设置证书登录,默认会使用密码登录。
- 初始化 Git 仓库
1 | # 选定一个目录作为 Git 仓库,假定是 /srv/sample.git,在 /srv 目录下输入命令 |
与 git init
不同的是,git init --bare
被用来创建“裸库”,裸库没有 work tree(工作区),只有 .git 目录,记录着版本历史,一般用于公共的远程中央仓库,因为服务器上的 Git 仓库纯粹是为了共享,所以不让用户直接登录到服务器上去改工作区。另外,服务器上的 Git 仓库通常都以 .git 结尾。
- 禁用 Shell 登录
出于安全考虑,第二步创建的 git 账户不允许登录 Shell(GitHub 就是如此,ssh git@github.com
),只允许 Git 相关操作,所以要把 Shell 登录改为 git-shell 登录。编辑 /etc/passwd
文件,为 git 用户指定的 git-shell 即可(参考 Linux passwd 相关章节)。
1 | git:x:1001:1001::/home/git:/bin/bash |
还要复制一个名为 git-shell-commands 的目录,要不然 ssh git@192.168.10.140
时会报 fatal:Interactive git shell is not enabled 错误。
1 | cp /usr/share/doc/git-1.8.3.1/contrib/git-shell-commands /home/git -R |
现在,不管是开机登录,还是 su 切换用户登录,还是 SSH 远程登录,git 用户登录的都将是 git-shell,进来看到的是 git>
而不是 [root@localhost ~]#
。另外,不能将 git 账户登录权限改为 /sbin/nologin
,禁用登录,要不然客户端在 clone 时会报 fatal: protocol error: bad line length character: This
错误。
- 克隆远程仓库
1 | git clone git@192.168.10.140:/srv/sample.git |
直接 clone 会提示这是一个空仓库。也可以在本地创建一个仓库,然后添加 remote 地址。
1 | git init |