GitHub 工作流

序言

  一些工作中常用的 git / GitHub 概念整理。

License

如何为自己的 Github 项目选择开源许可证?

分支与开发

对于正规一点的仓库,main branch 通常是受保护的,而我们的提交与推送都在单独的 branch 上进行。

查看分支

  • git branch 查看本地分支
  • git branch -r 查看远程分支
  • git branch -a 查看所有分支

注意:如果远程新建了 branch,在本地运行 git remote update origin 之前,新的 branch 是不会显示在 git branch -r 上的。顺便在任意 branch 上的 git pull 也能实现这一效果。

操作分支

  • git branch <Name> 创建新的本地分支,不会 checkout
  • git branch -m <OldName> <NewName> 重命名本地分支
  • git branch -d <Name> 删除本地分支,不能删除当前分支
  • git branch -d -r <Name> 删除远程分支

关联分支

  • git branch --set-upstream-to=origin/<RemoteName> <LocalName> 将本地分支与远程分支关联起来
  • git push --set-upstream origin <RemoteName> 在远程新建分支,并将当前分支与远程新分支关联并推送。

当然最简单的命令是本地识别到远程新分支后直接:
git checkout <RemoteName>
这个命令相当于
git checkout -b <LocalName> origin/<RemoteName>
即在本地新建分支,并且切换过去,并且与远程分支建立联系。

  • git branch -vv 查看本地分支核远程的对应关系

其他

  • 当你的 branch 准备好之后,就可以在 github 上发起 Pull Request,通知仓库相关的开发者检查代码以及帮忙合并。
    Pull Request
  • add, rm, commit, push 之类的操作直接用 VS 的 Git 更改 视图,或者 Git GUI 就好,比命令方便得多。
  • 一般来说,除非很紧急的情况,请在获得 approve 和 review 之后再进行合并。
  • 选择 Sauash Merge 的方式将 pr 内多个 commit 合并为一个 commit 提交到 main 分支上。

冲突与合并

冲突

在发起 pr 后,github 会自动帮你检查该分支是否与主分支有冲突。简单的冲突可以直接在网页端解决,也可以回到本地进行如下操作:

  • git checkout main 切换回主分支
  • git pull 拉取最新的主分支
  • 可以顺便确认一下主分支的编译与运行是否正常
  • git checkout <Name> 切换回冲突分支
  • git merge main 合并主分支与冲突分支
  • 进入 Git 更改 视图解决冲突
  • 提交与推送,查看 github

VS 提供了非常直观的图形界面来帮助我们解决冲突,在不确定如何解决的情况下可以联系冲突文件的上一个开发者帮忙合并。

合并

  • 想在当前 branch 应用某个别的 branch 的修改?同样是将对应的 branch pull 下来,merge 即可。
  • 不想 merge 整个 branch,或者 对方的 branch 还在开发中?使用 git cherry-pick <CommitHash> 以 commit 为粒度进行合并。

撤销

工作区

本地所有未 add 的改动我都不想要,怎么撤销?

  • git checkout . 清除未 add 的所有改动,无法删除新增的文件。

暂存区

本地 add 了的改动我也不想要,怎么撤销?

  • git reset [--soft | --mixed | --hard] [HEAD]
    • --mixed 默认模式,不删除改动代码,撤销 commit,撤销 add
    • --soft 不删除改动代码,撤销 commit,不撤销 add
    • --hard 删除改动代码,撤销 commit,撤销 add
    • HEAD 默认版本
      • HEADHEAD~0 表示当前版本
      • HEAD^HEAD~1 表示上一个版本
      • HEAD^^HEAD~2 表示上上个版本
      • 依此类推

当我们需要撤销暂存区里的代码时,可以 git reset 将暂存区回退到工作区,然后 git checkout . 清除工作区。
或者直接 git reset --hard 把工作区和暂存区的修改全扬了,这个指令也能删除暂存区中的新增文件,反而删除不了工作区中的新增文件。

Git 更改视图

其实对工作区和暂存区的操作完全可以由 VS 提供的图形界面完成,非常方便直观。

Commit

上一个 commit 太蠢了,怎么撤销?

  • git log 查看提交日志,获取想要回退的 commit 哈希
    ps: 按q退出
  • git reset --soft <CommitHash> 回退到 CommitHash 对应的 commit,即如果我要撤销 commit balabala,reset 的参数应该是 balabala 的上一个 commit 的哈希。
    这种情况下我比较习惯用 --soft 将 commit 回退到暂存区而非默认的工作区,如果此时本地工作区有修改便能将他们区分开来。

Push

commit 已经 push 上去了,怎么撤销?
在完成上一节之后,本地和远程其实处于一个冲突的状态,这时候只需要用
git push --force
强制将远程的 log 同步成本地的状态。

PR

pr 已经 merge 了,怎么撤销?
在该 pr 的最下方的 merge 右侧找到 Revert,这个按钮会自动创建一个新的 branch 以及 pr,并且在其中将原 pr 内的所有修改反向操作。当然尽量不要出现这种不得不 Revert 的情况。
Revert

Git Submodule

git 提供了子模块功能将一个仓库集成为另一个仓库的子目录,并且让他们保持独立的提交。
子模块有一个非常抽象的特性:在主仓库中如果不使用 submodule 相关的指令,子模块的内容不会受到任何 git 指令的影响,不受主仓库切换分支的影响,甚至不会在主仓库 clone 之后出现在你的硬盘里。

添加子模块

git submodule add <url> <path> 将 url 对应的仓库作为子模块添加到 path 路径下
添加子模块后主仓库会新增一个 .gitmodules 文件来描述子模块的信息。
.gitmodules
不想让第三方库的不受控的更新破坏自己的主仓库?将其 fork 一份再集成为子模块即可。

Clone 带有子模块的仓库

git clone --recursive <url> 将仓库内容以及子模块内容以及子模块的子模块内容递归地 clone 下来。
如果 clone 的时候没有带上 --recursive 参数,也可以在 clone 结束后在仓库目录运行 git submodule update --init --recursive 达成一样的效果。
--init 参数用于在 .git/config 中注册子模块信息。

更新子模块

git submodule update 将子模块更新到最新版本
git submodule update --remote 将子模块更新到远程的最新版本
有什么区别呢,这里要引出子模块另一个非常抽象的特性:区分六个概念:远程的主仓库本地的主仓库远程的子仓库本地的子仓库远程主仓库中的子仓库本地主仓库中的子仓库,这六个东西的版本是可以不同的。

  • 重点在于,主仓库中会存储一个子模块的 commit 版本,git submodule update 只能将 本地的子仓库 更新为 本地主仓库中的子仓库 版本。
  • 或者先使用 git pull本地主仓库中的子仓库 版本更新为 远程主仓库中的子仓库 版本,再结合使用 git submodule update 即可将 本地的子仓库 更新为 远程主仓库中的子仓库 版本。
    这也是 clone 之后,对着空空如也的子模块文件夹使用 git submodule update 即可将其更新的原因
  • git submodule update --remote 实际上相当于进入每一个子模块执行 git pull,即将 本地的子仓库 更新为 远程的子仓库
  • 最后一步,如何更新 远程主仓库中的子仓库 版本呢?在 本地主仓库中的子仓库 版本与 本地的子仓库 不同时,在主仓库执行 commit 即可更新 本地主仓库中的子仓库 版本。在 远程主仓库中的子仓库本地主仓库中的子仓库 版本不同时,在主仓库执行 push 即可更新 远程主仓库中的子仓库 版本。

其他

  • 如果 .gitmodules 中的 url 有变或者新增,git submodule update 是无法将其更新的,保险起见,我们可以在每一次 update 子模块之前执行 git submodule init
  • 只希望 git submodule update --remote 更新指定的子模块而非更新所有的子模块?使用 git submodule update --remote <Path> 指定子模块的路径即可。具体路径可以在 .gitmodules 文件中确认。

开发子模块

子模块最抽象的特性:当我们使用 git submodule update 对子模块进行更新之后,子模块实际上会处于一个游离的 branch 上并且这个 branch 是 main(不一定是 main,可以配置)的复制。

1
2
3
git branch
* (HEAD detached at abcdefg)
main

在这个 branch 上的所有工作不但无法进行 push,甚至会在下一次 git submodule update 之后丢失。

总结

子模块的设计非常复杂且反直觉,好在我们可以通过一些项目规范和开发习惯来规避一些使用子模块带来的的副作用。

  • 对子模块的 main branch 进行保护,所有的开发都在特定的 branch 上进行,以避免游离的 HEAD 带来的困扰。
  • 开发子模块时,将其单独 clone 下来或者复制出来,总之不要直接在主仓库里的子模块里进行开发,以免主仓库检测到子模块 commit 的更新。
  • 在主仓库中更新子模块时,不要在更新 主仓库中的子仓库 版本之前更新依赖于子模块新版本的代码。
  • 主仓库与子模块同步更新之后视情况通知其他开发者,毕竟在主仓库 git pull 只会更新依赖于子模块新版本的代码,而无法更新子模块。
  • 当我们在主仓库 git pull 之后发现检测出了子模块的更新(这种情况属于 本地的子仓库 落后于 本地主仓库中的子仓库 版本),手动执行 submodule update。
  • 项目保证主仓库只使用子模块的特定分支,且当 主仓库中的子仓库 产生冲突时,以最新的子模块版本为准。

参考

Git: submodule 子模块简明教程
7.11 Git 工具 - 子模块

GitHub Actions

GitHub Actions 是 github 推出的持续集成工具,简单来说就是可以在我们每次提交代码后在服务器上自动运行一些指令。我们不需要太复杂的 action,在仓库 build system 完备的基础上,只需要在代码提交后让 github 服务器自动运行我们的构建脚本并且检查仓库的构建与编译是否正常即可,以保证每一次 pr 都不会破坏仓库。

官方文档
GitHub Actions 使用 YAML 来定义工作流程。 每个工作流都作为单独的 YAML 文件存储在 .github/workflows/*.yml

一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
name: build

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
Windows:
runs-on: windows-latest
steps:
- name: Check out repository under $GITHUB_WORKSPACE.
uses: actions/checkout@v3

- name: Add MSBuild to PATH.
uses: microsoft/setup-msbuild@v1.1

- name: Setup project.
run: ${{github.workspace}}/Setup.bat

- name: Build x64 Debug.
run: msbuild ${{github.workspace}}/HinaEngine.sln /p:Configuration=Debug /p:Platform=x64

- name: Build x64 Release.
run: msbuild ${{github.workspace}}/HinaEngine.sln /p:Configuration=Release /p:Platform=x64

on

1
2
3
4
5
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

代表这个 actions 会被 main branch 上的 push 和 pr 动作触发。

jobs

1
2
3
jobs:
Windows:
runs-on: windows-latest
  • job 定义了最粗粒度的任务,默认每个 job 之间是并行的, 也可以用 needs: 来指定 job 之间的依赖关系,不过我们习惯使用多个 .yml 每个运行一个 job。
  • Windows: 是自定义的 job 名称,runs-on: 用于指定系统的运行环境,其他环境详见 选择 GitHub 托管的运行器

steps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
steps:
- name: Check out repository under $GITHUB_WORKSPACE.
uses: actions/checkout@v3

- name: Add MSBuild to PATH.
uses: microsoft/setup-msbuild@v1.1

- name: Setup project.
run: ${{github.workspace}}/Setup.bat

- name: Build x64 Debug.
run: msbuild ${{github.workspace}}/HinaEngine.sln /p:Configuration=Debug /p:Platform=x64

- name: Build x64 Release.
run: msbuild ${{github.workspace}}/HinaEngine.sln /p:Configuration=Release /p:Platform=x64
  • steps: 定义了该 job 中的每一步指令,在这里我们的流程是 1. 拉取仓库 2. 将 MSBuild 添加至环境变量 3. 构建与编译项目
  • 不同仓库中的一些操作是类似的,github 也在 Actions 市场 中提供了一系列封装好的命令。比如:
    • actions/checkout@v3 代表了将仓库拉取到 $GITHUB_WORKSPACECheckout
    • microsoft/setup-msbuild@v1.1 代表寻找 MSBuild 的路径,并且将其添加到环境变量。setup-msbuild
    • 检查这些 action 的官方页面以获取最新的版本号。
  • ${{github.workspace}} 代表了在命令行中该仓库的根目录。Setup.bat 是我们自定义的项目构建脚本,我们最终会在这个脚本中运行 premake 并且生成项目的 .sln 文件。
  • 接着在命令行中编译项目的 Debug 和 Release 版本。msbuild 命令行详见 MSBuild 命令行参考常用的 MSBuild 项目属性

Check Failed

Action 检查未通过是非常常见的,这时候只需要检查一下 Details 往往能帮我们定位错误。
.gitmodules
有时候他的报错会非常抽象,如果你认为这个 failed 可能不是你的问题,可以尝试在失败的 Actions 界面点击 Re-run job 按钮。

在 README 中添加状态徽章

github actions 会将自身运行的状态生成为一张图片,我们可以用这个链接访问它:

1
https://github.com/<OWNER>/<REPOSITORY>/actions/workflows/<WORKFLOW_FILE>/badge.svg

然后将其作为图片塞入 markdown 语法中并指向仓库的 actions 界面:

1
[![win64_vs2022_msvc](https://github.com/CatDogEngine/CatDogEngine/actions/workflows/win64_vs2022_msvc.yml/badge.svg?branch=main)](https://github.com/CatDogEngine/CatDogEngine/actions/workflows/win64_vs2022_msvc.yml)

效果:
win64_vs2022_msvc