第六届字节跳动青训营实践第一篇 | 青训营

114 阅读17分钟

Git 的正确使用姿势与最佳实践:团队协作和版本控制的最佳实践

本文将针对具体实践操作来讲解Git在团队协作和版本控制的具体操作。

1 Git是什么

  • Git是什么?

Git is a free and open source distributed version control system designed to handle everything from small to very large projects with speed and efficiency.

  • 版本控制是什么?

一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统

  • 为什么需要版本控制?

更好的关注变更,了解到每个版本的改动是什么,方便对改动的代码进行检查,预防事故发生;也能够随时切换到不同的版本,回滚误删误改的问题代码;

1.1 版本控制

版本控制.png

1.1.1 本地版本控制

最初的方式

  • 通过本地复制文件夹,来完成版本控制,- 般可以通过不同的文件名来区分版本

解决方案

  • 开发了一些本地的版本控制软件,其中最流行的是RCS

基本原理

  • 本地保存所有变更的补丁集,可以理解成就是所有的Diff,通过这些补丁,我们可以计算出每个版本的实际的文件内容

缺点

  • RCS这种本地版本控制存在最致命的缺陷就是只能在本地使用,无法进行团队协作,因此使用的场景非常有限,因此衍生出了集中式版本控制

1.1.2 集中版本控制

代表性工具:SVN

基本原理:

  1. 提供一个远端服务来保存文件,所有用户的提交都提交到该服务器中
  2. 增量保存每次提交的Diff, 如果提交的增量中和远端现存的文件存在冲突,则需要本地提前解决冲突

优点:

  1. 学习简单,更容易操作
  2. 支持二进制文件,对大文件支持更友好

缺点:

  1. 本地不存储版本管理的概念, 所有提交都只能联上服务器后才可以提交
  2. 分支上的支持不够好,对于大型项目团队合作比较困难
  3. 用户本地不保存所有版本的代码,如果服务端故障容易导致历史版本的丢失

1.1.2 分布式版本控制

代表性工具: Git

基本原理:

  1. 每个库都存有完整的提交历史,可以直接在本地进行代码提交
  2. 每次提交记录的都是完整的文件快照,而不是记录增量
  3. 通过Push等操作来完成和远端代码的同步

优点:

  1. 分布式开发,每个库都是完整的提交历史,支持本地提交,强调个体
  2. 分支管理功能强大,方便团队合作,多人协同开发
  3. 校验和机制保证完整性,一般只添加数据,很少执行删除操作,不容易导致代码丢失

缺点:

  1. 相对SVN更复杂,学习成本更高
  2. 对于大文件的支持不是特别好 (git-lfs 工具可以弥补这个功能)

1.2 Git发展历史

作者

Linus Torvalds (就是Linux这个项目的作者,同时也是Git的作者)。

开发原因

怀疑Linux团队对BitKeeper (另一种分布式版本控制系统,专有软件)进行了逆向工程,BitKeeper 不允许Linux团队继续无偿使用。因此决定自己开发一个分布式版本控制系统。

开发时间

大概花了两周时间,就完成了Git 的代码第一个版本, 后续Linux项目就开始使用Git进行维护。

Github

全球最大的代码托管平台,大部分的开源项目都放在这个平台上。

Gitlab

全球最大的开源代码托管平台,项目的所有代码都是开源的,便于在自己的服务器上完成Gitlab的搭建。

Gerrit

由Google开发的一个代码托管平台,Android 这个开源项目就托管在Gerrit 之上。 每一个平台都有每个平台的优势和适用。

git remote add origin <远程仓库URL>  # 添加远程仓库

git push -u origin <分支名>  # 将本地分支推送到远程仓库

2 Git基本使用方法

Git基本命令

mindmap
      Git命令
          配置
            git config
            git remote
          提交代码
            git add
            git commit
          远端同步
            拉取代码
              clone
              pull
              fetch
            推送代码
              push
    

常见问题

  1. 为什么我明明配置了Git 配置,但是依然没有办法拉取代码?
  • 免密认证没有配。
  • Instead Of配置没有配,配的SSH免密配置,但是使用的还是HTTP协议访问。
  1. 为什么我Fetch了远端分支,但是我看本地当前的分支历史还是没有变化?
  • Fetch会把代码拉取到本地的远端分支,但是并不会合并到当前分支,所以当前分支历史没有变化。

2.1 Git目录介绍

以Windows系统为例

  1. 项目初始化git init
mkdir study
cd study
git init
  1. 其他参数
  • initial-branch 初始化的分支
  • bare 创建一个裸仓库 (纯Git目录,没有工作目录)
  • template 可以通过模板来创建预先构建好的自定义git目录

可查看git的目录文件如下:

git仓库.png

直接打开文件系统也可以看到目录文件。

可输入cat .git/HEAD 可以看到HEAD里面存的是ref: refs/heads/master,表示当前的分支指向的是master

工作区&暂存区

工作暂存区.png

2.1.1 GitConfig

不同级别的Git配置分为三部分:

  • 全局(global)的配置。在当前用户的git/config里面
  • 系统(system)级别的配置。在etc/config里面,配置级别最高
  • 本地(local)级别的配置。在刚才git/config的文件下面 每个级别的配置可能重复,但是低级别的配置会覆盖高级别的配置。

2.1.2 常见Git配置

用户名配置

git config -- global user.name "xxx"

git config -- global user.email "12345@qq.com"

Instead of 配置

git config -- global url.git@github.com:.insteadOf http://github.com/

Git命令别名配置

git config -- global alias.cin"commit --amend --no-edit"

2.2 Git Remote

查看Remote

git remote -v,执行这个命令什么都没有,因为没有加过这个命令。

添加Remote

git remote add origin_ssh git@github.com:git/git.git

git remote add origin_http https://github.com/git/git.git

再查看remote,就会出来,继续cat -git/config会出现添加了remote的配置信息。

同一个Origin设置不同的Push和Fetch URL

git remote add origin git@github.com:git/git

git remote set-url --add --push origin git@github.com:MY_REPOSITY/git

git remote -v

2.2.1 HTTP Remote

go语言开发,每次代码要做一个认证很麻烦,需要配置免密的方式

URL:github.com/git/git.git

免密配置

  • 内存: git config -- global credential.helper 'cache -- timeout=3600',指定cache的时间为3600s,把密码存在内存里
  • 硬盘: git config -- global credential.helper "store -- file /path/to/ credential file',直接把密码写在硬盘里,让它可以永久保存。

不指定目录的情况默认是~/.git- credentials

  • 将密钥信息存在指定文件中,具体格式: ${scheme}://${user}:${password}@github.com

一般不用HTTP的协议去访问git,存在可能不安全的情况,也不太方便。

2.2.2 SSH Remote

URL: git@github.com:git/git.git

免密配置

SSH可以通过公私钥的机制,将生成公钥存放在服务端,从而实现免密访问 目前的Key的类型四种,分别是dsa、rsa、ecdsa、ed25519

默认使用的是rsa,由于一些安全问题,现在已经不推荐使用dsa和rsa了,优先推荐使用ed25519

ssh-keygen -t ed25519 -C"your_email@example.com”密钥默认存在~/.ssh/id_ed25519.pub在github里点击SSH and GPG keys新建一个keys粘贴过去。

2.3 Git Add

根据自己的操作系统新建一个文件

touch readme.md,再继续:

x vim readme.md,输入Hello World,保存一下

git status,目前没有提交的状态

git add .,再git status,再看一下x tree .git

可以看到文件内容跟之前的tree在objects里面会新增一个内容,新增的整个内容就是新增的obj的ID,可以通过命令去看一下文件是什么,存储后是加密的,但是可以通过命令去看到:

x git cat-file -p 367db03de93a4a938c86028e1e1c35dbd3a25be2//p后面把文件的ID输进去 会出来刚才保存的Hello World

2.4 Git Commit

前面已经把文件加到暂存区了,通过commit可以把它真正的提交到目录里面。

x git commit -m "add readme",再观察一下tree .git

可以注意下objects的目录变化,发现多了文件,可以通过-p的命令去看一下多出来的文件里面是什么内容。如果为blob,后面是readme.md结尾则说明这是一个目录数类型的object,代码除了本身的内容还会存在一个目录数。如果是tree,则可以看到这个文件上的变化。

2.5 Objects

接下来介绍一下Objects里面文件类型。

commit / tree / blob 在git里面都统一称为 Object,除此之外还有个tag的object。

Blob

  • 存储文件的内容

Tree

  • 存储文件的目录信息

Commit

存储提交信息,一个Commit可以对应唯一版本的代码

如何把这三个信息串联在一起呢?

  1. 通过Commit寻找到Tree信息,每个Commit都会存储对应的Tree ID.
  2. 通过Tree存储的信息,获取到对应的目录树信息。
  3. 从tree中获得blob 的ID,通过Blob ID获取对应的文件内容。

2.6 Refs

不止多了objects的文件内容,还多了refs/heads/master的内容。

cat .git/refs/heads/master,发现其实是commit下的objects里面的ID

创建一个新的refs:

git checkout -b test,切换到一个新的分支,再打开tree .git,可以发现再refs/heads下面创建了一个叫test的分支。

git checkout <分支名>  # 切换到分支

git merge <分支名>  # 合并分支到当前分支

Refs文件存储的内容

  • refs的内容就是对应的Commit ID
  • 因此把ref当做指针,指向对应的Commit来表示当前Ref对应的版本

不同种类的ref

  • refs/heads 前缀表示的是分支,除此之外还有其他种类的ref,比如refs/tags前缀表示的是标签。

Branch命令也可以创建一个新分支

git checkout -b,一般用于开发阶段,是可以不断添加Commit进行迭代的。

Tag 标签一般表示的是一个稳定版本,指向的 Commit 一般不会变更,通过git tag v0.0.1//版本命名命令生成tag。

2.7 Annotation Tag

什么是附注标签?

  • 一种特殊的Tag,可以给Tag提供一些额外的信息。 如何创建附注标签?
  • 通过git tag -a命令来完成附注标签的创建。
  • 然后通过git cat-file -p 2524xwscg35424da//输入ID进行查看该ID下的内容。

2.8 追溯历史版本

  • 获取当前版本代码

通过Ref指向的Commit可以获取唯一的代码版本。

  • 获取历史版本代码

Commit里面会存有parent commit字段,通过commit的串联获取历史版本代码。

  1. 修改文件,并提交,创建新的commit.

回到hello world的文件进行修改,vim readme.md,回到readme文件,提交一个新的commit,前面加一个#,变成#hello world,再实行一个x git addx git commit -m "update readme"现在objects又新增了。

git log,再git cat-file -p 785bxhwj33874gf//新增的ID,发现内容里多了一个parent的概念,有tree的话是存了一个全新的tree,因为文件内容变了,会生成一个全新的blob,目录数的信息也会改变。

两者的Diff

  1. 新增tree object 3a
  2. 新增blob object 55
  3. 新增commit object 64

test ref指向新的commit

2.9 修改历史版本

  1. commit --amend 通过这个命令可以修改最近的一次commit信息,修改之后commit id会变
  2. rebase 通过git rebase -i HEAD~3可以实现对最近三个commit的修改:
  • 合并 commit
  • 修改具体的commit message
  • 删除某个commit
  1. filter --branch 该命令可以指定删除所有提交中的某个文件或者全局修改邮箱地址等操作

2.10 Git GC

GC

通过git gc命令,可以删除一些不需要的object,以及会对object进行一些打包压缩来减少仓库的体积。

Reflog

reflog是用于记录操作日志,防止误操作后数据去失,通过reflog来找到丢失的数据,手动将日志设置过期。

指定时间

git gc prune=now指定的是修剪多久之前的对象,默认是两周前

2.11 Git Clone & Pull & Fetch

Clone

拉取完整的仓库到本地目录,可以指定分支,深度。

Fetch

将远端某些分支最新代码拉取到本地,不会执行merge操作,会修改refs/remote内的分支信息,如果需要和本地代码合并需要手动操作。

Pull

拉取远端某分支,并和本地代码进行合并,操作等同于git fetch + git merge,也可以通过git pull --rebase完成git fetch + git rebase操作。

可能存在冲突,需要解决冲突。

2.12 Git Push

Push是将本地代码同步至远端的方式。

常用命令

一般使用git push origin master命令即可完成

冲突问题

  1. 如果本地的commit记录和远端的commit历史不一致,则会产生冲突,比如git commit --amend or git rebase都有可能导致这个问题。
  2. 如果该分支就自己一个人使用,或者团队内确认过可以修改历史则可以通过git push origin master -f来完成强制推送,一般不推荐主干分支进行该操作,正常都应该解决冲突后再进行推送。

推送规则限制

可以通过保护分支,来配置一些保护规则,防止误操作,或者一些不合规的操作出现,导致代码丢失。

3 Git研发流程

常见问题

  1. 在Gerrit平台上使用Merge的方式合入代码
  • Gerrit是集中式工作流,不推荐使用Merge方式合入代码,应该是在主干分支开发后,直接Push。
  1. 不了解保护分支,Code Review, CI等概念,研发流程不规范
  • 保护分支:防止用户直接向主干分支提交代码,必须通过PR来进行合入。
  • Code Review, Cl: 都是在合入前的检查策略,Code Review是人工进行检查,CI 则是通过一些定制化的脚本来进行一些校验。
  1. 代码历史混乱,代码合并方式不清晰。
  • 不理解Fast Forward和Three Way Merge的区别,本地代码更新频繁的使用Three Way的方式,导致生成过多的Merge节点,使提交历史变得复杂不清晰。

3.1 不同的工作流

  1. 集中式工作流
  • 代表平台:Gerrit/SVN
  • 特点:只依托于主干分支进行开发,不存在其他分支
  • 合入方式:Fast-forward
  1. 分支管理工作流
  • 代表平台:Github / Gitlab
  • 特点:可以定义不同特性的开发分支,上线分支, 在开发分支完成开发后再通过MR/PR合入主干分支
  • 合入方式:自定义,Fast- Forward or Three _Way Merge都可以

3.2 集中式工作流

只依托于master分支进行研发活动

工作方式:

  1. 获取远端master代码
  2. 直接在master分支完成修改
  3. 提交前拉取最新的master代码和本地代码进行合并(使用rebase),如果有冲突需要解决冲突
  4. 提交本地代码到master

3.2.1 集中式工作流-Gerrit

  • Gerrit是由Google开发的一款代码托管平台, 主要的特点就是能够很好的进行代码评审。
  • 在aosp (android open source project)中使用的很广,Gerrit 的开发流程就是种集中式工作流。

基本原理:

  1. 依托于Change ID概念,每个提交生成一个单独的代码评审。
  2. 提交上去的代码不会存储在真正的refs/heads/下的分支中,而是存在一个refs/for/的引用。
  3. 通过refs/meta/config下的文件存储代码的配置,包括权限, 评审等配置,每个Change都必须要完成Review后才能合入。

优点:

  1. 提供强制的代码评审机制, 保证代码的质量
  2. 提供更丰富的权限功能,可以针对分支做细粒度的权限管控
  3. 保证master的历史整洁性
  4. Aosp 多仓的场景支持更好

缺点

  1. 开发人员较多的情况下,更容易出现冲突
  2. 对于多分支的支持较差,想要区分多个版本的线上代码时,更容易出现问题
  3. 一般只有管理员才能创建仓库,比较难以在项目之间形成代码复用,比如类似的fork操作就不支持

3.3 分支管理工作流

分支管理工作流特点
Git Flow分支类型丰富,规范严格
Github Flow只有主干分支和开发分支,规则简单
Gitlab Flow在主干分支和开发分支之上构建环境分支,版本分支,满足不同发布or环境的需要

3.3.1 分支管理工作流——GitFlow

Git Flow 是比较早期出现的分支管理策略。

  • 包含五种类型的分支
    • Master:主干分支
    • Develop:开发分支
    • Feature:特性分支
    • Release:发布分支
    • Hotfix:热修复分支

优点:

  • 如果能按照定义的标准严格执行,代码会很清晰,并且很难出现混乱。

缺点:

  • 流程过于复杂,上线的节奏会比较慢。
  • 由于太复杂,研发容易不按照标准执行,从而导致代码出现混乱。

3.3.1 分支管理工作流——Github Flow

Github的工作流,只有一个主干分支,基于Pull Request往主干分支中提交代码。

选择团队合作的方式:

  1. owner创建好仓库后,其他用户通过Fork的方式来创建自己的仓库,并在fork的仓库上进行开发
  2. owner创建好仓库后,统一给团队内成员分配权限,直接在同一个仓库内进行开发

每个团队成员在自己的分支上工作,避免直接在主分支上开发。

定期从主分支(如main或master)拉取最新代码,确保自己分支基于最新代码。

在完成任务后,将自己的分支合并到主分支,解决可能的冲突。

使用合适的分支命名约定,如feature/<功能名>或bugfix/<问题描述>。

创建一个Pull Request

  1. 创建一个main主分支
  2. 创建一个feature分支

按照vim readmex git addgit commit -m "add readme"git push origin maingit checkout -b feature,切换到一个新分支,vim readmex git add .x git commit -m "update readme"git push origin feature,发现remote有一个新的链接,将这个链接复制打开,可以找到操作的过程,例如之前案例里新增一个#等,可直接在网址上做讨论。

git log可以看到log里面。

3.3.2 分支管理工作流——Gitlab Flow

Gitlab推荐的工作流是在GitFlow和Github Flow上做出优化,既保持了单——主分支的简便,又可以适应不同的开发环境。

原则: upstream first, 上游优先

只有在上游分支采纳的代码才可以进入到下游分支,一般上游分支就是master。

3.4 代码合并

Fast-Forward

  • 不会产生一个merge节点,合并后保持一个线性历史,如果target分支有了更新,则需要通过rebase操作更新source branch后才可以合入。

git checkout -b test,切换到一个新分支,1s,再vim readme,输入##Whello world保存,再x git add .git commit -m "test",再checkout回main分支,git checkout maingit merge test --ff-only则合并,再git log,发现不会产生一个merge节点。

3.5 如何选择合适的工作流

选择原则:

没有最好的,只有最合适的

针对小型团队合作,推荐使用Github工作流即可

  1. 尽量保证少量多次,最好不要一 次性提交上千行代码
  2. 提交Pull Request后最少需要保证有CR后再合入
  3. 主干分支尽量保持整洁, 使用fast- forward合入方式,合入前进行rebase

大型团队合作,根据自己的需要指定不同的工作流,不需要局限在某种流程中。