OpenMMLab 贡献者成长体系教程
  • OpenMMLab 贡献者成长地图
  • 活动
    • 超级视客营
    • MMSIG任务领取指南
  • 通用贡献者教程(建设中)
    • 开源协议学习(建设中)
    • 环境配置(建设中)
      • Windows 环境配置
      • Github Codespaces 云 Linux 环境
      • Windows WSL 环境配置
      • Ubuntu 20.04 环境配置(建设中 2)
      • ubuntu docker 环境配置
    • 熟悉 GitHub/Git(建设中)
    • 代码规范
    • 文档类贡献指南(建设中)
      • MMSIG 文档贡献指南 (建设中)
      • 算法库文档贡献指南 (建设中)
    • Type Hints 贡献指南(建设中)
    • 单元测试贡献指南(建设中)
    • 源码阅读及调试技巧(建设中)
  • 算法库贡献者教程(建设中)
    • Playground(建设中)
    • MMEngine(建设中)
      • 新 Config 适配贡献指南
    • MMCV(建设中)
      • Transform 数据增强贡献指南
      • 算子贡献指南(建设中)
    • MMPreTrain(建设中)
      • 骨干网络贡献指南(建设中)
      • 自监督算法贡献指南(建设中)
    • MMDetection(建设中)
    • MMYOLO(建设中)
    • MMDetection3D(建设中)
    • MMOCR (建设中)
    • MMSegmentation(建设中)
      • 贡献一个标准格式的数据集
      • 贡献一个数据集加载方式(建设中)
    • MMPose (建设中)
    • MMEditing(建设中)
    • MMDeploy (建设中)
由 GitBook 提供支持
在本页
  • 1. 熟悉 Github
  • 1.1 Code页面
  • 1.2 Issues 页面
  • 1.3 Pull Requests 页面
  • 1.4 Actions 页面
  • 1.5 Insights 页面
  • 1.6 注意事项
  • 2. 学习 Git
  • 2.1 Git 是什么
  • 2.2 Git 中的一些基本概念
  • 2.3 Git 基础指令
  • 2.4 Git 进阶指令
  • 3. Git 实战
  • 3.1 常规开发流程
  • 3.2 一些常用的 Git 技巧
  • 3.3 FAQ
  • 4. 课后作业
  • 5. 参考资料

这有帮助吗?

在GitHub上编辑
  1. 通用贡献者教程(建设中)

熟悉 GitHub/Git(建设中)

上一页ubuntu docker 环境配置下一页代码规范

最后更新于1年前

这有帮助吗?

Git/GitHub 是开源社区贡献的必备技能,本文将详细介绍 OpenMMLab 社区中 GitHub 经常用到的几项功能,Github 是人类开源财富的聚集地,作为社区用户,我们需要充分熟悉 Github 的使用,汲取来自开源社区的能力。

本文将介绍

  • Github 相关介绍

  • Git 基础知识

  • Git 实战

1. 熟悉 Github

GitHub ()是最大的 Git 版本库托管平台,包括 OpenMMLab 在内的许多开源项目使用 GitHub 网站实现代码托管、版本管理、问题追踪、社区交流等功能。即使你还不熟悉 Git 的使用,也很可能已经使用过 GitHub 来查找所需的开源软件或资料。我们首先简单熟悉一下 GitHub 的页面和基本功能。 以 MMPose ()为例,一个软件项目在 GitHub 上的首页通常如下图所示:

图 1 软件项目首页(Code 页面)

在页面最上方的区域展示了该软件项目的名称和基本信息,如 Watch(关注)、Star(点赞)和 Fork(使用或开发)的人数。基本信息下方是一排功能标签,包括 Code、Issues、Pull Requests、Actions、Insights 等,下面会依次介绍。

1.1 Code页面

默认的 Code 页面,内容即为图1所示,主要包括:

  • 文件结构和代码内容:该区域显示了项目中所有的文件,可以在此浏览文件结构,或点进文件浏览其内容。

  • 其他信息:通常包括使用许可、发布版本、贡献者等信息。

  • README:如果当前浏览的路径下有 README.md 文件,其内容会显示在这一区域。通常软件项目会在根目录下包含 README.md 文件作为项目介绍。

1.2 Issues 页面

Issues 页面类似用于社区沟通的论坛。使用者可以在这里创建 issue 来提出遇到的问题,报告发现的 bug,或提出意见建议等;开发者可以在 issue 下进行回复,或将 issue 指定(assign)给特定的人来解决。此外,还可以用 issue 功能发布消息,如项目的 Roadmap、开发计划等,并将这些重要信息置顶。每个 issue 都会有一个编号,如 #900, 可以用于在其他 issue 或稍后将介绍的 Pull Request 中关联这个 issue。

1.3 Pull Requests 页面

Pull Requests 页面顾名思义用来浏览和管理 pull request(下简称 PR),如图4所示。通常,在 Github 上多人共同开发和维护一个项目时,会遵循一定的开发流程,这部分将在3.1中详细介绍。在此只简要说明开发流程,以引入 PR 的概念:

  • 开发者从官方代码仓库(如 open-mmlab/mmpose)fork 一份副本到自己的帐号 (如 zhang3/mmpose),并 clone 到本地。

  • 开发者在自己的代码仓库进行某项功能的开发。

  • 代码库的维护者或其他开发者会对 PR 进行 Review,并与作者共同进行讨论和修改。最终将修改完成的 PR 合入到官方代码仓库中。至此,一个开发项的开发周期完成。

1.4 Actions 页面

Actions 指 GitHub Actions,是 GitHub 提供的简化和方便开发流程的功能,用来在开发周期中自动触发执行特定的操作。如在 PR 被提交时,自动运行 CI; 在发布新版本时,自动编译并更新 pypi 上托管的安装包等。在 Actions 页面中,可以看到最近运行过的 action。点开其中一个action,可以看到详细信息、执行的具体操作步骤和输出的 log 等。如图 5 所示。

1.5 Insights 页面

Insights 页面展示了项目的汇总信息和统计数据,以方便开发者快速了解项目的整体开发情况、社区活跃度和关注点的等。通常我们较关注的项目有:

  • Pulse:项目的总览,包括最近的贡献者,PR,issue 等信息。

  • Contributors:代码贡献者信息。

  • Traffic: 项目流量信息,包括近期的访问量、clone 次数、访问者的来源以及访问量最高的项目内容等。

1.6 注意事项

在 PR 提交成功后,正常流程是由至少两个 Reviewer 对 PR 进行 Review,内容包括代码规范、命名、逻辑、功能等等,一般至少要经过 2 轮 Review 过程。

在 Review Approve 后由 maintainer 进行 merge 到指定分支。developer 未经 maintainer 允许不允许merge 代码,也就是说除了已经被许可情况下,除了 maintainer 其他人谁也不允许点 merge 代码,其merge 按钮大概位置在每个 PR 页面的最下面,图示内容如下:

上图最下面 Merge pull request 即为合并操作,灰色表示本身就没有合并权限,如果是绿色则表示有合并权限。

2. 学习 Git

2.1 Git 是什么

简单地说,Git 是一个版本控制系统,用来记录、管理和恢复对源代码(或任意类应的文件)所做的各种修改。与其他版本管理工具相比,Git 具有以下特点:

  • 快速:Git 的大部分操作都在本地进行,而不需要访问远程数据,因而非常快速。

  • 安全:Git 中所有数据记录在存储前都会计算校验和,并用校验和来引用。并且,Git 通常的操作只会向数据库中添加数据,而不会删除数据。因此,使用 Git 可以完整地记录版本信息,避免传输过程中的信息缺失或文件损坏,并且通常不会执行不可逆的操作。

  • 完全分布式:即各个客户端都保留完整的代码仓库历史记录,并可以指定和若干不同的远端代码仓库进行交互(如 GitHub 上的多个远程仓库)。

  • 强力支持非线性开发: Git 中的分支(branch)管理功能,可以支持大量开发者并行开发同一个软件项目。

2.2 Git 中的一些基本概念

2.2.1 工作区、暂存区和 Git 仓库区

现在请注意,如果你希望后面的学习更顺利,请记住下面 Git 中的这三个区,工作区(Working Directory)、暂存区(Staging Area) 以及 Git 仓库区(.git directory)。

  • 工作区(Working Directory)

当我们在本地创建一个 Git 项目,或者从 GitHub 上 clone 代码到本地后,项目所在的这个目录就是“工作区”。顾名思义,这里就是我们对项目文件进行编辑和使用的地方。如图 6 所示:

  • 暂存区(Stage/Index)

  • 仓库区 / 本地仓库(Repository)

在项目目录中,会发现一个.git隐藏目录,它不是软件代码的一部分,也不属于工作区,而是 Git 的版本仓库。简单来说,这个仓库区才是一个 Git 项目的“本体”,其中包含了所有历史版本的完整信息。而工作区正是对某个特定版本提取出来的内容。我们通常所说的仓库区,除了指.git目录这个物理位置,也指一种逻辑状态,即某个修改“记录进 Git 版本仓库”。

基本的 Git 工作流程如下:

  1. 在工作区中修改文件。

  2. git add将你想要下次提交的更改选择性地暂存,这样只会将更改的部分添加到暂存区。

  3. git commit提交更新,找到暂存区的文件,并将暂存永久性存储到 Git 目录。

如果 Git 目录中保存着特定版本的文件,就属于 已提交 状态。 如果文件已修改并放入暂存区,就属于 已暂存 状态。 如果自上次检出后,作了修改但还没有放到暂存区域,就是 已修改 状态。

2.2.2 文件状态

请记住,工作目录下的每一个文件都不外乎这两种状态:已跟踪 或 未跟踪。 已跟踪的文件是指那些被纳入了版本控制的文件,在上一次快照中有它们的记录,在工作一段时间后, 它们的状态可能是未修改(Unmodified),已修改(Modified)或已放入暂存区(Staged)。简而言之,已跟踪的文件就是 Git 已经知道的文件。

图7概括了文件状态的变化周期。

工作目录中除已跟踪文件外的其它所有文件都属于未跟踪文件,它们既不存在于上次快照的记录中,也没有被放入暂存区。 初次克隆某个仓库的时候,工作目录中的所有文件都属于已跟踪文件,并处于未修改状态,因为 Git 刚刚检出了它们, 而你尚未编辑过它们。

编辑过某些文件之后,由于自上次提交后你对它们做了修改,Git 将它们标记为已修改文件。 在工作时,你可以选择性地将这些修改过的文件放入暂存区,然后提交所有已暂存的修改,如此反复。

2.2.3 分支

有人把 Git 的分支模型称为它的“必杀技特性”,也正因为这一特性,使得 Git 从众多版本控制系统中脱颖而出。 为何 Git 的分支模型如此出众呢? Git 处理分支的方式可谓是难以置信的轻量,创建新分支这一操作几乎能在瞬间完成,并且在不同分支之间的切换操作也是一样便捷。 与许多其它版本控制系统不同,Git 鼓励在工作流程中频繁地使用分支与合并,哪怕一天之内进行许多次。 理解和精通这一特性,你便会意识到 Git 是如此的强大而又独特,并且从此真正改变你的开发方式。

2.3 Git 基础指令

# 查看 git checkout 指令文档
$ man git-checkout

# 查看 git status 指令参数说明
$ git status -h

2.3.1 Git 配置

  • git config

# 设置用户信息
git config --global user.name "John Doe"
git config --global user.email johndoe@example.com

# 设置 Git 自动记录密码(token),从而无需在每次 pull 或 push 时输入
# 注意:信息会以明文存储在本地,需考虑安全性
git config --global credential.helper store

上面指令中的 --global 指定了配置的层级。Git 中支持3个层级的配置,分别是 system(系统)、global(全局)和 local(本地),靠后的层级会覆盖掉靠前层级的配置。git config指令会将配置写入特定的文件中,因而我们也可以通过手动编辑配置文件的方式对 Git 进行配置。在 Linux 系统中,这文件的路径通常是:

  • system:/etc/gitconfig(需要 sudo 权限,不推荐)git

  • global: ~/.gitconfig (或者 ~/.config/git/config)

  • local:正在操作的仓库的 .git 路径下的配置文件, 即 [REPO_PATH]/.git/config

2.3.2 建立仓库

  • git init

git init指令用于在一个本地的路径下创建和初始化 Git 仓库。该指令会在项目目录下创建.git目录,但不会自动关联远程仓库(如 GitHub 上的远程仓库)。

# 创建本地代码仓库
$ cd ~/my_project/
$ git init
  • git clone

git clone指令用将远程代码仓库下载到本地,以创建本地仓库。这是实际开发中更常用的一种创建本地仓库的方式。

# 从远程仓库创建本地仓库
$ git clone https://github.com/open-mmlab/mmpose.git

git clone中使用的远程代码库 URL,可以在其 GitHub 首页看到(如图8),其他平台如 GitLab 也类似。

2.3.3 状态查询

  • git status

git status指令用于查看当前本地仓库的状态,包括所在的分支、文件状态等。

# 查看本地仓库状态
$ git status

On branch v2.0_dataset
Your branch is up to date with 'origin/v2.0_dataset'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   README.md

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   README_CN.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        new_file.py

在上面的例子中,可以看到返回的信息包括:

  • 本地处于 v2.0_dataset 分支

  • git diff

git diff指令用来比较仓库中的文件在修改先后的差异。常见的用法有

# 查看工作区与暂存区的差异(基本用法)
$ git diff

# 查看暂存区与最近一次提交的差异
$ git diff --staged

# 比较两个提交(或分支)的差异
$ git diff <branch-a> <branch-b>
# 例如:
$ git diff master dev

# 比较分支B的最近提交与分支A、B最近的公共提交之间的差异
# 即查看分支B在分支A的基础上加入了哪些内容
$ git diff <branch-a>...<branch-b>
# 例如
$ git diff master...dev

2.3.4 文件操作

  • git add

# 添加指定文件的修改到暂存区
$ git add <file-a> <file-b>

# 添加当前目录下的所有修改到暂存区
$ git add .
  • git commit

git commit指令用来将所有已添加到暂存区的修改生成一个提交对象,保存进仓库区,并将当前分支的指针(HEAD)移动到该次提交上。

# 提交暂存区修改
$ git commit

# 提交暂存区修改,并直接输入commit message
$ git commit -m "Some commit messages"

# 重做上一次提交,通常用于修改 commit message
$ git commit --amend

# 跳过git add,直接将工作区的修改提交
$ git commit -a

2.3.5 分支管理

  • git branch

git branch指令用来进行分支管理操作,如列出有的分支、创建新分支、删除分支及重命名分支等。

# 列出所有的本地分支
$ git branch

# 列出所有的本地和远程分支
$ git branch -a

# 在当前的提交对象上创建一个新分支(但并不自动切换到新分支)
$ git branch <branch-name>

# 重命名分支,若未指定old-branch,则默认重命名当前分支
$ git branch -m [<old-branch>] <new-branch>

# 删除分支
$ git branch -D <branch-name>
  • git checkout

git checkout指令主要用于切换分支。

# 切换到已有分支
$ git checkout <branch-name>

# 从当前提交创建并切换到新分支
$ git checkout -b <branch-name>

此外,git checkout指令还用于丢弃工作区中的修改(未提交到暂存区),或从其他提交对象(如其他分支,或特定commit等)中提取文件到当前工作区。

# 撤销指定文件的修改
$ git checkout -- <filename>

# 批量撤销(.py文件中的)修改
$ git checkout -- '*.py'

# 从指定分支中提取文件,添加到工作区
$ git checkout <branch-name> <filename>
  • git merge

# 合并指定分支到当前分支
$ git merge <branch-name>.

# 在合并过程中继续/中止
$ git merge --continue
$ git merge --abort

2.3.6 远程仓库管理与同步

  • git remote

git remote指令用来管理本地仓库与远程仓库的关联。

# 查看关联的远程仓库
$ git remote -v

origin  https://github.com/ly015/mmpose.git (fetch)
origin  https://github.com/ly015/mmpose.git (push)
upstream        https://github.com/open-mmlab/mmpose.git (fetch)
upstream        https://github.com/open-mmlab/mmpose.git (push)
# 添加关联的远程仓库
# git remote add <name> <url>
$ git remote add zhang3 https://github.com/zhang3/mmpose.git

# 重命名远程仓库
# git remote rename <old> <new>
$ git remote rename zhang3 li4

# 设置远程仓库URL
# git remote set-url <name> <url>
$ git remote set-url li4 https://github.com/li4/mmpose.git

# 删除关联远程仓库
# git remote remove <name>
$ git remote remove li4
  • git fetch

git fetch指令用于与一个远程的仓库交互,并且将远程仓库中有而本地仓库中没有的所有信息拉取下来,然后存储在本地仓库中。这个指令不会修改本地工作区、暂存区的内容,也不会修改本地分支的信息。例如,从远程仓库 origin 拉取的 master 分支会在本地存储为 origin/master, 而不会与原有的本地分支 master 相混淆。

# 从远程仓库(默认远程仓库为 origin)拉取信息
$ git fetch [<remote-name>]
  • git push

git push指令用于将本地仓库的分支推送到远程仓库,即比较本地分支与远程仓库中对应关联的分支,并将差异同步到远程仓库。该指令需要有远程仓库的写权限,因此这通常是需要验证的。

# 推送当前分支到默认远程仓库
$ git push

# 推送本地分支到指定远程仓库的指定分支。若 <local-branch-name> 和 <remote-branch-name>
# 之一未指定,则默认两者相同;若两者均未指定,则默认将当前本地分支推送到同名远程分支
$ git push <remote-name> <local-branch-name>:<remote-branch-name>

# 在指定远程仓库创建与本地分支关联的远程分支,并推送。
# 通常用于将本地创建的分支第一次推送到远程仓库
$ git push --set-upstream <branch-name>
  • git pull

git pull指令用于拉取远程分支到本地,即比较本地分支与远程仓库中对应关联的分支,并将差异合入本地分支。通常git pull指令可以看作是git fetch+git merge。如同一般的分支合并,如果远程关联分支和本地仓库中的提交历史存在冲突,则需要解决冲突后才能继续合并。

# 拉取当前本地分支关联的远程分支
$ git pull

# 拉取指定远程仓库的指定分支到当前本地分支
$ git pull <remote-name> <branch-name>

2.4 Git 进阶指令

  • git mv

git mv指令用于重命名或移动文件、目录或链接。与mv不同的是,git mv会自动将这一修改添加到暂存区。

# 移动文件或目录
$ git mv <source> <destination>

# 将文件或目录移动到指定路径下
$ git mv <source> ... <destination-directory>
  • git rm

git rm指令用于从工作区或暂存区移除文件。git rm会自动将这一修改添加到暂存区(即将该文件从已追踪的文件清单中移除),类似git mv。如果我们只想把文件从暂存区移除(即让 Git 不再跟踪该文件),但仍保留在工作区,则可以使用--cached选项。

# 从工作区和暂存区移除文件
$ git rm <filename>
# 仅从暂存区移除文件
$ git rm --cached <filename>
  • git stash

git stash指令用来临时保存一些还没有提交的工作,以便在分支上不需要提交未完成工作就可以清理工作目录。例如,我们正在dev分支编辑run.py文件,临时需要切换到master分支跑一个测试任务。在切换分支前,需要清理工作区和暂存区,但我们并不希望将未完成的修改提交到仓库,此时可以用git stash指令临时贮藏当前的工作,并在完成临时任务后回复这些工作:

# 临时贮藏 dev 分支当前的工作
$ git add run.py
$ git stash # or "git stash push"
# 此时,工作区和暂存区会被清理,恢复到最近一次提交的状态

# 切换到 master 分支,完成临时任务
$ git checkout master
$ python run.py

# 切换回 dev 分支,回复之前贮存的工作
$ git checkout dev
$ git stash pop

git stash会将贮藏的修改存放在一个栈上,因此可以多次使用git stash贮藏修改。栈的状态可以用git stash list查看,例如。

$ git stash list
stash@{0}: WIP on master: 049d078 added the index file
stash@{1}: WIP on master: c264051 Revert "added file_size"
stash@{2}: WIP on master: 21d80a5 added number to log

git stash的常见用法汇总如下

# 贮藏当前分支的修改
$ git stash [push]

# 显示贮藏栈情况
$ git stash list

# 应用一个的贮藏的工作,即将其恢复到工作区,但不删除栈中的内容
$ git stash apply  # 应用最近一个贮藏
$ git stash apply stash@{2}  # 应用指定的贮藏

# 从栈中弹出最近一个贮藏,并应用
$ git stash pop
  • git reset

git reset指令用来执行撤销操作。例如,它可以将当前分支的 HEAD 指针移动到指定的提交上,从而实现撤销到该提交之后的所有提交。根据指令参数,这种撤销可以是只移动 HEAD 指针,而不改变工作区内容(被撤销的提交内容会保留在工作区,变成已修改,未提交的状态);也可以直接修改工作区的内容。此外,git reset还可以用来撤销提交到暂存区的内容。一些例子如下:

# 基本用法
$ git reset [<commit>]  # 将 HEAD 指针移动到指定的提交(默认为最近一次提交),并重置暂存区
$ git reset [--mixed]  # 重置 HEAD 指针及暂存区
$ git reset --soft  # 仅重置 HEAD 指针
$ git reset --hard  # 重置 HEAD 指针、暂存区及工作区

# 撤销提交的例子
$ git reset HEAD~2  # 撤销当前分支最近的2次提交,但不改变工作区内容
$ git reset HEAD~2 --hard  # 撤销当前分支最近的2次提交,并将工作区内容回复到撤销后的状态

由于git reset --hard指令会直接修改工作区,有可能会导致数据丢失(Git 会真正销毁数据的仅有的几个操作之一),所以在这样使用前一定要确保自己知道其后果。

  • git cherry-pick

git cherry-pick指令用来获得在单个提交中引入的变更,然后尝试将作为一c个新的提交引入到你当前分支上。 这通常用来从另一个分支合并单个或几个提交,而不是整个分支。例如,当前有 main 和 dev 两个分支,提交历史如下:

如果我们只希望将 dev 分支中的 C3 提交合并到 main 分支中,可以使用以下方式:

$ git checkout main
$ git cherry-pick C3

此时,提交历史会变为:

git cherry-pick指令的常见用法如下:

# 合并单个提交到当前分支
$ git cherry-pick <commit>
# 合并两个提交到当前分支
$ git cherry-pick <commit1> <commit2>

# 当合并遇到冲突时,可能需要手动解决,与 merge 中类似
$ git cherry-pick --continue  # 继续 cherry-pick
$ git cherry-pick --abort  # 中止 cherry-pick
  • git revert

git revert指令用来撤销之前的提交。与git reset指令移动HEAD指针的方式,git revert会创建一个与之前提交的变更完全相反的新提交(本质上是一个特殊的 cherry-pick),从而实现撤销:

# 撤销单个或多个提交
$ git revert <commit1> [<commit2> ...]

# 同样,当遇到冲突时,解决的方式与 merge 中类似
$ git revert (--continue | --abort)

下图中可以更直观地对比git reset和git revert的区别:

  • git rebase

git rebase指令采用“变基”的方式整合不同分支的修改。回顾之前介绍的git merge指令,其工作方式是在当前分支创建一个新提交,在其中合并目标分支和当前分支的修改。与之不同,git rebase会首先寻找当前分支和目标分支的“分叉点”,并尝试将当前分支在“分叉点“之后的部分”转移“到目标分支的末尾。下图中对比了使用git rebase和git merge合并分支的不同:

$ git checkout main && git merge dev

$ git checkout dev && git rebase main

在上图中可以看到,git rebase和git merge都可以实现分支的整合,但git rebase可以避免提交历史中出现分叉,保持较为清晰的提交历史。因此在实际项目开发中,通常会在将开发分支合入主分支前,先将其 rebase 到最新的主分支(在 3.1 部分中可以看到这一步)。

git rebase指令的常见用法如下:

# 将当前分支 rebase 到目标分支 upstream-branch
$ git rebase <upstream-branch>

# 将指定分支 rebase 到目标分支 upstream-branch
$ git rebase <upstream-branch> <branch-name>
$ git rebase --onto <upstream-branch> <branch-name>~1  # 另一种方式

# 使用 -i 标志可以交互式的方式进行 rebase,使操作者可以手动选择对每个 commit 的处理方式。
# 一个常见用法是用这种方式合并当前分支的最近若干个 commit,例如:
$ git rebase -i HEAD~3

与 merge 类似,在 rebase 的过程中也可能会遇到冲突,需要手动解决。解决冲突的过程也与 merge 中相似(使用--continue,--abort等标志)。需要注意的是,由于 rebase 的工作方式是先切换到目标分支,然后依次添加当前分支中的提交,因此在处理冲突时,HEAD指向的是目标分支,这一点与 merge 稍有不同(但在实际开发中,通常 merge 时把主分支作为当前分支,而 rebase 时把主分支作为目标分支,所以 HEAD 都会指向主分支 master(或 main)分支,所以不至于混淆)。

3. Git 实战

3.1 常规开发流程

在这一部分,我们以在 MMPose 算法库中完成一个功能开发的过程为例,介绍基于 Git 的常规开发流程。

3.1.1 准备

  • Fork 源代码仓库

  • 创建本地仓库并关联远程仓库

到本地,并配制远程仓库地址。通常,我们会关联两个远程仓库: origin 为自己 fork 的副本仓库,upstream 为官方仓库

# clone 副本仓库。此时会自动关联远程仓库 origin
$ git clone https://github.com/ly015/mmpose.git
# 添加远程仓库 upstream
$ cd mmpose
$ git remote add upstream https://github.com/open-mmlab/mmpose.git
  • 配制 pre-commit Hook

3.1.2 开发

  • 创建开发分支

通常我们会为每个开发项创建一个独立的分支,以避免耦合带来的混乱。分支的命名最好能简洁地体现开发项的内容。

# 拉取官方远程仓库
$ git fetch upstream

# 从最新的 master 分支创建开发分支
$ git checkout upstream/master
$ git checkout -b feature
# 上面两行指令等价与下面的指令
# git checkout -b feature --no-track upstream/master
  • 完成功能开发

在开发过程中,建议多使用git add和git commit及时提交修改。

  • 将本地修改推送到远程仓库

在完成开发并将本地修改都提交到本地仓库后,使用git push指令将本地修改同步到远程仓库。注意,我们通常不会将修改推送到官方远程仓库,而是推送到自己 fork 的远程仓库,再通过向官方仓库提 PR 的方式向其贡献代码。(这样不需要官方远程仓库的推送权限,并且有利于维护官方仓库的内容整洁)

# 首次推送,在远程仓库中建立关联分支
$ 1.

# 非首次推送
$ git push

# 如果在本地进行了 merge,rebase 等操作,导致本地修改无法通过 fast-forwarded 方式
# 合入关联远程分支时,可以使用 -f 或 --force 参数强制更新远程分支(注意这是个风险操作)
$ git push -f

3.1.3 提交 PR

  • 开始创建 PR

当我们将本地的修改推送到自己的远程仓库后,需要向官方仓库提交一个 PR,要求官方仓库将这一修改合入到主分支(或其他指定分支)。创建 PR 可以在浏览器中操作。如果是近期推送的修改,在官方仓库或自己仓库的页面上都会出现提示,点击“Compare & pull request” 即可(如下图红色部分)。如果没有看到这个提示,也可以在自己仓库的页面上切换到开发分支,点击“Contribute”→“Open pull request”即可(如下图蓝色部分)。

  • 提交 PR

完成上一步后,会进入创建 PR 页面,如下图所示。这里我们需要填写 PR 相关的信息。红色部分是分支信息,默认将当前分支合入官方主分支,一般不用修改。绿色部分是 PR title 和 PR message,用来说明这个 PR 的内容。这部分非常重要,必须认真填写。PR title 一般格式为 [Prefix] Short description of the pull request (Suffix)Prefix:

  • 新增功能 [Feature]

  • 修 bug [Fix]

  • 文档相关 [Docs]

  • 开发中 [WIP] ( work in process 暂时不会被review)

  • 查看 CI 状态

PR 创建后会自动触发 CI(OpenMMLab 的算法库都基于 GitHub Actions 配置了 CI),完成代码格式、单元测试等检查工作。在 PR 页面可以查看 CI 的运行状态和结果,如下图所示。如果 CI 中有失败的项,可以点击“Details” 查看详细情况,修改后推送到自己的仓库,PR 也会随之更新。

3.1.4 代码 Review 及合入

PR 提交后,会由 reviewer 进行 review 并提出意见建议,再由作者修改并更新。这样的迭代通常会进行几轮,直到大家均认为该 PR 已经可以合入主分支后,由该项目的 maintainer 来进行合入操作。关于 Review 代码的方法和技巧,后续详细介绍,这里我们只简单介绍相关的 Github 操作。 如下图所示,在 PR 页面的 “Files changed” 标签页,可以看到该 PR 的所有修改。将鼠标指向某一行,会在行号出显示 “+” 按钮,点击即可输入 review 意见和建议。作者或其他 reviewer 也可以进行回复讨论。在 “Conversation” 标签页会显示这些对话内容。

PR 开发和 review 过程中如果 master 分支有相关的更新,需要及时 rebase,更新本地代码,以免和 master 分支有冲突。

3.2 一些常用的 Git 技巧

3.2.1 使用 gitignore文件

gitignore文件用来设置不希望 Git 进行跟踪和管理的文件路径,例如编译或运行时生成的临时文件、较大的数据文件等。gitignore文件是一个文本文件,通常路径是项目目录下的.gitignore,写法规范如下:

  • 每一行指定一个要忽略的文件路径,除了空行和以 “#” 开头的注释行

  • 文件路径可以使用 glob 模式匹配的写法

  • 路径以 “/” 结尾表示要忽略的是一个目录

  • 在文件路径前加入取反符号 “!” ,表示这是一个不应忽略的特例

以下是一个例子:

# 可使用"#"符号添加注释

# 忽略 "MANIFEST" 这个文件
MANIFEST

# 忽略所有 .pkl 文件
*.pkl
# 但是不忽略 a.pkl 这个文件
!a.pkl

# 忽略 data 目录下的所有文件
data/

# 忽略 model 目录下的所有 .pth 文件
model/**/*.pth

通常项目里已经提供了gitignore文件,必要时可以对其进行修改。gitignore文件本身也属于项目内容,需要添加修改 (git add)、提交修改(git commit)、开 PR 合入等。

3.2.2 配置 git mergetool

# 使用 meld 作为默认 difftool
[diff]
    tool = meld
[difftool "meld"]
    cmd = meld "$LOCAL" "$REMOTE"

# 使用 meld 作为默认 mergetool
[merge]
    tool = meld
[mergetool "meld"]
    cmd = meld "$LOCAL" "$MERGED" "$REMOTE" --output "$MERGED"

由于我们同时配制了 "difftool",所以在运行git diff指令时,也会启动 meld,用左右两列来对比文件差异。

3.2.3 添加自定义 pre-commit hook

- repo: local
    hooks:
      - id: update-copyright
        name: update-copyright
        description: Add OpenMMLab copyright header to files
        # specify entry point
        entry: .dev_scripts/github/update_copyright.py
        # specify hook executor
        language: python
        # specify files that this hook will check
        files: ^(demo|docs|docs_zh-CN|mmpose|tests|tools|\.dev_scripts)/.*\.(py|c|cpp|cu|sh)$
        exclude: ^demo/mm(detection|tracking)_cfg/.*$

3.2.4 使用 ssh 和远程仓库通信

与远程仓库通信可以使用 https 或 ssh 两种方式。在 2.3.6 中主要基于 https 方式进行了介绍,这里我们简单介绍 ssh 方式。在 clone 远程仓库到本地时,可以选择 ssh 地址,如下图所示:

3.3 FAQ

3.3.1 关于 Git 操作

  • 如何修改 commit 但不产生新的提交

  • 如何撤销 commit 但仍需保留修改

  • 如何合并多个 commit

$ git log
commit 7eea97ea4cd9ff50ccf7ce146c4b440a98c85c51 (HEAD -> master)
Author: ly015 <liyining0712@gmail.com>
Date:   Sat Oct 9 18:39:45 2021 +0800

    Add c.txt

commit 7d97740b91137ec8049c83726ba0c950090cb8f3
Author: ly015 <liyining0712@gmail.com>
Date:   Sat Oct 9 18:39:27 2021 +0800

    Add b.txt

commit 7f836e4b528a565b42d41c021197242b87a038f4
Author: ly015 <liyining0712@gmail.com>
Date:   Sat Oct 9 18:39:03 2021 +0800

    Add a.txt

使用git rebase对后2个提交进行合并:

$ git rebase -i HEAD~2

此时会出现交互界面,如下图左。可以看到相关的 commit 被逐行列出,并且每个 commit 前面有一个指令(pick),这个指令是可以按照需求修改的,所有可选项都在下方的注释中有说明。例如,我们现在希望合并2个 commit, 则可以将第一个 7d97740 的指令设置为 reword,将第二个 7eea97e 的指令设置为 fixup,如下图中,然后保存退出。由于第一个指令是 reword,接下来会跳出重新输入 commit message 的界面,我们将原本的 “Add b.txt” 改成 “ Add b.txt and c.txt ”, 并保存退出,如下图右。

此时我们再查看分支提交历史,会发现之前的 2 个 commit 已经被合并:

$ git log
commit a321f6787a3cb2f27f70f5f7877e158a2aea7e0d (HEAD -> master)
Author: ly015 <liyining0712@gmail.com>
Date:   Sat Oct 9 18:39:27 2021 +0800

    Add b.txt and c.txt

commit 7f836e4b528a565b42d41c021197242b87a038f4
Author: ly015 <liyining0712@gmail.com>
Date:   Sat Oct 9 18:39:03 2021 +0800

    Add a.txt
  • 如何删除 add 或者 commit 之后的大文件

3.3.2 关于 PR

  • 如何往源算法库中其他人提的 PR 中提交代码

# 拉取 PR#222 到本地并为其创建一个本地分支
$ git fetch upstream pull/222/head:<branchname>
# 切换到 PR 分支
$ git checkout <branchname>
  • 如何手动运行 CI

可以在 PR 页面的 Checks 标签页,点击右上角的 “Re-run all jobs” 按键,即可重新运行 CI。

4. 课后作业

  • 提交一个 PR

  • 向源算法库中其他人提的 PR 中提交代码

  • 解决分支冲突

5. 参考资料

图 2 基本信息和功能标签
图 3a Issues 页面: issue 列表
图 3b Issues 页面: 一个 issue 的内容

开发者将自己的修改推到自己的远程仓库,并向官方仓库发出申请,要求官方仓库拉取(pull)此次修改,即将此次修改加入到官方代码中。这样的请求就叫做 PR。(与之相对的还有 GitLab 平台所采用的 merge request,拓展阅读:)

在 PR 列表中点进某个 PR 后,可以看到其内容包括 PR 的描述信息、作者提交代码的历史、Reviewer的意见以及和作者的往来沟通等,这部分在 中也会详细介绍。与 issue 类似,每个 PR 也会有一个编号,用来在别处引用或关联该 PR。

图 4a Requests 页面: PR 列表
图 4b Pull Requests 页面: 一个 PR 的内容
图 5a Actions 页面:action 列表
图5b Actions 页面:一个运行 CI 的action的详细信息

如本文第 部分所述,GitHub 是基于 Git 的版本库托管网站。那么 Git 究竟是什么,又如何使用呢? 在这部分中,我们将介绍这一强大工具的基本概念和用法。

图 6 MMPose 工作区和仓库区

暂存区是 Git 中独有的一个概念,它其实是位于.git目录中的一个索引文件,记录了下一次提交(commit)时将要存入仓库区的文件列表信息。当我们在工作区进行了一些修改后,并不直接将其提交进版本仓库,而是先通过git add指令将改动放入暂存区(即记录将要提交的文件索引)。在若干次git add后,再通过git commit指令将暂存区的修改提交到仓库区。这样的设计,让使用者可以更灵活地选择每次提交的内容。这里涉及到的 git 指令,将在 部分详细介绍。

结合 中工作区、暂存区和仓库区的概念,我们接下来介绍 Git 工作区中的文件状态:

图7 Git 文件状态变化周期 (图片来源:Git - Git 是什么?)

在这部分,我们介绍一些常用的 Git 指令。Git 的指令通常有着强大而丰富的功能,可以灵活地设置众多参数。这里只给出每个指令最基础和常见的用法,更多用法可以在 Git 官方教程()中学习,或者在命令行中查看对应指令的文档,如:

图8 GitHub 上远程仓库的URL

有一个新增文件new_file.py处于未被追踪的状态(关于文件状态,可回顾 文件状态)

git add指令用来将工作区的修改添加到暂存区。可参考本文 中关于工作区、暂存区和文件状态的介绍。

由于git checkout的功能较多且分散,在较新版本的 Git 中引入了git switch指令,来代替git checkout的分支切换功能,可以参考 .

git merge指令用来合并分支。关于分支合并的说明,可以参考 部分。

在上面的例子中,可以看到已经关联的远程仓库有2个,命名分别是 origin(URL:)和 upstream(URL:)。git remote除了用于查看关联仓库,还用于进行关联的管理:

图9a git cherry-pick 示意(操作前)
图9b git cherry-pick 示意(操作后)
图10 git revert 与 git reset 对比 左:初始状态 中:执行 git reset C2 右:执行git revert C3
图11 git rebase 与 git merge 对比:初始状态
图11b git rebase 与 git merge 对比:在 main 分支(当前分支)上合并 dev 分支(目标分支)
图11c git rebase 与 git merge 对比:将 dev 分支(当前分支)rebase 到 main 分支(目标分支)

在 MMPose 的 Github 页面()点击 Fork 按键,在自己的 Github 帐号下创建一个副本仓库(如:)。

参考 中的说明,在本地仓库配制 Pre-commit Hook。 Pre-commit Hook 是 Git 支持的钩子函数的一种,通常用于在提交修改(Commit)前自动完成代码风格检查等工作。以上准备工作只是第一次提 PR 之前需要进行,后续开发工作,在每个 PR 开发中都需要做。

图 12 开始创建 PR

PR message 的主要修改内容,结果,以及对其他部分的影响,通常我们的代码库会准备 ,只需要按照模板填入对应的内容即可。另外, PR message 可以关联相关的 issue 和 PR,通过 fixes/resolves issue ID 可以在 PR merge 的时候 close issue。蓝色部分是其他信息,在这里可以指定 reviewer 来 review 这个 PR。完成所有信息后,点击下方的“Create pull request”即可完成 PR 提交。

图 13 创建 PR
图 13 CI 运行状态和结果
图 14 在 PR 中提交 review comment

在合并分支时,经常需要手动解决冲突,这里可以选用一些可视化的工具帮助我们提高效率,例如 VS Code 就有较好的可视化功能,可以协助解决冲突。除此之外我们也可以借助第三方工具,这里我们以 这个工具为例,介绍 Git 中 mergetool 的配置和使用。首先需要在本地安装 meld,可以参考其官网的文档。安装完成后,可以在 Git 配置文件(见 2.3.1onfig)中写入如下内容:

完成这些配制后,当需要解决冲突时,Git 会自动启动 meld,界面如下图所示。可以看到界面分成三列,对应与上面设置中的L11,分别是本地分支内容、合并后内容和目标(远程)分支内容。在这个界面中可以清晰对比两个分支的不同,还可以三列之间的箭头状按钮选快速选择内容添加到对应文件中。这里要注意,LOCAL 和 REMOTE 对应的分支,在 merge 和 rebase 两种情况下是不同的,这在 中做过介绍。

图 15 使用 meld 进行可视化分支合并

在 中我们提到,Git 提供了 pre-commit hook 机制来帮助开发者自动完成格式检查等工作。一些常用的格式格式检查三方库,都提供了可以直接调用的 hook,可以在.pre-commit-config.yaml文件进行配制。除此之外,我们也可以在项目中添加本地代码脚本作为 pre-commit hook 以实现特定功能。以 MMPose 中自动给代码文件添加版权信息的功能为例。我们首先添加了实现该功能的脚本,该脚本以一个文件列表作为输入参数,检查并添加版权信息到这些文件中。如果所有输入文件均已包含版权信息而未作修改,则正常返回 0, 否则返回 1。如果该 hook 在提交时对代码做了修改,就会中止这次提交,待开发者添加这些修改后重新提交。最后,我们将该脚本添加到.pre-commit-config.yaml中即可:

关于 Git hook 的详细介绍可以参考

图 16 使用 ssh 通信的远程仓库地址

使用 ssh 方式的仓库同步操作与使用 https 方式没有区别(如git clone,git pull,git push等)。ssh 方式需要配置公钥-私钥对,并将公钥提交到 GitHub 上,用以在通信时进行身份验证,不需要每次输入账号密码。配置 ssh 秘钥的具体的做法可以参考这里的文档:

请参考 中 git commit --amend的用法。请注意,严格来说这个操作是用一个新的提交替换了原来的提交。

请参考 中 git reset --mixed或git reset --soft的用法。

请参考 中 git rebase -i HEAD~n的用法,这里举一个简单的例子。假如当前分支历史中有3个提交,如下:

图 17a 使用git rebase合并多个 commit
图 17b 使用git rebase合并多个 commit
图 17c 使用git rebase合并多个 commit

在开发中会遇到不慎将一些文件错误添加到 Git 仓库中的情况(比如未及时更新gitignore文件就可能会导致这种情况)。尤其是以一些大文件(如数据文件或临时文件),如果被错误包含在项目仓库中,会使仓库变得很大而严重影响和远程仓库通信的效率(如git clone,git push,git pull等操作会变得很慢)。如果只是通过git add添加到了暂存区,只需要用前面介绍过的git rm --cached或git reset清理暂存区即可。但如果已经通过git commit提交到了仓库,情况会变得比较棘手,因为 Git 仓库会记录完整的提交历史,即使撤销这次提交,或者在后续提交中移除这些错误添加的文件,它们依旧会存在与仓库中。这种情况也不是无法解决的,这篇文档 中给出了一个清除历史提交中错误引入的大文件的完整例子,大家可以仔细学习并 follow 其步骤。

在创建 PR 时,通常会默认勾选 “Allow edits and access to secrets by maintainers”,这将允许其他具有官方仓库 Write 权限的用户提交修改到这个 PR 对应的分支。这在多人合作开发时会用到。当需要向别人的 PR 中提交代码时,需要将作者的远程仓库地址添加到 remote 中([参考 git remote]),然后拉取 PR 对应的分支。在本地完成修改后,再推送到作者的远程仓库即可。在将 PR 拉取到本地时,除了从作者的远程仓库拉取,还可以用以下简单的指令:

[1] Git 官方教程:

[2]

[3]

[4] 廖雪峰 Git 教程:

https://stackoverflow.com/questions/22199432/pull-request-vs-merge-request
Git - 设置与配置
Git - git-switch Documentation
https://github.com/ly015/mmpose.git
https://github.com/open-mmlab/mmpose.git
https://github.com/open-mmlab/mmpose
https://github.com/ly015/mmpose
Contributing to OpenMMLab
PR 模板
meld
Git - Git Hooks
About SSH - GitHub Docs
Git - 维护与数据恢复
https://git-scm.com/book/zh/v2
https://learngitbranching.js.org/
https://git-school.github.io/visualizing-git/
https://www.liaoxuefeng.com/wiki/896043488029600
3.1
1
2.4
2.2.1
2.2.2
2.2
2.2.3
git-rebase 说明
.dev_scripts/github/update_copyright.py
3.1.1
2.3.4
2.4
2.4
2.3.6
https://github.com
https://github.com/open-mmlab/mmpose