Git的正确使用姿势与最佳实践|青训营笔记

162 阅读14分钟

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.

版本控制

版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。通过版本控制,能更好的关注变更,了解到每个版本的改动是什么,方便对改动的代码进行检查,预防事故发生;也能够随时切换到不同的版本,回滚误删误改的问题代码。

版本控制工具分类:

版本控制类型代表性工具解决的问题
本地版本控制RCS本地代码的版本控制
集中式版本控制SVN提供一个远端服务器来维护代码版本,本地不保存代码版本,解决多人协作问题
分布式版本控制Git每个仓库都能记录版本历史,解决只有一个服务器保存版本的问题

本地版本控制

最初的方式是通过本地复制文件夹,来完成版本控制,一般可以通过不同的文件名来区分版本。后来开发了一些本地的版本控制软件,其中最流行的是 RCS, RCS原理是本地保存所有变更的补丁集,可以理解成就是所有的Diff,通过这些补丁,我们可以计算出每个版本的实际的文件内容。

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

集中版本控制

集中版本控制代表工具是SVN,它的原理是提供一个远端服务来保存文件,所有用户的提交都提交到该服务器中,增量保存每次提交的Diff,如果提交的增量中和远端现存的文件存在冲突,则需要本地提前解决冲突。

优点是学习简单,更容易操作;且支持二进制文件,对大文件支持更友好。

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

分布式版本控制

分布式版本控制代表工具是Git,它的原理是每个库都存有完整的提交历史,可以直接在本地进行代码提交;每次提交记录的都是完整的文件快照,而不是记录增量;通过 Push 等操作来完成和远端代码的同步。

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

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

Git发展历史

Git的作者是Linus Torvalds,由于bitkeeper不是开源的,所以一开始其实linux团队的人就很排斥使用该软件。Bitkeper是一个专有软件,由于Linux项目的一个开发人员,写了一个工具去连接Bitieper,因此被怀疑是对Bitkceper做了逆向工程,因此这个公司就不允许 Linux团队继续使用。大概花了两周时间,就完成了Git的代码第一个版本,后续Linux项目就开始使用Git 进行维护。

随着Git的发展,基于Git也衍生出了很多平台,除此之外,还有BitBucket,Coding,码云,阿里云效平台等等,每个平台都有自己的使用场景和优势,我们选择最合适自己的平台即可。

平台描述
Github全球最大的代码托管平台,大部分的开源项目都放在这个平台上。
Gitlab全球最大的开源代码托管平台,项目的所有代码都是开源的,便于在自己的服务器上完成 Gitlab的搭建。
Gerrit由Google开发的一个代码托管平台,Android这个开源项目就托管在Gerrit之上。

Git基本使用

基本命令

image.png

目录介绍

在这里我们要重点关心一下这个git目录,因为我们后续每一个git操作都会映射到这个git目录之中,通过这里面的文件我们可以映射出所有版本的代码。

.GIT
├─hooks
├─info
├─logs
│  └─refs
│      ├─heads
│      └─remotes
│          └─origin
├─objects
│  ├─info
│  └─pack
└─refs
    ├─heads
    ├─remotes
    │  └─origin
    └─tags

工作区、暂存区的数据流动:

Config

通过git config --help命令可以查看config的配置,Git配置有多个级别,每个级别的配置可能重复,但是低级别的配置会覆盖高级别的配置。

--global
For writing options: write to global ~/.gitconfig file rather than the repository .git/config, write to $XDG_CONFIG_HOME/git/config file if this file exists and the ~/.gitconfig file doesn’t.
For reading options: read only from global ~/.gitconfig and from $XDG_CONFIG_HOME/git/config rather than from all available files.
See also FILES.
--system
For writing options: write to system-wide $(prefix)/etc/gitconfig rather than the repository .git/config.
For reading options: read only from system-wide $(prefix)/etc/gitconfig rather than from all available files.
See also FILES.
--local
For writing options: write to the repository .git/config file. This is the default behavior.
For reading options: read only from the repository .git/config rather than from all available files.
See also FILES.

常见配置:

用户名配置:
git config --global user.name "liaoxingju"
git config --global user.email liaoxingju@bytedance.com

Instead of 配置:
git config --global url.git@github.com:.insteadOf https://github.com/

Git命令别名配置:
git config --global alias.cin "commit --amend --no-edit"

Remote

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

同一个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

上述仓库拥有多个remote,从没有写权限的仓库fetch代码,push到自己有权限的仓库。

我们知道了什么是remote配置后,那我们本地是如何与remote进行通信的呢,一般会通过 http 和ssh 两种协议,这两种协议都需要对身份进行认证,类似go这种语言,依赖库很多,所以我们需要不断的输入认证的账号密码,肯定是—件很麻烦的事情,因此我们需要配置一下免密的认证方式。

免密配置:
内存: git config --global credential.helper 'cache --timeout=3600'
硬盘: git config --global credential.helper "store --file /path/to/credential-file"
不指定目录的情况默认是~/.git-credentials

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

SSH可以通过公私钥的机制,将生成公钥存放在服务端,从而实现免密访问目前的Key的类型四种,分别是dsa、rsa、ecdsa、ed25519。默认使用的是rsa,由于一些安全问题,现在已经不推荐使用dsa和rsa了,优先推荐使用ed25519。

ssh-keygen -t ed25519 -C "your email@example.com"
密钥默认存在~/.ssh/id_ed25519.pub文件中

Add

使用git add命令可以建立跟踪文件,使用git status可以查看哪些文件未被跟踪。

Commit

使用git commit将文件提交到本地仓库中。

Objects

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

Blob: 存储文件的内容。

Tree: 存储文件的目录信息。

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

可以通过命令将这三种文件联系到一起:

1.通过Commit寻找到Tree信息,每个Commit都会存储对应的Tree ID。
git cat-file -p 373137d6db9018d97279384fb5644e880a9c1b1b

2.通过Tree存储的信息,获取到对应的目录树信息。
git cat-file -p 239ec593c6a2192e76c005435f748b2ad28be832

3.从tree中获得blob 的ID,通过Blob ID获取对应的文件内容。
git cat-file -p e69de29bb2d1d6434b8b29ae775ad8c2e48c5391

Refs

当我们创建一个分支后,发现refs中的heads也有变化,refs的内容就是对应的Commit lD,因此把ref当做指针,指向对应的Commit来表示当前Ref 对应的版本。refs/heads前缀表示的是分支,除此之外还有其他种类的ref,比如refs/tags前缀表示的是标签。

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

git tag命令可以创建标签,标签一般表示的是一个稳定版本,指向的Commit一般不会变更。

Annotation Tag

一种特殊的Tag,可以给Tag提供一些额外的信息,通过git tag -a命令来完成附注标签的创建。

追溯历史版本

通过Ref 指向的Commit 可以获取唯一的代码版本,Commit 里面会存有parent commit字段,通过commit 的串联获取历史版本代码。

修改历史版本

1.commit -amend
通过这个命令可以修改最近的一次 commit 信息,修改之后commit id会变

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

通过git commit --amend命令,尝试修改一下commit message,修改Commit后我们可以发现git object又出现了变化新增commit object 7f,但是之前的commit object 63并没有被删除。此时63就变成了悬空的object,顾名思义就是没有ref指向的object,

GC

GC:

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

Reflog:

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

指定时间。

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

完整的Git视图

git的各种操作:

操作后的目录变化:

远端操作

Clone:

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

Fetch:

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

Pull:

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

Push:

Push是将本地代码同步至远端的方式。可以通过保护分支,来配置一些保护规则,防止误操作,或者一些不合规的操作出现,导致代码丢失。

Git研发流程

工作流

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

集中式工作流

集中式工作流是指只依托于master分支进行研发活动。

工作方式如下:

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

集中式工作流的代表平台是Gerrit,Gerrit是由Gooqle开发的一款代码托管平台,主要的特点就是能够很好的进行代码评审。在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 操作就不支持。

分支管理工作流

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

Git Flow

Git Flow是比较早期出现的分支管理策略。包含五种类型的分支:

  1. Master:主干分支
  2. Develop:开发分支
  3. Feature:特性分支
  4. Release:发布分支
  5. Hotfix:热修复分支

优点:

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

缺点:

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

Github Flow

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

选择团队合作的方式:

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

Gitlab Flow

Gitlab推荐的工作流是在GitFlow和Github Flow上做出优化,既保持了单一主分支的简便,又可以适应不同的开发环境。原则是upstream first 上游优先, 只有在上游分支采纳的代码才可以进入到下游分支,一般上游分支就是master。

代码合并

Fast-Forward

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

Three-Way Merge

三方合并,会产生一个新的merge节点。

如何选择合适的工作流?没有最好的,只有最合适的, 针对小型团队合作,推荐使用Github工作流即可

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

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

常见问题

  1. 在Gerrit平台上使用Merge的方式合入代码?

Gerrit是集中式工作流,不推荐使用Merge方式合入代码,应该是在主干分支开发后,直接Push。

  1. 不了解保护分支,Code Review,Cl等概念,研发流程不规范。

通过使用保护分支防止用户直接向主干分支提交代码,必须通过PR来进行合入。Code Review、CI等都是在合入前的检查策略,Code Review是人工进行检查,CI则是通过一些定制化的脚本来进行一些校验。

  1. 代码历史混乱,代码合并方式不清晰。

不理解Fast Forward和Three Way Merge的区别,本地代码更新频繁的使用Three Way的方式,导致生成过多的Merge节点,使提交历史变得复杂不清晰。

课后作业

  1. 熟悉一个开源项目,学习开源代码,整理成学习笔记,提交到Github 上。
  2. 尝试向开源项目提一个Pull Request。