feat(pr): surface merged_at in pr +view

Signed-off-by: 林晨 (Leo Cheng) <chengkelfan@qq.com>
This commit is contained in:
林晨 (Leo Cheng) 2026-07-08 12:43:32 +08:00
parent 9749a4c832
commit c4a0ac1f13
No known key found for this signature in database
GPG Key ID: 24FCF87A069356B9
3 changed files with 78 additions and 3 deletions

View File

@ -0,0 +1,28 @@
# PR View Merged Timestamp
## Summary
`pr +view` now surfaces the merge timestamp at the top level of its output.
The non-v1 detail endpoint `/{owner}/{repo}/pulls/{id}` nests the merge time under
`pull_request.merged_at` (an ISO-8601 string such as `2026-07-05T12:52:05+08:00`),
but the CLI previously only lifted `closed_at`. Merged PRs therefore showed no merge
time, mirroring upstream issue #14.
The `closed_at` enrichment is renamed to `enrichPullRequestTimestamps` and extended so
that, when `pull_request.merged_at` is present, it is copied to `merged_at` at the top
level (and the boolean `merged`, when present, is surfaced alongside it). This matches
`gh pr view`, which exposes `mergedAt`. The existing `closed_at` behavior is unchanged.
## Example
```bash
gitlink-cli pr +view --owner Gitlink --repo forgeplus --id 42
```
```json
{
"merged_at": "2026-07-05T12:52:05+08:00",
"merged": true
}
```

View File

@ -138,7 +138,7 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
if err != nil {
return err
}
if err := enrichPullRequestClosedAt(ctx, env); err != nil {
if err := enrichPullRequestTimestamps(ctx, env); err != nil {
return err
}
return ctx.Output(env)
@ -470,13 +470,22 @@ func extractIssueID(env *output.Envelope) (int64, error) {
return int64(idFloat), nil
}
func enrichPullRequestClosedAt(ctx *common.RuntimeContext, env *output.Envelope) error {
func enrichPullRequestTimestamps(ctx *common.RuntimeContext, env *output.Envelope) error {
data, ok := env.Data.(map[string]interface{})
if !ok {
return nil
}
pr, ok := data["pull_request"].(map[string]interface{})
if !ok || !isClosedPullRequest(pr) || stringField(pr, "closed_at") != "" {
if !ok {
return nil
}
if mergedAt := stringField(pr, "merged_at"); mergedAt != "" {
data["merged_at"] = mergedAt
if merged, ok := pr["merged"].(bool); ok {
data["merged"] = merged
}
}
if !isClosedPullRequest(pr) || stringField(pr, "closed_at") != "" {
return nil
}
issue, ok := data["issue"].(map[string]interface{})

View File

@ -249,6 +249,44 @@ func TestPRView(t *testing.T) {
}
}
func TestPRViewSurfacesMergedAt(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/owner/repo/pulls/42.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
writeJSON(t, w, map[string]interface{}{
"id": float64(42),
"pull_request": map[string]interface{}{
"merged_at": "2026-07-05T12:52:05+08:00",
"merged": true,
"pull_request_staus": "merged",
},
})
}))
defer server.Close()
ctx := &common.RuntimeContext{
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
Owner: "owner",
Repo: "repo",
Format: "json",
}
env, err := ctx.CallAPI("GET", "/owner/repo/pulls/42", nil)
if err != nil {
t.Fatalf("CallAPI error: %v", err)
}
if err := enrichPullRequestTimestamps(ctx, env); err != nil {
t.Fatalf("enrich error: %v", err)
}
data, ok := env.Data.(map[string]interface{})
if !ok {
t.Fatalf("unexpected data type: %T", env.Data)
}
assertEqual(t, data["merged_at"], "2026-07-05T12:52:05+08:00")
assertEqual(t, data["merged"], true)
}
// --- merge ---
func TestPRMerge(t *testing.T) {