19 Commits

Author SHA1 Message Date
293fa65131 Merge branch 'update-with-local-changes'
Some checks failed
goreleaser / goreleaser (push) Failing after 22s
goreleaser / release-image (push) Failing after 2m57s
# Conflicts:
#	cmd/actions.go
#	cmd/actions/runs.go
#	cmd/issues/dependencies.go
#	docs/CLI.md
#	modules/api/client.go
2026-05-02 18:59:09 +02:00
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
5c4620d940 fix: validate order of / and # in cross-repo dependency parsing
Some checks failed
goreleaser / release-image (push) Failing after 27m28s
goreleaser / goreleaser (push) Failing after 27m29s
The previous parsing logic for cross-repo dependencies (owner/repo#123)
only checked if both "/" and "#" were present, but didn't verify that
"/" came before "#". This could cause inputs like "#123/owner/repo" to
incorrectly match the cross-repo pattern.

Now explicitly check that slashIdx < hashIdx before treating as cross-repo.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 12:59:29 +00:00
8336a0a8e0 feat(issues): add dependency management commands
Add commands to manage issue dependencies using the Gitea API:
- `tea issues dependencies <index>` - list dependencies
- `tea issues dependencies add <index> <dep>` - add a dependency
- `tea issues dependencies remove <index> <dep>` - remove a dependency

Supports cross-repo dependencies with owner/repo#index syntax.
Supports all output formats (table, json, csv, etc.).

Closes #4

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 12:59:29 +00:00
3651a60024 Merge pull request '[Issue #1] Add Actions logs commands using existing Gitea 1.25 API' (#2) from issue-1-actions-logs-commands into main
Some checks failed
goreleaser / goreleaser (push) Failing after 20s
goreleaser / release-image (push) Failing after 2m21s
2025-12-30 19:14:08 +00:00
Hugo Nijhuis-Mekkelholt
6e88132305 chore: retrigger CI
Some checks failed
check-and-test / Run govulncheck (pull_request) Successful in 36s
check-and-test / check-and-test (pull_request) Failing after 1m21s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 20:07:01 +01:00
Hugo Nijhuis-Mekkelholt
38f93a4997 docs: generate documentation for actions commands
Some checks failed
check-and-test / Run govulncheck (pull_request) Successful in 2m18s
check-and-test / check-and-test (pull_request) Failing after 2m22s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 20:02:57 +01:00
Hugo Nijhuis-Mekkelholt
9fdbd4aafc fix: formatting and error message capitalization
Some checks failed
check-and-test / check-and-test (pull_request) Failing after 1m34s
check-and-test / Run govulncheck (pull_request) Successful in 1m54s
- Run go fmt on modules/api/types.go to fix struct alignment
- Use lowercase error messages to match codebase conventions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 19:57:49 +01:00
Hugo Nijhuis-Mekkelholt
7aa00e6f54 fix: address code review feedback
Some checks failed
check-and-test / check-and-test (pull_request) Failing after 1m38s
check-and-test / Run govulncheck (pull_request) Successful in 1m55s
- Fix GetRaw() error handling to check status before reading body
- Add context.Context support to all HTTP requests
- Use consistent capitalized error messages
- Update copyright year from 2024 to 2025
- Add unit tests for modules/api/client.go
- Add tests for runs, jobs, logs commands

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 19:51:52 +01:00
Hugo Nijhuis-Mekkelholt
cea501523e feat(actions): add runs, jobs, and logs commands
Some checks failed
check-and-test / Run govulncheck (pull_request) Successful in 1m43s
check-and-test / check-and-test (pull_request) Failing after 2m29s
Implement tea actions commands to view workflow runs and logs using
the Gitea 1.25 API endpoints directly. This adds:
- `tea actions runs` - list workflow runs for a repository
- `tea actions jobs <run-id>` - list jobs for a specific run
- `tea actions logs <job-id>` - display logs for a specific job

Also adds a new `modules/api` package for making raw authenticated
HTTP requests to Gitea API endpoints not yet supported by the go-sdk.

Closes #1

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 19:40:19 +01:00
36 changed files with 1208 additions and 583 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

@@ -42,6 +42,7 @@ var CmdActions = cli.Command{
}, },
} }
func runActionsDefault(_ stdctx.Context, cmd *cli.Command) error { func runActionsDefault(ctx stdctx.Context, cmd *cli.Command) error {
return cli.ShowSubcommandHelp(cmd) // Default to showing help
return cli.ShowCommandHelp(ctx, cmd, "actions")
} }

62
cmd/actions/jobs_test.go Normal file
View File

@@ -0,0 +1,62 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
)
func TestJobsCommandFlags(t *testing.T) {
cmd := CmdActionsJobs
// Test that required flags exist
expectedFlags := []string{"output", "remote", "login", "repo"}
for _, flagName := range expectedFlags {
found := false
for _, flag := range cmd.Flags {
for _, name := range flag.Names() {
if name == flagName {
found = true
break
}
}
if found {
break
}
}
if !found {
t.Errorf("Expected flag %s not found in CmdActionsJobs", flagName)
}
}
}
func TestJobsCommandProperties(t *testing.T) {
cmd := CmdActionsJobs
if cmd.Name != "jobs" {
t.Errorf("Expected command name 'jobs', got %s", cmd.Name)
}
if len(cmd.Aliases) == 0 || cmd.Aliases[0] != "job" {
t.Errorf("Expected alias 'job' for jobs command")
}
if cmd.Usage == "" {
t.Error("Jobs command should have usage text")
}
if cmd.Description == "" {
t.Error("Jobs command should have description")
}
if cmd.ArgsUsage != "<run-id>" {
t.Errorf("Expected ArgsUsage '<run-id>', got %s", cmd.ArgsUsage)
}
if cmd.Action == nil {
t.Error("Jobs command should have an action")
}
}

62
cmd/actions/logs_test.go Normal file
View File

@@ -0,0 +1,62 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
)
func TestLogsCommandFlags(t *testing.T) {
cmd := CmdActionsLogs
// Test that required flags exist
expectedFlags := []string{"output", "remote", "login", "repo"}
for _, flagName := range expectedFlags {
found := false
for _, flag := range cmd.Flags {
for _, name := range flag.Names() {
if name == flagName {
found = true
break
}
}
if found {
break
}
}
if !found {
t.Errorf("Expected flag %s not found in CmdActionsLogs", flagName)
}
}
}
func TestLogsCommandProperties(t *testing.T) {
cmd := CmdActionsLogs
if cmd.Name != "logs" {
t.Errorf("Expected command name 'logs', got %s", cmd.Name)
}
if len(cmd.Aliases) == 0 || cmd.Aliases[0] != "log" {
t.Errorf("Expected alias 'log' for logs command")
}
if cmd.Usage == "" {
t.Error("Logs command should have usage text")
}
if cmd.Description == "" {
t.Error("Logs command should have description")
}
if cmd.ArgsUsage != "<job-id>" {
t.Errorf("Expected ArgsUsage '<job-id>', got %s", cmd.ArgsUsage)
}
if cmd.Action == nil {
t.Error("Logs command should have an action")
}
}

58
cmd/actions/runs_test.go Normal file
View File

@@ -0,0 +1,58 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
)
func TestRunsCommandFlags(t *testing.T) {
cmd := CmdActionsRuns
// Test that required flags exist
expectedFlags := []string{"page", "limit", "output", "remote", "login", "repo"}
for _, flagName := range expectedFlags {
found := false
for _, flag := range cmd.Flags {
for _, name := range flag.Names() {
if name == flagName {
found = true
break
}
}
if found {
break
}
}
if !found {
t.Errorf("Expected flag %s not found in CmdActionsRuns", flagName)
}
}
}
func TestRunsCommandProperties(t *testing.T) {
cmd := CmdActionsRuns
if cmd.Name != "runs" {
t.Errorf("Expected command name 'runs', got %s", cmd.Name)
}
if len(cmd.Aliases) == 0 || cmd.Aliases[0] != "run" {
t.Errorf("Expected alias 'run' for runs command")
}
if cmd.Usage == "" {
t.Error("Runs command should have usage text")
}
if cmd.Description == "" {
t.Error("Runs command should have description")
}
if cmd.Action == nil {
t.Error("Runs command should have an action")
}
}

View File

@@ -61,11 +61,19 @@ func runReleaseAttachmentDelete(_ stdctx.Context, cmd *cli.Command) error {
return err return err
} }
existing, _, err := client.ListReleaseAttachments(ctx.Owner, ctx.Repo, release.ID, gitea.ListReleaseAttachmentsOptions{ var existing []*gitea.Attachment
ListOptions: gitea.ListOptions{Page: -1}, for page := 1; ; {
}) page_attachments, resp, err := client.ListReleaseAttachments(ctx.Owner, ctx.Repo, release.ID, gitea.ListReleaseAttachmentsOptions{
if err != nil { ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
return err })
if err != nil {
return err
}
existing = append(existing, page_attachments...)
if resp == nil || resp.NextPage == 0 {
break
}
page = resp.NextPage
} }
for _, name := range ctx.Args().Slice()[1:] { for _, name := range ctx.Args().Slice()[1:] {

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{
if err != nil { ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
fmt.Printf("error while loading reviews: %v\n", err) })
if err != nil {
fmt.Printf("error while loading reviews: %v\n", err)
break
}
reviews = append(reviews, page_reviews...)
if resp == nil || resp.NextPage == 0 {
break
}
page = resp.NextPage
} }
if ctx.IsSet("output") { if ctx.IsSet("output") {

View File

@@ -11,19 +11,25 @@ import (
// GetReleaseByTag finds a release by its tag name. // GetReleaseByTag finds a release by its tag name.
func GetReleaseByTag(owner, repo, tag string, client *gitea.Client) (*gitea.Release, error) { func GetReleaseByTag(owner, repo, tag string, client *gitea.Client) (*gitea.Release, error) {
rl, _, err := client.ListReleases(owner, repo, gitea.ListReleasesOptions{ for page := 1; ; {
ListOptions: gitea.ListOptions{Page: -1}, rl, resp, err := client.ListReleases(owner, repo, gitea.ListReleasesOptions{
}) ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
if err != nil { })
return nil, err if err != nil {
} return nil, err
if len(rl) == 0 {
return nil, fmt.Errorf("repo does not have any release")
}
for _, r := range rl {
if r.TagName == tag {
return r, nil
} }
if page == 1 && len(rl) == 0 {
return nil, fmt.Errorf("repo does not have any release")
}
for _, r := range rl {
if r.TagName == tag {
return r, nil
}
}
if resp == nil || resp.NextPage == 0 {
break
}
page = resp.NextPage
} }
return nil, fmt.Errorf("release tag does not exist") return nil, fmt.Errorf("release tag does not exist")
} }

View File

@@ -89,30 +89,26 @@ func runWebhooksCreate(ctx stdctx.Context, cmd *cli.Command) error {
config["secret"] = secret config["secret"] = secret
} }
if branchFilter != "" {
config["branch_filter"] = branchFilter
}
if authHeader != "" {
config["authorization_header"] = authHeader
}
var hook *gitea.Hook var hook *gitea.Hook
if c.IsGlobal { if c.IsGlobal {
return fmt.Errorf("global webhooks not yet supported in this version") return fmt.Errorf("global webhooks not yet supported in this version")
} else if len(c.Org) > 0 { } else if len(c.Org) > 0 {
hook, _, err = client.CreateOrgHook(c.Org, gitea.CreateHookOption{ hook, _, err = client.CreateOrgHook(c.Org, gitea.CreateHookOption{
Type: webhookType, Type: webhookType,
Config: config, Config: config,
Events: events, Events: events,
Active: active, Active: active,
BranchFilter: branchFilter,
AuthorizationHeader: authHeader,
}) })
} else { } else {
hook, _, err = client.CreateRepoHook(c.Owner, c.Repo, gitea.CreateHookOption{ hook, _, err = client.CreateRepoHook(c.Owner, c.Repo, gitea.CreateHookOption{
Type: webhookType, Type: webhookType,
Config: config, Config: config,
Events: events, Events: events,
Active: active, Active: active,
BranchFilter: branchFilter,
AuthorizationHeader: authHeader,
}) })
} }
if err != nil { if err != nil {

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 {
@@ -184,11 +148,13 @@ func TestWebhookConfigConstruction(t *testing.T) {
func TestWebhookCreateOptions(t *testing.T) { func TestWebhookCreateOptions(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
webhookType string webhookType string
events []string events []string
active bool active bool
config map[string]string config map[string]string
branchFilter string
authHeader string
}{ }{
{ {
name: "Gitea webhook", name: "Gitea webhook",
@@ -200,6 +166,8 @@ func TestWebhookCreateOptions(t *testing.T) {
"http_method": "post", "http_method": "post",
"content_type": "json", "content_type": "json",
}, },
branchFilter: "main",
authHeader: "X-Token: abc",
}, },
{ {
name: "Slack webhook", name: "Slack webhook",
@@ -228,16 +196,20 @@ func TestWebhookCreateOptions(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
option := gitea.CreateHookOption{ option := gitea.CreateHookOption{
Type: gitea.HookType(tt.webhookType), Type: gitea.HookType(tt.webhookType),
Config: tt.config, Config: tt.config,
Events: tt.events, Events: tt.events,
Active: tt.active, Active: tt.active,
BranchFilter: tt.branchFilter,
AuthorizationHeader: tt.authHeader,
} }
assert.Equal(t, gitea.HookType(tt.webhookType), option.Type) assert.Equal(t, gitea.HookType(tt.webhookType), option.Type)
assert.Equal(t, tt.events, option.Events) assert.Equal(t, tt.events, option.Events)
assert.Equal(t, tt.active, option.Active) assert.Equal(t, tt.active, option.Active)
assert.Equal(t, tt.config, option.Config) assert.Equal(t, tt.config, option.Config)
assert.Equal(t, tt.branchFilter, option.BranchFilter)
assert.Equal(t, tt.authHeader, option.AuthorizationHeader)
}) })
} }
} }

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
@@ -126,15 +129,19 @@ func runWebhooksUpdate(ctx stdctx.Context, cmd *cli.Command) error {
return fmt.Errorf("global webhooks not yet supported in this version") return fmt.Errorf("global webhooks not yet supported in this version")
} else if len(c.Org) > 0 { } else if len(c.Org) > 0 {
_, err = client.EditOrgHook(c.Org, int64(webhookID), gitea.EditHookOption{ _, err = client.EditOrgHook(c.Org, int64(webhookID), gitea.EditHookOption{
Config: config, Config: config,
Events: events, Events: events,
Active: &active, Active: &active,
BranchFilter: branchFilter,
AuthorizationHeader: authHeader,
}) })
} else { } else {
_, err = client.EditRepoHook(c.Owner, c.Repo, int64(webhookID), gitea.EditHookOption{ _, err = client.EditRepoHook(c.Owner, c.Repo, int64(webhookID), gitea.EditHookOption{
Config: config, Config: config,
Events: events, Events: events,
Active: &active, Active: &active,
BranchFilter: branchFilter,
AuthorizationHeader: authHeader,
}) })
} }
if err != nil { if err != nil {

View File

@@ -128,12 +128,10 @@ func TestUpdateActiveInactiveFlags(t *testing.T) {
func TestUpdateConfigPreservation(t *testing.T) { func TestUpdateConfigPreservation(t *testing.T) {
// Test that existing configuration is preserved when not updated // Test that existing configuration is preserved when not updated
originalConfig := map[string]string{ originalConfig := map[string]string{
"url": "https://old.example.com/webhook", "url": "https://old.example.com/webhook",
"secret": "old-secret", "secret": "old-secret",
"branch_filter": "main", "http_method": "post",
"authorization_header": "Bearer old-token", "content_type": "json",
"http_method": "post",
"content_type": "json",
} }
tests := []struct { tests := []struct {
@@ -147,53 +145,32 @@ func TestUpdateConfigPreservation(t *testing.T) {
"url": "https://new.example.com/webhook", "url": "https://new.example.com/webhook",
}, },
expectedConfig: map[string]string{ expectedConfig: map[string]string{
"url": "https://new.example.com/webhook", "url": "https://new.example.com/webhook",
"secret": "old-secret", "secret": "old-secret",
"branch_filter": "main", "http_method": "post",
"authorization_header": "Bearer old-token", "content_type": "json",
"http_method": "post",
"content_type": "json",
}, },
}, },
{ {
name: "Update secret and auth header", name: "Update secret",
updates: map[string]string{ updates: map[string]string{
"secret": "new-secret", "secret": "new-secret",
"authorization_header": "X-Token: new-token",
}, },
expectedConfig: map[string]string{ expectedConfig: map[string]string{
"url": "https://old.example.com/webhook", "url": "https://old.example.com/webhook",
"secret": "new-secret", "secret": "new-secret",
"branch_filter": "main", "http_method": "post",
"authorization_header": "X-Token: new-token", "content_type": "json",
"http_method": "post",
"content_type": "json",
},
},
{
name: "Clear branch filter",
updates: map[string]string{
"branch_filter": "",
},
expectedConfig: map[string]string{
"url": "https://old.example.com/webhook",
"secret": "old-secret",
"branch_filter": "",
"authorization_header": "Bearer old-token",
"http_method": "post",
"content_type": "json",
}, },
}, },
{ {
name: "No updates", name: "No updates",
updates: map[string]string{}, updates: map[string]string{},
expectedConfig: map[string]string{ expectedConfig: map[string]string{
"url": "https://old.example.com/webhook", "url": "https://old.example.com/webhook",
"secret": "old-secret", "secret": "old-secret",
"branch_filter": "main", "http_method": "post",
"authorization_header": "Bearer old-token", "content_type": "json",
"http_method": "post",
"content_type": "json",
}, },
}, },
} }
@@ -217,6 +194,61 @@ func TestUpdateConfigPreservation(t *testing.T) {
} }
} }
func TestUpdateBranchFilterAndAuthHeaderHandling(t *testing.T) {
tests := []struct {
name string
originalBranchFilter string
originalAuthHeader string
setBranchFilter bool
newBranchFilter string
setAuthorizationHeader bool
newAuthHeader string
expectedBranchFilter string
expectedAuthHeader string
}{
{
name: "Preserve values",
originalBranchFilter: "main",
originalAuthHeader: "Bearer old-token",
expectedBranchFilter: "main",
expectedAuthHeader: "Bearer old-token",
},
{
name: "Update branch filter",
originalBranchFilter: "main",
setBranchFilter: true,
newBranchFilter: "develop",
expectedBranchFilter: "develop",
expectedAuthHeader: "",
},
{
name: "Update authorization header",
originalAuthHeader: "Bearer old-token",
setAuthorizationHeader: true,
newAuthHeader: "X-Token: new-token",
expectedBranchFilter: "",
expectedAuthHeader: "X-Token: new-token",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
branchFilter := tt.originalBranchFilter
if tt.setBranchFilter {
branchFilter = tt.newBranchFilter
}
authHeader := tt.originalAuthHeader
if tt.setAuthorizationHeader {
authHeader = tt.newAuthHeader
}
assert.Equal(t, tt.expectedBranchFilter, branchFilter)
assert.Equal(t, tt.expectedAuthHeader, authHeader)
})
}
}
func TestUpdateEventsHandling(t *testing.T) { func TestUpdateEventsHandling(t *testing.T) {
tests := []struct { tests := []struct {
name string name string

View File

@@ -274,7 +274,7 @@ Manage and checkout pull requests
**--comments**: Whether to display comments (will prompt if not provided & run interactively) **--comments**: Whether to display comments (will prompt if not provided & run interactively)
**--fields, -f**="": Comma-separated list of fields to print. Available values: **--fields, -f**="": Comma-separated list of fields to print. Available values:
index,state,author,author-id,url,title,body,mergeable,base,base-commit,head,diff,patch,created,updated,deadline,assignees,milestone,labels,comments,ci index,state,author,author-id,url,title,body,mergeable,base,base-commit,head,diff,patch,created,updated,deadline,assignees,milestone,labels,comments
(default: "index,title,state,author,milestone,updated,labels") (default: "index,title,state,author,milestone,updated,labels")
**--limit, --lm**="": specify limit of items per page (default: 30) **--limit, --lm**="": specify limit of items per page (default: 30)
@@ -296,7 +296,7 @@ Manage and checkout pull requests
List pull requests of the repository List pull requests of the repository
**--fields, -f**="": Comma-separated list of fields to print. Available values: **--fields, -f**="": Comma-separated list of fields to print. Available values:
index,state,author,author-id,url,title,body,mergeable,base,base-commit,head,diff,patch,created,updated,deadline,assignees,milestone,labels,comments,ci index,state,author,author-id,url,title,body,mergeable,base,base-commit,head,diff,patch,created,updated,deadline,assignees,milestone,labels,comments
(default: "index,title,state,author,milestone,updated,labels") (default: "index,title,state,author,milestone,updated,labels")
**--limit, --lm**="": specify limit of items per page (default: 30) **--limit, --lm**="": specify limit of items per page (default: 30)
@@ -345,8 +345,6 @@ Deletes local & remote feature-branches for a closed pull request
Create a pull-request Create a pull-request
**--agit**: Create an agit flow pull request
**--allow-maintainer-edits, --edits**: Enable maintainers to push to the base branch of created pull **--allow-maintainer-edits, --edits**: Enable maintainers to push to the base branch of created pull
**--assignees, -a**="": Comma-separated list of usernames to assign **--assignees, -a**="": Comma-separated list of usernames to assign
@@ -373,8 +371,6 @@ Create a pull-request
**--title, -t**="": **--title, -t**="":
**--topic**="": Topic name for agit flow pull request
### close ### close
Change state of one or more pull requests to 'closed' Change state of one or more pull requests to 'closed'
@@ -399,36 +395,6 @@ Change state of one or more pull requests to 'open'
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional **--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
### edit, e
Edit one or more pull requests
**--add-assignees, -a**="": Comma-separated list of usernames to assign
**--add-labels, -L**="": Comma-separated list of labels to assign. Takes precedence over --remove-labels
**--add-reviewers, -r**="": Comma-separated list of usernames to request review from
**--deadline, -D**="": Deadline timestamp to assign
**--description, -d**="":
**--login, -l**="": Use a different Gitea Login. Optional
**--milestone, -m**="": Milestone to assign
**--referenced-version, -v**="": commit-hash or tag name to assign
**--remote, -R**="": Discover Gitea login from remote. Optional
**--remove-labels**="": Comma-separated list of labels to remove
**--remove-reviewers**="": Comma-separated list of usernames to remove from reviewers
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
**--title, -t**="":
### review ### review
Interactively review a pull request Interactively review a pull request
@@ -483,46 +449,6 @@ Merge a pull request
**--title, -t**="": Merge commit title **--title, -t**="": Merge commit title
### review-comments, rc
List review comments on a pull request
**--fields, -f**="": Comma-separated list of fields to print. Available values:
id,body,reviewer,path,line,resolver,created,updated,url
(default: "id,path,line,body,reviewer,resolver")
**--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
### resolve
Resolve a review comment on a pull request
**--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
### unresolve
Unresolve a review comment on a pull request
**--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
## labels, label ## labels, label
Manage issue labels Manage issue labels
@@ -923,16 +849,12 @@ Operate on tracked times of a repository's issues & pulls
**--from, -f**="": Show only times tracked after this date **--from, -f**="": Show only times tracked after this date
**--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--mine, -m**: Show all times tracked by you across all repositories (overrides command arguments) **--mine, -m**: Show all times tracked by you across all repositories (overrides command arguments)
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional **--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
@@ -987,16 +909,12 @@ List tracked times on issues & pulls
**--from, -f**="": Show only times tracked after this date **--from, -f**="": Show only times tracked after this date
**--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--mine, -m**: Show all times tracked by you across all repositories (overrides command arguments) **--mine, -m**: Show all times tracked by you across all repositories (overrides command arguments)
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional **--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
@@ -1065,7 +983,7 @@ Delete users Organizations
## repos, repo ## repos, repo
Manage repositories Show repository details
**--fields, -f**="": Comma-separated list of fields to print. Available values: **--fields, -f**="": Comma-separated list of fields to print. Available values:
description,forks,id,name,owner,stars,ssh,updated,url,permission,type description,forks,id,name,owner,stars,ssh,updated,url,permission,type
@@ -1077,8 +995,6 @@ Manage repositories
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--owner, -O**="": List repos of a specific owner (org or user)
**--page, -p**="": specify page (default: 1) **--page, -p**="": specify page (default: 1)
**--starred, -s**: List your starred repos instead **--starred, -s**: List your starred repos instead
@@ -1101,8 +1017,6 @@ List repositories you have access to
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--owner, -O**="": List repos of a specific owner (org or user)
**--page, -p**="": specify page (default: 1) **--page, -p**="": specify page (default: 1)
**--starred, -s**: List your starred repos instead **--starred, -s**: List your starred repos instead
@@ -1273,32 +1187,6 @@ Delete an existing repository
**--owner, -O**="": owner of the repo **--owner, -O**="": owner of the repo
### edit, e
Edit repository properties
**--archived**="": Set archived [true/false]
**--default-branch**="": Set default branch
**--description, --desc**="": New description of the repository
**--login, -l**="": Use a different Gitea Login. Optional
**--name**="": New name of the repository
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--private**="": Set private [true/false]
**--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
**--template**="": Set template [true/false]
**--website**="": New website URL of the repository
## branches, branch, b ## branches, branch, b
Consult branches Consult branches
@@ -1381,26 +1269,6 @@ Unprotect branches
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional **--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
### rename, rn
Rename a branch
**--fields, -f**="": Comma-separated list of fields to print. Available values:
name,protected,user-can-merge,user-can-push,protection
(default: "name,protected,user-can-merge,user-can-push")
**--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
## actions, action ## actions, action
Manage repository actions Manage repository actions
@@ -1511,18 +1379,8 @@ Delete an action variable
### runs, run ### runs, run
Manage workflow runs
#### list, ls
List workflow runs List workflow runs
**--actor**="": Filter by actor username (who triggered the run)
**--branch**="": Filter by branch name
**--event**="": Filter by event type (push, pull_request, etc.)
**--limit, --lm**="": specify limit of items per page (default: 30) **--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
@@ -1535,17 +1393,9 @@ List workflow runs
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional **--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
**--since**="": Show runs started after this time (e.g., '24h', '2024-01-01') ### jobs, job
**--status**="": Filter by status (success, failure, pending, queued, in_progress, skipped, canceled) List jobs for a workflow run
**--until**="": Show runs started before this time (e.g., '2024-01-01')
#### view, show, get
View workflow run details
**--jobs**: show jobs table
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
@@ -1555,99 +1405,9 @@ View workflow run details
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional **--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
#### delete, remove, rm, cancel ### logs, log
Delete or cancel a workflow run Display logs for a job
**--confirm, -y**: confirm deletion without prompting
**--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
#### logs, log
View workflow run logs
**--follow, -f**: follow log output (like tail -f), requires job to be in progress
**--job**="": specific job ID to view logs for (if omitted, shows all jobs)
**--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
### workflows, workflow
Manage repository workflows
#### list, ls
List repository workflows
**--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
#### view, show, get
View workflow details
**--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
#### dispatch, trigger, run
Dispatch a workflow run
**--follow, -f**: follow log output after dispatching
**--input, -i**="": workflow input in key=value format (can be specified multiple times)
**--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--ref, -r**="": branch or tag to dispatch on (default: current branch)
**--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
#### enable
Enable a workflow
**--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
#### disable
Disable a workflow
**--confirm, -y**: confirm disable without prompting
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
@@ -1984,27 +1744,3 @@ List Users
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional **--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
## api
Make an authenticated API request
**--Field, -F**="": Add a typed field to the request body (key=value, @file, or @- for stdin)
**--data, -d**="": Raw JSON request body (use @file to read from file, @- for stdin)
**--field, -f**="": Add a string field to the request body (key=value)
**--header, -H**="": Add a custom header (key:value)
**--include, -i**: Include HTTP status and response headers in output (written to stderr)
**--login, -l**="": Use a different Gitea Login. Optional
**--method, -X**="": HTTP method (GET, POST, PUT, PATCH, DELETE) (default: "GET")
**--output, -o**="": Write response body to file instead of stdout (use '-' for stdout)
**--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional

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=

203
modules/api/client_test.go Normal file
View File

@@ -0,0 +1,203 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package api
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"code.gitea.io/tea/modules/config"
)
func TestNewClient(t *testing.T) {
login := &config.Login{
URL: "https://gitea.example.com",
Token: "test-token",
Insecure: false,
}
client := NewClient(login)
if client == nil {
t.Fatal("NewClient returned nil")
}
if client.login != login {
t.Error("Client login not set correctly")
}
if client.httpClient == nil {
t.Error("Client httpClient not set")
}
}
func TestNewClientInsecure(t *testing.T) {
login := &config.Login{
URL: "https://gitea.example.com",
Token: "test-token",
Insecure: true,
}
client := NewClient(login)
if client == nil {
t.Fatal("NewClient returned nil")
}
// Verify that insecure transport is configured
if client.httpClient.Transport == nil {
t.Error("Expected custom transport for insecure client")
}
}
func TestGet(t *testing.T) {
// Create a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request
if r.Method != "GET" {
t.Errorf("Expected GET request, got %s", r.Method)
}
if r.Header.Get("Authorization") != "token test-token" {
t.Errorf("Expected authorization header, got %s", r.Header.Get("Authorization"))
}
if r.Header.Get("Accept") != "application/json" {
t.Errorf("Expected accept header, got %s", r.Header.Get("Accept"))
}
// Return test response
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}))
defer server.Close()
login := &config.Login{
URL: server.URL,
Token: "test-token",
}
client := NewClient(login)
var result map[string]string
_, err := client.Get(context.Background(), "/test", &result)
if err != nil {
t.Fatalf("Get returned error: %v", err)
}
if result["status"] != "ok" {
t.Errorf("Expected status 'ok', got %s", result["status"])
}
}
func TestGetError(t *testing.T) {
// Create a test server that returns an error
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("not found"))
}))
defer server.Close()
login := &config.Login{
URL: server.URL,
Token: "test-token",
}
client := NewClient(login)
var result map[string]string
_, err := client.Get(context.Background(), "/test", &result)
if err == nil {
t.Fatal("Expected error for 404 response")
}
expectedError := "API request failed with status 404"
if err.Error()[:len(expectedError)] != expectedError {
t.Errorf("Expected error starting with '%s', got '%s'", expectedError, err.Error())
}
}
func TestGetRaw(t *testing.T) {
expectedBody := "raw log content here"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
t.Errorf("Expected GET request, got %s", r.Method)
}
if r.Header.Get("Authorization") != "token test-token" {
t.Errorf("Expected authorization header, got %s", r.Header.Get("Authorization"))
}
w.Write([]byte(expectedBody))
}))
defer server.Close()
login := &config.Login{
URL: server.URL,
Token: "test-token",
}
client := NewClient(login)
body, err := client.GetRaw(context.Background(), "/logs")
if err != nil {
t.Fatalf("GetRaw returned error: %v", err)
}
if string(body) != expectedBody {
t.Errorf("Expected body '%s', got '%s'", expectedBody, string(body))
}
}
func TestGetRawError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("internal error"))
}))
defer server.Close()
login := &config.Login{
URL: server.URL,
Token: "test-token",
}
client := NewClient(login)
_, err := client.GetRaw(context.Background(), "/logs")
if err == nil {
t.Fatal("Expected error for 500 response")
}
expectedError := "API request failed with status 500"
if err.Error()[:len(expectedError)] != expectedError {
t.Errorf("Expected error starting with '%s', got '%s'", expectedError, err.Error())
}
}
func TestGetWithContextCancellation(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// This should not be reached if context is cancelled
w.Write([]byte("ok"))
}))
defer server.Close()
login := &config.Login{
URL: server.URL,
Token: "test-token",
}
client := NewClient(login)
// Create a cancelled context
ctx, cancel := context.WithCancel(context.Background())
cancel()
var result map[string]string
_, err := client.Get(ctx, "/test", &result)
if err == nil {
t.Fatal("Expected error for cancelled context")
}
}

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

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

@@ -81,17 +81,17 @@ func TestWebhookDetails(t *testing.T) {
ID: 123, ID: 123,
Type: "gitea", Type: "gitea",
Config: map[string]string{ Config: map[string]string{
"url": "https://example.com/webhook", "url": "https://example.com/webhook",
"content_type": "json", "content_type": "json",
"http_method": "post", "http_method": "post",
"branch_filter": "main,develop", "secret": "secret-value",
"secret": "secret-value",
"authorization_header": "Bearer token123",
}, },
Events: []string{"push", "pull_request", "issues"}, BranchFilter: "main,develop",
Active: true, AuthorizationHeader: "Bearer token123",
Created: now.Add(-24 * time.Hour), Events: []string{"push", "pull_request", "issues"},
Updated: now, Active: true,
Created: now.Add(-24 * time.Hour),
Updated: now,
}, },
}, },
{ {
@@ -238,16 +238,14 @@ func TestWebhookConfigHandling(t *testing.T) {
{ {
name: "Config with all fields", name: "Config with all fields",
config: map[string]string{ config: map[string]string{
"url": "https://example.com/webhook", "url": "https://example.com/webhook",
"secret": "my-secret", "secret": "my-secret",
"authorization_header": "Bearer token", "content_type": "json",
"content_type": "json", "http_method": "post",
"http_method": "post",
"branch_filter": "main",
}, },
expectedURL: "https://example.com/webhook", expectedURL: "https://example.com/webhook",
hasSecret: true, hasSecret: true,
hasAuthHeader: true, hasAuthHeader: false,
}, },
{ {
name: "Config with minimal fields", name: "Config with minimal fields",
@@ -341,17 +339,17 @@ func TestWebhookDetailsFormatting(t *testing.T) {
ID: 123, ID: 123,
Type: "gitea", Type: "gitea",
Config: map[string]string{ Config: map[string]string{
"url": "https://example.com/webhook", "url": "https://example.com/webhook",
"content_type": "json", "content_type": "json",
"http_method": "post", "http_method": "post",
"branch_filter": "main,develop", "secret": "secret-value",
"secret": "secret-value",
"authorization_header": "Bearer token123",
}, },
Events: []string{"push", "pull_request", "issues"}, BranchFilter: "main,develop",
Active: true, AuthorizationHeader: "Bearer token123",
Created: now.Add(-24 * time.Hour), Events: []string{"push", "pull_request", "issues"},
Updated: now, Active: true,
Created: now.Add(-24 * time.Hour),
Updated: now,
} }
// Test that all expected fields are included in details // Test that all expected fields are included in details
@@ -379,8 +377,8 @@ func TestWebhookDetailsFormatting(t *testing.T) {
assert.Equal(t, "https://example.com/webhook", hook.Config["url"]) assert.Equal(t, "https://example.com/webhook", hook.Config["url"])
assert.Equal(t, "json", hook.Config["content_type"]) assert.Equal(t, "json", hook.Config["content_type"])
assert.Equal(t, "post", hook.Config["http_method"]) assert.Equal(t, "post", hook.Config["http_method"])
assert.Equal(t, "main,develop", hook.Config["branch_filter"]) assert.Equal(t, "main,develop", hook.BranchFilter)
assert.Contains(t, hook.Config, "secret") assert.Contains(t, hook.Config, "secret")
assert.Contains(t, hook.Config, "authorization_header") assert.Equal(t, "Bearer token123", hook.AuthorizationHeader)
assert.Equal(t, []string{"push", "pull_request", "issues"}, hook.Events) assert.Equal(t, []string{"push", "pull_request", "issues"}, hook.Events)
} }

View File

@@ -13,16 +13,23 @@ import (
// ResolveLabelNames returns a list of label IDs for a given list of label names // ResolveLabelNames returns a list of label IDs for a given list of label names
func ResolveLabelNames(client *gitea.Client, owner, repo string, labelNames []string) ([]int64, error) { func ResolveLabelNames(client *gitea.Client, owner, repo string, labelNames []string) ([]int64, error) {
labelIDs := make([]int64, 0, len(labelNames)) labelIDs := make([]int64, 0, len(labelNames))
labels, _, err := client.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{ page := 1
ListOptions: gitea.ListOptions{Page: -1}, for {
}) labels, resp, err := client.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{
if err != nil { ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
return nil, err })
} if err != nil {
for _, l := range labels { return nil, err
if utils.Contains(labelNames, l.Name) {
labelIDs = append(labelIDs, l.ID)
} }
for _, l := range labels {
if utils.Contains(labelNames, l.Name) {
labelIDs = append(labelIDs, l.ID)
}
}
if resp == nil || resp.NextPage == 0 {
break
}
page = resp.NextPage
} }
return labelIDs, nil return labelIDs, nil
} }

View File

@@ -166,11 +166,19 @@ func generateToken(login config.Login, user, pass, otp, scopes string) (string,
} }
client := login.Client(opts...) client := login.Client(opts...)
tl, _, err := client.ListAccessTokens(gitea.ListAccessTokensOptions{ var tl []*gitea.AccessToken
ListOptions: gitea.ListOptions{Page: -1}, for page := 1; ; {
}) page_tokens, resp, err := client.ListAccessTokens(gitea.ListAccessTokensOptions{
if err != nil { ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
return "", err })
if err != nil {
return "", err
}
tl = append(tl, page_tokens...)
if resp == nil || resp.NextPage == 0 {
break
}
page = resp.NextPage
} }
host, _ := os.Hostname() host, _ := os.Hostname()
tokenName := host + "-tea" tokenName := host + "-tea"

View File

@@ -19,11 +19,22 @@ import (
// a matching private key in ~/.ssh/. If no match is found, path is empty. // a matching private key in ~/.ssh/. If no match is found, path is empty.
func findSSHKey(client *gitea.Client) (string, error) { func findSSHKey(client *gitea.Client) (string, error) {
// get keys registered on gitea instance // get keys registered on gitea instance
keys, _, err := client.ListMyPublicKeys(gitea.ListPublicKeysOptions{ var keys []*gitea.PublicKey
ListOptions: gitea.ListOptions{Page: -1}, for page := 1; ; {
}) page_keys, resp, err := client.ListMyPublicKeys(gitea.ListPublicKeysOptions{
if err != nil || len(keys) == 0 { ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
return "", err })
if err != nil {
return "", err
}
keys = append(keys, page_keys...)
if resp == nil || resp.NextPage == 0 {
break
}
page = resp.NextPage
}
if len(keys) == 0 {
return "", nil
} }
// enumerate ~/.ssh/*.pub files // enumerate ~/.ssh/*.pub files

View File

@@ -14,11 +14,19 @@ import (
func ListPullReviewComments(ctx *context.TeaContext, idx int64) ([]*gitea.PullReviewComment, error) { func ListPullReviewComments(ctx *context.TeaContext, idx int64) ([]*gitea.PullReviewComment, error) {
c := ctx.Login.Client() c := ctx.Login.Client()
reviews, _, err := c.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{ var reviews []*gitea.PullReview
ListOptions: gitea.ListOptions{Page: -1}, for page := 1; ; {
}) page_reviews, resp, err := c.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{
if err != nil { ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
return nil, err })
if err != nil {
return nil, err
}
reviews = append(reviews, page_reviews...)
if resp == nil || resp.NextPage == 0 {
break
}
page = resp.NextPage
} }
var allComments []*gitea.PullReviewComment var allComments []*gitea.PullReviewComment

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