10 Commits

Author SHA1 Message Date
d3e43d7623 feat(issues): add dependency management commands
Some checks failed
check-and-test / Integration Test (pull_request) Failing after 1m7s
check-and-test / Lint Build And Unit Coverage (pull_request) Failing after 3m19s
2026-05-02 16:00:01 +02:00
Lunny Xiao
83b718ac34 Move integration tests to tests/ directory (#973)
Reviewed-on: https://gitea.com/gitea/tea/pulls/973
2026-05-02 04:18:45 +00:00
Renovate Bot
1f6fd97fc1 fix(deps): update module github.com/go-authgate/sdk-go to v0.9.0 (#974)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [github.com/go-authgate/sdk-go](https://github.com/go-authgate/sdk-go) | `v0.8.0` → `v0.9.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fgo-authgate%2fsdk-go/v0.9.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fgo-authgate%2fsdk-go/v0.8.0/v0.9.0?slim=true) |

---

### Release Notes

<details>
<summary>go-authgate/sdk-go (github.com/go-authgate/sdk-go)</summary>

### [`v0.9.0`](https://github.com/go-authgate/sdk-go/releases/tag/v0.9.0)

[Compare Source](https://github.com/go-authgate/sdk-go/compare/v0.8.0...v0.9.0)

#### Changelog

##### Documentation updates

- [`86d33f3`](86d33f315c): docs(jwksauth): tighten readme table column widths ([@&#8203;appleboy](https://github.com/appleboy))

##### Others

- [`545d96f`](545d96fd43): refactor(jwksauth)!: rename Tenant to Domain and add Tenant sub-claim ([#&#8203;25](https://github.com/go-authgate/sdk-go/issues/25)) ([@&#8203;appleboy](https://github.com/appleboy))
- [`1e73580`](1e73580c87): feat(jwksauth)!: adopt slog-style Logger interface ([#&#8203;24](https://github.com/go-authgate/sdk-go/issues/24)) ([@&#8203;appleboy](https://github.com/appleboy))
- [`7af1bc4`](7af1bc4637): test(jwksauth): fix stale Tenant references in policy reject test ([#&#8203;26](https://github.com/go-authgate/sdk-go/issues/26)) ([@&#8203;appleboy](https://github.com/appleboy))

</details>

---

Reviewed-on: https://gitea.com/gitea/tea/pulls/974
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-05-02 02:04:23 +00:00
Renovate Bot
27e6083e23 fix(deps): update module github.com/go-authgate/sdk-go to v0.8.0 (#972)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [github.com/go-authgate/sdk-go](https://github.com/go-authgate/sdk-go) | `v0.7.0` → `v0.8.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fgo-authgate%2fsdk-go/v0.8.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fgo-authgate%2fsdk-go/v0.7.0/v0.8.0?slim=true) |

---

### Release Notes

<details>
<summary>go-authgate/sdk-go (github.com/go-authgate/sdk-go)</summary>

### [`v0.8.0`](https://github.com/go-authgate/sdk-go/releases/tag/v0.8.0)

[Compare Source](https://github.com/go-authgate/sdk-go/compare/v0.7.0...v0.8.0)

#### Changelog

##### Refactor

- [`62ccff0`](62ccff06c8): refactor(jwksauth): share OIDC discovery and drop tenant cache ([#&#8203;23](https://github.com/go-authgate/sdk-go/issues/23)) ([@&#8203;appleboy](https://github.com/appleboy))
- [`088ee3b`](088ee3bd2d): refactor(sdk): harden HTTP reads and improve code quality ([#&#8203;18](https://github.com/go-authgate/sdk-go/issues/18)) ([@&#8203;appleboy](https://github.com/appleboy))
- [`aa17bc2`](aa17bc2373): refactor: simplify oauth client and token source flows ([#&#8203;22](https://github.com/go-authgate/sdk-go/issues/22)) ([@&#8203;appleboy](https://github.com/appleboy))

</details>

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/972
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-05-01 23:16:49 +00:00
Lunny Xiao
5d2d1a6e0c fix(webhook): Fix when creating webhook, branch filter and auth header cannot be added (#964)
Fix #963

Reviewed-on: https://gitea.com/gitea/tea/pulls/964
2026-05-01 16:45:52 +00:00
Oleksii Zaremskyi
88421bb888 fix: read --assignee flag value instead of nonexistent --assigned-to (#971)
## What this PR does

`tea issues list --assignee USERNAME` currently returns every issue regardless of the assignee value — even nonexistent users return a full unfiltered list. Discovered against **tea v0.14.0** (with go-sdk v0.24.1) and reproduced on current `master` (commit `dd81b33`). This PR fixes that.

## Root cause

Two distinct bugs on the same flag, both in `cmd/issues/list.go`:

1. **Per-repo path** (`tea issues list --repo OWNER/REPO --assignee USER`): the code reads `ctx.String("assigned-to")` for `AssignedBy`, but the flag is defined as `--assignee` in `cmd/flags/issue_pr.go:66`. The lookup always returns `""`, so the SDK omits the `assigned_by` query parameter and the API returns everything.

2. **Global path** (`tea issues list --assignee USER`, no `--repo`): this hits `/repos/issues/search`, which silently ignores `assigned_by`. Even after fix #1 the no-repo form would still return unfiltered results. Verified directly:
   - `GET /repos/issues/search?assigned_by=USER&owner=ORG&state=open` → all open issues
   - `GET /repos/issues/search?assigned=true&owner=ORG&state=open` → only the issues assigned to the authenticated user

   The endpoint only supports `assigned=true` (boolean self-filter), not arbitrary-user filtering, and `ListIssueOption` doesn't expose that field. Rather than misleading the caller, the no-repo path now returns a clear error.

## Changes

Both changes are in `cmd/issues/list.go`:

1. Read `ctx.String("assignee")` instead of the non-existent flag name `"assigned-to"` (lines 80 and 97).
2. In the no-`--repo` branch, return `errors.New("--assignee requires --repo (...)")` when the flag is set.

`cmd/pulls/list.go` does not expose an assignee filter, so it's unaffected. The `--author` mapping (`CreatedBy ← ctx.String("author")`) was already correct and is the model the fix follows.

## Manual verification

Tested against a local Gitea instance with three open issues (only one assigned to the test user):

| Command | Before | After |
|---|---|---|
| `tea issues list --repo X --assignee me` | all 3 | only the 1 assigned ✓ |
| `tea issues list --repo X --assignee nonexistent` | all 3 | `Error: not found` ✓ |
| `tea issues list --repo X --author me` | only the 1 (control) | unchanged ✓ |
| `tea issues list --assignee me` (no `--repo`) | all 3 (silent) | clear error ✓ |
| `tea issues list` (no flags) | all 3 | unchanged ✓ |

---------

Co-authored-by: claude_1 <claude_1@bot.gqx.lol>
Reviewed-on: https://gitea.com/gitea/tea/pulls/971
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Oleksii Zaremskyi <grossqx@gmail.com>
Co-committed-by: Oleksii Zaremskyi <grossqx@gmail.com>
2026-05-01 16:39:48 +00:00
Wesley Moore
dd81b33cec Fix man page section (#969)
Co-authored-by: Wesley Moore <wes@wezm.net>
Co-committed-by: Wesley Moore <wes@wezm.net>
2026-04-29 15:04:55 +00:00
Renovate Bot
b100d4c939 fix(deps): update module github.com/go-authgate/sdk-go to v0.7.0 (#970)
Reviewed-on: https://gitea.com/gitea/tea/pulls/970
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-04-29 03:28:15 +00:00
Renovate Bot
892905d482 chore(deps): update docker.gitea.com/gitea docker tag to v1.26.1 (#968)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [docker.gitea.com/gitea](https://github.com/go-gitea/gitea) | service | patch | `1.26.0` → `1.26.1` |

---

### Release Notes

<details>
<summary>go-gitea/gitea (docker.gitea.com/gitea)</summary>

### [`v1.26.1`](https://github.com/go-gitea/gitea/releases/tag/v1.26.1)

[Compare Source](https://github.com/go-gitea/gitea/compare/v1.26.0...v1.26.1)

- BUGFIXES   \* Add event.schedule context for schedule actions task ([#&#8203;37320](https://github.com/go-gitea/gitea/issues/37320)) ([#&#8203;37348](https://github.com/go-gitea/gitea/issues/37348))   \* Fix an issue where changing an organization's visibility caused problems when users had forked its repositories. ([#&#8203;37324](https://github.com/go-gitea/gitea/issues/37324)) ([#&#8203;37344](https://github.com/go-gitea/gitea/issues/37344))   \* Use modern "git update-index --cacheinfo" syntax to support more file names ([#&#8203;37338](https://github.com/go-gitea/gitea/issues/37338)) ([#&#8203;37343](https://github.com/go-gitea/gitea/issues/37343))   \* Fix URL related escaping for oauth2 ([#&#8203;37334](https://github.com/go-gitea/gitea/issues/37334)) ([#&#8203;37340](https://github.com/go-gitea/gitea/issues/37340))   \* When the requested arch rpm is missing fall back to noarch ([#&#8203;37236](https://github.com/go-gitea/gitea/issues/37236)) ([#&#8203;37339](https://github.com/go-gitea/gitea/issues/37339))   \* Fix actions concurrency groups cross-branch leak ([#&#8203;37311](https://github.com/go-gitea/gitea/issues/37311)) ([#&#8203;37331](https://github.com/go-gitea/gitea/issues/37331))   \* Fix bug when accessing user badges ([#&#8203;37321](https://github.com/go-gitea/gitea/issues/37321)) ([#&#8203;37329](https://github.com/go-gitea/gitea/issues/37329))   \* Fix AppFullLink ([#&#8203;37325](https://github.com/go-gitea/gitea/issues/37325)) ([#&#8203;37328](https://github.com/go-gitea/gitea/issues/37328))   \* Fix container auth for public instance ([#&#8203;37290](https://github.com/go-gitea/gitea/issues/37290)) ([#&#8203;37294](https://github.com/go-gitea/gitea/issues/37294))   \* Enhance GetActionWorkflow to support fallback references ([#&#8203;37189](https://github.com/go-gitea/gitea/issues/37189)) ([#&#8203;37283](https://github.com/go-gitea/gitea/issues/37283))   \* Fix vite manifest update masking build errors ([#&#8203;37279](https://github.com/go-gitea/gitea/issues/37279)) ([#&#8203;37310](https://github.com/go-gitea/gitea/issues/37310))   \* Fix Mermaid diagrams failing when node labels contain line breaks ([#&#8203;37296](https://github.com/go-gitea/gitea/issues/37296)) ([#&#8203;37299](https://github.com/go-gitea/gitea/issues/37299))   \* Use TriggerEvent instead of Event in workflow runs API response for scheduled runs ([#&#8203;37288](https://github.com/go-gitea/gitea/issues/37288)) [#&#8203;37360](https://github.com/go-gitea/gitea/issues/37360)   \* Add URL to Learn more about blocking a user. ([#&#8203;37355](https://github.com/go-gitea/gitea/issues/37355)) [#&#8203;37367](https://github.com/go-gitea/gitea/issues/37367)   \* Fix button layout shift when collapsing file tree in editor ([#&#8203;37363](https://github.com/go-gitea/gitea/issues/37363)) [#&#8203;37375](https://github.com/go-gitea/gitea/issues/37375)   \* Fix org team assignee/reviewer lookups for team member permissions ([#&#8203;37365](https://github.com/go-gitea/gitea/issues/37365)) [#&#8203;37391](https://github.com/go-gitea/gitea/issues/37391)   \* Fix repo init README EOL ([#&#8203;37388](https://github.com/go-gitea/gitea/issues/37388)) [#&#8203;37399](https://github.com/go-gitea/gitea/issues/37399)   \* Fix: dump with default zip type produces uncompressed zip ([#&#8203;37401](https://github.com/go-gitea/gitea/issues/37401))[#&#8203;37402](https://github.com/go-gitea/gitea/issues/37402)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNDAuMCIsInVwZGF0ZWRJblZlciI6IjQzLjE0MC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/tea/pulls/968
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-04-25 18:12:06 +00:00
Alain Thiffault
5103496232 fix(pagination): replace Page:-1 with explicit pagination loops (#967)
## Summary

\`Page: -1\` in the Gitea SDK calls \`setDefaults()\` which sets both \`Page=0\` and \`PageSize=0\`, resulting in \`?page=0&limit=0\` being sent to the server. The server interprets \`limit=0\` as "use server default" (typically 30 items via \`DEFAULT_PAGING_NUM\`), not "return everything". Any resource beyond the first page of results was silently invisible.

This affected 8 call sites, with the most user-visible impact being \`tea issues edit --add-labels\` and \`tea pulls edit --add-labels\` silently failing to apply labels on repositories with more than ~30 labels.

## Affected call sites

| File | API call | User-visible impact |
|---|---|---|
| \`modules/task/labels.go\` | \`ListRepoLabels\` | \`issues/pulls edit --add-labels\` fails silently |
| \`modules/interact/issue_create.go\` | \`ListRepoLabels\` | interactive label picker missing labels |
| \`modules/task/pull_review_comment.go\` | \`ListPullReviews\` | review comments truncated |
| \`modules/task/login_ssh.go\` | \`ListMyPublicKeys\` | SSH key auto-detection fails |
| \`modules/task/login_create.go\` | \`ListAccessTokens\` | token name deduplication misses existing tokens |
| \`cmd/pulls.go\` | \`ListPullReviews\` | PR detail view missing reviews |
| \`cmd/releases/utils.go\` | \`ListReleases\` | tag lookup fails on repos with many releases |
| \`cmd/attachments/delete.go\` | \`ListReleaseAttachments\` | attachment deletion fails when many attachments exist |

## Fix

Each call site is replaced with an explicit pagination loop that follows \`resp.NextPage\` until all pages are exhausted.

Reviewed-on: https://gitea.com/gitea/tea/pulls/967
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Alain Thiffault <athiffau@effectivemomentum.com>
Co-committed-by: Alain Thiffault <athiffau@effectivemomentum.com>
2026-04-23 17:06:42 +00:00
30 changed files with 813 additions and 310 deletions

View File

@@ -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

View File

@@ -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) .

View File

@@ -61,12 +61,20 @@ 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{
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
}) })
if err != nil { if err != nil {
return err 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:] {
var attachment *gitea.Attachment var attachment *gitea.Attachment

View File

@@ -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
View 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
}

View File

@@ -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,

View File

@@ -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)
})
}, },
} }

View File

@@ -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{
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
}) })
if err != nil { if err != nil {
fmt.Printf("error while loading reviews: %v\n", err) 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") {

View File

@@ -11,13 +11,14 @@ 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 { if err != nil {
return nil, err return nil, err
} }
if len(rl) == 0 { if page == 1 && len(rl) == 0 {
return nil, fmt.Errorf("repo does not have any release") return nil, fmt.Errorf("repo does not have any release")
} }
for _, r := range rl { for _, r := range rl {
@@ -25,5 +26,10 @@ func GetReleaseByTag(owner, repo, tag string, client *gitea.Client) (*gitea.Rele
return r, nil 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")
} }

View File

@@ -89,14 +89,6 @@ 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")
@@ -106,6 +98,8 @@ func runWebhooksCreate(ctx stdctx.Context, cmd *cli.Command) error {
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{
@@ -113,6 +107,8 @@ func runWebhooksCreate(ctx stdctx.Context, cmd *cli.Command) error {
Config: config, Config: config,
Events: events, Events: events,
Active: active, Active: active,
BranchFilter: branchFilter,
AuthorizationHeader: authHeader,
}) })
} }
if err != nil { if err != nil {

View File

@@ -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 {
@@ -189,6 +153,8 @@ func TestWebhookCreateOptions(t *testing.T) {
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",
@@ -232,12 +200,16 @@ func TestWebhookCreateOptions(t *testing.T) {
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)
}) })
} }
} }

View File

@@ -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
@@ -129,12 +132,16 @@ func runWebhooksUpdate(ctx stdctx.Context, cmd *cli.Command) error {
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 {

View File

@@ -130,8 +130,6 @@ func TestUpdateConfigPreservation(t *testing.T) {
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",
"authorization_header": "Bearer old-token",
"http_method": "post", "http_method": "post",
"content_type": "json", "content_type": "json",
} }
@@ -149,37 +147,18 @@ func TestUpdateConfigPreservation(t *testing.T) {
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",
"authorization_header": "Bearer old-token",
"http_method": "post", "http_method": "post",
"content_type": "json", "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",
"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", "http_method": "post",
"content_type": "json", "content_type": "json",
}, },
@@ -190,8 +169,6 @@ func TestUpdateConfigPreservation(t *testing.T) {
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",
"authorization_header": "Bearer old-token",
"http_method": "post", "http_method": "post",
"content_type": "json", "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
View File

@@ -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
View File

@@ -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
View 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"`
}

View 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)
}

View File

@@ -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)
}

View File

@@ -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{ r.LabelMap = make(map[string]int64)
ListOptions: gitea.ListOptions{Page: -1}, 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 { if err != nil {
r.Err = err r.Err = err
done <- r done <- r
return return
} }
r.LabelMap = make(map[string]int64) for _, l := range labels {
r.LabelList = make([]string, len(labels))
for i, l := range labels {
r.LabelMap[l.Name] = l.ID r.LabelMap[l.Name] = l.ID
r.LabelList[i] = l.Name r.LabelList = append(r.LabelList, l.Name)
}
if resp == nil || resp.NextPage == 0 {
break
}
page = resp.NextPage
} }
done <- r done <- r

View File

@@ -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")
} }
} }

View File

@@ -84,10 +84,10 @@ func TestWebhookDetails(t *testing.T) {
"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",
}, },
BranchFilter: "main,develop",
AuthorizationHeader: "Bearer token123",
Events: []string{"push", "pull_request", "issues"}, Events: []string{"push", "pull_request", "issues"},
Active: true, Active: true,
Created: now.Add(-24 * time.Hour), Created: now.Add(-24 * time.Hour),
@@ -240,14 +240,12 @@ func TestWebhookConfigHandling(t *testing.T) {
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",
@@ -344,10 +342,10 @@ func TestWebhookDetailsFormatting(t *testing.T) {
"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",
}, },
BranchFilter: "main,develop",
AuthorizationHeader: "Bearer token123",
Events: []string{"push", "pull_request", "issues"}, Events: []string{"push", "pull_request", "issues"},
Active: true, Active: true,
Created: now.Add(-24 * time.Hour), Created: now.Add(-24 * time.Hour),
@@ -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)
} }

View File

@@ -13,8 +13,10 @@ 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{
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -24,6 +26,11 @@ func ResolveLabelNames(client *gitea.Client, owner, repo string, labelNames []st
labelIDs = append(labelIDs, l.ID) labelIDs = append(labelIDs, l.ID)
} }
} }
if resp == nil || resp.NextPage == 0 {
break
}
page = resp.NextPage
}
return labelIDs, nil return labelIDs, nil
} }

View File

@@ -166,12 +166,20 @@ 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{
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
}) })
if err != nil { if err != nil {
return "", err 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"

View File

@@ -19,12 +19,23 @@ 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{
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
}) })
if err != nil || len(keys) == 0 { if err != nil {
return "", err 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
glob, err := utils.AbsPathWithExpansion("~/.ssh/*.pub") glob, err := utils.AbsPathWithExpansion("~/.ssh/*.pub")

View File

@@ -14,12 +14,20 @@ 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{
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
}) })
if err != nil { if err != nil {
return nil, err 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
for _, review := range reviews { for _, review := range reviews {

10
tests/README.md Normal file
View 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`

View File

@@ -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
} }

View 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)
}

View File

@@ -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")

View File

@@ -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)
}
})
}) })
} }
} }