GitHub 工作流
序言
一些工常用的 Git / GitHub 概念整理。
License
分支与开发
对于正规一点的仓库,main branch 通常是受保护的,我们的提交与推送都在另外的 branch 上进行。
查看分支
git branch
查看本地分支git branch -r
查看远程分支git branch -a
查看所有分支git branch -vv
查看本地与远程的对应关系
操作分支
git checkout -b <Name>
创建新的本地分支并 checkoutgit branch <Name>
创建新的本地分支,不会 checkoutgit branch -d <Name>
删除本地分支git branch -d -r <Name>
删除远程分支
也可以在网页上新建分支,本地直接 checkout 过去
PR
- 当你的 branch 准备好之后,就可以在 github 上发起 Pull Request,通知仓库相关的开发者检查代码以及帮忙合并。
- 一般来说,除非很紧急的情况,请在获得 approve 和 review 之后再进行合并。
- 选择 Sauash Merge 的方式将 pr 内多个 commit 合并为一个 commit 提交到 main 分支上。
- add, rm, commit, push 之类的操作直接用 VS 的
Git 更改
视图,或者Git GUI
就行,比命令方便得多。
冲突与合并
冲突
在发起 pr 后,github 会自动帮你检查该分支是否与主分支有冲突。简单的冲突可以直接在网页端解决,也可以回到本地手动 merge。
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,撤销 addHEAD
默认版本HEAD
和HEAD~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,即如果我要撤销 commitbalabala
,reset 的参数应该是balabala
的上一个 commit 的哈希。
这种情况下我比较习惯用--soft
将 commit 回退到暂存区而非默认的工作区,如果此时本地工作区有修改便能将他们区分开来。
Push
commit 已经 push 上去了,怎么撤销?
在完成上一节之后,本地和远程其实处于一个冲突的状态,这时候只需要用
git push --force
强制将远程的 log 同步成本地的状态。
PR
pr 已经 merge 了,怎么撤销?
在该 pr 的最下方的 merge 右侧找到 Revert
,这个按钮会自动创建一个新的 branch 以及 pr,并且在其中将原 pr 内的所有修改反向操作。当然尽量不要出现这种不得不 Revert 的情况。
历史中某一次 commit message
如何修改特定 commit 的 message 呢。
git rebase -i <CommitHash>
,这里的哈希是目标 commit 的上一次 commit。
然后就会打开 Vim(我这边自动关联到 VSCode 上了,整挺好),将你需要修改的 commit 前的 pick
改为 reword
,其余命令在注释里有更详细的解释。
然后修改 message,最后 force push。
注意该 commit 之后的所有 commit 哈希都会改变,提交时间也会变,而且原 commit 的页面依旧会缓存在 Github 上。
同样尽量不要出现不得不这么做的情况。
添加 .gitignore 之前就被追踪到的文件
git status --ignored
查看被忽略的文件
从 Git 索引中移除这些文件
git rm --cached <file>
移除单个文件
git rm --cached -r <directory>
递归移除整个目录
或者
git rm --cached -r .
移除所有文件
git add .
重新添加所有文件
Git Submodule
git 提供了子模块功能将一个仓库集成为另一个仓库的子目录,并且让他们保持独立的提交。
子模块有一个非常抽象的特性:在主仓库中如果不使用 submodule
相关的指令,子模块的内容不会受到任何 git 指令的影响,不受主仓库切换分支的影响,甚至不会在主仓库 clone 之后出现在你的硬盘里。
添加子模块
git submodule add <url> <path>
将 url 对应的仓库作为子模块添加到 path 路径下
添加子模块后主仓库会新增一个 .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 |
|
在这个 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 |
|
on
1 |
|
代表这个 actions 会被 main branch 上的 push 和 pr 动作触发。
jobs
1 |
|
- job 定义了最粗粒度的任务,默认每个 job 之间是并行的, 也可以用
needs:
来指定 job 之间的依赖关系,不过我们习惯使用多个 .yml 每个运行一个 job。 Windows:
是自定义的 job 名称,runs-on:
用于指定系统的运行环境,其他环境详见 选择 GitHub 托管的运行器。
steps
1 |
|
steps:
定义了该 job 中的每一步指令,在这里我们的流程是 1. 拉取仓库 2. 将 MSBuild 添加至环境变量 3. 构建与编译项目- 不同仓库中的一些操作是类似的,github 也在 Actions 市场 中提供了一系列封装好的命令。比如:
actions/checkout@v3
代表了将仓库拉取到$GITHUB_WORKSPACE
。Checkoutmicrosoft/setup-msbuild@v1.1
代表寻找 MSBuild 的路径,并且将其添加到环境变量。setup-msbuild- 检查这些 action 的官方页面以获取最新的版本号。
${{github.workspace}}
代表了在命令行中该仓库的根目录。Setup.bat
是我们自定义的项目构建脚本,我们最终会在这个脚本中运行 premake 并且生成项目的 .sln 文件。- 接着在命令行中编译项目的 Debug 和 Release 版本。msbuild 命令行详见 MSBuild 命令行参考 和 常用的 MSBuild 项目属性。
Check Failed
Action 检查未通过是非常常见的,这时候只需要检查一下 Details 往往能帮我们定位错误。
有时候他的报错会非常抽象,如果你认为这个 failed 可能不是你的问题,可以尝试在失败的 Actions 界面点击 Re-run job
按钮。
在 README 中添加状态徽章
github actions 会将自身运行的状态生成为一张图片,我们可以用这个链接访问它:
1 |
|
然后将其作为图片塞入 markdown 语法中并指向仓库的 actions 界面:
1 |
|