114: 深入刨析 Github CRL

GitHub 地址:https://github.com/cli/cli

项目布局

Github CTL (gh) 学习对后面的 GitHub actions 来说,帮助是非常大的,尤其是学会 gh 的开发,通过调用 gh 进而调用 GitHub api,对我们来说也是帮助很大。

  • cmd/ - main 用于构建二进制文件(如 gh 可执行文件)的软件包
  • pkg/ -大多数其他软件包,包括单个gh命令的实现
  • docs/ -面向维护者和贡献者的文档
  • script/ -构建和发布脚本
  • internal/ - Go软件包高度特定于我们的需求,因此是内部的
  • go.mod -此项目的外部Go依赖项,由Go在构建时自动获取

由于历史原因,一些辅助的Go包位于项目的顶层:

  • api/ -向GitHub API发出请求的主要实用程序
  • context/ -弃用:仅用于引用git远程
  • git/ -从本地git仓库收集信息的实用程序
  • test/ -弃用:不使用
  • utils/ -弃用:仅用于打印表输出

为什么要学习 gh ,第二个因素

OpenIM 也需要做一个 imctl,其实是和 gh 类似的,就和 kubernetes 的 kubeadm 与 client-go 的关系一样。OpenIM 需要先调用 core sdk 客户端进而掌控 OpenIM ,所以 imctl 的定位就绕不开 gh 了。

上手

和其他项目一样,第一步先从项目的编译开始,我们阅读 Makefile 文件:

本质上,实际上:

root@PS2023EVRHNCXG:~/workspaces/cubxxw/cli# make
GOOS= GOARCH= GOARM= GOFLAGS= CGO_ENABLED= go build -o script/build script/build.go
go build -trimpath -ldflags "-X github.com/cli/cli/v2/internal/build.Date=2023-08-24 -X github.com/cli/cli/v2/internal/build.Version=v2.33.0 " -o bin/gh ./cmd/gh
  1. go run script/build.go -确保获取所有外部Go依赖项,然后将 cmd/gh/main.go 文件编译为 bin/gh 二进制文件。

  2. gh auth login 认证,使用 Github Token

  3. bin/gh issue list --limit 5 -运行新构建的 bin/gh 二进制文件(注意:在Windows上,您必须使用反斜杠(如 bin\gh ),并将以下参数传递给进程:三号。

    root@PS2023EVRHNCXG:~/workspaces/cubxxw/cli# bin/gh issue list --limit 5
    
    Showing 5 of 431 open issues in cli/cli
    
    #7889  add "--all" flag to gh alias delete command                             enhancement, needs-triage  about 18 hours ago
    #7885  gh pr checks <pr-id> --watch: "something went wrong "                   bug, needs-triage          about 1 day ago
    #7883  Add JSON Output Option to `gh workflow list`                            enhancement, help wanted   about 18 hours ago
    #7881  `gh repo list <user> --json parent` fails with SAML error               bug, needs-triage          about 2 days ago
    #7875  Fined Grained API Token are not supported since 2022 that they are out  bug, needs-user-input      about 2 days ago
    
  4. cmd/gh/main.go 中的 func main() 是第一个运行的Go函数。传递给进程的参数可通过 os.Args 获得。

  5. main 包使用 root.NewCmdRoot() 初始化“root”命令,并使用 rootCmd.ExecuteC() 将执行分派给它。

  6. 根命令代表顶层的 gh 命令,并且知道如何将执行分派给嵌套在它下面的任何其他gh命令。

  7. 基于 ["issue", "list"] 参数,执行到达pkg/cmd/issue/list/list. go中 cobra.CommandRunE 块。

  8. 最初作为参数传递的 --limit 5 标志被自动解析,其值存储为 opts.LimitResults

  9. 调用 func listRun() ,负责实现 gh issue list 命令的逻辑。

  10. 该命令从GitHub API等来源收集信息,然后将最终输出写入标准输出和标准错误流,可在 opts.IO 中获得。

  11. 程序执行现在回到 cmd/gh/main.gofunc main() 。如果在处理命令时出现任何Go错误,则函数将以非零退出状态中止该过程。否则,该过程以指示成功的状态0结束。

如何添加新命令

这个问题很有意思,因为 imctl 将会用 imctl new 去做一个新的命名出来,而对于 gh 来说,虽然看上去很不爽,但是他们的代码模块还是很优雅的。

  1. 为新命令创建一个包,例如对于新命令 gh boom ,创建以下目录结构: pkg/cmd/boom/
  2. 新的包应该公开一个方法,例如 NewCmdBoom() ,它接受 *cmdutil.Factory 类型并返回 *cobra.Command
    1. 任何特定于此命令的逻辑都应该保留在命令的包中,而不是添加到任何“全局”包中,如 apiutils
  3. 使用上一步中的方法生成命令并将其添加到命令树中,通常在 NewCmdRoot() 方法中。

参与一个 gh 命令行工具的开发,肯定是少不了 new command 的,那么与此同时,如何做好测试显得尤为重要。

通常情况下,gh命令会执行以下操作:

  1. 从当前目录中的git存储库中查找信息
  2. 查询GitHub API
  3. 扫描用户的 ~/.ssh/config 文件
  4. 克隆或获取git存储库等

当然,在运行测试时,这些事情都不应该真实的发生,除非您确信任何文件系统操作都严格地限定在测试本身所创建和维护的位置。

为了避免实际运行诸如发出真实的的API请求或发送到 git 命令之类的事情,我们对它们进行了存根。您应该看看在一些现有测试中是如何做到这一点的。

要使代码可测试,请编写小的、独立的功能块,这些功能块被设计为组合在一起。在执行单个功能时,更喜欢使用表驱动测试来维护不同测试输入和期望的变化。