mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
tea pr list prints a header, a "Showing N of M" line and the ID, TITLE,
BRANCH, STATE, AUTHOR and CREATED columns on a terminal, fit to its
width, and tab-separated rows when piped. Fork heads read
OWNER:BRANCH, drafts and merged pull requests have their own state,
and STATE appears on a terminal only when --state is not open. New
filters: --state merged, -d/--draft, -H/--head, -a/--assignee,
-S/--search and -w/--web, plus the gh shorthands -s, -A, -B, -l and
-m; --milestone takes a title or an ID, and @me works for --author and
--assignee. An empty list says so on stderr. draft, merged_at and
merged_by are exported in JSON, and reviewers, reviewer_teams and
merged_by, which Gitea's list leaves out, are read when --json names
them.
issue view and pr view render through terminal.View. Pull requests
show Draft and Merged badges, "alice merged into main from fix" with
the merge time, each reviewer's latest state ("bob (Approved), carol
(Requested)"), a checks summary pointing at tea pr checks, and why a
pull request cannot be merged. --comments includes reviews without a
body ("dave approved these changes") and comment IDs. Issues show
their due date and ref, and both kinds hint at --comments when there
are comments. pr view accepts #N, URLs and branch names, and its JSON
adds review_decision. Check links are absolute.
Signed-off-by: bircni <bircni@icloud.com>
194 lines
12 KiB
Go
194 lines
12 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package app
|
||
|
||
import (
|
||
"context"
|
||
"net/http"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
// plainTTY makes deps render for a terminal without color or markdown, so views compare as text.
|
||
func plainTTY(deps *Dependencies, serverURL string) {
|
||
deps.Streams.OutTTY = true
|
||
deps.Getenv = serverEnv(serverURL, map[string]string{"NO_COLOR": "1", "TEA_MARKDOWN": "raw"})
|
||
}
|
||
|
||
func TestDiscussionViews(t *testing.T) {
|
||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
switch r.URL.Path {
|
||
case "/api/v1/repos/alice/project/issues/3":
|
||
_, _ = w.Write([]byte(`{"number":3,"title":"Crash\u001b[2J","body":"Steps:\r\n\u001b]52;c;aGk=\u0007run\r","html_url":"https://example.org/i/3","state":"open","user":{"login":"bob"},"created_at":"2026-01-01T00:00:00Z","labels":[{"name":"bug","color":"ff0000"}],"assignees":[{"login":"alice"}],"comments":1}`))
|
||
case "/api/v1/repos/alice/project/issues/3/dependencies", "/api/v1/repos/alice/project/issues/3/blocks", "/api/v1/repos/alice/project/pulls/4/reviews":
|
||
_, _ = w.Write([]byte(`[]`))
|
||
case "/api/v1/repos/alice/project/issues/3/comments":
|
||
_, _ = w.Write([]byte(`[{"id":79,"body":"Confirmed\u001b[31m\n\tok","user":{"login":"carol"},"created_at":"2026-01-02T00:00:00Z"}]`))
|
||
case "/api/v1/repos/alice/project/pulls/4":
|
||
_, _ = w.Write([]byte(`{"number":4,"title":"Fix","html_url":"https://example.org/p/4","state":"closed","merged":true,"merged_at":"2026-01-03T12:00:00Z","merged_by":{"login":"alice"},"user":{"login":"bob"},"created_at":"2026-01-03T00:00:00Z","base":{"ref":"main"},"head":{"ref":"fix"},"requested_reviewers":[{"login":"carol"}]}`))
|
||
default:
|
||
t.Errorf("unexpected %s %s", r.Method, r.URL.Path)
|
||
}
|
||
}))
|
||
defer server.Close()
|
||
deps, out, _ := serverDeps(t, server.URL)
|
||
deps.Now = func() time.Time { return time.Date(2026, 1, 4, 0, 0, 0, 0, time.UTC) }
|
||
require.NoError(t, Execute(context.Background(), deps, []string{"issue", "view", "3", "-R", "alice/project", "--comments"}))
|
||
require.Equal(t, "title:\tCrash[2J\nstate:\topen\nauthor:\tbob\nlabels:\tbug\nassignees:\talice\nmilestone:\t\ndue:\t\nref:\t\nblocked by:\t\nblocks:\t\ncomments:\t1\nnumber:\t3\nurl:\thttps://example.org/i/3\n--\nSteps:\n]52;c;aGk=run\n"+
|
||
"--\nauthor:\tcarol\ncreated_at:\t2026-01-02T00:00:00Z\nid:\t79\n--\nConfirmed[31m\n\tok\n", out.String())
|
||
out.Reset()
|
||
plainTTY(&deps, server.URL)
|
||
require.NoError(t, Execute(context.Background(), deps, []string{"issue", "view", "3", "-R", "alice/project"}))
|
||
require.Equal(t, "Crash[2J #3\nOpen • bob opened about 3 days ago • 1 comment\nLabels: bug\nAssignees: alice\n\nSteps:\n]52;c;aGk=run\n\n"+
|
||
"1 comment — use --comments to view them\n\nView this issue on Gitea: https://example.org/i/3\n", out.String())
|
||
out.Reset()
|
||
require.NoError(t, Execute(context.Background(), deps, []string{"issue", "view", "3", "-R", "alice/project", "--comments"}))
|
||
require.Contains(t, out.String(), "\n\ncarol • about 2 days ago • #79\nConfirmed[31m\n\tok\n\nView this issue on Gitea:")
|
||
out.Reset()
|
||
require.NoError(t, Execute(context.Background(), deps, []string{"pr", "view", "4", "-R", "alice/project"}))
|
||
require.Equal(t, "Fix #4\nMerged • alice merged into main from fix • about 12 hours ago • 0 comments\nReviewers: carol (Requested)\n\nNo description provided\n\nView this pull request on Gitea: https://example.org/p/4\n", out.String())
|
||
}
|
||
|
||
func TestIssueViewDependencies(t *testing.T) {
|
||
supported := true
|
||
var paths []string
|
||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
paths = append(paths, r.URL.Path)
|
||
switch {
|
||
case r.URL.Path == "/api/v1/repos/alice/project/issues/3":
|
||
_, _ = w.Write([]byte(`{"number":3,"title":"Crash","html_url":"https://example.org/i/3","state":"open","user":{"login":"bob"},"created_at":"2026-01-03T00:00:00Z","due_date":"2026-12-24T23:59:59Z","ref":"main"}`))
|
||
case !supported:
|
||
w.WriteHeader(http.StatusNotFound)
|
||
case r.URL.Query().Get("page") != "1":
|
||
_, _ = w.Write([]byte(`[]`))
|
||
case r.URL.Path == "/api/v1/repos/alice/project/issues/3/dependencies":
|
||
_, _ = w.Write([]byte(`[{"number":1,"repository":{"full_name":"alice/project"}},{"number":5,"repository":{"full_name":"bob/lib"}},{"number":0,"title":"HIDDEN"}]`))
|
||
case r.URL.Path == "/api/v1/repos/alice/project/issues/3/blocks":
|
||
_, _ = w.Write([]byte(`[{"number":9,"repository":{"full_name":"alice/project"}}]`))
|
||
default:
|
||
t.Errorf("unexpected %s %s", r.Method, r.URL.Path)
|
||
}
|
||
}))
|
||
defer server.Close()
|
||
deps, out, _ := serverDeps(t, server.URL)
|
||
deps.Now = func() time.Time { return time.Date(2026, 1, 4, 0, 0, 0, 0, time.UTC) }
|
||
plainTTY(&deps, server.URL)
|
||
args := []string{"issue", "view", "3", "-R", "alice/project"}
|
||
require.NoError(t, Execute(context.Background(), deps, args))
|
||
require.Equal(t, "Crash #3\nOpen • bob opened about 1 day ago • 0 comments\nDue: Dec 24, 2026 (in about 11 months)\nRef: main\nBlocked by: #1, bob/lib#5, hidden\nBlocks: #9\n\nNo description provided\n\nView this issue on Gitea: https://example.org/i/3\n", out.String())
|
||
out.Reset()
|
||
deps.Streams.OutTTY = false
|
||
require.NoError(t, Execute(context.Background(), deps, args))
|
||
require.Contains(t, out.String(), "\ndue:\t2026-12-24T23:59:59Z\nref:\tmain\nblocked by:\t#1, bob/lib#5, hidden\nblocks:\t#9\n")
|
||
out.Reset()
|
||
supported, paths = false, nil
|
||
require.NoError(t, Execute(context.Background(), deps, args))
|
||
require.Contains(t, out.String(), "\nblocked by:\t\nblocks:\t\n")
|
||
require.Equal(t, []string{"/api/v1/repos/alice/project/issues/3", "/api/v1/repos/alice/project/issues/3/dependencies"}, paths)
|
||
out.Reset()
|
||
paths = nil
|
||
require.NoError(t, Execute(context.Background(), deps, append(args, "--json=number")))
|
||
require.Equal(t, []string{"/api/v1/repos/alice/project/issues/3"}, paths)
|
||
}
|
||
|
||
func TestPullViewCommentsIncludeReviews(t *testing.T) {
|
||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
switch r.URL.Path {
|
||
case "/api/v1/repos/alice/project/pulls/4":
|
||
_, _ = w.Write([]byte(`{"number":4,"title":"Fix","html_url":"https://example.org/p/4","state":"open","mergeable":true,"user":{"login":"bob"},"created_at":"2026-01-01T00:00:00Z","base":{"ref":"main"},"head":{"ref":"fix"},"comments":1}`))
|
||
case "/api/v1/repos/alice/project/issues/4/comments":
|
||
_, _ = w.Write([]byte(`[{"body":"Looks close","user":{"login":"carol"},"created_at":"2026-01-02T00:00:00Z"}]`))
|
||
case "/api/v1/repos/alice/project/pulls/4/reviews":
|
||
if r.URL.Query().Get("page") != "1" {
|
||
_, _ = w.Write([]byte(`[]`))
|
||
return
|
||
}
|
||
_, _ = w.Write([]byte(`[
|
||
{"id":1,"body":"Please rename","state":"REQUEST_CHANGES","user":{"login":"alice"},"submitted_at":"2026-01-01T12:00:00Z","comments_count":1},
|
||
{"id":2,"body":"","state":"APPROVED","user":{"login":"dave"},"submitted_at":"2026-01-03T00:00:00Z"},
|
||
{"id":3,"body":"Draft","state":"PENDING","user":{"login":"erin"},"submitted_at":"2026-01-03T00:00:00Z"},
|
||
{"id":4,"body":"Great","state":"APPROVED","user":{"login":"alice"},"submitted_at":"2026-01-03T06:00:00Z"}
|
||
]`))
|
||
case "/api/v1/repos/alice/project/pulls/4/reviews/1/comments":
|
||
_, _ = w.Write([]byte(`[{"id":9,"body":"Typo here","path":"main.go","position":12,"user":{"login":"alice"},"created_at":"2026-01-01T12:00:00Z"}]`))
|
||
default:
|
||
t.Errorf("unexpected %s %s", r.Method, r.URL.Path)
|
||
w.WriteHeader(http.StatusNotFound)
|
||
}
|
||
}))
|
||
defer server.Close()
|
||
deps, out, _ := serverDeps(t, server.URL)
|
||
deps.Now = func() time.Time { return time.Date(2026, 1, 4, 0, 0, 0, 0, time.UTC) }
|
||
require.NoError(t, Execute(context.Background(), deps, []string{"pr", "view", "4", "-R", "alice/project", "--comments"}))
|
||
require.Equal(t, "title:\tFix\nstate:\topen\nauthor:\tbob\nbase:\tmain\nhead:\tfix\nlabels:\t\nassignees:\t\nreviewers:\talice (Approved), dave (Approved)\nmilestone:\t\nchecks:\t\nmergeable:\tyes\ncomments:\t1\nnumber:\t4\nurl:\thttps://example.org/p/4\n--\n\n"+
|
||
"--\nauthor:\talice\ncreated_at:\t2026-01-01T12:00:00Z\nreview:\tREQUEST_CHANGES\n--\nPlease rename\n"+
|
||
"--\nauthor:\talice\ncreated_at:\t2026-01-01T12:00:00Z\nid:\t9\npath:\tmain.go:12\n--\nTypo here\n"+
|
||
"--\nauthor:\tcarol\ncreated_at:\t2026-01-02T00:00:00Z\n--\nLooks close\n"+
|
||
"--\nauthor:\tdave\ncreated_at:\t2026-01-03T00:00:00Z\nreview:\tAPPROVED\n--\n\n"+
|
||
"--\nauthor:\talice\ncreated_at:\t2026-01-03T06:00:00Z\nreview:\tAPPROVED\n--\nGreat\n", out.String())
|
||
out.Reset()
|
||
plainTTY(&deps, server.URL)
|
||
require.NoError(t, Execute(context.Background(), deps, []string{"pr", "view", "4", "-R", "alice/project", "--comments"}))
|
||
require.Equal(t, "Fix #4\nOpen • bob wants to merge into main from fix • about 3 days ago • 1 comment\nReviewers: alice (Approved), dave (Approved)\nMergeable: yes\n\nNo description provided\n\n"+
|
||
"alice requested changes • about 2 days ago\nPlease rename\n\n"+
|
||
"alice commented on main.go:12 • about 2 days ago\nTypo here\n\n"+
|
||
"carol • about 2 days ago\nLooks close\n\n"+
|
||
"dave approved these changes • about 1 day ago\n\n"+
|
||
"alice approved these changes • about 18 hours ago\nGreat\n\n"+
|
||
"View this pull request on Gitea: https://example.org/p/4\n", out.String())
|
||
}
|
||
|
||
func TestPullViewStates(t *testing.T) {
|
||
for _, tc := range []struct {
|
||
name, pull, statuses, reviews, want, decision string
|
||
}{
|
||
{
|
||
name: "draft",
|
||
pull: `{"number":15,"title":"WIP: experiment","state":"open","draft":true,"mergeable":false,"user":{"login":"alice"},"base":{"ref":"main"},"head":{"ref":"experiment"}}`,
|
||
want: "WIP: experiment #15\nDraft • alice wants to merge into main from experiment • about 1 day ago • 0 comments\nMergeable: no — draft; mark it ready with: tea pr ready 15\n",
|
||
decision: "",
|
||
},
|
||
{
|
||
name: "fork with checks and conflicts",
|
||
pull: `{"number":17,"title":"Improve greeting","state":"open","mergeable":false,"user":{"login":"bob"},"base":{"ref":"main"},"head":{"ref":"greet","sha":"abc","repo":{"full_name":"bob/project"}},"requested_reviewers":[{"login":"carol"}]}`,
|
||
statuses: `[{"id":1,"context":"ci / test","status":"pending"},{"id":2,"context":"lint","status":"failure"}]`,
|
||
reviews: `[{"id":4,"state":"REQUEST_CHANGES","user":{"login":"alice"}}]`,
|
||
want: "Improve greeting #17\nOpen • bob wants to merge into main from bob:greet • about 1 day ago • 0 comments\nReviewers: alice (Changes requested), carol (Requested)\nChecks: 1 failing, 1 pending (tea pr checks 17)\nMergeable: no — conflicts with main\n",
|
||
decision: "changes_requested",
|
||
},
|
||
} {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
switch {
|
||
case r.URL.Path == "/api/v1/repos/alice/project/pulls/15" || r.URL.Path == "/api/v1/repos/alice/project/pulls/17":
|
||
_, _ = w.Write([]byte(tc.pull[:len(tc.pull)-1] + `,"created_at":"2026-01-03T00:00:00Z","html_url":"https://example.org/p"}`))
|
||
case r.URL.Query().Get("page") != "1":
|
||
_, _ = w.Write([]byte(`[]`))
|
||
case r.URL.Path == "/api/v1/repos/alice/project/statuses/abc":
|
||
_, _ = w.Write([]byte(tc.statuses))
|
||
case tc.reviews == "":
|
||
_, _ = w.Write([]byte(`[]`))
|
||
default:
|
||
_, _ = w.Write([]byte(tc.reviews))
|
||
}
|
||
}))
|
||
defer server.Close()
|
||
deps, out, _ := serverDeps(t, server.URL)
|
||
deps.Now = func() time.Time { return time.Date(2026, 1, 4, 0, 0, 0, 0, time.UTC) }
|
||
plainTTY(&deps, server.URL)
|
||
number := map[string]string{"draft": "15"}[tc.name]
|
||
if number == "" {
|
||
number = "17"
|
||
}
|
||
require.NoError(t, Execute(context.Background(), deps, []string{"pr", "view", number, "-R", "alice/project"}))
|
||
require.Equal(t, tc.want+"\nNo description provided\n\nView this pull request on Gitea: https://example.org/p\n", out.String())
|
||
out.Reset()
|
||
require.NoError(t, Execute(context.Background(), deps, []string{"pr", "view", number, "-R", "alice/project", "--json=draft,review_decision"}))
|
||
require.Contains(t, out.String(), `"review_decision": "`+tc.decision+`"`)
|
||
})
|
||
}
|
||
}
|