Compare commits
10 Commits
a58c35c3e2
...
update-wit
| Author | SHA1 | Date | |
|---|---|---|---|
| d3e43d7623 | |||
|
|
83b718ac34 | ||
|
|
1f6fd97fc1 | ||
|
|
27e6083e23 | ||
|
|
5d2d1a6e0c | ||
|
|
88421bb888 | ||
|
|
dd81b33cec | ||
|
|
b100d4c939 | ||
|
|
892905d482 | ||
|
|
5103496232 |
@@ -12,13 +12,9 @@ jobs:
|
|||||||
# uses: golang/govulncheck-action@v1
|
# uses: golang/govulncheck-action@v1
|
||||||
# with:
|
# with:
|
||||||
# go-version-file: 'go.mod'
|
# go-version-file: 'go.mod'
|
||||||
check-and-test:
|
check-and-unit:
|
||||||
|
name: Lint Build And Unit Coverage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
|
||||||
HTTP_PROXY: ""
|
|
||||||
GITEA_TEA_TEST_URL: "http://gitea:3000"
|
|
||||||
GITEA_TEA_TEST_USERNAME: "test01"
|
|
||||||
GITEA_TEA_TEST_PASSWORD: "test01"
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
@@ -32,14 +28,30 @@ jobs:
|
|||||||
make fmt-check
|
make fmt-check
|
||||||
make docs-check
|
make docs-check
|
||||||
make build
|
make build
|
||||||
- run: curl --noproxy "*" http://gitea:3000/api/v1/version # verify connection to instance
|
- name: unit test and coverage
|
||||||
- name: test and coverage
|
|
||||||
run: |
|
run: |
|
||||||
make test
|
|
||||||
make unit-test-coverage
|
make unit-test-coverage
|
||||||
|
|
||||||
|
integration-test:
|
||||||
|
name: Integration Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
HTTP_PROXY: ""
|
||||||
|
GITEA_TEA_TEST_URL: "http://gitea:3000"
|
||||||
|
GITEA_TEA_TEST_USERNAME: "test01"
|
||||||
|
GITEA_TEA_TEST_PASSWORD: "test01"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: 'go.mod'
|
||||||
|
- run: curl --noproxy "*" http://gitea:3000/api/v1/version # verify connection to instance
|
||||||
|
- name: integration test
|
||||||
|
run: |
|
||||||
|
make integration-test
|
||||||
services:
|
services:
|
||||||
gitea:
|
gitea:
|
||||||
image: docker.gitea.com/gitea:1.26.0
|
image: docker.gitea.com/gitea:1.26.1
|
||||||
cmd:
|
cmd:
|
||||||
- bash
|
- bash
|
||||||
- -c
|
- -c
|
||||||
|
|||||||
27
Makefile
27
Makefile
@@ -30,7 +30,10 @@ LDFLAGS := -X "code.gitea.io/tea/modules/version.Version=$(TEA_VERSION)" -X "cod
|
|||||||
# override to allow passing additional goflags via make CLI
|
# override to allow passing additional goflags via make CLI
|
||||||
override GOFLAGS := $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)'
|
override GOFLAGS := $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)'
|
||||||
|
|
||||||
PACKAGES ?= $(shell $(GO) list ./...)
|
PACKAGES ?= $(shell $(GO) list ./... | grep -v '^code.gitea.io/tea/tests')
|
||||||
|
UNIT_PACKAGES ?= $(PACKAGES)
|
||||||
|
INTEGRATION_PACKAGES ?= $(shell $(GO) list ./tests/... 2>/dev/null)
|
||||||
|
INTEGRATION_TEST_TAGS ?= testtools
|
||||||
SOURCES ?= $(shell find . -name "*.go" -type f)
|
SOURCES ?= $(shell find . -name "*.go" -type f)
|
||||||
|
|
||||||
# OS specific vars.
|
# OS specific vars.
|
||||||
@@ -64,11 +67,11 @@ vet:
|
|||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint:
|
lint:
|
||||||
$(GO) run $(GOLANGCI_LINT_PACKAGE) run
|
$(GO) run $(GOLANGCI_LINT_PACKAGE) run --build-tags testtools
|
||||||
|
|
||||||
.PHONY: lint-fix
|
.PHONY: lint-fix
|
||||||
lint-fix:
|
lint-fix:
|
||||||
$(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix
|
$(GO) run $(GOLANGCI_LINT_PACKAGE) run --build-tags testtools --fix
|
||||||
|
|
||||||
.PHONY: fmt-check
|
.PHONY: fmt-check
|
||||||
fmt-check:
|
fmt-check:
|
||||||
@@ -93,13 +96,24 @@ docs-check:
|
|||||||
exit 1; \
|
exit 1; \
|
||||||
fi;
|
fi;
|
||||||
|
|
||||||
|
.PHONY: unit-test
|
||||||
|
unit-test:
|
||||||
|
$(GO) test $(UNIT_PACKAGES)
|
||||||
|
|
||||||
|
.PHONY: integration-test
|
||||||
|
integration-test:
|
||||||
|
@if [ -n "$(INTEGRATION_PACKAGES)" ]; then \
|
||||||
|
$(GO) test -tags='$(INTEGRATION_TEST_TAGS)' $(INTEGRATION_PACKAGES); \
|
||||||
|
else \
|
||||||
|
echo "No integration test packages found"; \
|
||||||
|
fi
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test: unit-test integration-test
|
||||||
$(GO) test -tags='sqlite sqlite_unlock_notify' $(PACKAGES)
|
|
||||||
|
|
||||||
.PHONY: unit-test-coverage
|
.PHONY: unit-test-coverage
|
||||||
unit-test-coverage:
|
unit-test-coverage:
|
||||||
$(GO) test -tags='sqlite sqlite_unlock_notify' -cover -coverprofile coverage.out $(PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1
|
$(GO) test -cover -coverprofile coverage.out $(UNIT_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1
|
||||||
|
|
||||||
.PHONY: tidy
|
.PHONY: tidy
|
||||||
tidy:
|
tidy:
|
||||||
@@ -122,4 +136,3 @@ $(EXECUTABLE): $(SOURCES)
|
|||||||
.PHONY: build-image
|
.PHONY: build-image
|
||||||
build-image:
|
build-image:
|
||||||
docker build --build-arg VERSION=$(TEA_VERSION) -t gitea/tea:$(TEA_VERSION_TAG) .
|
docker build --build-arg VERSION=$(TEA_VERSION) -t gitea/tea:$(TEA_VERSION_TAG) .
|
||||||
|
|
||||||
|
|||||||
@@ -61,11 +61,19 @@ func runReleaseAttachmentDelete(_ stdctx.Context, cmd *cli.Command) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
existing, _, err := client.ListReleaseAttachments(ctx.Owner, ctx.Repo, release.ID, gitea.ListReleaseAttachmentsOptions{
|
var existing []*gitea.Attachment
|
||||||
ListOptions: gitea.ListOptions{Page: -1},
|
for page := 1; ; {
|
||||||
})
|
page_attachments, resp, err := client.ListReleaseAttachments(ctx.Owner, ctx.Repo, release.ID, gitea.ListReleaseAttachmentsOptions{
|
||||||
if err != nil {
|
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||||
return err
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
existing = append(existing, page_attachments...)
|
||||||
|
if resp == nil || resp.NextPage == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
page = resp.NextPage
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, name := range ctx.Args().Slice()[1:] {
|
for _, name := range ctx.Args().Slice()[1:] {
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ var CmdIssues = cli.Command{
|
|||||||
&issues.CmdIssuesEdit,
|
&issues.CmdIssuesEdit,
|
||||||
&issues.CmdIssuesReopen,
|
&issues.CmdIssuesReopen,
|
||||||
&issues.CmdIssuesClose,
|
&issues.CmdIssuesClose,
|
||||||
|
&issues.CmdIssuesDependencies,
|
||||||
},
|
},
|
||||||
Flags: append([]cli.Flag{
|
Flags: append([]cli.Flag{
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
|
|||||||
266
cmd/issues/dependencies.go
Normal file
266
cmd/issues/dependencies.go
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package issues
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
stdctx "context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"code.gitea.io/tea/cmd/flags"
|
||||||
|
"code.gitea.io/tea/modules/api"
|
||||||
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
"code.gitea.io/tea/modules/utils"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CmdIssuesDependencies represents the subcommand for issue dependencies
|
||||||
|
var CmdIssuesDependencies = cli.Command{
|
||||||
|
Name: "dependencies",
|
||||||
|
Aliases: []string{"deps", "dep"},
|
||||||
|
Usage: "Manage issue dependencies",
|
||||||
|
Description: "List, add, or remove dependencies for an issue",
|
||||||
|
ArgsUsage: "<issue index>",
|
||||||
|
Action: runDependenciesList,
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
&cmdDependenciesList,
|
||||||
|
&cmdDependenciesAdd,
|
||||||
|
&cmdDependenciesRemove,
|
||||||
|
},
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdDependenciesList = cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Usage: "List dependencies for an issue",
|
||||||
|
Description: "List issues that the specified issue depends on (blockers)",
|
||||||
|
ArgsUsage: "<issue index>",
|
||||||
|
Action: runDependenciesList,
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdDependenciesAdd = cli.Command{
|
||||||
|
Name: "add",
|
||||||
|
Usage: "Add a dependency to an issue",
|
||||||
|
Description: "Make an issue depend on another issue (blocker)",
|
||||||
|
ArgsUsage: "<issue index> <dependency index or owner/repo#index>",
|
||||||
|
Action: runDependenciesAdd,
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdDependenciesRemove = cli.Command{
|
||||||
|
Name: "remove",
|
||||||
|
Aliases: []string{"rm"},
|
||||||
|
Usage: "Remove a dependency from an issue",
|
||||||
|
Description: "Remove a dependency from an issue",
|
||||||
|
ArgsUsage: "<issue index> <dependency index or owner/repo#index>",
|
||||||
|
Action: runDependenciesRemove,
|
||||||
|
Flags: flags.AllDefaultFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
// runDependenciesList lists dependencies for an issue
|
||||||
|
func runDependenciesList(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Ensure(context.CtxRequirement{RemoteRepo: true})
|
||||||
|
|
||||||
|
if cmd.Args().Len() < 1 {
|
||||||
|
return fmt.Errorf("issue index required")
|
||||||
|
}
|
||||||
|
|
||||||
|
index, err := utils.ArgToIndex(cmd.Args().First())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := api.NewClient(c.Login)
|
||||||
|
path := fmt.Sprintf("/repos/%s/%s/issues/%d/dependencies", c.Owner, c.Repo, index)
|
||||||
|
|
||||||
|
resp, err := client.Do("GET", path, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var deps []*gitea.Issue
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
if err := decoder.Decode(&deps); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(deps) == 0 {
|
||||||
|
fmt.Printf("Issue #%d has no dependencies\n", index)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
print.IssuesPullsList(deps, c.Output, []string{"index", "state", "title", "owner", "repo"})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runDependenciesAdd adds a dependency to an issue
|
||||||
|
func runDependenciesAdd(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Ensure(context.CtxRequirement{RemoteRepo: true})
|
||||||
|
|
||||||
|
if cmd.Args().Len() < 2 {
|
||||||
|
return fmt.Errorf("usage: tea issues dependencies add <issue index> <dependency index or owner/repo#index>")
|
||||||
|
}
|
||||||
|
|
||||||
|
index, err := utils.ArgToIndex(cmd.Args().First())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid issue index: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
depOwner, depRepo, depIndex, err := parseDependencyArg(cmd.Args().Get(1), c.Owner, c.Repo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := api.NewClient(c.Login)
|
||||||
|
path := fmt.Sprintf("/repos/%s/%s/issues/%d/dependencies", c.Owner, c.Repo, index)
|
||||||
|
|
||||||
|
reqBody := api.IssueDependencyRequest{
|
||||||
|
Owner: depOwner,
|
||||||
|
Repo: depRepo,
|
||||||
|
Index: depIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do("POST", path, bytes.NewReader(body), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
if depOwner == c.Owner && depRepo == c.Repo {
|
||||||
|
fmt.Printf("Added dependency: issue #%d now depends on #%d\n", index, depIndex)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Added dependency: issue #%d now depends on %s/%s#%d\n", index, depOwner, depRepo, depIndex)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runDependenciesRemove removes a dependency from an issue
|
||||||
|
func runDependenciesRemove(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
|
c, err := context.InitCommand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Ensure(context.CtxRequirement{RemoteRepo: true})
|
||||||
|
|
||||||
|
if cmd.Args().Len() < 2 {
|
||||||
|
return fmt.Errorf("usage: tea issues dependencies remove <issue index> <dependency index or owner/repo#index>")
|
||||||
|
}
|
||||||
|
|
||||||
|
index, err := utils.ArgToIndex(cmd.Args().First())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid issue index: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
depOwner, depRepo, depIndex, err := parseDependencyArg(cmd.Args().Get(1), c.Owner, c.Repo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := api.NewClient(c.Login)
|
||||||
|
path := fmt.Sprintf("/repos/%s/%s/issues/%d/dependencies", c.Owner, c.Repo, index)
|
||||||
|
|
||||||
|
reqBody := api.IssueDependencyRequest{
|
||||||
|
Owner: depOwner,
|
||||||
|
Repo: depRepo,
|
||||||
|
Index: depIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do("DELETE", path, bytes.NewReader(body), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
if depOwner == c.Owner && depRepo == c.Repo {
|
||||||
|
fmt.Printf("Removed dependency: issue #%d no longer depends on #%d\n", index, depIndex)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Removed dependency: issue #%d no longer depends on %s/%s#%d\n", index, depOwner, depRepo, depIndex)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDependencyArg parses a dependency argument which can be:
|
||||||
|
// - "123" or "#123" (same repo)
|
||||||
|
// - "owner/repo#123" (cross-repo)
|
||||||
|
func parseDependencyArg(arg, defaultOwner, defaultRepo string) (owner, repo string, index int64, err error) {
|
||||||
|
// Check for cross-repo format: owner/repo#123
|
||||||
|
// Ensure "/" comes before "#" to distinguish from same-repo "#123"
|
||||||
|
slashIdx := strings.Index(arg, "/")
|
||||||
|
hashIdx := strings.Index(arg, "#")
|
||||||
|
if slashIdx != -1 && hashIdx != -1 && slashIdx < hashIdx {
|
||||||
|
parts := strings.SplitN(arg, "#", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return "", "", 0, fmt.Errorf("invalid dependency format: %s (expected owner/repo#index)", arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
repoPath := parts[0]
|
||||||
|
indexStr := parts[1]
|
||||||
|
|
||||||
|
repoParts := strings.SplitN(repoPath, "/", 2)
|
||||||
|
if len(repoParts) != 2 {
|
||||||
|
return "", "", 0, fmt.Errorf("invalid dependency format: %s (expected owner/repo#index)", arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
owner = repoParts[0]
|
||||||
|
repo = repoParts[1]
|
||||||
|
|
||||||
|
index, err = strconv.ParseInt(indexStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", 0, fmt.Errorf("invalid issue index: %s", indexStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return owner, repo, index, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same repo format: 123 or #123
|
||||||
|
index, err = utils.ArgToIndex(arg)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", 0, fmt.Errorf("invalid issue index: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultOwner, defaultRepo, index, nil
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ package issues
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
stdctx "context"
|
stdctx "context"
|
||||||
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/tea/cmd/flags"
|
"code.gitea.io/tea/cmd/flags"
|
||||||
@@ -77,7 +78,7 @@ func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error {
|
|||||||
Type: kind,
|
Type: kind,
|
||||||
KeyWord: ctx.String("keyword"),
|
KeyWord: ctx.String("keyword"),
|
||||||
CreatedBy: ctx.String("author"),
|
CreatedBy: ctx.String("author"),
|
||||||
AssignedBy: ctx.String("assigned-to"),
|
AssignedBy: ctx.String("assignee"),
|
||||||
MentionedBy: ctx.String("mentions"),
|
MentionedBy: ctx.String("mentions"),
|
||||||
Labels: labels,
|
Labels: labels,
|
||||||
Milestones: milestones,
|
Milestones: milestones,
|
||||||
@@ -88,13 +89,15 @@ func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if ctx.IsSet("assignee") {
|
||||||
|
return errors.New("--assignee requires --repo (global issue search does not support assignee filter)")
|
||||||
|
}
|
||||||
issues, _, err = ctx.Login.Client().ListIssues(gitea.ListIssueOption{
|
issues, _, err = ctx.Login.Client().ListIssues(gitea.ListIssueOption{
|
||||||
ListOptions: flags.GetListOptions(cmd),
|
ListOptions: flags.GetListOptions(cmd),
|
||||||
State: state,
|
State: state,
|
||||||
Type: kind,
|
Type: kind,
|
||||||
KeyWord: ctx.String("keyword"),
|
KeyWord: ctx.String("keyword"),
|
||||||
CreatedBy: ctx.String("author"),
|
CreatedBy: ctx.String("author"),
|
||||||
AssignedBy: ctx.String("assigned-to"),
|
|
||||||
MentionedBy: ctx.String("mentions"),
|
MentionedBy: ctx.String("mentions"),
|
||||||
Labels: labels,
|
Labels: labels,
|
||||||
Milestones: milestones,
|
Milestones: milestones,
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ var CmdGenerateManPage = cli.Command{
|
|||||||
Hidden: true,
|
Hidden: true,
|
||||||
Flags: DocRenderFlags,
|
Flags: DocRenderFlags,
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
return RenderDocs(cmd, cmd.Root(), docs.ToMan)
|
return RenderDocs(cmd, cmd.Root(), func(cmd *cli.Command) (string, error) {
|
||||||
|
return docs.ToManWithSection(cmd, 1)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
19
cmd/pulls.go
19
cmd/pulls.go
@@ -109,11 +109,20 @@ func runPullDetail(_ stdctx.Context, cmd *cli.Command, index string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
reviews, _, err := client.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{
|
var reviews []*gitea.PullReview
|
||||||
ListOptions: gitea.ListOptions{Page: -1},
|
for page := 1; ; {
|
||||||
})
|
page_reviews, resp, err := client.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{
|
||||||
if err != nil {
|
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||||
fmt.Printf("error while loading reviews: %v\n", err)
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error while loading reviews: %v\n", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
reviews = append(reviews, page_reviews...)
|
||||||
|
if resp == nil || resp.NextPage == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
page = resp.NextPage
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.IsSet("output") {
|
if ctx.IsSet("output") {
|
||||||
|
|||||||
@@ -11,19 +11,25 @@ import (
|
|||||||
|
|
||||||
// GetReleaseByTag finds a release by its tag name.
|
// GetReleaseByTag finds a release by its tag name.
|
||||||
func GetReleaseByTag(owner, repo, tag string, client *gitea.Client) (*gitea.Release, error) {
|
func GetReleaseByTag(owner, repo, tag string, client *gitea.Client) (*gitea.Release, error) {
|
||||||
rl, _, err := client.ListReleases(owner, repo, gitea.ListReleasesOptions{
|
for page := 1; ; {
|
||||||
ListOptions: gitea.ListOptions{Page: -1},
|
rl, resp, err := client.ListReleases(owner, repo, gitea.ListReleasesOptions{
|
||||||
})
|
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||||
if err != nil {
|
})
|
||||||
return nil, err
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
if len(rl) == 0 {
|
|
||||||
return nil, fmt.Errorf("repo does not have any release")
|
|
||||||
}
|
|
||||||
for _, r := range rl {
|
|
||||||
if r.TagName == tag {
|
|
||||||
return r, nil
|
|
||||||
}
|
}
|
||||||
|
if page == 1 && len(rl) == 0 {
|
||||||
|
return nil, fmt.Errorf("repo does not have any release")
|
||||||
|
}
|
||||||
|
for _, r := range rl {
|
||||||
|
if r.TagName == tag {
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if resp == nil || resp.NextPage == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
page = resp.NextPage
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("release tag does not exist")
|
return nil, fmt.Errorf("release tag does not exist")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,30 +89,26 @@ func runWebhooksCreate(ctx stdctx.Context, cmd *cli.Command) error {
|
|||||||
config["secret"] = secret
|
config["secret"] = secret
|
||||||
}
|
}
|
||||||
|
|
||||||
if branchFilter != "" {
|
|
||||||
config["branch_filter"] = branchFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
if authHeader != "" {
|
|
||||||
config["authorization_header"] = authHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
var hook *gitea.Hook
|
var hook *gitea.Hook
|
||||||
if c.IsGlobal {
|
if c.IsGlobal {
|
||||||
return fmt.Errorf("global webhooks not yet supported in this version")
|
return fmt.Errorf("global webhooks not yet supported in this version")
|
||||||
} else if len(c.Org) > 0 {
|
} else if len(c.Org) > 0 {
|
||||||
hook, _, err = client.CreateOrgHook(c.Org, gitea.CreateHookOption{
|
hook, _, err = client.CreateOrgHook(c.Org, gitea.CreateHookOption{
|
||||||
Type: webhookType,
|
Type: webhookType,
|
||||||
Config: config,
|
Config: config,
|
||||||
Events: events,
|
Events: events,
|
||||||
Active: active,
|
Active: active,
|
||||||
|
BranchFilter: branchFilter,
|
||||||
|
AuthorizationHeader: authHeader,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
hook, _, err = client.CreateRepoHook(c.Owner, c.Repo, gitea.CreateHookOption{
|
hook, _, err = client.CreateRepoHook(c.Owner, c.Repo, gitea.CreateHookOption{
|
||||||
Type: webhookType,
|
Type: webhookType,
|
||||||
Config: config,
|
Config: config,
|
||||||
Events: events,
|
Events: events,
|
||||||
Active: active,
|
Active: active,
|
||||||
|
BranchFilter: branchFilter,
|
||||||
|
AuthorizationHeader: authHeader,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -79,8 +79,6 @@ func TestWebhookConfigConstruction(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
url string
|
url string
|
||||||
secret string
|
secret string
|
||||||
branchFilter string
|
|
||||||
authHeader string
|
|
||||||
expectedKeys []string
|
expectedKeys []string
|
||||||
expectedValues map[string]string
|
expectedValues map[string]string
|
||||||
}{
|
}{
|
||||||
@@ -106,44 +104,16 @@ func TestWebhookConfigConstruction(t *testing.T) {
|
|||||||
"secret": "my-secret",
|
"secret": "my-secret",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "Config with branch filter",
|
|
||||||
url: "https://example.com/webhook",
|
|
||||||
branchFilter: "main,develop",
|
|
||||||
expectedKeys: []string{"url", "http_method", "content_type", "branch_filter"},
|
|
||||||
expectedValues: map[string]string{
|
|
||||||
"url": "https://example.com/webhook",
|
|
||||||
"http_method": "post",
|
|
||||||
"content_type": "json",
|
|
||||||
"branch_filter": "main,develop",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Config with auth header",
|
|
||||||
url: "https://example.com/webhook",
|
|
||||||
authHeader: "Bearer token123",
|
|
||||||
expectedKeys: []string{"url", "http_method", "content_type", "authorization_header"},
|
|
||||||
expectedValues: map[string]string{
|
|
||||||
"url": "https://example.com/webhook",
|
|
||||||
"http_method": "post",
|
|
||||||
"content_type": "json",
|
|
||||||
"authorization_header": "Bearer token123",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "Complete config",
|
name: "Complete config",
|
||||||
url: "https://example.com/webhook",
|
url: "https://example.com/webhook",
|
||||||
secret: "secret123",
|
secret: "secret123",
|
||||||
branchFilter: "main",
|
expectedKeys: []string{"url", "http_method", "content_type", "secret"},
|
||||||
authHeader: "X-Token: abc",
|
|
||||||
expectedKeys: []string{"url", "http_method", "content_type", "secret", "branch_filter", "authorization_header"},
|
|
||||||
expectedValues: map[string]string{
|
expectedValues: map[string]string{
|
||||||
"url": "https://example.com/webhook",
|
"url": "https://example.com/webhook",
|
||||||
"http_method": "post",
|
"http_method": "post",
|
||||||
"content_type": "json",
|
"content_type": "json",
|
||||||
"secret": "secret123",
|
"secret": "secret123",
|
||||||
"branch_filter": "main",
|
|
||||||
"authorization_header": "X-Token: abc",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -159,12 +129,6 @@ func TestWebhookConfigConstruction(t *testing.T) {
|
|||||||
if tt.secret != "" {
|
if tt.secret != "" {
|
||||||
config["secret"] = tt.secret
|
config["secret"] = tt.secret
|
||||||
}
|
}
|
||||||
if tt.branchFilter != "" {
|
|
||||||
config["branch_filter"] = tt.branchFilter
|
|
||||||
}
|
|
||||||
if tt.authHeader != "" {
|
|
||||||
config["authorization_header"] = tt.authHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check all expected keys exist
|
// Check all expected keys exist
|
||||||
for _, key := range tt.expectedKeys {
|
for _, key := range tt.expectedKeys {
|
||||||
@@ -184,11 +148,13 @@ func TestWebhookConfigConstruction(t *testing.T) {
|
|||||||
|
|
||||||
func TestWebhookCreateOptions(t *testing.T) {
|
func TestWebhookCreateOptions(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
webhookType string
|
webhookType string
|
||||||
events []string
|
events []string
|
||||||
active bool
|
active bool
|
||||||
config map[string]string
|
config map[string]string
|
||||||
|
branchFilter string
|
||||||
|
authHeader string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Gitea webhook",
|
name: "Gitea webhook",
|
||||||
@@ -200,6 +166,8 @@ func TestWebhookCreateOptions(t *testing.T) {
|
|||||||
"http_method": "post",
|
"http_method": "post",
|
||||||
"content_type": "json",
|
"content_type": "json",
|
||||||
},
|
},
|
||||||
|
branchFilter: "main",
|
||||||
|
authHeader: "X-Token: abc",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Slack webhook",
|
name: "Slack webhook",
|
||||||
@@ -228,16 +196,20 @@ func TestWebhookCreateOptions(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
option := gitea.CreateHookOption{
|
option := gitea.CreateHookOption{
|
||||||
Type: gitea.HookType(tt.webhookType),
|
Type: gitea.HookType(tt.webhookType),
|
||||||
Config: tt.config,
|
Config: tt.config,
|
||||||
Events: tt.events,
|
Events: tt.events,
|
||||||
Active: tt.active,
|
Active: tt.active,
|
||||||
|
BranchFilter: tt.branchFilter,
|
||||||
|
AuthorizationHeader: tt.authHeader,
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, gitea.HookType(tt.webhookType), option.Type)
|
assert.Equal(t, gitea.HookType(tt.webhookType), option.Type)
|
||||||
assert.Equal(t, tt.events, option.Events)
|
assert.Equal(t, tt.events, option.Events)
|
||||||
assert.Equal(t, tt.active, option.Active)
|
assert.Equal(t, tt.active, option.Active)
|
||||||
assert.Equal(t, tt.config, option.Config)
|
assert.Equal(t, tt.config, option.Config)
|
||||||
|
assert.Equal(t, tt.branchFilter, option.BranchFilter)
|
||||||
|
assert.Equal(t, tt.authHeader, option.AuthorizationHeader)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,11 +97,14 @@ func runWebhooksUpdate(ctx stdctx.Context, cmd *cli.Command) error {
|
|||||||
if cmd.IsSet("secret") {
|
if cmd.IsSet("secret") {
|
||||||
config["secret"] = cmd.String("secret")
|
config["secret"] = cmd.String("secret")
|
||||||
}
|
}
|
||||||
|
branchFilter := hook.BranchFilter
|
||||||
if cmd.IsSet("branch-filter") {
|
if cmd.IsSet("branch-filter") {
|
||||||
config["branch_filter"] = cmd.String("branch-filter")
|
branchFilter = cmd.String("branch-filter")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authHeader := hook.AuthorizationHeader
|
||||||
if cmd.IsSet("authorization-header") {
|
if cmd.IsSet("authorization-header") {
|
||||||
config["authorization_header"] = cmd.String("authorization-header")
|
authHeader = cmd.String("authorization-header")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update events if specified
|
// Update events if specified
|
||||||
@@ -126,15 +129,19 @@ func runWebhooksUpdate(ctx stdctx.Context, cmd *cli.Command) error {
|
|||||||
return fmt.Errorf("global webhooks not yet supported in this version")
|
return fmt.Errorf("global webhooks not yet supported in this version")
|
||||||
} else if len(c.Org) > 0 {
|
} else if len(c.Org) > 0 {
|
||||||
_, err = client.EditOrgHook(c.Org, int64(webhookID), gitea.EditHookOption{
|
_, err = client.EditOrgHook(c.Org, int64(webhookID), gitea.EditHookOption{
|
||||||
Config: config,
|
Config: config,
|
||||||
Events: events,
|
Events: events,
|
||||||
Active: &active,
|
Active: &active,
|
||||||
|
BranchFilter: branchFilter,
|
||||||
|
AuthorizationHeader: authHeader,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
_, err = client.EditRepoHook(c.Owner, c.Repo, int64(webhookID), gitea.EditHookOption{
|
_, err = client.EditRepoHook(c.Owner, c.Repo, int64(webhookID), gitea.EditHookOption{
|
||||||
Config: config,
|
Config: config,
|
||||||
Events: events,
|
Events: events,
|
||||||
Active: &active,
|
Active: &active,
|
||||||
|
BranchFilter: branchFilter,
|
||||||
|
AuthorizationHeader: authHeader,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -128,12 +128,10 @@ func TestUpdateActiveInactiveFlags(t *testing.T) {
|
|||||||
func TestUpdateConfigPreservation(t *testing.T) {
|
func TestUpdateConfigPreservation(t *testing.T) {
|
||||||
// Test that existing configuration is preserved when not updated
|
// Test that existing configuration is preserved when not updated
|
||||||
originalConfig := map[string]string{
|
originalConfig := map[string]string{
|
||||||
"url": "https://old.example.com/webhook",
|
"url": "https://old.example.com/webhook",
|
||||||
"secret": "old-secret",
|
"secret": "old-secret",
|
||||||
"branch_filter": "main",
|
"http_method": "post",
|
||||||
"authorization_header": "Bearer old-token",
|
"content_type": "json",
|
||||||
"http_method": "post",
|
|
||||||
"content_type": "json",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -147,53 +145,32 @@ func TestUpdateConfigPreservation(t *testing.T) {
|
|||||||
"url": "https://new.example.com/webhook",
|
"url": "https://new.example.com/webhook",
|
||||||
},
|
},
|
||||||
expectedConfig: map[string]string{
|
expectedConfig: map[string]string{
|
||||||
"url": "https://new.example.com/webhook",
|
"url": "https://new.example.com/webhook",
|
||||||
"secret": "old-secret",
|
"secret": "old-secret",
|
||||||
"branch_filter": "main",
|
"http_method": "post",
|
||||||
"authorization_header": "Bearer old-token",
|
"content_type": "json",
|
||||||
"http_method": "post",
|
|
||||||
"content_type": "json",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Update secret and auth header",
|
name: "Update secret",
|
||||||
updates: map[string]string{
|
updates: map[string]string{
|
||||||
"secret": "new-secret",
|
"secret": "new-secret",
|
||||||
"authorization_header": "X-Token: new-token",
|
|
||||||
},
|
},
|
||||||
expectedConfig: map[string]string{
|
expectedConfig: map[string]string{
|
||||||
"url": "https://old.example.com/webhook",
|
"url": "https://old.example.com/webhook",
|
||||||
"secret": "new-secret",
|
"secret": "new-secret",
|
||||||
"branch_filter": "main",
|
"http_method": "post",
|
||||||
"authorization_header": "X-Token: new-token",
|
"content_type": "json",
|
||||||
"http_method": "post",
|
|
||||||
"content_type": "json",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Clear branch filter",
|
|
||||||
updates: map[string]string{
|
|
||||||
"branch_filter": "",
|
|
||||||
},
|
|
||||||
expectedConfig: map[string]string{
|
|
||||||
"url": "https://old.example.com/webhook",
|
|
||||||
"secret": "old-secret",
|
|
||||||
"branch_filter": "",
|
|
||||||
"authorization_header": "Bearer old-token",
|
|
||||||
"http_method": "post",
|
|
||||||
"content_type": "json",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "No updates",
|
name: "No updates",
|
||||||
updates: map[string]string{},
|
updates: map[string]string{},
|
||||||
expectedConfig: map[string]string{
|
expectedConfig: map[string]string{
|
||||||
"url": "https://old.example.com/webhook",
|
"url": "https://old.example.com/webhook",
|
||||||
"secret": "old-secret",
|
"secret": "old-secret",
|
||||||
"branch_filter": "main",
|
"http_method": "post",
|
||||||
"authorization_header": "Bearer old-token",
|
"content_type": "json",
|
||||||
"http_method": "post",
|
|
||||||
"content_type": "json",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -217,6 +194,61 @@ func TestUpdateConfigPreservation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpdateBranchFilterAndAuthHeaderHandling(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
originalBranchFilter string
|
||||||
|
originalAuthHeader string
|
||||||
|
setBranchFilter bool
|
||||||
|
newBranchFilter string
|
||||||
|
setAuthorizationHeader bool
|
||||||
|
newAuthHeader string
|
||||||
|
expectedBranchFilter string
|
||||||
|
expectedAuthHeader string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Preserve values",
|
||||||
|
originalBranchFilter: "main",
|
||||||
|
originalAuthHeader: "Bearer old-token",
|
||||||
|
expectedBranchFilter: "main",
|
||||||
|
expectedAuthHeader: "Bearer old-token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update branch filter",
|
||||||
|
originalBranchFilter: "main",
|
||||||
|
setBranchFilter: true,
|
||||||
|
newBranchFilter: "develop",
|
||||||
|
expectedBranchFilter: "develop",
|
||||||
|
expectedAuthHeader: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update authorization header",
|
||||||
|
originalAuthHeader: "Bearer old-token",
|
||||||
|
setAuthorizationHeader: true,
|
||||||
|
newAuthHeader: "X-Token: new-token",
|
||||||
|
expectedBranchFilter: "",
|
||||||
|
expectedAuthHeader: "X-Token: new-token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
branchFilter := tt.originalBranchFilter
|
||||||
|
if tt.setBranchFilter {
|
||||||
|
branchFilter = tt.newBranchFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
authHeader := tt.originalAuthHeader
|
||||||
|
if tt.setAuthorizationHeader {
|
||||||
|
authHeader = tt.newAuthHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expectedBranchFilter, branchFilter)
|
||||||
|
assert.Equal(t, tt.expectedAuthHeader, authHeader)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUpdateEventsHandling(t *testing.T) {
|
func TestUpdateEventsHandling(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -12,7 +12,7 @@ require (
|
|||||||
github.com/adrg/xdg v0.5.3
|
github.com/adrg/xdg v0.5.3
|
||||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
||||||
github.com/enescakir/emoji v1.0.0
|
github.com/enescakir/emoji v1.0.0
|
||||||
github.com/go-authgate/sdk-go v0.6.1
|
github.com/go-authgate/sdk-go v0.9.0
|
||||||
github.com/go-git/go-git/v5 v5.18.0
|
github.com/go-git/go-git/v5 v5.18.0
|
||||||
github.com/muesli/termenv v0.16.0
|
github.com/muesli/termenv v0.16.0
|
||||||
github.com/olekukonko/tablewriter v1.1.4
|
github.com/olekukonko/tablewriter v1.1.4
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -116,6 +116,12 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
|||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
github.com/go-authgate/sdk-go v0.6.1 h1:oQREINU63YckTRdJ+0VBmN6ewFSMXa0D862w8624/jw=
|
github.com/go-authgate/sdk-go v0.6.1 h1:oQREINU63YckTRdJ+0VBmN6ewFSMXa0D862w8624/jw=
|
||||||
github.com/go-authgate/sdk-go v0.6.1/go.mod h1:55PLAPuu8GDK0omOwG6lx4c+9/T6dJwZd8kecUueLEk=
|
github.com/go-authgate/sdk-go v0.6.1/go.mod h1:55PLAPuu8GDK0omOwG6lx4c+9/T6dJwZd8kecUueLEk=
|
||||||
|
github.com/go-authgate/sdk-go v0.7.0 h1:hUqUMzsDkb+l5EiL+aX2LaFon/3mbjHmxm97qHHHL3k=
|
||||||
|
github.com/go-authgate/sdk-go v0.7.0/go.mod h1:Afx/Dbyvf8pw4YeOqVEVdDW2WHhn534Sb2/TaFQktuU=
|
||||||
|
github.com/go-authgate/sdk-go v0.8.0 h1:uYJMOv//qwMEJeiFTUvXGXozEHGUOsS6zfOVXxEwat4=
|
||||||
|
github.com/go-authgate/sdk-go v0.8.0/go.mod h1:Afx/Dbyvf8pw4YeOqVEVdDW2WHhn534Sb2/TaFQktuU=
|
||||||
|
github.com/go-authgate/sdk-go v0.9.0 h1:VgQNjcKXtMONNiVf4coC/J69H78CkTt3CJ8maiQSf6Y=
|
||||||
|
github.com/go-authgate/sdk-go v0.9.0/go.mod h1:Afx/Dbyvf8pw4YeOqVEVdDW2WHhn534Sb2/TaFQktuU=
|
||||||
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
|
|||||||
66
modules/api/types.go
Normal file
66
modules/api/types.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// ActionRun represents a workflow run
|
||||||
|
type ActionRun struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Title string `json:"display_title"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Conclusion string `json:"conclusion"`
|
||||||
|
Event string `json:"event"`
|
||||||
|
HeadBranch string `json:"head_branch"`
|
||||||
|
HeadSHA string `json:"head_sha"`
|
||||||
|
RunNumber int64 `json:"run_number"`
|
||||||
|
RunAttempt int64 `json:"run_attempt"`
|
||||||
|
HTMLURL string `json:"html_url"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
StartedAt *time.Time `json:"started_at"`
|
||||||
|
CompletedAt *time.Time `json:"completed_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionRunList is a list of workflow runs
|
||||||
|
type ActionRunList struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
WorkflowRuns []*ActionRun `json:"workflow_runs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionJob represents a job within a workflow run
|
||||||
|
type ActionJob struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
RunID int64 `json:"run_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Conclusion string `json:"conclusion"`
|
||||||
|
HTMLURL string `json:"html_url"`
|
||||||
|
StartedAt *time.Time `json:"started_at"`
|
||||||
|
CompletedAt *time.Time `json:"completed_at"`
|
||||||
|
Steps []*ActionJobStep `json:"steps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionJobStep represents a step within a job
|
||||||
|
type ActionJobStep struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Number int64 `json:"number"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Conclusion string `json:"conclusion"`
|
||||||
|
StartedAt *time.Time `json:"started_at"`
|
||||||
|
CompletedAt *time.Time `json:"completed_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionJobList is a list of jobs
|
||||||
|
type ActionJobList struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
Jobs []*ActionJob `json:"jobs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssueDependencyRequest is the request body for adding/removing issue dependencies
|
||||||
|
type IssueDependencyRequest struct {
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Repo string `json:"repo"`
|
||||||
|
Index int64 `json:"index"`
|
||||||
|
}
|
||||||
13
modules/config/testtools.go
Normal file
13
modules/config/testtools.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
//go:build testtools
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// AcquireConfigLockForTesting exposes the internal lock helper to integration tests.
|
||||||
|
func AcquireConfigLockForTesting(lockPath string, timeout time.Duration) (func() error, error) {
|
||||||
|
return acquireConfigLock(lockPath, timeout)
|
||||||
|
}
|
||||||
@@ -4,14 +4,9 @@
|
|||||||
package context
|
package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/tea/modules/config"
|
"code.gitea.io/tea/modules/config"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/urfave/cli/v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_MatchLogins(t *testing.T) {
|
func Test_MatchLogins(t *testing.T) {
|
||||||
@@ -70,47 +65,3 @@ func Test_MatchLogins(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInitCommand_WithRepoSlugSkipsLocalRepoDetection(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
config.SetConfigForTesting(config.LocalConfig{
|
|
||||||
Logins: []config.Login{{
|
|
||||||
Name: "test-login",
|
|
||||||
URL: "https://gitea.example.com",
|
|
||||||
Token: "token",
|
|
||||||
User: "login-user",
|
|
||||||
Default: true,
|
|
||||||
}},
|
|
||||||
})
|
|
||||||
|
|
||||||
cmd := exec.Command("git", "init", "--object-format=sha256", tmpDir)
|
|
||||||
cmd.Env = os.Environ()
|
|
||||||
require.NoError(t, cmd.Run())
|
|
||||||
|
|
||||||
oldWd, err := os.Getwd()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, os.Chdir(tmpDir))
|
|
||||||
t.Cleanup(func() {
|
|
||||||
require.NoError(t, os.Chdir(oldWd))
|
|
||||||
})
|
|
||||||
|
|
||||||
cliCmd := cli.Command{
|
|
||||||
Name: "branches",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
&cli.StringFlag{Name: "login"},
|
|
||||||
&cli.StringFlag{Name: "repo"},
|
|
||||||
&cli.StringFlag{Name: "remote"},
|
|
||||||
&cli.StringFlag{Name: "output"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
require.NoError(t, cliCmd.Set("repo", "owner/repo"))
|
|
||||||
|
|
||||||
ctx, err := InitCommand(&cliCmd)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, "owner", ctx.Owner)
|
|
||||||
require.Equal(t, "repo", ctx.Repo)
|
|
||||||
require.Equal(t, "owner/repo", ctx.RepoSlug)
|
|
||||||
require.Nil(t, ctx.LocalRepo)
|
|
||||||
require.NotNil(t, ctx.Login)
|
|
||||||
require.Equal(t, "test-login", ctx.Login.Name)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -180,19 +180,25 @@ func fetchIssueSelectables(login *config.Login, owner, repo string, done chan is
|
|||||||
r.MilestoneList[i] = m.Title
|
r.MilestoneList[i] = m.Title
|
||||||
}
|
}
|
||||||
|
|
||||||
labels, _, err := c.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{
|
|
||||||
ListOptions: gitea.ListOptions{Page: -1},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
r.Err = err
|
|
||||||
done <- r
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r.LabelMap = make(map[string]int64)
|
r.LabelMap = make(map[string]int64)
|
||||||
r.LabelList = make([]string, len(labels))
|
r.LabelList = make([]string, 0)
|
||||||
for i, l := range labels {
|
for page := 1; ; {
|
||||||
r.LabelMap[l.Name] = l.ID
|
labels, resp, err := c.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{
|
||||||
r.LabelList[i] = l.Name
|
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
r.Err = err
|
||||||
|
done <- r
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, l := range labels {
|
||||||
|
r.LabelMap[l.Name] = l.ID
|
||||||
|
r.LabelList = append(r.LabelList, l.Name)
|
||||||
|
}
|
||||||
|
if resp == nil || resp.NextPage == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
page = resp.NextPage
|
||||||
}
|
}
|
||||||
|
|
||||||
done <- r
|
done <- r
|
||||||
|
|||||||
@@ -67,13 +67,21 @@ func WebhookDetails(hook *gitea.Hook) {
|
|||||||
if method, ok := hook.Config["http_method"]; ok {
|
if method, ok := hook.Config["http_method"]; ok {
|
||||||
fmt.Printf("- **HTTP Method**: %s\n", method)
|
fmt.Printf("- **HTTP Method**: %s\n", method)
|
||||||
}
|
}
|
||||||
if branchFilter, ok := hook.Config["branch_filter"]; ok && branchFilter != "" {
|
branchFilter := hook.BranchFilter
|
||||||
|
if branchFilter == "" {
|
||||||
|
branchFilter = hook.Config["branch_filter"]
|
||||||
|
}
|
||||||
|
if branchFilter != "" {
|
||||||
fmt.Printf("- **Branch Filter**: %s\n", branchFilter)
|
fmt.Printf("- **Branch Filter**: %s\n", branchFilter)
|
||||||
}
|
}
|
||||||
if _, hasSecret := hook.Config["secret"]; hasSecret {
|
if _, hasSecret := hook.Config["secret"]; hasSecret {
|
||||||
fmt.Printf("- **Secret**: (configured)\n")
|
fmt.Printf("- **Secret**: (configured)\n")
|
||||||
}
|
}
|
||||||
if _, hasAuth := hook.Config["authorization_header"]; hasAuth {
|
hasAuth := hook.AuthorizationHeader != ""
|
||||||
|
if !hasAuth {
|
||||||
|
_, hasAuth = hook.Config["authorization_header"]
|
||||||
|
}
|
||||||
|
if hasAuth {
|
||||||
fmt.Printf("- **Authorization Header**: (configured)\n")
|
fmt.Printf("- **Authorization Header**: (configured)\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,17 +81,17 @@ func TestWebhookDetails(t *testing.T) {
|
|||||||
ID: 123,
|
ID: 123,
|
||||||
Type: "gitea",
|
Type: "gitea",
|
||||||
Config: map[string]string{
|
Config: map[string]string{
|
||||||
"url": "https://example.com/webhook",
|
"url": "https://example.com/webhook",
|
||||||
"content_type": "json",
|
"content_type": "json",
|
||||||
"http_method": "post",
|
"http_method": "post",
|
||||||
"branch_filter": "main,develop",
|
"secret": "secret-value",
|
||||||
"secret": "secret-value",
|
|
||||||
"authorization_header": "Bearer token123",
|
|
||||||
},
|
},
|
||||||
Events: []string{"push", "pull_request", "issues"},
|
BranchFilter: "main,develop",
|
||||||
Active: true,
|
AuthorizationHeader: "Bearer token123",
|
||||||
Created: now.Add(-24 * time.Hour),
|
Events: []string{"push", "pull_request", "issues"},
|
||||||
Updated: now,
|
Active: true,
|
||||||
|
Created: now.Add(-24 * time.Hour),
|
||||||
|
Updated: now,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -238,16 +238,14 @@ func TestWebhookConfigHandling(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Config with all fields",
|
name: "Config with all fields",
|
||||||
config: map[string]string{
|
config: map[string]string{
|
||||||
"url": "https://example.com/webhook",
|
"url": "https://example.com/webhook",
|
||||||
"secret": "my-secret",
|
"secret": "my-secret",
|
||||||
"authorization_header": "Bearer token",
|
"content_type": "json",
|
||||||
"content_type": "json",
|
"http_method": "post",
|
||||||
"http_method": "post",
|
|
||||||
"branch_filter": "main",
|
|
||||||
},
|
},
|
||||||
expectedURL: "https://example.com/webhook",
|
expectedURL: "https://example.com/webhook",
|
||||||
hasSecret: true,
|
hasSecret: true,
|
||||||
hasAuthHeader: true,
|
hasAuthHeader: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Config with minimal fields",
|
name: "Config with minimal fields",
|
||||||
@@ -341,17 +339,17 @@ func TestWebhookDetailsFormatting(t *testing.T) {
|
|||||||
ID: 123,
|
ID: 123,
|
||||||
Type: "gitea",
|
Type: "gitea",
|
||||||
Config: map[string]string{
|
Config: map[string]string{
|
||||||
"url": "https://example.com/webhook",
|
"url": "https://example.com/webhook",
|
||||||
"content_type": "json",
|
"content_type": "json",
|
||||||
"http_method": "post",
|
"http_method": "post",
|
||||||
"branch_filter": "main,develop",
|
"secret": "secret-value",
|
||||||
"secret": "secret-value",
|
|
||||||
"authorization_header": "Bearer token123",
|
|
||||||
},
|
},
|
||||||
Events: []string{"push", "pull_request", "issues"},
|
BranchFilter: "main,develop",
|
||||||
Active: true,
|
AuthorizationHeader: "Bearer token123",
|
||||||
Created: now.Add(-24 * time.Hour),
|
Events: []string{"push", "pull_request", "issues"},
|
||||||
Updated: now,
|
Active: true,
|
||||||
|
Created: now.Add(-24 * time.Hour),
|
||||||
|
Updated: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that all expected fields are included in details
|
// Test that all expected fields are included in details
|
||||||
@@ -379,8 +377,8 @@ func TestWebhookDetailsFormatting(t *testing.T) {
|
|||||||
assert.Equal(t, "https://example.com/webhook", hook.Config["url"])
|
assert.Equal(t, "https://example.com/webhook", hook.Config["url"])
|
||||||
assert.Equal(t, "json", hook.Config["content_type"])
|
assert.Equal(t, "json", hook.Config["content_type"])
|
||||||
assert.Equal(t, "post", hook.Config["http_method"])
|
assert.Equal(t, "post", hook.Config["http_method"])
|
||||||
assert.Equal(t, "main,develop", hook.Config["branch_filter"])
|
assert.Equal(t, "main,develop", hook.BranchFilter)
|
||||||
assert.Contains(t, hook.Config, "secret")
|
assert.Contains(t, hook.Config, "secret")
|
||||||
assert.Contains(t, hook.Config, "authorization_header")
|
assert.Equal(t, "Bearer token123", hook.AuthorizationHeader)
|
||||||
assert.Equal(t, []string{"push", "pull_request", "issues"}, hook.Events)
|
assert.Equal(t, []string{"push", "pull_request", "issues"}, hook.Events)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,16 +13,23 @@ import (
|
|||||||
// ResolveLabelNames returns a list of label IDs for a given list of label names
|
// ResolveLabelNames returns a list of label IDs for a given list of label names
|
||||||
func ResolveLabelNames(client *gitea.Client, owner, repo string, labelNames []string) ([]int64, error) {
|
func ResolveLabelNames(client *gitea.Client, owner, repo string, labelNames []string) ([]int64, error) {
|
||||||
labelIDs := make([]int64, 0, len(labelNames))
|
labelIDs := make([]int64, 0, len(labelNames))
|
||||||
labels, _, err := client.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{
|
page := 1
|
||||||
ListOptions: gitea.ListOptions{Page: -1},
|
for {
|
||||||
})
|
labels, resp, err := client.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{
|
||||||
if err != nil {
|
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||||
return nil, err
|
})
|
||||||
}
|
if err != nil {
|
||||||
for _, l := range labels {
|
return nil, err
|
||||||
if utils.Contains(labelNames, l.Name) {
|
|
||||||
labelIDs = append(labelIDs, l.ID)
|
|
||||||
}
|
}
|
||||||
|
for _, l := range labels {
|
||||||
|
if utils.Contains(labelNames, l.Name) {
|
||||||
|
labelIDs = append(labelIDs, l.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if resp == nil || resp.NextPage == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
page = resp.NextPage
|
||||||
}
|
}
|
||||||
return labelIDs, nil
|
return labelIDs, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,11 +166,19 @@ func generateToken(login config.Login, user, pass, otp, scopes string) (string,
|
|||||||
}
|
}
|
||||||
client := login.Client(opts...)
|
client := login.Client(opts...)
|
||||||
|
|
||||||
tl, _, err := client.ListAccessTokens(gitea.ListAccessTokensOptions{
|
var tl []*gitea.AccessToken
|
||||||
ListOptions: gitea.ListOptions{Page: -1},
|
for page := 1; ; {
|
||||||
})
|
page_tokens, resp, err := client.ListAccessTokens(gitea.ListAccessTokensOptions{
|
||||||
if err != nil {
|
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||||
return "", err
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
tl = append(tl, page_tokens...)
|
||||||
|
if resp == nil || resp.NextPage == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
page = resp.NextPage
|
||||||
}
|
}
|
||||||
host, _ := os.Hostname()
|
host, _ := os.Hostname()
|
||||||
tokenName := host + "-tea"
|
tokenName := host + "-tea"
|
||||||
|
|||||||
@@ -19,11 +19,22 @@ import (
|
|||||||
// a matching private key in ~/.ssh/. If no match is found, path is empty.
|
// a matching private key in ~/.ssh/. If no match is found, path is empty.
|
||||||
func findSSHKey(client *gitea.Client) (string, error) {
|
func findSSHKey(client *gitea.Client) (string, error) {
|
||||||
// get keys registered on gitea instance
|
// get keys registered on gitea instance
|
||||||
keys, _, err := client.ListMyPublicKeys(gitea.ListPublicKeysOptions{
|
var keys []*gitea.PublicKey
|
||||||
ListOptions: gitea.ListOptions{Page: -1},
|
for page := 1; ; {
|
||||||
})
|
page_keys, resp, err := client.ListMyPublicKeys(gitea.ListPublicKeysOptions{
|
||||||
if err != nil || len(keys) == 0 {
|
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||||
return "", err
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
keys = append(keys, page_keys...)
|
||||||
|
if resp == nil || resp.NextPage == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
page = resp.NextPage
|
||||||
|
}
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// enumerate ~/.ssh/*.pub files
|
// enumerate ~/.ssh/*.pub files
|
||||||
|
|||||||
@@ -14,11 +14,19 @@ import (
|
|||||||
func ListPullReviewComments(ctx *context.TeaContext, idx int64) ([]*gitea.PullReviewComment, error) {
|
func ListPullReviewComments(ctx *context.TeaContext, idx int64) ([]*gitea.PullReviewComment, error) {
|
||||||
c := ctx.Login.Client()
|
c := ctx.Login.Client()
|
||||||
|
|
||||||
reviews, _, err := c.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{
|
var reviews []*gitea.PullReview
|
||||||
ListOptions: gitea.ListOptions{Page: -1},
|
for page := 1; ; {
|
||||||
})
|
page_reviews, resp, err := c.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{
|
||||||
if err != nil {
|
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||||
return nil, err
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reviews = append(reviews, page_reviews...)
|
||||||
|
if resp == nil || resp.NextPage == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
page = resp.NextPage
|
||||||
}
|
}
|
||||||
|
|
||||||
var allComments []*gitea.PullReviewComment
|
var allComments []*gitea.PullReviewComment
|
||||||
|
|||||||
10
tests/README.md
Normal file
10
tests/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
This directory contains integration tests that exercise tea against external services or external executables.
|
||||||
|
|
||||||
|
- Unit tests stay next to the packages they cover.
|
||||||
|
- Integration tests live under `tests/` so they can be run separately.
|
||||||
|
|
||||||
|
Common targets:
|
||||||
|
|
||||||
|
- `make unit-test`
|
||||||
|
- `make integration-test`
|
||||||
|
- `make test`
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
//go:build unix
|
//go:build unix
|
||||||
|
|
||||||
package config
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -12,10 +12,11 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfigLock_CrossProcess(t *testing.T) {
|
func TestConfigLock_CrossProcess(t *testing.T) {
|
||||||
// Create a temp directory for test
|
|
||||||
tmpDir, err := os.MkdirTemp("", "tea-lock-test")
|
tmpDir, err := os.MkdirTemp("", "tea-lock-test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create temp dir: %v", err)
|
t.Fatalf("failed to create temp dir: %v", err)
|
||||||
@@ -24,15 +25,16 @@ func TestConfigLock_CrossProcess(t *testing.T) {
|
|||||||
|
|
||||||
lockPath := filepath.Join(tmpDir, "config.yml.lock")
|
lockPath := filepath.Join(tmpDir, "config.yml.lock")
|
||||||
|
|
||||||
// Acquire lock in main process
|
unlock, err := config.AcquireConfigLockForTesting(lockPath, 5*time.Second)
|
||||||
unlock, err := acquireConfigLock(lockPath, 5*time.Second)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to acquire lock: %v", err)
|
t.Fatalf("failed to acquire lock: %v", err)
|
||||||
}
|
}
|
||||||
defer unlock()
|
defer func() {
|
||||||
|
if err := unlock(); err != nil {
|
||||||
|
t.Fatalf("failed to release lock: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Spawn a subprocess that tries to acquire the same lock
|
|
||||||
// The subprocess should fail to acquire within timeout
|
|
||||||
script := fmt.Sprintf(`
|
script := fmt.Sprintf(`
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -48,19 +50,16 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
// Try non-blocking lock
|
|
||||||
err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
|
err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Lock is held - expected behavior
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
// Lock was acquired - unexpected
|
|
||||||
syscall.Flock(int(file.Fd()), syscall.LOCK_UN)
|
syscall.Flock(int(file.Fd()), syscall.LOCK_UN)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
`, lockPath)
|
`, lockPath)
|
||||||
|
|
||||||
// Write and run the test script
|
|
||||||
scriptPath := filepath.Join(tmpDir, "locktest.go")
|
scriptPath := filepath.Join(tmpDir, "locktest.go")
|
||||||
if err := os.WriteFile(scriptPath, []byte(script), 0o600); err != nil {
|
if err := os.WriteFile(scriptPath, []byte(script), 0o600); err != nil {
|
||||||
t.Fatalf("failed to write test script: %v", err)
|
t.Fatalf("failed to write test script: %v", err)
|
||||||
@@ -78,5 +77,4 @@ func main() {
|
|||||||
t.Errorf("subprocess execution failed: %v", err)
|
t.Errorf("subprocess execution failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Exit code 0 means lock was properly held - success
|
|
||||||
}
|
}
|
||||||
59
tests/integration/context_init_test.go
Normal file
59
tests/integration/context_init_test.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
|
teacontext "code.gitea.io/tea/modules/context"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInitCommand_WithRepoSlugSkipsLocalRepoDetection(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
config.SetConfigForTesting(config.LocalConfig{
|
||||||
|
Logins: []config.Login{{
|
||||||
|
Name: "test-login",
|
||||||
|
URL: "https://gitea.example.com",
|
||||||
|
Token: "token",
|
||||||
|
User: "login-user",
|
||||||
|
Default: true,
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := exec.Command("git", "init", "--object-format=sha256", tmpDir)
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
require.NoError(t, cmd.Run())
|
||||||
|
|
||||||
|
oldWd, err := os.Getwd()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, os.Chdir(tmpDir))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, os.Chdir(oldWd))
|
||||||
|
})
|
||||||
|
|
||||||
|
cliCmd := cli.Command{
|
||||||
|
Name: "branches",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{Name: "login"},
|
||||||
|
&cli.StringFlag{Name: "repo"},
|
||||||
|
&cli.StringFlag{Name: "remote"},
|
||||||
|
&cli.StringFlag{Name: "output"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.NoError(t, cliCmd.Set("repo", "owner/repo"))
|
||||||
|
|
||||||
|
ctx, err := teacontext.InitCommand(&cliCmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "owner", ctx.Owner)
|
||||||
|
require.Equal(t, "repo", ctx.Repo)
|
||||||
|
require.Equal(t, "owner/repo", ctx.RepoSlug)
|
||||||
|
require.Nil(t, ctx.LocalRepo)
|
||||||
|
require.NotNil(t, ctx.Login)
|
||||||
|
require.Equal(t, "test-login", ctx.Login.Name)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package git
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -9,11 +9,11 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
teagit "code.gitea.io/tea/modules/git"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRepoFromPath_Worktree(t *testing.T) {
|
func TestRepoFromPath_Worktree(t *testing.T) {
|
||||||
// Create a temporary directory for test
|
|
||||||
tmpDir, err := os.MkdirTemp("", "tea-worktree-test-*")
|
tmpDir, err := os.MkdirTemp("", "tea-worktree-test-*")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
@@ -21,21 +21,17 @@ func TestRepoFromPath_Worktree(t *testing.T) {
|
|||||||
mainRepoPath := filepath.Join(tmpDir, "main-repo")
|
mainRepoPath := filepath.Join(tmpDir, "main-repo")
|
||||||
worktreePath := filepath.Join(tmpDir, "worktree")
|
worktreePath := filepath.Join(tmpDir, "worktree")
|
||||||
|
|
||||||
// Initialize main repository
|
|
||||||
cmd := exec.Command("git", "init", mainRepoPath)
|
cmd := exec.Command("git", "init", mainRepoPath)
|
||||||
assert.NoError(t, cmd.Run())
|
assert.NoError(t, cmd.Run())
|
||||||
|
|
||||||
// Configure git for the test
|
|
||||||
cmd = exec.Command("git", "-C", mainRepoPath, "config", "user.email", "test@example.com")
|
cmd = exec.Command("git", "-C", mainRepoPath, "config", "user.email", "test@example.com")
|
||||||
assert.NoError(t, cmd.Run())
|
assert.NoError(t, cmd.Run())
|
||||||
cmd = exec.Command("git", "-C", mainRepoPath, "config", "user.name", "Test User")
|
cmd = exec.Command("git", "-C", mainRepoPath, "config", "user.name", "Test User")
|
||||||
assert.NoError(t, cmd.Run())
|
assert.NoError(t, cmd.Run())
|
||||||
|
|
||||||
// Add a remote to the main repository
|
|
||||||
cmd = exec.Command("git", "-C", mainRepoPath, "remote", "add", "origin", "https://gitea.com/owner/repo.git")
|
cmd = exec.Command("git", "-C", mainRepoPath, "remote", "add", "origin", "https://gitea.com/owner/repo.git")
|
||||||
assert.NoError(t, cmd.Run())
|
assert.NoError(t, cmd.Run())
|
||||||
|
|
||||||
// Create an initial commit (required for worktree)
|
|
||||||
readmePath := filepath.Join(mainRepoPath, "README.md")
|
readmePath := filepath.Join(mainRepoPath, "README.md")
|
||||||
err = os.WriteFile(readmePath, []byte("# Test Repo\n"), 0o644)
|
err = os.WriteFile(readmePath, []byte("# Test Repo\n"), 0o644)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -44,19 +40,14 @@ func TestRepoFromPath_Worktree(t *testing.T) {
|
|||||||
cmd = exec.Command("git", "-C", mainRepoPath, "commit", "-m", "Initial commit")
|
cmd = exec.Command("git", "-C", mainRepoPath, "commit", "-m", "Initial commit")
|
||||||
assert.NoError(t, cmd.Run())
|
assert.NoError(t, cmd.Run())
|
||||||
|
|
||||||
// Create a worktree
|
|
||||||
cmd = exec.Command("git", "-C", mainRepoPath, "worktree", "add", worktreePath, "-b", "test-branch")
|
cmd = exec.Command("git", "-C", mainRepoPath, "worktree", "add", worktreePath, "-b", "test-branch")
|
||||||
assert.NoError(t, cmd.Run())
|
assert.NoError(t, cmd.Run())
|
||||||
|
|
||||||
// Test: Open repository from worktree path
|
repo, err := teagit.RepoFromPath(worktreePath)
|
||||||
repo, err := RepoFromPath(worktreePath)
|
|
||||||
assert.NoError(t, err, "Should be able to open worktree")
|
assert.NoError(t, err, "Should be able to open worktree")
|
||||||
|
|
||||||
// Test: Read config from worktree (should read from main repo's config)
|
|
||||||
config, err := repo.Config()
|
config, err := repo.Config()
|
||||||
assert.NoError(t, err, "Should be able to read config")
|
assert.NoError(t, err, "Should be able to read config")
|
||||||
|
|
||||||
// Verify that remotes are accessible from worktree
|
|
||||||
assert.NotEmpty(t, config.Remotes, "Should be able to read remotes from worktree")
|
assert.NotEmpty(t, config.Remotes, "Should be able to read remotes from worktree")
|
||||||
assert.Contains(t, config.Remotes, "origin", "Should have origin remote")
|
assert.Contains(t, config.Remotes, "origin", "Should have origin remote")
|
||||||
assert.Equal(t, "https://gitea.com/owner/repo.git", config.Remotes["origin"].URLs[0], "Should have correct remote URL")
|
assert.Equal(t, "https://gitea.com/owner/repo.git", config.Remotes["origin"].URLs[0], "Should have correct remote URL")
|
||||||
@@ -1,28 +1,66 @@
|
|||||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package repos
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"code.gitea.io/tea/cmd/repos"
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
"code.gitea.io/tea/modules/task"
|
"code.gitea.io/tea/modules/task"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func useTempConfigPath(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
configPath := filepath.Join(t.TempDir(), "config.yml")
|
||||||
|
config.SetConfigPathForTesting(configPath)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
config.SetConfigPathForTesting("")
|
||||||
|
})
|
||||||
|
|
||||||
|
return configPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func createIntegrationLogin(t *testing.T, giteaURL string) *config.Login {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
_ = useTempConfigPath(t)
|
||||||
|
|
||||||
|
username := os.Getenv("GITEA_TEA_TEST_USERNAME")
|
||||||
|
password := os.Getenv("GITEA_TEA_TEST_PASSWORD")
|
||||||
|
require.NotEmpty(t, username, "GITEA_TEA_TEST_USERNAME is required for integration tests")
|
||||||
|
require.NotEmpty(t, password, "GITEA_TEA_TEST_PASSWORD is required for integration tests")
|
||||||
|
|
||||||
|
require.NoError(t, task.CreateLogin("integration", "", username, password, "", "", "", giteaURL, "", "", true, false, false, false))
|
||||||
|
|
||||||
|
login, err := config.GetLoginByName("integration")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, login)
|
||||||
|
|
||||||
|
return login
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateRepoObjectFormat(t *testing.T) {
|
func TestCreateRepoObjectFormat(t *testing.T) {
|
||||||
giteaURL := os.Getenv("GITEA_TEA_TEST_URL")
|
giteaURL := os.Getenv("GITEA_TEA_TEST_URL")
|
||||||
if giteaURL == "" {
|
if giteaURL == "" {
|
||||||
t.Skip("GITEA_TEA_TEST_URL is not set, skipping test")
|
t.Skip("GITEA_TEA_TEST_URL is not set, skipping test")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
login := createIntegrationLogin(t, giteaURL)
|
||||||
|
client := login.Client()
|
||||||
timestamp := time.Now().Unix()
|
timestamp := time.Now().Unix()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
args []string
|
args []string
|
||||||
@@ -56,22 +94,15 @@ func TestCreateRepoObjectFormat(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
giteaUserName := os.Getenv("GITEA_TEA_TEST_USERNAME")
|
|
||||||
giteaUserPasword := os.Getenv("GITEA_TEA_TEST_PASSWORD")
|
|
||||||
|
|
||||||
err := task.CreateLogin("test", "", giteaUserName, giteaUserPasword, "", "", "", giteaURL, "", "", true, false, false, false)
|
|
||||||
if err != nil && err.Error() != "login name 'test' has already been used" {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
reposCmd := &cli.Command{
|
reposCmd := &cli.Command{
|
||||||
Name: "repos",
|
Name: "repos",
|
||||||
Commands: []*cli.Command{&CmdRepoCreate},
|
Commands: []*cli.Command{&repos.CmdRepoCreate},
|
||||||
}
|
}
|
||||||
tt.args = append(tt.args, "--login", "test")
|
|
||||||
args := append([]string{"repos", "create"}, tt.args...)
|
args := append([]string{"repos", "create"}, tt.args...)
|
||||||
|
args = append(args, "--login", login.Name)
|
||||||
|
|
||||||
err := reposCmd.Run(context.Background(), args)
|
err := reposCmd.Run(context.Background(), args)
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
@@ -82,7 +113,12 @@ func TestCreateRepoObjectFormat(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if _, delErr := client.DeleteRepo(login.User, tt.wantOpts.Name); delErr != nil {
|
||||||
|
t.Logf("failed to delete integration test repo %q: %v", tt.wantOpts.Name, delErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user