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
|
||||
# with:
|
||||
# go-version-file: 'go.mod'
|
||||
check-and-test:
|
||||
check-and-unit:
|
||||
name: Lint Build And Unit Coverage
|
||||
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
|
||||
@@ -32,14 +28,30 @@ jobs:
|
||||
make fmt-check
|
||||
make docs-check
|
||||
make build
|
||||
- run: curl --noproxy "*" http://gitea:3000/api/v1/version # verify connection to instance
|
||||
- name: test and coverage
|
||||
- name: unit test and coverage
|
||||
run: |
|
||||
make test
|
||||
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:
|
||||
gitea:
|
||||
image: docker.gitea.com/gitea:1.26.0
|
||||
image: docker.gitea.com/gitea:1.26.1
|
||||
cmd:
|
||||
- bash
|
||||
- -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 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)
|
||||
|
||||
# OS specific vars.
|
||||
@@ -64,11 +67,11 @@ vet:
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
$(GO) run $(GOLANGCI_LINT_PACKAGE) run
|
||||
$(GO) run $(GOLANGCI_LINT_PACKAGE) run --build-tags testtools
|
||||
|
||||
.PHONY: lint-fix
|
||||
lint-fix:
|
||||
$(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix
|
||||
$(GO) run $(GOLANGCI_LINT_PACKAGE) run --build-tags testtools --fix
|
||||
|
||||
.PHONY: fmt-check
|
||||
fmt-check:
|
||||
@@ -93,13 +96,24 @@ docs-check:
|
||||
exit 1; \
|
||||
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
|
||||
test:
|
||||
$(GO) test -tags='sqlite sqlite_unlock_notify' $(PACKAGES)
|
||||
test: unit-test integration-test
|
||||
|
||||
.PHONY: 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
|
||||
tidy:
|
||||
@@ -122,4 +136,3 @@ $(EXECUTABLE): $(SOURCES)
|
||||
.PHONY: build-image
|
||||
build-image:
|
||||
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
|
||||
}
|
||||
|
||||
existing, _, err := client.ListReleaseAttachments(ctx.Owner, ctx.Repo, release.ID, gitea.ListReleaseAttachmentsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: -1},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
var existing []*gitea.Attachment
|
||||
for page := 1; ; {
|
||||
page_attachments, resp, err := client.ListReleaseAttachments(ctx.Owner, ctx.Repo, release.ID, gitea.ListReleaseAttachmentsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
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:] {
|
||||
|
||||
@@ -62,6 +62,7 @@ var CmdIssues = cli.Command{
|
||||
&issues.CmdIssuesEdit,
|
||||
&issues.CmdIssuesReopen,
|
||||
&issues.CmdIssuesClose,
|
||||
&issues.CmdIssuesDependencies,
|
||||
},
|
||||
Flags: append([]cli.Flag{
|
||||
&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 (
|
||||
stdctx "context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
@@ -77,7 +78,7 @@ func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error {
|
||||
Type: kind,
|
||||
KeyWord: ctx.String("keyword"),
|
||||
CreatedBy: ctx.String("author"),
|
||||
AssignedBy: ctx.String("assigned-to"),
|
||||
AssignedBy: ctx.String("assignee"),
|
||||
MentionedBy: ctx.String("mentions"),
|
||||
Labels: labels,
|
||||
Milestones: milestones,
|
||||
@@ -88,13 +89,15 @@ func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
} 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{
|
||||
ListOptions: flags.GetListOptions(cmd),
|
||||
State: state,
|
||||
Type: kind,
|
||||
KeyWord: ctx.String("keyword"),
|
||||
CreatedBy: ctx.String("author"),
|
||||
AssignedBy: ctx.String("assigned-to"),
|
||||
MentionedBy: ctx.String("mentions"),
|
||||
Labels: labels,
|
||||
Milestones: milestones,
|
||||
|
||||
@@ -29,7 +29,9 @@ var CmdGenerateManPage = cli.Command{
|
||||
Hidden: true,
|
||||
Flags: DocRenderFlags,
|
||||
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
|
||||
}
|
||||
|
||||
reviews, _, err := client.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: -1},
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("error while loading reviews: %v\n", err)
|
||||
var reviews []*gitea.PullReview
|
||||
for page := 1; ; {
|
||||
page_reviews, resp, err := client.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
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") {
|
||||
|
||||
@@ -11,19 +11,25 @@ import (
|
||||
|
||||
// GetReleaseByTag finds a release by its tag name.
|
||||
func GetReleaseByTag(owner, repo, tag string, client *gitea.Client) (*gitea.Release, error) {
|
||||
rl, _, err := client.ListReleases(owner, repo, gitea.ListReleasesOptions{
|
||||
ListOptions: gitea.ListOptions{Page: -1},
|
||||
})
|
||||
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
|
||||
for 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 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")
|
||||
}
|
||||
|
||||
@@ -89,30 +89,26 @@ func runWebhooksCreate(ctx stdctx.Context, cmd *cli.Command) error {
|
||||
config["secret"] = secret
|
||||
}
|
||||
|
||||
if branchFilter != "" {
|
||||
config["branch_filter"] = branchFilter
|
||||
}
|
||||
|
||||
if authHeader != "" {
|
||||
config["authorization_header"] = authHeader
|
||||
}
|
||||
|
||||
var hook *gitea.Hook
|
||||
if c.IsGlobal {
|
||||
return fmt.Errorf("global webhooks not yet supported in this version")
|
||||
} else if len(c.Org) > 0 {
|
||||
hook, _, err = client.CreateOrgHook(c.Org, gitea.CreateHookOption{
|
||||
Type: webhookType,
|
||||
Config: config,
|
||||
Events: events,
|
||||
Active: active,
|
||||
Type: webhookType,
|
||||
Config: config,
|
||||
Events: events,
|
||||
Active: active,
|
||||
BranchFilter: branchFilter,
|
||||
AuthorizationHeader: authHeader,
|
||||
})
|
||||
} else {
|
||||
hook, _, err = client.CreateRepoHook(c.Owner, c.Repo, gitea.CreateHookOption{
|
||||
Type: webhookType,
|
||||
Config: config,
|
||||
Events: events,
|
||||
Active: active,
|
||||
Type: webhookType,
|
||||
Config: config,
|
||||
Events: events,
|
||||
Active: active,
|
||||
BranchFilter: branchFilter,
|
||||
AuthorizationHeader: authHeader,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -79,8 +79,6 @@ func TestWebhookConfigConstruction(t *testing.T) {
|
||||
name string
|
||||
url string
|
||||
secret string
|
||||
branchFilter string
|
||||
authHeader string
|
||||
expectedKeys []string
|
||||
expectedValues map[string]string
|
||||
}{
|
||||
@@ -106,44 +104,16 @@ func TestWebhookConfigConstruction(t *testing.T) {
|
||||
"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",
|
||||
url: "https://example.com/webhook",
|
||||
secret: "secret123",
|
||||
branchFilter: "main",
|
||||
authHeader: "X-Token: abc",
|
||||
expectedKeys: []string{"url", "http_method", "content_type", "secret", "branch_filter", "authorization_header"},
|
||||
expectedKeys: []string{"url", "http_method", "content_type", "secret"},
|
||||
expectedValues: map[string]string{
|
||||
"url": "https://example.com/webhook",
|
||||
"http_method": "post",
|
||||
"content_type": "json",
|
||||
"secret": "secret123",
|
||||
"branch_filter": "main",
|
||||
"authorization_header": "X-Token: abc",
|
||||
"url": "https://example.com/webhook",
|
||||
"http_method": "post",
|
||||
"content_type": "json",
|
||||
"secret": "secret123",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -159,12 +129,6 @@ func TestWebhookConfigConstruction(t *testing.T) {
|
||||
if 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
|
||||
for _, key := range tt.expectedKeys {
|
||||
@@ -184,11 +148,13 @@ func TestWebhookConfigConstruction(t *testing.T) {
|
||||
|
||||
func TestWebhookCreateOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
webhookType string
|
||||
events []string
|
||||
active bool
|
||||
config map[string]string
|
||||
name string
|
||||
webhookType string
|
||||
events []string
|
||||
active bool
|
||||
config map[string]string
|
||||
branchFilter string
|
||||
authHeader string
|
||||
}{
|
||||
{
|
||||
name: "Gitea webhook",
|
||||
@@ -200,6 +166,8 @@ func TestWebhookCreateOptions(t *testing.T) {
|
||||
"http_method": "post",
|
||||
"content_type": "json",
|
||||
},
|
||||
branchFilter: "main",
|
||||
authHeader: "X-Token: abc",
|
||||
},
|
||||
{
|
||||
name: "Slack webhook",
|
||||
@@ -228,16 +196,20 @@ func TestWebhookCreateOptions(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
option := gitea.CreateHookOption{
|
||||
Type: gitea.HookType(tt.webhookType),
|
||||
Config: tt.config,
|
||||
Events: tt.events,
|
||||
Active: tt.active,
|
||||
Type: gitea.HookType(tt.webhookType),
|
||||
Config: tt.config,
|
||||
Events: tt.events,
|
||||
Active: tt.active,
|
||||
BranchFilter: tt.branchFilter,
|
||||
AuthorizationHeader: tt.authHeader,
|
||||
}
|
||||
|
||||
assert.Equal(t, gitea.HookType(tt.webhookType), option.Type)
|
||||
assert.Equal(t, tt.events, option.Events)
|
||||
assert.Equal(t, tt.active, option.Active)
|
||||
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") {
|
||||
config["secret"] = cmd.String("secret")
|
||||
}
|
||||
branchFilter := hook.BranchFilter
|
||||
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") {
|
||||
config["authorization_header"] = cmd.String("authorization-header")
|
||||
authHeader = cmd.String("authorization-header")
|
||||
}
|
||||
|
||||
// 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")
|
||||
} else if len(c.Org) > 0 {
|
||||
_, err = client.EditOrgHook(c.Org, int64(webhookID), gitea.EditHookOption{
|
||||
Config: config,
|
||||
Events: events,
|
||||
Active: &active,
|
||||
Config: config,
|
||||
Events: events,
|
||||
Active: &active,
|
||||
BranchFilter: branchFilter,
|
||||
AuthorizationHeader: authHeader,
|
||||
})
|
||||
} else {
|
||||
_, err = client.EditRepoHook(c.Owner, c.Repo, int64(webhookID), gitea.EditHookOption{
|
||||
Config: config,
|
||||
Events: events,
|
||||
Active: &active,
|
||||
Config: config,
|
||||
Events: events,
|
||||
Active: &active,
|
||||
BranchFilter: branchFilter,
|
||||
AuthorizationHeader: authHeader,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -128,12 +128,10 @@ func TestUpdateActiveInactiveFlags(t *testing.T) {
|
||||
func TestUpdateConfigPreservation(t *testing.T) {
|
||||
// Test that existing configuration is preserved when not updated
|
||||
originalConfig := map[string]string{
|
||||
"url": "https://old.example.com/webhook",
|
||||
"secret": "old-secret",
|
||||
"branch_filter": "main",
|
||||
"authorization_header": "Bearer old-token",
|
||||
"http_method": "post",
|
||||
"content_type": "json",
|
||||
"url": "https://old.example.com/webhook",
|
||||
"secret": "old-secret",
|
||||
"http_method": "post",
|
||||
"content_type": "json",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
@@ -147,53 +145,32 @@ func TestUpdateConfigPreservation(t *testing.T) {
|
||||
"url": "https://new.example.com/webhook",
|
||||
},
|
||||
expectedConfig: map[string]string{
|
||||
"url": "https://new.example.com/webhook",
|
||||
"secret": "old-secret",
|
||||
"branch_filter": "main",
|
||||
"authorization_header": "Bearer old-token",
|
||||
"http_method": "post",
|
||||
"content_type": "json",
|
||||
"url": "https://new.example.com/webhook",
|
||||
"secret": "old-secret",
|
||||
"http_method": "post",
|
||||
"content_type": "json",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update secret and auth header",
|
||||
name: "Update secret",
|
||||
updates: map[string]string{
|
||||
"secret": "new-secret",
|
||||
"authorization_header": "X-Token: new-token",
|
||||
"secret": "new-secret",
|
||||
},
|
||||
expectedConfig: map[string]string{
|
||||
"url": "https://old.example.com/webhook",
|
||||
"secret": "new-secret",
|
||||
"branch_filter": "main",
|
||||
"authorization_header": "X-Token: new-token",
|
||||
"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",
|
||||
"url": "https://old.example.com/webhook",
|
||||
"secret": "new-secret",
|
||||
"http_method": "post",
|
||||
"content_type": "json",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "No updates",
|
||||
updates: map[string]string{},
|
||||
expectedConfig: map[string]string{
|
||||
"url": "https://old.example.com/webhook",
|
||||
"secret": "old-secret",
|
||||
"branch_filter": "main",
|
||||
"authorization_header": "Bearer old-token",
|
||||
"http_method": "post",
|
||||
"content_type": "json",
|
||||
"url": "https://old.example.com/webhook",
|
||||
"secret": "old-secret",
|
||||
"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) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
2
go.mod
2
go.mod
@@ -12,7 +12,7 @@ require (
|
||||
github.com/adrg/xdg v0.5.3
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
||||
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/muesli/termenv v0.16.0
|
||||
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/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.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/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||
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
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/tea/modules/config"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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.LabelList = make([]string, len(labels))
|
||||
for i, l := range labels {
|
||||
r.LabelMap[l.Name] = l.ID
|
||||
r.LabelList[i] = l.Name
|
||||
r.LabelList = make([]string, 0)
|
||||
for page := 1; ; {
|
||||
labels, resp, err := c.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{
|
||||
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
|
||||
|
||||
@@ -67,13 +67,21 @@ func WebhookDetails(hook *gitea.Hook) {
|
||||
if method, ok := hook.Config["http_method"]; ok {
|
||||
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)
|
||||
}
|
||||
if _, hasSecret := hook.Config["secret"]; hasSecret {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,17 +81,17 @@ func TestWebhookDetails(t *testing.T) {
|
||||
ID: 123,
|
||||
Type: "gitea",
|
||||
Config: map[string]string{
|
||||
"url": "https://example.com/webhook",
|
||||
"content_type": "json",
|
||||
"http_method": "post",
|
||||
"branch_filter": "main,develop",
|
||||
"secret": "secret-value",
|
||||
"authorization_header": "Bearer token123",
|
||||
"url": "https://example.com/webhook",
|
||||
"content_type": "json",
|
||||
"http_method": "post",
|
||||
"secret": "secret-value",
|
||||
},
|
||||
Events: []string{"push", "pull_request", "issues"},
|
||||
Active: true,
|
||||
Created: now.Add(-24 * time.Hour),
|
||||
Updated: now,
|
||||
BranchFilter: "main,develop",
|
||||
AuthorizationHeader: "Bearer token123",
|
||||
Events: []string{"push", "pull_request", "issues"},
|
||||
Active: true,
|
||||
Created: now.Add(-24 * time.Hour),
|
||||
Updated: now,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -238,16 +238,14 @@ func TestWebhookConfigHandling(t *testing.T) {
|
||||
{
|
||||
name: "Config with all fields",
|
||||
config: map[string]string{
|
||||
"url": "https://example.com/webhook",
|
||||
"secret": "my-secret",
|
||||
"authorization_header": "Bearer token",
|
||||
"content_type": "json",
|
||||
"http_method": "post",
|
||||
"branch_filter": "main",
|
||||
"url": "https://example.com/webhook",
|
||||
"secret": "my-secret",
|
||||
"content_type": "json",
|
||||
"http_method": "post",
|
||||
},
|
||||
expectedURL: "https://example.com/webhook",
|
||||
hasSecret: true,
|
||||
hasAuthHeader: true,
|
||||
hasAuthHeader: false,
|
||||
},
|
||||
{
|
||||
name: "Config with minimal fields",
|
||||
@@ -341,17 +339,17 @@ func TestWebhookDetailsFormatting(t *testing.T) {
|
||||
ID: 123,
|
||||
Type: "gitea",
|
||||
Config: map[string]string{
|
||||
"url": "https://example.com/webhook",
|
||||
"content_type": "json",
|
||||
"http_method": "post",
|
||||
"branch_filter": "main,develop",
|
||||
"secret": "secret-value",
|
||||
"authorization_header": "Bearer token123",
|
||||
"url": "https://example.com/webhook",
|
||||
"content_type": "json",
|
||||
"http_method": "post",
|
||||
"secret": "secret-value",
|
||||
},
|
||||
Events: []string{"push", "pull_request", "issues"},
|
||||
Active: true,
|
||||
Created: now.Add(-24 * time.Hour),
|
||||
Updated: now,
|
||||
BranchFilter: "main,develop",
|
||||
AuthorizationHeader: "Bearer token123",
|
||||
Events: []string{"push", "pull_request", "issues"},
|
||||
Active: true,
|
||||
Created: now.Add(-24 * time.Hour),
|
||||
Updated: now,
|
||||
}
|
||||
|
||||
// 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, "json", hook.Config["content_type"])
|
||||
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, "authorization_header")
|
||||
assert.Equal(t, "Bearer token123", hook.AuthorizationHeader)
|
||||
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
|
||||
func ResolveLabelNames(client *gitea.Client, owner, repo string, labelNames []string) ([]int64, error) {
|
||||
labelIDs := make([]int64, 0, len(labelNames))
|
||||
labels, _, err := client.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: -1},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, l := range labels {
|
||||
if utils.Contains(labelNames, l.Name) {
|
||||
labelIDs = append(labelIDs, l.ID)
|
||||
page := 1
|
||||
for {
|
||||
labels, resp, err := client.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -166,11 +166,19 @@ func generateToken(login config.Login, user, pass, otp, scopes string) (string,
|
||||
}
|
||||
client := login.Client(opts...)
|
||||
|
||||
tl, _, err := client.ListAccessTokens(gitea.ListAccessTokensOptions{
|
||||
ListOptions: gitea.ListOptions{Page: -1},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
var tl []*gitea.AccessToken
|
||||
for page := 1; ; {
|
||||
page_tokens, resp, err := client.ListAccessTokens(gitea.ListAccessTokensOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tl = append(tl, page_tokens...)
|
||||
if resp == nil || resp.NextPage == 0 {
|
||||
break
|
||||
}
|
||||
page = resp.NextPage
|
||||
}
|
||||
host, _ := os.Hostname()
|
||||
tokenName := host + "-tea"
|
||||
|
||||
@@ -19,11 +19,22 @@ import (
|
||||
// a matching private key in ~/.ssh/. If no match is found, path is empty.
|
||||
func findSSHKey(client *gitea.Client) (string, error) {
|
||||
// get keys registered on gitea instance
|
||||
keys, _, err := client.ListMyPublicKeys(gitea.ListPublicKeysOptions{
|
||||
ListOptions: gitea.ListOptions{Page: -1},
|
||||
})
|
||||
if err != nil || len(keys) == 0 {
|
||||
return "", err
|
||||
var keys []*gitea.PublicKey
|
||||
for page := 1; ; {
|
||||
page_keys, resp, err := client.ListMyPublicKeys(gitea.ListPublicKeysOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
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
|
||||
|
||||
@@ -14,11 +14,19 @@ import (
|
||||
func ListPullReviewComments(ctx *context.TeaContext, idx int64) ([]*gitea.PullReviewComment, error) {
|
||||
c := ctx.Login.Client()
|
||||
|
||||
reviews, _, err := c.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: -1},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var reviews []*gitea.PullReview
|
||||
for page := 1; ; {
|
||||
page_reviews, resp, err := c.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
package config
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -12,10 +12,11 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/tea/modules/config"
|
||||
)
|
||||
|
||||
func TestConfigLock_CrossProcess(t *testing.T) {
|
||||
// Create a temp directory for test
|
||||
tmpDir, err := os.MkdirTemp("", "tea-lock-test")
|
||||
if err != nil {
|
||||
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")
|
||||
|
||||
// Acquire lock in main process
|
||||
unlock, err := acquireConfigLock(lockPath, 5*time.Second)
|
||||
unlock, err := config.AcquireConfigLockForTesting(lockPath, 5*time.Second)
|
||||
if err != nil {
|
||||
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(`
|
||||
package main
|
||||
|
||||
@@ -48,19 +50,16 @@ func main() {
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Try non-blocking lock
|
||||
err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
|
||||
if err != nil {
|
||||
// Lock is held - expected behavior
|
||||
os.Exit(0)
|
||||
}
|
||||
// Lock was acquired - unexpected
|
||||
|
||||
syscall.Flock(int(file.Fd()), syscall.LOCK_UN)
|
||||
os.Exit(1)
|
||||
}
|
||||
`, lockPath)
|
||||
|
||||
// Write and run the test script
|
||||
scriptPath := filepath.Join(tmpDir, "locktest.go")
|
||||
if err := os.WriteFile(scriptPath, []byte(script), 0o600); err != nil {
|
||||
t.Fatalf("failed to write test script: %v", err)
|
||||
@@ -78,5 +77,4 @@ func main() {
|
||||
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.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
teagit "code.gitea.io/tea/modules/git"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRepoFromPath_Worktree(t *testing.T) {
|
||||
// Create a temporary directory for test
|
||||
tmpDir, err := os.MkdirTemp("", "tea-worktree-test-*")
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(tmpDir)
|
||||
@@ -21,21 +21,17 @@ func TestRepoFromPath_Worktree(t *testing.T) {
|
||||
mainRepoPath := filepath.Join(tmpDir, "main-repo")
|
||||
worktreePath := filepath.Join(tmpDir, "worktree")
|
||||
|
||||
// Initialize main repository
|
||||
cmd := exec.Command("git", "init", mainRepoPath)
|
||||
assert.NoError(t, cmd.Run())
|
||||
|
||||
// Configure git for the test
|
||||
cmd = exec.Command("git", "-C", mainRepoPath, "config", "user.email", "test@example.com")
|
||||
assert.NoError(t, cmd.Run())
|
||||
cmd = exec.Command("git", "-C", mainRepoPath, "config", "user.name", "Test User")
|
||||
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")
|
||||
assert.NoError(t, cmd.Run())
|
||||
|
||||
// Create an initial commit (required for worktree)
|
||||
readmePath := filepath.Join(mainRepoPath, "README.md")
|
||||
err = os.WriteFile(readmePath, []byte("# Test Repo\n"), 0o644)
|
||||
assert.NoError(t, err)
|
||||
@@ -44,19 +40,14 @@ func TestRepoFromPath_Worktree(t *testing.T) {
|
||||
cmd = exec.Command("git", "-C", mainRepoPath, "commit", "-m", "Initial commit")
|
||||
assert.NoError(t, cmd.Run())
|
||||
|
||||
// Create a worktree
|
||||
cmd = exec.Command("git", "-C", mainRepoPath, "worktree", "add", worktreePath, "-b", "test-branch")
|
||||
assert.NoError(t, cmd.Run())
|
||||
|
||||
// Test: Open repository from worktree path
|
||||
repo, err := RepoFromPath(worktreePath)
|
||||
repo, err := teagit.RepoFromPath(worktreePath)
|
||||
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()
|
||||
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.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")
|
||||
@@ -1,28 +1,66 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repos
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/cmd/repos"
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"code.gitea.io/tea/modules/task"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"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) {
|
||||
giteaURL := os.Getenv("GITEA_TEA_TEST_URL")
|
||||
if giteaURL == "" {
|
||||
t.Skip("GITEA_TEA_TEST_URL is not set, skipping test")
|
||||
}
|
||||
|
||||
login := createIntegrationLogin(t, giteaURL)
|
||||
client := login.Client()
|
||||
timestamp := time.Now().Unix()
|
||||
|
||||
tests := []struct {
|
||||
name 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 {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reposCmd := &cli.Command{
|
||||
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(args, "--login", login.Name)
|
||||
|
||||
err := reposCmd.Run(context.Background(), args)
|
||||
if tt.wantErr {
|
||||
@@ -82,7 +113,12 @@ func TestCreateRepoObjectFormat(t *testing.T) {
|
||||
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