Git指南

本文最后更新于:2023年12月17日 下午

基本概念

简单介绍

Git是目前世界上最先进的分布式版本控制系统(没有之一)。Git是由Linux内核社区以Linus为主基于BitKeeper开发的分布式版本控制系统,具有快速、简单、分布式、能够高效管理大规模项目的特点。

本地仓库结构

Git本地仓库逻辑结构可以分为三部分:

  • 工作区 Working Directory:就是存放项目文件、代码的地方,对于添加、修改、删除文件的操作,都发生在工作区中;
  • 暂存区 Staging Area、Index:用于临时存放你的改动,将工作区中的操作进行保存;
  • 版本库 Repository:把所有的commit组织起来,也就是保存所有提交的数据,记录历史版本。
上图也描绘了基本的工作流程,当你在工作区完成阶段性操作后,就可以执行`add`命令,将修改保存到暂存区。当你觉得完成这次开发后,就可以执行`commit`命令,提交到版本库。此外`.git`这个目录是在git自带的一个隐藏项目,在初始化的时候会自动生成,里面的各个目录就记录着实现版本控制所需的数据,例如对象库,对象概念将在稍后介绍。

网上也有不少关于git的文章,下图来自于这篇文章Git:工作区、暂存区、版本库、远程仓库[1],更为详细地展示了git整个流程。其中 Remote 表示是远程仓库,可以认为是代码托管的服务器/远程备份,像我们最为熟知的就是Github。

git基本流程

Git中的对象

Git中有三种基本对象:

  1. blob:文件,保存文件的数据内容
  2. tree:目录树,保存目录结构和文件名等信息
  3. commit:提交,保存提交内容、作者、提交者、提交时间等信息

注意:这里说的是基本对象,还有说法是tag也是一种对象,用来标记某一次提交(commit) ,类似于里程碑的一种记录

而对象有对象名,是40个字符(20字节的16进制表示)的字符串,由SHA-1计算文件内容的哈希值,当没有冲突歧义的时候,可以就用前几位字符表示(但不可以少于4位)。

在git中,对象之间的关系如下图所示,commit包含一个tree对象(主目录),而一个tree对象可以包含多个blob(文件)和多个tree(子目录)。

blob,tree,commit之间的关系

可以用get cat-file -t [对象名]查看某个对象的类型,如下图所示,该对象是一个tree:

查看对象类型

可以用get cat-file -p [对象名]查看某个对象的内容,如下图所示,可以看到该tree对象下有一个blob文件,和两个tree对象(两个子目录):

查看对象内容

单分支管理

初始化和提交

1
2
3
4
5
# 将自动地在当前目录完成版本库的初始化,即在当前目录下生成一个.git的隐藏目录
git init

# 将自动在当前目录下创建一个指定项目名的目录,并在该目录下创建一个名为.git的隐藏目录
git init <项目名>

一个新的.git目录下通常会包括7个文件/目录:

  • hooks:包含客户端或服务端的钩子脚本(hooks scripts)
  • info:默认包含一个全局性忽略文件
  • objects:存储所有的对象
  • refs:存储所有分支和tag(指向commit对象的指针)
  • config:版本库级别的配置文件
  • description:仅供GitWeb程序使用
  • HEAD:当前指针,用于当前操作

而当进行add操作之后就会出现一个index文件

  • index:保存暂存区的信息

最为常用的就是addcommit的命令了:

1
2
3
4
# 将path所指定的目录或文件添加到暂存区
git add <path>
# 将暂存区中<path>所指定的目录或文件添加到版本库,同时产生一个新的commit,path默认为当前目录
git commit [<path>] -m "<提交说明>"

但其实这两个命令,是由许多个更底层的命令组成的:

  • git update-index --add --cacheinfo 命令将对象库中的blob添加到暂存区
  • git update-index --add 将工作区中的文件添加到暂存区(同时在对象库中生成blob对象)
  • git write-tree 命令根据当前暂存区内容在对象库中新建一个tree对象
  • git read-tree 命令将tree对象读到暂存区
  • git commit-tree 命令可以将指定的tree对象”封装“成commit对象保存在版本库中

add和commit的命令分解

提交历史和管理

这里就列举一些比较关键常用的命令吧:

  • git diff :可以显示工作区、暂存区、版本库中文件具体的差别
  • git log [-n] :显示最近提交的commit记录;-n 参数表示显示最近n条记录;还可以通过添加 --pretty=raw 参数来显示commit对象的内容
  • git status:显示工作区、暂存区、版本库中文件增删改的情况
  • git stash [list | [pop|apply] [--index] [stash名] ]:将当前的修改进度暂时保存起来
    • 没有参数就是现场保存
    • 参数为list表示查看保存现场的栈的内容
    • 参数为pop或apply表示将栈中的内容恢复,区别在于pop后栈的记录会被删除而apply不会
  • git blame <文件名> :可以按行查看指定文件的最后一次修改的commit,修改人,时间

多分支管理

游标(head):可以看成是一个指针,指向某个特定的commit对象或者分支,即当前操作的默认对象,如git log,git status等命令中,版本库的当前操作commit就是head所指向的commit对象。

分支(branch):可以看成是一个指针,指向了一个提交(commit),在该分支上的所有提交都会对该分支进行更新,保证该分支一直指向最新的commit

注意:head可以指向分支或者commit,但是分支总是指向commit

经典场景:功能特性的开发——在添加一个新功能时可以通过再新的分支上进行开发,从而使得功能特性的开发不会影响到正常的版本迭代,在开发完成后再通过分支的合并将该功能添加到最新的版本上。

  • git branch <分支名>:该命令可以在当前head指向的commit位置创建一个指定分支名的branch分支。master就是一个branch
  • git checkout [-b] < commit >:该命令的主要功能为改变head游标,将其指向commit
    • 因为branch和tag本质上都是一个commit对象,所以这里用了< commit >参数,其实也可以写成分支名
    • -b 参数是创建并切换

有时候在操作切换分支的时候,我们可能会遇到一个很经典的问题:**分离头指针状态(detached HEAD)**。该状态表明当前head没有指向任何认知,而是指向了一个独立的commit,如下图所示。

分离头指针状态

想要恢复也很简单,使用checkout命令切换到一个分支上即可。


分支会随着commit的提交而更新,但是有时候需要记录一个稳定的版本,如v1.1,v1.2,此时就需要引入tag。

tag可以记录某个特定的commit,并使其不随着commit的提交而改变其指向的commit位置。tag可以看成是一个指向commit的常量指针,所有对commit的操作都可以对tag使用。

  • git tag < tag名 >:可以在当前head指向的commit位置建立一个指定tag名的tag

当前需要对分支进行修改/重置的时候,可以使用下面的命令,将当前分支指向某commit

  • git reset [--soft | --mixed | --head] < commit >
    • 中间参数默认为 –mixed
    • --soft:会将当前指向的branch指向指定的commit
    • --mixed(或无参数):除了改变branch的位置外,还会使用该commit的内容更新暂存区的内容
    • --hard :除了改变branch的位之外,还会用该commit的内容更新暂存区和工作区的内容(危险操作!贸然使用容易丢失当前工作区的修改

分支修改


还有几个分支相关的重要命令:

  • git merge < commit >:将当前head所指向的分支和指定commit分支进行合并,最后会生成一个新的commit,该commit的parent指针同时指向两个commit

  • git rebase < commit >: 变基操作,该命令将提交到某一分支上的所有修改都移至另一分支上

  • git rebase -i HEAD~3 : -i 选项进入交互式界面,可以对前三个commit进行压缩

关于merge和rebase的相关概念可以参考这篇文章git rebase 和 merge 的区别[2],这里就不再重复造轮子了,下图就是简单示意,上面是merge后的分支变化,下面是rebase后的分支变化。

merge(上)rebase(下)

多人协作

多人协作时就会涉及远程仓库的概念,简单理解就是远程服务器上的git版本库。目前常见的基于git的代码托管平台有Github、Gitee、Gitlab等,可以简单的把代码托管平台看成更是一个远程的git仓库,通过push、fetch等命令来进行操作。

这里就简单罗列相关命令:

  • git clone :在当前目录下创建指定的git仓库的克隆
  • git push:将当前指向的分支推送至远程版本库
  • git fetch:将远程版本库中本地没有的记录拉取到本地
  • git pull:该命令等于git fetchgit merge [fetch命令获得的head]
  • git remote -v :可以查看当前使用push、fetch、pull命令同步的远程版本库

多人协作开发

大型项目协作开发

Github上项目的开发流程如上图所示,如果我们对别人的项目感兴趣,可以通过fork操作,将他人的项目仓库复制一个自己的项目仓库,然后通过fetch、push对个人仓库进行操作。而通过Pull requests操作,可以申请将自己个人仓库上的特定分支与他人仓库上的特定分支进行合并。


但是如果我们把带有bug的分支提交到远端,并合入了 master ,这时候就需要用到 git revert命令来撤销 commit 修改,具体可以参考 git revert 命令 。进一步的,思考这样一个场景,如果一个 repo 的运营人员不小心在提交的 commit 上暴露了隐私信息(例如数据库账号密码),那么此时应该怎么处理呢? 这里我提供的思路是,本地使用 git reset --hard来重置commit 历史,并覆盖远程分支。

总结

大体上把git重要的知识点都介绍了一下,但实际上主要常用的命令可能就那么几个,add、commit、push、pull、merge、rebase、checkout,把这几个命令用熟了基本够用了,遇到不会的现查就是了(话不能这么说啊)。 总归是了解得越多越好,只不过上面这几个命令算是基本要求了,不能不会啊~

最后再推荐几个git相关的学习参考网站:

备注/参考


Git指南
https://2017zhangyuxuan.github.io/2022/03/12/2022-03/2022-03-12 Git指南/
作者
Zhang Yuxuan
发布于
2022年3月12日
许可协议