forked from Gitlink/gitlink-cli
feat(pr): add +commits and +check-merge
- pr +commits: list commits of a pull request (GET /pulls/:id/commits) - pr +check-merge: pre-flight check_can_merge before creating a PR. Detects same-branch no-ops and existing duplicate merge requests, including the cross-fork case via --fork-project-id (which also sets is_original, matching the server's duplicate lookup). Production-verified on gitlink.org.cn: commits listing, a 'can merge' pass, and duplicate detection that points at the existing PR. 2 unit tests, README examples, bilingual i18n keys. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
c09645da62
commit
a0370fb96b
|
|
@ -420,6 +420,14 @@ gitlink-cli pr +reopen --owner Gitlink --repo forgeplus -i 42
|
||||||
# View changed files
|
# View changed files
|
||||||
gitlink-cli pr +files --owner Gitlink --repo forgeplus -i 42
|
gitlink-cli pr +files --owner Gitlink --repo forgeplus -i 42
|
||||||
|
|
||||||
|
# List commits of a pull request
|
||||||
|
gitlink-cli pr +commits --owner Gitlink --repo forgeplus -i 42
|
||||||
|
|
||||||
|
# Pre-flight: can a merge request be created between two branches?
|
||||||
|
gitlink-cli pr +check-merge --owner Gitlink --repo forgeplus --head develop --base master
|
||||||
|
# Cross-fork variant
|
||||||
|
gitlink-cli pr +check-merge --owner Gitlink --repo forgeplus --head feat/x --base master --fork-project-id 12345
|
||||||
|
|
||||||
# List PR patchset versions
|
# List PR patchset versions
|
||||||
gitlink-cli pr +versions --owner Gitlink --repo forgeplus -i 42
|
gitlink-cli pr +versions --owner Gitlink --repo forgeplus -i 42
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,10 @@
|
||||||
"cmd.org.list.short": "List organizations",
|
"cmd.org.list.short": "List organizations",
|
||||||
"cmd.org.members.short": "List organization members",
|
"cmd.org.members.short": "List organization members",
|
||||||
"cmd.org.short": "Organization operations",
|
"cmd.org.short": "Organization operations",
|
||||||
|
"cmd.pr.check_merge.short": "Pre-flight check whether a merge request can be created between two branches",
|
||||||
"cmd.pr.close.short": "Close a pull request",
|
"cmd.pr.close.short": "Close a pull request",
|
||||||
"cmd.pr.comment.short": "Add a comment to a pull request",
|
"cmd.pr.comment.short": "Add a comment to a pull request",
|
||||||
|
"cmd.pr.commits.short": "List commits of a pull request",
|
||||||
"cmd.pr.create.short": "Create a pull request",
|
"cmd.pr.create.short": "Create a pull request",
|
||||||
"cmd.pr.diff.short": "Show diff for a pull request",
|
"cmd.pr.diff.short": "Show diff for a pull request",
|
||||||
"cmd.pr.files.short": "List changed files in a pull request",
|
"cmd.pr.files.short": "List changed files in a pull request",
|
||||||
|
|
@ -182,6 +184,7 @@
|
||||||
"flag.pr.base": "Target branch",
|
"flag.pr.base": "Target branch",
|
||||||
"flag.pr.body": "PR description",
|
"flag.pr.body": "PR description",
|
||||||
"flag.pr.file": "Filter diff by file path",
|
"flag.pr.file": "Filter diff by file path",
|
||||||
|
"flag.pr.fork_project_id": "Fork project ID for cross-fork merge checks",
|
||||||
"flag.pr.head": "Source branch",
|
"flag.pr.head": "Source branch",
|
||||||
"flag.pr.id": "PR number",
|
"flag.pr.id": "PR number",
|
||||||
"flag.pr.merge_method": "Merge method: merge, rebase, squash",
|
"flag.pr.merge_method": "Merge method: merge, rebase, squash",
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,10 @@
|
||||||
"cmd.org.list.short": "列出组织",
|
"cmd.org.list.short": "列出组织",
|
||||||
"cmd.org.members.short": "列出组织成员",
|
"cmd.org.members.short": "列出组织成员",
|
||||||
"cmd.org.short": "组织操作",
|
"cmd.org.short": "组织操作",
|
||||||
|
"cmd.pr.check_merge.short": "预检两个分支之间能否创建合并请求",
|
||||||
"cmd.pr.close.short": "关闭拉取请求",
|
"cmd.pr.close.short": "关闭拉取请求",
|
||||||
"cmd.pr.comment.short": "给拉取请求添加评论",
|
"cmd.pr.comment.short": "给拉取请求添加评论",
|
||||||
|
"cmd.pr.commits.short": "列出合并请求的提交",
|
||||||
"cmd.pr.create.short": "创建拉取请求",
|
"cmd.pr.create.short": "创建拉取请求",
|
||||||
"cmd.pr.diff.short": "显示拉取请求 diff",
|
"cmd.pr.diff.short": "显示拉取请求 diff",
|
||||||
"cmd.pr.files.short": "列出拉取请求中的变更文件",
|
"cmd.pr.files.short": "列出拉取请求中的变更文件",
|
||||||
|
|
@ -182,6 +184,7 @@
|
||||||
"flag.pr.base": "目标分支",
|
"flag.pr.base": "目标分支",
|
||||||
"flag.pr.body": "PR 描述",
|
"flag.pr.body": "PR 描述",
|
||||||
"flag.pr.file": "按文件路径筛选 diff",
|
"flag.pr.file": "按文件路径筛选 diff",
|
||||||
|
"flag.pr.fork_project_id": "跨 fork 合并检查用的 fork 项目 ID",
|
||||||
"flag.pr.head": "源分支",
|
"flag.pr.head": "源分支",
|
||||||
"flag.pr.id": "PR 编号",
|
"flag.pr.id": "PR 编号",
|
||||||
"flag.pr.merge_method": "合并方式:merge、rebase、squash",
|
"flag.pr.merge_method": "合并方式:merge、rebase、squash",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package pr
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||||
|
|
@ -227,6 +228,57 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
||||||
return ctx.Output(env)
|
return ctx.Output(env)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "commits",
|
||||||
|
Description: tr.T("cmd.pr.commits.short"),
|
||||||
|
Flags: []common.Flag{
|
||||||
|
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true},
|
||||||
|
},
|
||||||
|
Run: func(ctx *common.RuntimeContext) error {
|
||||||
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
id, _ := ctx.RequireArg("id")
|
||||||
|
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s/commits", ctx.RepoPath(), id), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ctx.Output(env)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "check-merge",
|
||||||
|
Description: tr.T("cmd.pr.check_merge.short"),
|
||||||
|
Flags: []common.Flag{
|
||||||
|
{Name: "head", Usage: tr.T("flag.pr.head"), Required: true},
|
||||||
|
{Name: "base", Usage: tr.T("flag.pr.base"), Required: true},
|
||||||
|
{Name: "fork-project-id", Usage: tr.T("flag.pr.fork_project_id")},
|
||||||
|
},
|
||||||
|
Run: func(ctx *common.RuntimeContext) error {
|
||||||
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
head, _ := ctx.RequireArg("head")
|
||||||
|
base, _ := ctx.RequireArg("base")
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"head": head,
|
||||||
|
"base": base,
|
||||||
|
}
|
||||||
|
if forkID := ctx.Arg("fork-project-id"); forkID != "" {
|
||||||
|
id, err := strconv.Atoi(forkID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("--fork-project-id must be an integer, got %q", forkID)
|
||||||
|
}
|
||||||
|
payload["fork_project_id"] = id
|
||||||
|
payload["is_original"] = true
|
||||||
|
}
|
||||||
|
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/pulls/check_can_merge", payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ctx.Output(env)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "diff",
|
Name: "diff",
|
||||||
Description: tr.T("cmd.pr.diff.short"),
|
Description: tr.T("cmd.pr.diff.short"),
|
||||||
|
|
|
||||||
|
|
@ -531,3 +531,46 @@ func assertEqual(t *testing.T, got interface{}, want interface{}) {
|
||||||
t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want)
|
t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPRCommitsUsesCommitsEndpoint(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "GET" || r.URL.Path != "/owner/repo/pulls/355/commits.json" {
|
||||||
|
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
|
writeJSON(t, w, map[string]interface{}{"commits_count": float64(1), "commits": []interface{}{}})
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
if err := runPRShortcut(t, server, "commits", map[string]string{"id": "355"}); err != nil {
|
||||||
|
t.Fatalf("commits failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPRCheckMergePostsBranchesAndForkTuple(t *testing.T) {
|
||||||
|
var payload map[string]interface{}
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" || r.URL.Path != "/owner/repo/pulls/check_can_merge.json" {
|
||||||
|
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
|
payload = decodeJSON(t, r)
|
||||||
|
writeJSON(t, w, map[string]interface{}{"status": float64(0), "message": "可以合并"})
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
err := runPRShortcut(t, server, "check-merge", map[string]string{
|
||||||
|
"head": "feat/x", "base": "master", "fork-project-id": "1549132",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("check-merge failed: %v", err)
|
||||||
|
}
|
||||||
|
assertEqual(t, payload["head"], "feat/x")
|
||||||
|
assertEqual(t, payload["base"], "master")
|
||||||
|
assertEqual(t, payload["fork_project_id"], float64(1549132))
|
||||||
|
assertEqual(t, payload["is_original"], true)
|
||||||
|
|
||||||
|
if err := runPRShortcut(t, server, "check-merge", map[string]string{
|
||||||
|
"head": "a", "base": "b", "fork-project-id": "abc",
|
||||||
|
}); err == nil {
|
||||||
|
t.Fatal("expected error for non-integer --fork-project-id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue