代码版本管理工具(git/gerrit/repo)

注意
本文最后更新于 2024-03-18,文中内容可能已过时。

原文链接:https://ovea-y.cn/code_version_control_tools__git_gerrit_repo/

一、版本控制工具的历史

在版本控制软件出现之前,就具备diff与patch工具来对源码进行比较和打补丁了,在CVS出来的一段时间里,Linus一直在使用diff与patch工具管理着Linux的代码。diff与patch也是源码版本控制中最基本的概念。

-u 表示使用 unified 格式

-r 表示比较目录

-N 表示将不存在的文件当作空文件处理,这样新添加的文件也会出现在patch文件中

text

diff -urN a.c b.c > c.patch

Pasted image 20240318221704

通过patch可以将原始文件变成目标文件,也可以将目标文件变为原始文件。

以上面例子继续讲解,使用patch将a.c变成b.c

text

patch a.c c.patch

或者将b.c变成a.c

text

patch -R b.c c.patch

RCS全称**Revision Control System,**RCS远比SVN、CVS要久远,它的作用是将diff集合使用自己的格式存储在存储设备中。使用这些diff集合,就可以将文件回溯到之前的任意一个版本。

Pasted image 20240318221718

通过diff -n a.c b.c 产生RCS格式的diff内容

Pasted image 20240318221733

集中版本控制工具相比RCS,每个人都可以知道项目中其他人在做些什么,管理员也可以进行一些细粒度的控制。

集中式代码管理核心是服务器。拉取代码,解决冲突,提交代码都必须经过服务器,如果脱离了服务器就无法工作。

Pasted image 20240318221743

Pasted image 20240318221751

分布式管理控制系统,在每个客户端都有仓库的完整数据备份,可以看到所有的提交历史记录。任何服务器出现问题,都可以从其他客户端将存储库复制回服务器进行恢复。

在使用分布式管理控制软件的时候,开发流程被大大简化了,每个人都可以以不同的方式在同一个项目中进行协作开发。

Pasted image 20240318221802

二、Git原理

git不再存储的信息是一组文件差异信息,而是存储类似文件系统的一系列快照。每当提交或保存项目状态的时候,git都会记录当前文件的信息,并且存储其快照的引用。

Pasted image 20240318221814

git所有内容在存储前都会计算SHA-1校验和,然后以校验和作为引用。在进行数据传输的过程如果发生损坏或者丢失都可以通过该校验和得知文件被损坏的信息。

git一般只能添加数据,当你提交过代码之后,这段代码就永远不可能在之后被抹除了。

git中管理的项目主要有三种状态

  • 修改:意味着已经修改项目中的文件,但是并未提交到数据库
  • 暂存:在当前版本标记了已经修改的文件,之后便可以提交快照
  • 提交:意味着将数据安全存储到本地数据库中

它们对应着git项目的三个主要部分:工作树、暂存区域和git目录

Pasted image 20240318221846

工作树是项目中checkout的某一个版本,它们从git目录的压缩数据库中提取并释放到存储设备中,可以访问和修改。

暂存区域是一个文件,通常也在git目录中,保存了下次将要提交的文件列表信息,也被称为索引。

git目录是git存储项目元数据和对象数据库的地方,它也是在clone存储库时实际复制的内容。

在进行提交时,git会保存一个提交对象(commit)。该提交对象包含一个指向暂存内容快照的指针,作者的信息,以及指向它父对象的指针。

第一次提交没有父对象,普通提交产生一个父对象,多分支合并产生的提交对象有多个父对象。

当进行git commit操作时

  1. git会计算每一个子目录的校验和,然后在git仓库中将这些校验和保存为树对象(tree)。
  2. git会创建一个提交对象,它除了包含上述信息以外,还包含指向这个树对象(根目录文件夹)的指针。

下图 tree 记录的是文件夹信息,blob记录的是文件快照。

Pasted image 20240318221900

另一个例子:

Pasted image 20240318221912

Pasted image 20240318221923

一共有四种存储对象:

  • blob存储文件数据,通常是文件
  • tree:类似目录,用来管理tree和blob
  • commit指向一个tree标记项目某个特定时间点状态
  • tag:用来标记某一个提交(commit)

关于分支

分支其实是指向commit对象的指针,例如git目录下的HEAD记录着现在分支指针存储的位置。

Pasted image 20240318222037

指针的都存储在refs/heads/[分支名]的文件中。

Pasted image 20240318222047

多次提交后的链状结构

由于每次提交后,都会有指向父commit对象的指针,因此后续的版本都可以向上进行跟踪。

Pasted image 20240318222056

git分支的本质就是指向commit对象的指针。git的默认分支名是master,之后进行提交操作之后,名为master的指针会自动指向最后提交的commit对象。

下图的HEAD也是一个指针,它指向当前checkout代码的位置。

Pasted image 20240318222105

通过git branch可以创建分支,例如git branch testing。需要注意,通过这个指令创建分支之后,HEAD指针依旧指向master指针。

Pasted image 20240318222114

通过git checkout切换分支,类如git checkout testing

Pasted image 20240318222128

假如此时进行提交,HEAD指针以及它指向的指针都会自动移动到新的commit对象上。

Pasted image 20240318222141

假如此时checkout回到master分支,进行了不同的修改并提交,就会产生下面的效果。分叉。

Pasted image 20240318222152

text

git log --oneline --decorate --graph --all

上面的指令可以在命令行显示项目提交历史、各分支指向和项目分叉情况。

text

git log --graph --decorate --oneline --simplify-by-decoration --all

只查看分叉相关的信息

Pasted image 20240318222205

假如现在有一个场景,在iss53分支上进行着项目问题的修复工作,正准备将修复成果合入到master分支,此时执行git merge [分支名]指令即可。由于master分支和iss53分支不在一条链上(直接祖先),所以git在合并的时候需要进行一些额外工作,git会使用两个分支的末端快照(C4和C5)以及两个分支的公共祖先(C2)进行三方合并。

Pasted image 20240318222214

git将三方合并的结果做了一个新的快照,并且自动创建了一个新的commit指向它,这个commit存在多个父提交。

Pasted image 20240318222223

当在两个不同的分支中,对同一个文件的同一部分进行不同的修改,git此时无法自动对它进行合并,因此会产生冲突消息,让开发者来处理。此时git进行了合并,但是没有创建commit对象。当开发者执行git add之后,git就会把它们标记为冲突已经解决,此时再次commit会创建commit对象。

text

<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
 please contact us at support@github.com
</div>
>>>>>>> iss53:index.html

三、Git常用操作

git cherry-pick [commit的SHA1值]

大家最常用的操作(因为gerrit为每一个commit都提供了cherry-pick的快捷方式),将一个commit添加到当前commit对象后面,强制将该commit的父指针指向当前commit对象。由于时间和父指针信息都改变了,因此该commit对象和另一个分支的commit对象已经出现差异了,不再是同一个。

Pasted image 20240318222306

Pasted image 20240318222317

由于cherry-pick导致一堆“重复”的commit对象出现,让分枝树变得异常庞大且难以追踪而且很容易发生丢失change的情况!

未来进行补git树图(一堆cherry-pick的commit对象…)

但是cherry-pick适合一种情况,当拉出一个稳定分支之后,如果后续有问题需要修复,可以通过cherry-pick的方式将修复节点挑选出来,直接应用在稳定分支中,而不需要将此前的节点合入。

git merge [分支名|SHA1值]

一般更好的方式是通过merge的方式将开发版分支提交到稳定版中,这样会保留分支merge信息,不会产生多余的commit对象。这样可以更好的跟踪项目修改变动情况,在分叉树中会保留merge的信息。

但是多个团队进行修改的仓库比如framework还是更适合cherry-pick,此类项目的merge操作适合owner控制风险进行操作,一次merge会包含很多不同部门的修改,如果此前有cherry-pick,甚至还会发生冲突!更严重的是发生不可知的冲突!只有大家都是用merge操作才能规避这种问题。

Screenshot 2024-03-18 at 22.26.12

后续补个merge的git树图,和cherry-pick的进行对比

d71af279-6f95-47c8-84c3-34d320930ce4

用法是git rebase [分支名/SHA1值],可以将另一个分支上的所有提交全部拷贝到当前分支(由于这样做,该分支的分支指针会一并跟着HEAD指针移动,因此这个分支的所有commit对象就将会“丢失”,除非有其他的分支指针也指向这个commit对象),此时它们commit对象的父指针和时间会发生变化。

特点是让提交历史变成线性,此前的分支情况将不会被记录。一般用于拉取远程仓库代码时使用,如果拉取远程仓库时使用merge,会有一笔多余的merge提交记录。

2419eb29-debe-411b-adc6-7a430e1d917c

正常情况下,push操作是下面这样

git push [远程服务器名或url] [本地分支名或SHA1]:refs/heads/[远程分支名]

也可以简化成

git push [远程服务器名或url] [本地分支名或SHA1]:[远程分支名]

如果本地分支和远程分支名相同,则可以直接省略远程分支名。

git push [远程服务器名或url] [本地分支名]

但是提交gerrit需要按照下面的方式进行push

git push [远程服务器名或url] [本地分支名或SHA1]:refs/for/[远程分支名]

作用是拉取远程仓库的代码。

例如git fetch origin refs/heads/main-t是拉取origin远程服务器上的master-t分支代码。

git fetch origin refs/heads/main-t && git cherry-pick FEATCH_HEAD

上面则是只拉取某个commit节点的代码,然后后续跟着对该节点的操作。

pull是fetch与merge的组合使用方法。

当然如果加上–rebase参数,pull将会变成fetch与rebase的组合。

正常情况下一般都是使用git pull –rebase [远程服务器名] [分支名]的方式拉取更新的服务器端的代码,当然也可以简化为git pull –rebase(如果说找不到远程端或分支名,还是需要输入全部的指令来进行指定)。

用于将git数据库中某个版本的文件数据还原,并将HEAD指针指向相应的commit对象。HEAD不指向任何分支指针的时候,被称为分离头部,才是可以通过git switch -c [分支名]创建一个新的分支指针并checkout到该指针。

HEAD指针同一时间只能带着一个分支指针在commit时进行移动!

Pasted image 20240318223126

git checkout -b [分支名]也具有创建新的分支指针的作用。

通过git branch可以看到当前仓库所有的分支指针。

后续补图,branch的信息

通过git branch -d [分支名]可以删除对应的分支指针。

也可以通过git branch [分支名]创建分支指针。

reflog是git用于记录本地仓库分支更新的一种机制,它会记录所有分支曾经指向过的commit对象(但是只会保留90天),因此我们通过git reflog,可以看到没有被任何分支或标签指向的commit对象。

后续补图,reflog的信息

此时就可以通过git reset的方式回退操作,例如git reset HEAD@{2}

reset指令用于回退版本,可以指定回退到某一次提交的版本。

它一共有三种回退模式:

  • soft: 仅回退到某个版本,没有其他效果
  • mixed(默认):重置暂存区的文件和回退的版本保持一致,工作区文件内容不变(即已经修改的文件,没有添加到暂存区,更没有commit的文件)
  • hard:撤销工作区的所有未提交修改内容,将暂存区与工作区都回退到之前的版本。

使用例子:

  • git reset –hard HEAD或git reset –hard HEAD~0
    • 表示回退到当前版本,实际上是用于清除所有的未提交修改。
  • git reset –hard HEAD^或git reset –hard HEAD~1
    • 表示回退到上一个版本(相对于HEAD指针),并清除所有的未提交修改。

用于保存当前工作记录,将暂存区和工作区的改动都保存起来。

执行完git stash之后,工作区和暂存区将会恢复到修改之前的样子。

通过git stash list可以查看保存的列表。

通过git stash pop [–index/stash_id]可以将保存的信息还原,还原后会删除这个保存记录。

通过git stash apply [–index/stash_id]可以将保存的信息还原,但是不删除保存的记录

通过git stash drop [stash_id]用于删除某个存储进度,不指定则删除最新的

通过git stash clear清除所有存储的记录

四、Gerrit

Gerrit 是一个基于 web 的代码评审工具, 它基于git版本控制系统。旨在提供一个轻量级框架, 用于在代码入库之前对每个提交进行审阅。开发人员的修改首先将上传到Gerrit, 但实际上并不成为项目的一部分, 直到它们被审阅和接受。

Pasted image 20240318223233

它的工作流程类似上面这个图。

提交gerrit审阅的方法

在需要提交的仓库执行git log,找到远程仓库的信息

后续补图

git push [remote服务器名(此处是origin)] HEAD:refs/for/[远程分支名(此处是main-t-stable)]

(此处为git push origin HEAD:refs/for/main-t-stable)

此处的意思是,让git将HEAD指针位置以及之前对远程分支main-t-stable的修改部分,全部提交到gerrit的一个审阅区域。可以注意提交的时候远程路径位置是refs/for/[分支]而不是refs/heads/[分支],refs/for/是gerrit定义的一个路径。

如果使用repo upload进行提交,repo会自动提交到refs/for的位置。

查看gerrit项目的分支

Screenshot 2024-03-18 at 22.34.25

由于在repo init的时候进行了一些优化,只拉取了当前正在使用的仓库分支,因此如果需要拉取对应分支,需要进行下面的操作。

拉取对应分支的例子:

text

# 当前默认拉取的分支是release-t-1401
git fetch origin refs/heads/main-t
git fetch origin refs/heads/release-t-1400
git fetch origin refs/heads/release-t-1320:aosp13 
# 上面aosp13是指拉取分支的同时,在本地为它创建一个aosp13的分支名
# 如果没有起名字,则分支名默认是origin/远程分支名,但是实际上checkout时可以省略远程服务器,
# 只checkout远程分支名,此时git会自动在本地创建一个分支来跟踪对应的远程分支

然后通过checkout切换分支

五、Repo的使用

Android使用Git作为代码管理工具,开发了Gerrit进行代码审核以便更好的对代码进行集中式管理,还开发了Repo命令行工具,对Git命令进行封装,将几百个Git库有效的进行组织。Repo并不是用来取代Git,而是用Python对Git进行了一定的封装,简化了对多个Git版本库的管理。对应Repo管理的任何一个版本库,都需要使用Git命令进行操作。

  1. 运行repo init,克隆Android的一个清单库。这个清单库是通过XML技术建立的版本库清单。清单库中的manifest.xml文件,列出了几百个版本库的克隆方式。包括版本库的地址和工作区地址的对应关系,以及分支的对应关系。
  2. 运行repo sync,分别克隆这几百个版本库到本地的工作区。
  3. 运行repo start创建并切换到本地工作分支。
  4. 本地修改代码,通过Git相关命令在某些项目中进行操作。
  5. 通过repo upload命令将代码修改发布到代码审核服务器。

Pasted image 20240318223553

text

repo init -b main -u https://android.googlesource.com/platform/manifest

关于init指令参数的介绍

-b参数:指定拉取的分支。

-m参数:指定拉取的manifest。

推荐的代码拉取方式

  1. 使用manifest的名字创建代码拉取目录
    1. mkdir aosp-main; cd aosp-main
  2. 在目录中进行初始化和代码拉取
    1. repo init -b main -u https://android.googlesource.com/platform/manifest
    2. repo sync

repo初始化之后,当前默认使用的清单文件位于.repo/manifest.xml,它记录了当前使用的manifest文件,是在初始化时通过-m指定的。

后续补图

在.repo/manifests文件夹中,manifest仓库中所有的manifest。.repo/manifests本身也是一个git仓库。

后续补图 <default remote=“public” revision=“main-t”>

  • remote记录的是远程仓库服务器的一些信息,拉取代码时的服务器url,上传代码时的服务器url,review代码时使用的服务器url,以及为这条信息起一个名字。
  • default这里标识了后续项目默认都使用public配置的远程服务器信息,以及默认拉取分支是main-t
  • include用于包含其他的manifest文件。
  • project条目记录了一个项目名、以及它拉取之后需要checkout的路径。这里也可以配置使用的remote配置和使用的分支。groups记录了这个项目所配置的组,在拉取代码的时候可以指定只拉取某些group的项目。

repo smartsync: 可以配置同步方式,比如遇到错误后停止,或者强制删除未提交修改的项目再同步

代码拉取 repo sync 项目不存在时,相当于执行

git clone

项目存在时,相当于

git remote update

git rebase origin/branch
分支创建 repo start 是git checkout –b的封装,但是不同的是git创建分支是从当前分支拉取,repo创建分支是从manifest中指定的commit对象上直接创建的。
检出commit对象 repo checkout 对git checkout封装
读取各项目分支列表,并且汇总显示 repo branches

直接读取.git/refs的饮用获取分支列表以及发布状态
查看项目工作区下的文件差异 repo diff 是对git diff的封装
将工作区改动添加到暂存区 repo stage 对git add –interactive封装
repo prune 对git branch –d封装,用于扫描各项目已经合并的分支并删除
repo abandon 对git branch –d的封装
repo status 对git diff-index、git diff-filse命令的封装
repo upload 封装git push,但是是推送到类似gerrit的指令

六、GRF机制

为了节省成本,延长设备的更新周期而提出的一个方案。允许Framework层继续更新,而供应商的软件版本无需进行更新(主要是指具有硬件依赖的代码,比如HAL接口以及Kernel下运行的软件)。

Pasted image 20240318224212

vendor部分软件可以冻结四个Android大版本。

但是vendor在第五年必须提供新的版本。

Pasted image 20240318224221

GRF机制的出现,就是现在一个机型某个版本的代码,有两个不同的manifest来控制项目和版本的原因。其中Vendor部分包含Android全套代码(但是缺少很多定制的部分),产物包括Kernel和Framework部分。而System则不包含Android全套代码,缺少kernel以及其他和设备相关的部分,但是包含了framework的定制部分。

其中System的代码对应着System、System_ext、product等分区项目的构建,Vendor的代码对应着boot、recovery、odm等分区的构建。

原文链接:https://ovea-y.cn/code_version_control_tools__git_gerrit_repo/

相关内容