mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
fix(milestone): address milestones by title or ID everywhere
milestone view v2.0 failed with "invalid resource number" although its usage promised <id-or-name>, and issue create --milestone v1.0 failed with a strconv error. Every milestone argument (view, edit, delete, close, reopen, milestone issue add|remove|list) and issue create|edit -m/--milestone now accept a title or an ID: a number is an ID first, then a title; --by-name matches numeric titles only. --milestone-name stays as a hidden name of --milestone, and issue edit gains --remove-milestone (as does --milestone 0). Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
12 files changed
+220
-147
No files matched your search
@@ -6,6 +6,7 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
@@ -58,12 +59,13 @@ func TestMilestoneDeleteReturnsJSONSnapshotOnlyAfterSuccess(t *testing.T) {
|
||||
calls := []string{}
|
||||
fail := false
|
||||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/api/v1/repos/alice/project/milestones/9", r.URL.Path)
|
||||
calls = append(calls, r.Method)
|
||||
if r.Method == http.MethodGet {
|
||||
_, _ = w.Write([]byte(`{"id":9,"title":"Release one","state":"open"}`))
|
||||
require.Equal(t, "/api/v1/repos/alice/project/milestones", r.URL.Path)
|
||||
writeMilestones(w, r, `[{"id":9,"title":"Release one","state":"open"}]`)
|
||||
return
|
||||
}
|
||||
require.Equal(t, "/api/v1/repos/alice/project/milestones/9", r.URL.Path)
|
||||
require.Equal(t, http.MethodDelete, r.Method)
|
||||
if fail {
|
||||
w.WriteHeader(403)
|
||||
@@ -76,7 +78,7 @@ func TestMilestoneDeleteReturnsJSONSnapshotOnlyAfterSuccess(t *testing.T) {
|
||||
args := []string{"milestone", "delete", "9", "-R", "alice/project", "--yes", "--json=id,title"}
|
||||
require.NoError(t, Execute(context.Background(), deps, args))
|
||||
require.JSONEq(t, `{"id":9,"title":"Release one"}`, out.String())
|
||||
require.Equal(t, []string{"GET", "DELETE"}, calls)
|
||||
require.Equal(t, []string{"GET", "GET", "DELETE"}, calls)
|
||||
fail = true
|
||||
out.Reset()
|
||||
require.Error(t, Execute(context.Background(), deps, args))
|
||||
@@ -113,5 +115,77 @@ func TestMilestoneIssueCommandsSelectByName(t *testing.T) {
|
||||
require.NoError(t, Execute(context.Background(), deps, []string{"milestone", "issue", "add", "Release one", "3", "--by-name", "-R", "alice/project", "--json=number"}))
|
||||
require.NotContains(t, calls, "GET /api/v1/repos/alice/project/milestones/9")
|
||||
err := Execute(context.Background(), deps, []string{"milestone", "issue", "remove", " ", "3", "--by-name", "-R", "alice/project"})
|
||||
require.ErrorContains(t, err, "milestone name cannot be empty")
|
||||
require.ErrorContains(t, err, "milestone title or ID cannot be empty")
|
||||
}
|
||||
|
||||
// writeMilestones answers a milestone listing with page, a JSON array, as its first and only page.
|
||||
func writeMilestones(w http.ResponseWriter, r *http.Request, page string) {
|
||||
if r.URL.Query().Get("page") != "1" {
|
||||
page = `[]`
|
||||
}
|
||||
_, _ = w.Write([]byte(page))
|
||||
}
|
||||
|
||||
// payloadField returns the JSON request body's field name as text.
|
||||
func payloadField(t *testing.T, r *http.Request, name string) string {
|
||||
t.Helper()
|
||||
var payload map[string]any
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&payload))
|
||||
return fmt.Sprint(payload[name])
|
||||
}
|
||||
|
||||
func TestMilestonesAcceptTitlesOrIDs(t *testing.T) {
|
||||
var calls []string
|
||||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method + " " + r.URL.Path {
|
||||
case "GET /api/v1/repos/alice/project/milestones":
|
||||
writeMilestones(w, r, `[{"id":3,"title":"v1.0","state":"open"},{"id":4,"title":"2026","state":"closed"},{"id":5,"title":"v2.0"}]`)
|
||||
case "GET /api/v1/repos/alice/project/milestones/3", "GET /api/v1/repos/alice/project/milestones/4":
|
||||
_, _ = w.Write([]byte(`{"id":3,"title":"v1.0","state":"open"}`))
|
||||
case "PATCH /api/v1/repos/alice/project/milestones/3", "PATCH /api/v1/repos/alice/project/milestones/4":
|
||||
calls = append(calls, r.Method+" "+r.URL.Path)
|
||||
_, _ = w.Write([]byte(`{"id":3,"title":"v1.0","state":"closed"}`))
|
||||
case "POST /api/v1/repos/alice/project/issues":
|
||||
requirePayload(t, `{"title":"x","milestone":3}`, r.Body)
|
||||
calls = append(calls, r.Method+" "+r.URL.Path)
|
||||
_, _ = w.Write([]byte(`{"number":8}`))
|
||||
case "PATCH /api/v1/repos/alice/project/issues/8":
|
||||
calls = append(calls, r.Method+" "+r.URL.Path+" milestone="+payloadField(t, r, "milestone"))
|
||||
_, _ = w.Write([]byte(`{"number":8}`))
|
||||
default:
|
||||
t.Errorf("unexpected %s %s", r.Method, r.URL.Path)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
deps, out, _ := serverDeps(t, server.URL)
|
||||
run := func(args ...string) error {
|
||||
return Execute(context.Background(), deps, append(args, "-R", "alice/project", "--json", "--yes"))
|
||||
}
|
||||
require.NoError(t, run("milestone", "view", "v1.0"))
|
||||
require.Contains(t, out.String(), `"id":3`)
|
||||
require.NoError(t, run("milestone", "close", "V1.0", "2026"))
|
||||
require.NoError(t, run("issue", "create", "--title", "x", "-m", "v1.0"))
|
||||
require.NoError(t, run("issue", "edit", "8", "--milestone-name", "3"))
|
||||
require.NoError(t, run("issue", "edit", "8", "--remove-milestone"))
|
||||
require.NoError(t, run("issue", "edit", "8", "--milestone", "0"))
|
||||
require.Equal(t, []string{
|
||||
"PATCH /api/v1/repos/alice/project/milestones/3",
|
||||
"PATCH /api/v1/repos/alice/project/milestones/4",
|
||||
"POST /api/v1/repos/alice/project/issues",
|
||||
"PATCH /api/v1/repos/alice/project/issues/8 milestone=3",
|
||||
"PATCH /api/v1/repos/alice/project/issues/8 milestone=0",
|
||||
"PATCH /api/v1/repos/alice/project/issues/8 milestone=0",
|
||||
}, calls)
|
||||
for _, test := range []struct {
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{[]string{"milestone", "view", "v9"}, `milestone "v9" not found in alice/project`},
|
||||
{[]string{"milestone", "close", "v1.0", "3"}, "milestone 3 is selected more than once"},
|
||||
{[]string{"issue", "create", "--title", "x", "--milestone", "nope"}, `milestone "nope" not found in alice/project`},
|
||||
} {
|
||||
require.ErrorContains(t, run(test.args...), test.want)
|
||||
}
|
||||
require.Len(t, calls, 6)
|
||||
}
|
||||
Reference in new issue
Block a user