Merge PR #387: feat(commit): add commit group (+list, +recent, +diff, +file
# Conflicts: # internal/i18n/locales/en-US.json # internal/i18n/locales/zh-CN.json # shortcuts/commit/commit.go # shortcuts/commit/commit_test.go # shortcuts/register.go # shortcuts/register_test.go
This commit is contained in:
commit
6f7137084b
15
README.md
15
README.md
|
|
@ -797,3 +797,18 @@ See [skills/gitlink-shared/REFERENCE.md](skills/gitlink-shared/REFERENCE.md).
|
|||
## License
|
||||
|
||||
[MulanPSL-2.0](https://license.coscl.org.cn/MulanPSL2)
|
||||
|
||||
### Commits
|
||||
|
||||
```bash
|
||||
# List commits (optionally from a ref)
|
||||
gitlink-cli commit +list --owner Gitlink --repo forgeplus -s develop
|
||||
|
||||
# Recent commits filtered by keyword
|
||||
gitlink-cli commit +recent --owner Gitlink --repo forgeplus -k fix
|
||||
|
||||
# Diff and changed files of a commit
|
||||
gitlink-cli commit +diff --owner Gitlink --repo forgeplus -s <sha>
|
||||
gitlink-cli commit +files --owner Gitlink --repo forgeplus -s <sha> -f app/models/user.rb
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -16,11 +16,27 @@
|
|||
"cmd.ci.restart.short": "Restart a build",
|
||||
"cmd.ci.short": "CI/CD operations",
|
||||
"cmd.ci.stop.short": "Stop a build",
|
||||
"cmd.commit.diff.short": "Show the diff of a commit",
|
||||
"cmd.commit.files.short": "List files changed by a commit",
|
||||
"cmd.commit.list.short": "List commits, optionally starting from a ref",
|
||||
"cmd.commit.recent.short": "List recent commits, filterable by keyword",
|
||||
"cmd.commit.short": "Commit operations",
|
||||
"cmd.config.get.short": "Get a configuration value",
|
||||
"cmd.config.init.short": "Initialize configuration file",
|
||||
"cmd.config.list.short": "List all configuration values",
|
||||
"cmd.config.set.short": "Set a configuration value",
|
||||
"cmd.config.short": "Manage gitlink-cli configuration",
|
||||
"cmd.dataset.create.long": "Create the dataset of a repository with a title, description, optional license and research paper content.",
|
||||
"cmd.dataset.create.short": "Create a repository's dataset",
|
||||
"cmd.dataset.delete_attachment.long": "Delete a dataset attachment by its UUID. This is destructive: preview with --dry-run, then pass --yes to confirm.",
|
||||
"cmd.dataset.delete_attachment.short": "Delete a dataset attachment by UUID",
|
||||
"cmd.dataset.list.long": "List datasets for one or more GitLink projects by their numeric project IDs.",
|
||||
"cmd.dataset.list.short": "List datasets by project IDs",
|
||||
"cmd.dataset.short": "Dataset operations",
|
||||
"cmd.dataset.update.long": "Update the dataset of a repository (title, description, optional license and research paper content).",
|
||||
"cmd.dataset.update.short": "Update a repository's dataset",
|
||||
"cmd.dataset.view.long": "View a repository's dataset and its attachments. Use --page/--limit to paginate attachments.",
|
||||
"cmd.dataset.view.short": "View a repository's dataset",
|
||||
"cmd.doctor.long": "Run local diagnostics for gitlink-cli configuration, authentication, repository context and API connectivity.",
|
||||
"cmd.doctor.short": "Diagnose gitlink-cli environment problems",
|
||||
"cmd.issue.batch_close.long": "Close filtered issues in bulk.\n\nThis command defaults to dry-run mode and only prints matching issues.\nPass --yes to execute remote close operations. Use restrictive filters and a small limit.\n\nExamples:\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20 --yes",
|
||||
|
|
@ -41,8 +57,6 @@
|
|||
"cmd.org.list.short": "List organizations",
|
||||
"cmd.org.members.short": "List organization members",
|
||||
"cmd.org.short": "Organization operations",
|
||||
"cmd.pr.checkout.long": "Fetch a pull request head branch and check it out locally.\n\nThe command reads PR source repository information from GitLink, so it works for both same-repository PRs and fork PRs without requiring a local remote named origin.\n\nExamples:\n gitlink-cli pr +checkout -i 42\n gitlink-cli pr +checkout -i 42 --branch review/pr-42\n gitlink-cli pr +checkout -i 42 --force",
|
||||
"cmd.pr.checkout.short": "Fetch and checkout a pull request locally",
|
||||
"cmd.pr.close.short": "Close a pull request",
|
||||
"cmd.pr.comment.short": "Add a comment to a pull request",
|
||||
"cmd.pr.create.short": "Create a pull request",
|
||||
|
|
@ -56,6 +70,17 @@
|
|||
"cmd.pr.version_diff.short": "Show diff for a pull request patchset version",
|
||||
"cmd.pr.versions.short": "List pull request patchset versions",
|
||||
"cmd.pr.view.short": "View pull request details",
|
||||
"cmd.profile.ability.long": "Show a user's development ability scores (influence, contribution, activity, experience, language) and language breakdown, sourced from the GitLink platform statistics API.",
|
||||
"cmd.profile.ability.short": "Show a user's development ability scores",
|
||||
"cmd.profile.activity.long": "Show a user's recent activity statistics: daily issues, pull requests, and commits over a time window.",
|
||||
"cmd.profile.activity.short": "Show a user's recent activity statistics",
|
||||
"cmd.profile.contribution.long": "Show a user's contribution heatmap (daily contribution counts) for a given year.",
|
||||
"cmd.profile.contribution.short": "Show a user's contribution heatmap",
|
||||
"cmd.profile.major.long": "Show a user's major/discipline categories (e.g. deep learning, quantum computing) inferred from their projects.",
|
||||
"cmd.profile.major.short": "Show a user's major/discipline categories",
|
||||
"cmd.profile.role.long": "Show a user's role positioning derived from the GitLink platform statistics API.",
|
||||
"cmd.profile.role.short": "Show a user's role positioning",
|
||||
"cmd.profile.short": "User profile and statistics operations",
|
||||
"cmd.release.create.short": "Create a release",
|
||||
"cmd.release.delete.short": "Delete a release",
|
||||
"cmd.release.list.short": "List releases",
|
||||
|
|
@ -67,6 +92,7 @@
|
|||
"cmd.repo.info.short": "Show repository details",
|
||||
"cmd.repo.list.short": "List repositories for a user or organization",
|
||||
"cmd.repo.short": "Repository operations",
|
||||
"cmd.repo.tree.short": "List repository files and directories",
|
||||
"cmd.root.long": "Manage repositories, issues, pull requests, releases, CI and workflows on GitLink.",
|
||||
"cmd.root.short": "GitLink CLI - command-line tool for GitLink",
|
||||
"cmd.search.repos.short": "Search repositories",
|
||||
|
|
@ -89,7 +115,9 @@
|
|||
"error.auth.store_token_failed": "failed to store token: {message}",
|
||||
"error.auth.token_empty": "token cannot be empty",
|
||||
"error.config.save_failed": "failed to save config: {message}",
|
||||
"error.dataset.delete_confirm": "dataset attachment deletion is destructive; run --dry-run first, then pass --yes to confirm",
|
||||
"error.missing_required_flag": "required flag --{name} is missing",
|
||||
"error.profile.user_required": "could not determine target user; pass --user or run gitlink-cli auth login",
|
||||
"error.unsupported_language": "unsupported language: {lang}",
|
||||
"flag.api.batch_continue_on_error": "Continue running remaining batch requests after a failure",
|
||||
"flag.api.batch_dry_run": "Preview batch requests without sending remote requests",
|
||||
|
|
@ -107,6 +135,21 @@
|
|||
"flag.ci.stage": "Stage number",
|
||||
"flag.ci.step": "Step number",
|
||||
"flag.comment.body": "Comment body",
|
||||
"flag.commit.filepath": "Restrict to a single file path",
|
||||
"flag.commit.keyword": "Filter commits by keyword",
|
||||
"flag.commit.sha": "Commit SHA",
|
||||
"flag.commit.sha_start": "Ref (branch, tag, or SHA) to list commits from",
|
||||
"flag.dataset.description": "Dataset description",
|
||||
"flag.dataset.dry_run": "Preview the request without writing the dataset",
|
||||
"flag.dataset.dry_run_delete": "Preview the request without deleting the attachment",
|
||||
"flag.dataset.ids": "Comma-separated project IDs to query datasets for",
|
||||
"flag.dataset.license_id": "License ID",
|
||||
"flag.dataset.limit": "Attachment page size",
|
||||
"flag.dataset.page": "Attachment page number",
|
||||
"flag.dataset.paper_content": "Research paper content",
|
||||
"flag.dataset.title": "Dataset title",
|
||||
"flag.dataset.uuid": "Attachment UUID",
|
||||
"flag.dataset.yes": "Confirm the destructive deletion",
|
||||
"flag.debug": "Enable debug output",
|
||||
"flag.description": "Description",
|
||||
"flag.doctor.skip_network": "Skip authenticated API connectivity checks",
|
||||
|
|
@ -147,9 +190,6 @@
|
|||
"flag.pr.assignee_id": "Assignee user ID",
|
||||
"flag.pr.base": "Target branch",
|
||||
"flag.pr.body": "PR description",
|
||||
"flag.pr.checkout_branch": "Local branch name to create (default: pr-<id>)",
|
||||
"flag.pr.checkout_dry_run": "Print the git commands without modifying the local repository",
|
||||
"flag.pr.checkout_force": "Reset the local branch if it already exists",
|
||||
"flag.pr.file": "Filter diff by file path",
|
||||
"flag.pr.head": "Source branch",
|
||||
"flag.pr.id": "PR number",
|
||||
|
|
@ -165,6 +205,10 @@
|
|||
"flag.pr.tag_id": "Issue tag ID",
|
||||
"flag.pr.title": "PR title",
|
||||
"flag.pr.version_id": "Patchset version ID",
|
||||
"flag.profile.end_time": "End time (Unix timestamp)",
|
||||
"flag.profile.start_time": "Start time (Unix timestamp)",
|
||||
"flag.profile.user": "Target user login (defaults to the authenticated user)",
|
||||
"flag.profile.year": "Year for the contribution heatmap (e.g. 2025)",
|
||||
"flag.release.body": "Release notes",
|
||||
"flag.release.id": "Release ID",
|
||||
"flag.release.id_or_tag": "Release ID or tag",
|
||||
|
|
@ -177,6 +221,8 @@
|
|||
"flag.repo.description": "Repository description",
|
||||
"flag.repo.name": "Repository name",
|
||||
"flag.repo.private": "Make repository private (true/false)",
|
||||
"flag.repo.tree.path": "Directory path to list (default: repository root)",
|
||||
"flag.repo.tree.ref": "Branch, tag, or commit ref",
|
||||
"flag.search.keyword": "Search keyword",
|
||||
"flag.sort_by": "Sort field",
|
||||
"flag.sort_direction": "Sort direction: asc, desc",
|
||||
|
|
|
|||
|
|
@ -16,11 +16,27 @@
|
|||
"cmd.ci.restart.short": "重启构建",
|
||||
"cmd.ci.short": "CI/CD 操作",
|
||||
"cmd.ci.stop.short": "停止构建",
|
||||
"cmd.commit.diff.short": "查看提交的 diff",
|
||||
"cmd.commit.files.short": "列出提交变更的文件",
|
||||
"cmd.commit.list.short": "列出提交,可从指定 ref 开始",
|
||||
"cmd.commit.recent.short": "列出最近提交,可按关键字过滤",
|
||||
"cmd.commit.short": "提交操作",
|
||||
"cmd.config.get.short": "获取配置项",
|
||||
"cmd.config.init.short": "初始化配置文件",
|
||||
"cmd.config.list.short": "列出所有配置项",
|
||||
"cmd.config.set.short": "设置配置项",
|
||||
"cmd.config.short": "管理 gitlink-cli 配置",
|
||||
"cmd.dataset.create.long": "为仓库创建数据集,包含标题、描述、可选许可证和研究论文内容。",
|
||||
"cmd.dataset.create.short": "创建仓库数据集",
|
||||
"cmd.dataset.delete_attachment.long": "按 UUID 删除数据集附件。该操作具有破坏性:先用 --dry-run 预览,再传 --yes 确认。",
|
||||
"cmd.dataset.delete_attachment.short": "按 UUID 删除数据集附件",
|
||||
"cmd.dataset.list.long": "按数字项目 ID 列出一个或多个 GitLink 项目的数据集。",
|
||||
"cmd.dataset.list.short": "按项目 ID 列出数据集",
|
||||
"cmd.dataset.short": "数据集操作",
|
||||
"cmd.dataset.update.long": "更新仓库数据集(标题、描述、可选许可证和研究论文内容)。",
|
||||
"cmd.dataset.update.short": "更新仓库数据集",
|
||||
"cmd.dataset.view.long": "查看仓库的数据集及其附件。用 --page/--limit 对附件分页。",
|
||||
"cmd.dataset.view.short": "查看仓库数据集",
|
||||
"cmd.doctor.long": "诊断 gitlink-cli 的配置、认证、仓库上下文和 API 连通性问题。",
|
||||
"cmd.doctor.short": "诊断 gitlink-cli 环境问题",
|
||||
"cmd.issue.batch_close.long": "批量关闭筛选后的议题。\n\n该命令默认处于 dry-run 模式,只打印匹配的议题。\n传入 --yes 后执行远端关闭操作。请使用严格筛选条件和较小 limit。\n\n示例:\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20 --yes",
|
||||
|
|
@ -41,8 +57,6 @@
|
|||
"cmd.org.list.short": "列出组织",
|
||||
"cmd.org.members.short": "列出组织成员",
|
||||
"cmd.org.short": "组织操作",
|
||||
"cmd.pr.checkout.long": "拉取拉取请求的源分支并切换到本地分支。\n\n命令会从 GitLink 读取 PR 的源仓库信息,因此同仓库 PR 和 fork PR 都可以使用,不依赖本地 remote 必须命名为 origin。\n\n示例:\n gitlink-cli pr +checkout -i 42\n gitlink-cli pr +checkout -i 42 --branch review/pr-42\n gitlink-cli pr +checkout -i 42 --force",
|
||||
"cmd.pr.checkout.short": "拉取并切换到拉取请求分支",
|
||||
"cmd.pr.close.short": "关闭拉取请求",
|
||||
"cmd.pr.comment.short": "给拉取请求添加评论",
|
||||
"cmd.pr.create.short": "创建拉取请求",
|
||||
|
|
@ -56,6 +70,17 @@
|
|||
"cmd.pr.version_diff.short": "显示拉取请求补丁集版本 diff",
|
||||
"cmd.pr.versions.short": "列出拉取请求补丁集版本",
|
||||
"cmd.pr.view.short": "查看拉取请求详情",
|
||||
"cmd.profile.ability.long": "显示用户的开发能力评分(影响力、贡献度、活跃度、项目经验、语言能力)及语言分布,数据来自 GitLink 平台统计接口。",
|
||||
"cmd.profile.ability.short": "显示用户开发能力评分",
|
||||
"cmd.profile.activity.long": "显示用户近期活动统计:时间范围内每日的疑修、合并请求和提交数量。",
|
||||
"cmd.profile.activity.short": "显示用户近期活动统计",
|
||||
"cmd.profile.contribution.long": "显示用户在指定年份的贡献热力图(每日贡献次数)。",
|
||||
"cmd.profile.contribution.short": "显示用户贡献热力图",
|
||||
"cmd.profile.major.long": "显示根据用户项目推断的专业/学科领域(如深度学习、量子计算)。",
|
||||
"cmd.profile.major.short": "显示用户专业/学科定位",
|
||||
"cmd.profile.role.long": "显示来自 GitLink 平台统计接口的用户角色定位。",
|
||||
"cmd.profile.role.short": "显示用户角色定位",
|
||||
"cmd.profile.short": "用户画像与统计操作",
|
||||
"cmd.release.create.short": "创建发布",
|
||||
"cmd.release.delete.short": "删除发布",
|
||||
"cmd.release.list.short": "列出发布",
|
||||
|
|
@ -67,6 +92,7 @@
|
|||
"cmd.repo.info.short": "显示仓库详情",
|
||||
"cmd.repo.list.short": "列出用户或组织的仓库",
|
||||
"cmd.repo.short": "仓库操作",
|
||||
"cmd.repo.tree.short": "列出仓库文件和目录",
|
||||
"cmd.root.long": "用于管理 GitLink 上的仓库、议题、拉取请求、发布、CI 和工作流。",
|
||||
"cmd.root.short": "GitLink CLI - GitLink 命令行工具",
|
||||
"cmd.search.repos.short": "搜索仓库",
|
||||
|
|
@ -89,7 +115,9 @@
|
|||
"error.auth.store_token_failed": "保存 Token 失败:{message}",
|
||||
"error.auth.token_empty": "Token 不能为空",
|
||||
"error.config.save_failed": "保存配置失败:{message}",
|
||||
"error.dataset.delete_confirm": "删除数据集附件具有破坏性;请先 --dry-run 预览,再传 --yes 确认",
|
||||
"error.missing_required_flag": "缺少必需参数 --{name}",
|
||||
"error.profile.user_required": "无法确定目标用户;请通过 --user 指定,或先运行 gitlink-cli auth login 登录",
|
||||
"error.unsupported_language": "不支持的语言:{lang}",
|
||||
"flag.api.batch_continue_on_error": "批处理请求失败后继续执行后续请求",
|
||||
"flag.api.batch_dry_run": "预览批处理请求,不发送远端请求",
|
||||
|
|
@ -107,6 +135,21 @@
|
|||
"flag.ci.stage": "阶段编号",
|
||||
"flag.ci.step": "步骤编号",
|
||||
"flag.comment.body": "评论内容",
|
||||
"flag.commit.filepath": "仅限单个文件路径",
|
||||
"flag.commit.keyword": "按关键字过滤提交",
|
||||
"flag.commit.sha": "提交 SHA",
|
||||
"flag.commit.sha_start": "起始 ref(分支、标签或 SHA)",
|
||||
"flag.dataset.description": "数据集描述",
|
||||
"flag.dataset.dry_run": "预览请求,不写入数据集",
|
||||
"flag.dataset.dry_run_delete": "预览请求,不删除附件",
|
||||
"flag.dataset.ids": "用于查询数据集的项目 ID,逗号分隔",
|
||||
"flag.dataset.license_id": "许可证 ID",
|
||||
"flag.dataset.limit": "附件每页数量",
|
||||
"flag.dataset.page": "附件页码",
|
||||
"flag.dataset.paper_content": "研究论文内容",
|
||||
"flag.dataset.title": "数据集标题",
|
||||
"flag.dataset.uuid": "附件 UUID",
|
||||
"flag.dataset.yes": "确认执行破坏性删除",
|
||||
"flag.debug": "启用调试输出",
|
||||
"flag.description": "描述",
|
||||
"flag.doctor.skip_network": "跳过需要访问 GitLink 的认证连通性检查",
|
||||
|
|
@ -147,9 +190,6 @@
|
|||
"flag.pr.assignee_id": "指派人用户 ID",
|
||||
"flag.pr.base": "目标分支",
|
||||
"flag.pr.body": "PR 描述",
|
||||
"flag.pr.checkout_branch": "要创建的本地分支名(默认:pr-<id>)",
|
||||
"flag.pr.checkout_dry_run": "只打印 git 命令,不修改本地仓库",
|
||||
"flag.pr.checkout_force": "本地分支已存在时重置该分支",
|
||||
"flag.pr.file": "按文件路径筛选 diff",
|
||||
"flag.pr.head": "源分支",
|
||||
"flag.pr.id": "PR 编号",
|
||||
|
|
@ -165,6 +205,10 @@
|
|||
"flag.pr.tag_id": "议题标签 ID",
|
||||
"flag.pr.title": "PR 标题",
|
||||
"flag.pr.version_id": "补丁集版本 ID",
|
||||
"flag.profile.end_time": "结束时间(Unix 时间戳)",
|
||||
"flag.profile.start_time": "开始时间(Unix 时间戳)",
|
||||
"flag.profile.user": "目标用户登录名(默认为当前认证用户)",
|
||||
"flag.profile.year": "贡献热力图的年份(如 2025)",
|
||||
"flag.release.body": "发布说明",
|
||||
"flag.release.id": "发布 ID",
|
||||
"flag.release.id_or_tag": "发布 ID 或标签",
|
||||
|
|
@ -177,6 +221,8 @@
|
|||
"flag.repo.description": "仓库描述",
|
||||
"flag.repo.name": "仓库名称",
|
||||
"flag.repo.private": "设为私有仓库(true/false)",
|
||||
"flag.repo.tree.path": "要列出的目录路径(默认:仓库根目录)",
|
||||
"flag.repo.tree.ref": "分支、标签或提交引用",
|
||||
"flag.search.keyword": "搜索关键词",
|
||||
"flag.sort_by": "排序字段",
|
||||
"flag.sort_direction": "排序方向:asc、desc",
|
||||
|
|
|
|||
|
|
@ -4,22 +4,28 @@ import (
|
|||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func Shortcuts() []*common.Shortcut {
|
||||
func v1RepoPath(ctx *common.RuntimeContext) string {
|
||||
return fmt.Sprintf("/v1/%s/%s", ctx.Owner, ctx.Repo)
|
||||
}
|
||||
|
||||
func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
||||
tr := shortcutTranslator(translators...)
|
||||
return []*common.Shortcut{
|
||||
{
|
||||
Name: "list",
|
||||
Description: "List commits in a repository",
|
||||
Description: tr.T("cmd.commit.list.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "sha", Short: "s", Usage: "Branch, tag, or commit SHA"},
|
||||
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
{Name: "sha", Short: "s", Usage: tr.T("flag.commit.sha_start")},
|
||||
{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
|
|
@ -27,24 +33,71 @@ func Shortcuts() []*common.Shortcut {
|
|||
if sha := ctx.Arg("sha"); sha != "" {
|
||||
q.Set("sha", sha)
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/v1/%s/%s/commits", ctx.Owner, ctx.Repo), q)
|
||||
env, err := ctx.CallAPIWithQuery("GET", v1RepoPath(ctx)+"/commits", q)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取提交列表失败: %w", err)
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "view",
|
||||
Description: "View files changed in a commit",
|
||||
Name: "recent",
|
||||
Description: tr.T("cmd.commit.recent.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "sha", Short: "s", Usage: "Commit SHA", Required: true},
|
||||
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
{Name: "keyword", Short: "k", Usage: tr.T("flag.commit.keyword")},
|
||||
{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
if keyword := ctx.Arg("keyword"); keyword != "" {
|
||||
q.Set("keyword", keyword)
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", v1RepoPath(ctx)+"/commits/recent", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "diff",
|
||||
Description: tr.T("cmd.commit.diff.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "sha", Short: "s", Usage: tr.T("flag.commit.sha"), Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
sha, err := ctx.RequireArg("sha")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/commits/%s/diff", v1RepoPath(ctx), url.PathEscape(sha)), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "files",
|
||||
Description: tr.T("cmd.commit.files.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "sha", Short: "s", Usage: tr.T("flag.commit.sha"), Required: true},
|
||||
{Name: "filepath", Short: "f", Usage: tr.T("flag.commit.filepath")},
|
||||
{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
sha, err := ctx.RequireArg("sha")
|
||||
if err != nil {
|
||||
|
|
@ -53,58 +106,22 @@ func Shortcuts() []*common.Shortcut {
|
|||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/v1/%s/%s/commits/%s/files", ctx.Owner, ctx.Repo, sha), q)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查看提交详情失败: %w", err)
|
||||
if fp := ctx.Arg("filepath"); fp != "" {
|
||||
q.Set("filepath", fp)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "diff",
|
||||
Description: "Show diff for a commit",
|
||||
Flags: []common.Flag{
|
||||
{Name: "sha", Short: "s", Usage: "Commit SHA", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
sha, err := ctx.RequireArg("sha")
|
||||
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("%s/commits/%s/files", v1RepoPath(ctx), url.PathEscape(sha)), q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("/v1/%s/%s/commits/%s/diff", ctx.Owner, ctx.Repo, sha), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取提交 Diff 失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "blame",
|
||||
Description: "Show blame for a file",
|
||||
Flags: []common.Flag{
|
||||
{Name: "path", Short: "p", Usage: "File path", Required: true},
|
||||
{Name: "sha", Short: "s", Usage: "Branch, tag, or commit SHA", Default: "master"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return fmt.Errorf("解析仓库信息失败: %w", err)
|
||||
}
|
||||
filePath, err := ctx.RequireArg("path")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("filepath", filePath)
|
||||
q.Set("sha", ctx.Arg("sha"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/v1/%s/%s/blame", ctx.Owner, ctx.Repo), q)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取 Blame 信息失败: %w", err)
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator {
|
||||
if len(translators) > 0 && translators[0] != nil {
|
||||
return translators[0]
|
||||
}
|
||||
return i18n.Default()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,160 +1,97 @@
|
|||
package commit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func TestCommitList(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/commits.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"total_count": 1,
|
||||
"commits": []map[string]interface{}{
|
||||
{"sha": "abc123", "commit_message": "initial commit"},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runCommitShortcut(t, server, "list", map[string]string{})
|
||||
if err != nil {
|
||||
t.Fatalf("list shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitListWithSHA(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/commits.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("sha") != "develop" {
|
||||
t.Fatalf("expected sha=develop, got %s", r.URL.Query().Get("sha"))
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"total_count": 0,
|
||||
"commits": []map[string]interface{}{},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runCommitShortcut(t, server, "list", map[string]string{"sha": "develop"})
|
||||
if err != nil {
|
||||
t.Fatalf("list with sha failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitView(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" && r.URL.Path == "/v1/owner/repo/commits/abc123/files.json" {
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"file_nums": 1,
|
||||
"files": []map[string]interface{}{
|
||||
{"filename": "main.go", "additions": 10, "deletions": 2},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runCommitShortcut(t, server, "view", map[string]string{"sha": "abc123"})
|
||||
if err != nil {
|
||||
t.Fatalf("view shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitDiff(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" && r.URL.Path == "/v1/owner/repo/commits/abc123/diff.json" {
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"file_nums": 1,
|
||||
"total_addition": 10,
|
||||
"total_deletion": 2,
|
||||
"files": []map[string]interface{}{{"name": "main.go"}},
|
||||
})
|
||||
return
|
||||
}
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runCommitShortcut(t, server, "diff", map[string]string{"sha": "abc123"})
|
||||
if err != nil {
|
||||
t.Fatalf("diff shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitBlame(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/blame.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("filepath") != "main.go" {
|
||||
t.Fatalf("expected filepath=main.go, got %s", r.URL.Query().Get("filepath"))
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"file_name": "main.go",
|
||||
"num_lines": 20,
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runCommitShortcut(t, server, "blame", map[string]string{"path": "main.go", "sha": "master"})
|
||||
if err != nil {
|
||||
t.Fatalf("blame shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// === helpers ===
|
||||
|
||||
func runCommitShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
|
||||
t.Helper()
|
||||
shortcut := findCommitShortcut(t, name)
|
||||
ctx := &common.RuntimeContext{
|
||||
Client: &client.Client{
|
||||
HTTP: server.Client(),
|
||||
BaseURL: server.URL,
|
||||
},
|
||||
Owner: "owner",
|
||||
Repo: "repo",
|
||||
Format: "json",
|
||||
Args: args,
|
||||
}
|
||||
return shortcut.Run(ctx)
|
||||
}
|
||||
|
||||
func findCommitShortcut(t *testing.T, name string) *common.Shortcut {
|
||||
func runShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
|
||||
t.Helper()
|
||||
for _, s := range Shortcuts() {
|
||||
if s.Name == name {
|
||||
return s
|
||||
ctx := &common.RuntimeContext{
|
||||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||||
Owner: "owner",
|
||||
Repo: "repo",
|
||||
Format: "json",
|
||||
Args: args,
|
||||
}
|
||||
if ctx.Args == nil {
|
||||
ctx.Args = map[string]string{}
|
||||
}
|
||||
return s.Run(ctx)
|
||||
}
|
||||
}
|
||||
t.Fatalf("shortcut %q not found", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeJSON(t *testing.T, w http.ResponseWriter, payload interface{}) {
|
||||
t.Helper()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
t.Fatalf("failed to write response: %v", err)
|
||||
func TestCommitListForwardsShaAndPaging(t *testing.T) {
|
||||
var query string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/commits.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
query = r.URL.RawQuery
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"total_count":1,"commits":[]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "list", map[string]string{"sha": "develop", "page": "2", "limit": "5"}); err != nil {
|
||||
t.Fatalf("list failed: %v", err)
|
||||
}
|
||||
for _, want := range []string{"sha=develop", "page=2", "limit=5"} {
|
||||
if !strings.Contains(query, want) {
|
||||
t.Fatalf("expected %q in query, got %q", want, query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqual(t *testing.T, got, want interface{}) {
|
||||
t.Helper()
|
||||
if fmt.Sprintf("%v", got) != fmt.Sprintf("%v", want) {
|
||||
t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want)
|
||||
func TestCommitRecentForwardsKeyword(t *testing.T) {
|
||||
var query string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/commits/recent.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
query = r.URL.RawQuery
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"commits":[]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "recent", map[string]string{"keyword": "fix", "page": "1", "limit": "20"}); err != nil {
|
||||
t.Fatalf("recent failed: %v", err)
|
||||
}
|
||||
if !strings.Contains(query, "keyword=fix") {
|
||||
t.Fatalf("expected keyword=fix in query, got %q", query)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitDiffAndFilesBuildShaPaths(t *testing.T) {
|
||||
var paths []string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
paths = append(paths, r.URL.Path)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"ok":true}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "diff", map[string]string{"sha": "abc123"}); err != nil {
|
||||
t.Fatalf("diff failed: %v", err)
|
||||
}
|
||||
if err := runShortcut(t, server, "files", map[string]string{"sha": "abc123", "filepath": "app/x.rb", "page": "1", "limit": "20"}); err != nil {
|
||||
t.Fatalf("files failed: %v", err)
|
||||
}
|
||||
if paths[0] != "/v1/owner/repo/commits/abc123/diff.json" {
|
||||
t.Fatalf("diff path = %s", paths[0])
|
||||
}
|
||||
if paths[1] != "/v1/owner/repo/commits/abc123/files.json" {
|
||||
t.Fatalf("files path = %s", paths[1])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,23 +6,27 @@ import (
|
|||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/branch"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/ci"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/commit"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/compare"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/dataset"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/health"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/ignore"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/issue"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/label"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/license"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/member"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/milestone"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/notification"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/org"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/pipeline"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/pr"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/profile"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/release"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/repo"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/search"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/user"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/webhook"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/wiki"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/workflow"
|
||||
)
|
||||
|
||||
|
|
@ -33,47 +37,55 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
tr = translators[0]
|
||||
}
|
||||
groups := map[string][]*common.Shortcut{
|
||||
"repo": repo.Shortcuts(tr),
|
||||
"issue": issue.Shortcuts(tr),
|
||||
"label": label.Shortcuts(),
|
||||
"license": license.Shortcuts(),
|
||||
"member": member.Shortcuts(),
|
||||
"milestone": milestone.Shortcuts(),
|
||||
"notification": notification.Shortcuts(),
|
||||
"pipeline": pipeline.Shortcuts(),
|
||||
"pr": pr.Shortcuts(tr),
|
||||
"release": release.Shortcuts(tr),
|
||||
"branch": branch.Shortcuts(tr),
|
||||
"org": org.Shortcuts(tr),
|
||||
"user": user.Shortcuts(tr),
|
||||
"search": search.Shortcuts(tr),
|
||||
"ci": ci.Shortcuts(tr),
|
||||
"compare": compare.Shortcuts(),
|
||||
"webhook": webhook.Shortcuts(tr),
|
||||
"health": health.Shortcuts(tr),
|
||||
"workflow": workflow.Shortcuts(),
|
||||
"repo": repo.Shortcuts(tr),
|
||||
"issue": issue.Shortcuts(tr),
|
||||
"label": label.Shortcuts(),
|
||||
"license": license.Shortcuts(),
|
||||
"member": member.Shortcuts(),
|
||||
"milestone": milestone.Shortcuts(),
|
||||
"pipeline": pipeline.Shortcuts(),
|
||||
"pr": pr.Shortcuts(tr),
|
||||
"profile": profile.Shortcuts(tr),
|
||||
"release": release.Shortcuts(tr),
|
||||
"branch": branch.Shortcuts(tr),
|
||||
"org": org.Shortcuts(tr),
|
||||
"user": user.Shortcuts(tr),
|
||||
"search": search.Shortcuts(tr),
|
||||
"ci": ci.Shortcuts(tr),
|
||||
"commit": commit.Shortcuts(tr),
|
||||
"compare": compare.Shortcuts(),
|
||||
"dataset": dataset.Shortcuts(tr),
|
||||
"webhook": webhook.Shortcuts(tr),
|
||||
"wiki": wiki.Shortcuts(),
|
||||
"health": health.Shortcuts(tr),
|
||||
"ignore": ignore.Shortcuts(),
|
||||
"workflow": workflow.Shortcuts(),
|
||||
}
|
||||
|
||||
descriptions := map[string]string{
|
||||
"repo": tr.T("cmd.repo.short"),
|
||||
"issue": tr.T("cmd.issue.short"),
|
||||
"label": "Issue label operations",
|
||||
"license": "License operations",
|
||||
"member": "Repository member operations",
|
||||
"milestone": "Milestone operations",
|
||||
"notification": "Notification and message operations",
|
||||
"pipeline": "Pipeline operations",
|
||||
"pr": tr.T("cmd.pr.short"),
|
||||
"release": tr.T("cmd.release.short"),
|
||||
"branch": tr.T("cmd.branch.short"),
|
||||
"org": tr.T("cmd.org.short"),
|
||||
"user": tr.T("cmd.user.short"),
|
||||
"search": tr.T("cmd.search.short"),
|
||||
"ci": tr.T("cmd.ci.short"),
|
||||
"compare": "Compare branches, tags, or commits",
|
||||
"webhook": tr.T("cmd.webhook.short"),
|
||||
"health": "Project health data collection",
|
||||
"workflow": "AI agent workflow analysis",
|
||||
"repo": tr.T("cmd.repo.short"),
|
||||
"issue": tr.T("cmd.issue.short"),
|
||||
"label": "Issue label operations",
|
||||
"license": "License operations",
|
||||
"member": "Repository member operations",
|
||||
"milestone": "Milestone operations",
|
||||
"pipeline": "Pipeline operations",
|
||||
"pr": tr.T("cmd.pr.short"),
|
||||
"profile": tr.T("cmd.profile.short"),
|
||||
"release": tr.T("cmd.release.short"),
|
||||
"branch": tr.T("cmd.branch.short"),
|
||||
"org": tr.T("cmd.org.short"),
|
||||
"user": tr.T("cmd.user.short"),
|
||||
"search": tr.T("cmd.search.short"),
|
||||
"ci": tr.T("cmd.ci.short"),
|
||||
"commit": tr.T("cmd.commit.short"),
|
||||
"compare": "Compare branches, tags, or commits",
|
||||
"dataset": tr.T("cmd.dataset.short"),
|
||||
"webhook": tr.T("cmd.webhook.short"),
|
||||
"wiki": "Wiki page management",
|
||||
"health": "Project health data collection",
|
||||
"ignore": tr.T("cmd.ignore.short"),
|
||||
"workflow": "AI agent workflow analysis",
|
||||
}
|
||||
|
||||
for name, shortcuts := range groups {
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ func TestRegisterAll(t *testing.T) {
|
|||
RegisterAll(root)
|
||||
|
||||
expectedGroups := []string{
|
||||
"repo", "issue", "label", "license", "pr", "release", "branch",
|
||||
"repo", "issue", "label", "license", "pr", "profile", "release", "branch",
|
||||
"org", "user", "search", "ci", "workflow",
|
||||
"compare", "member", "milestone", "pipeline", "webhook",
|
||||
"health", "notification",
|
||||
"commit", "compare", "member", "milestone", "pipeline", "webhook",
|
||||
"dataset", "health", "ignore", "wiki",
|
||||
}
|
||||
|
||||
groupSet := map[string]bool{}
|
||||
|
|
|
|||
Loading…
Reference in New Issue