Compare commits

...

178 Commits

Author SHA1 Message Date
Renovate Bot 989651408e fix(deps): update go dependencies 2026-07-16 00:16:20 +00:00
Renovate Bot 7f0213940d fix(deps): update go dependencies (#1051)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [charm.land/lipgloss/v2](https://github.com/charmbracelet/lipgloss) | `v2.0.4` → `v2.0.5` | ![age](https://developer.mend.io/api/mc/badges/age/go/charm.land%2flipgloss%2fv2/v2.0.5?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/charm.land%2flipgloss%2fv2/v2.0.4/v2.0.5?slim=true) |
| [github.com/urfave/cli/v3](https://github.com/urfave/cli) | `v3.10.0` → `v3.10.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2furfave%2fcli%2fv3/v3.10.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2furfave%2fcli%2fv3/v3.10.0/v3.10.1?slim=true) |
| [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) | [`v0.46.0` → `v0.47.0`](https://cs.opensource.google/go/x/sys/+/refs/tags/v0.46.0...refs/tags/v0.47.0) | ![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2fsys/v0.47.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2fsys/v0.46.0/v0.47.0?slim=true) |

---

### Release Notes

<details>
<summary>charmbracelet/lipgloss (charm.land/lipgloss/v2)</summary>

### [`v2.0.5`](https://github.com/charmbracelet/lipgloss/releases/tag/v2.0.5)

[Compare Source](https://github.com/charmbracelet/lipgloss/compare/v2.0.4...v2.0.5)

### Graphemes, Schmraphemes

If you’re using emojis in the terminal you're in for a rough ride. That said, we do what we can. This release brings some in some very specific edge case rendering improvements. Enjoy.

Enjoy!

#### Changelog

- [`10f9584`](https://github.com/charmbracelet/lipgloss/commit/10f9584edb197ddbbfc789081d33b6fadaea5742): chore(deps): bump ultraviolet for emoji-related improvements

***

<a href="https://charm.land/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-banner-next.jpg" width="400"></a>

Thoughts? Questions? We love hearing from you. Feel free to reach out on [X](https://x.com/charmcli), [Discord](https://charm.land/discord), [Slack](https://charm.land/slack), [The Fediverse](https://mastodon.social/@&#8203;charmcli), [Bluesky](https://bsky.app/profile/charm.land).

</details>

<details>
<summary>urfave/cli (github.com/urfave/cli/v3)</summary>

### [`v3.10.1`](https://github.com/urfave/cli/releases/tag/v3.10.1)

[Compare Source](https://github.com/urfave/cli/compare/v3.10.0...v3.10.1)

#### What's Changed

- chore(deps): bump actions/checkout from 6 to 7 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;2371](https://github.com/urfave/cli/pull/2371)
- fix: align gfmrun example counter with actual runnable count by [@&#8203;dearchap](https://github.com/dearchap) in [#&#8203;2369](https://github.com/urfave/cli/pull/2369)
- v3: yield the version flag's -v alias to a user-defined flag by [@&#8203;c-tonneslan](https://github.com/c-tonneslan) in [#&#8203;2330](https://github.com/urfave/cli/pull/2330)
- fix: keep completion subcommand order deterministic in help output by [@&#8203;suzuki-shunsuke](https://github.com/suzuki-shunsuke) in [#&#8203;2374](https://github.com/urfave/cli/pull/2374)
- fix: allow DefaultCommand to handle its own flags by [@&#8203;lihan3238](https://github.com/lihan3238) in [#&#8203;2322](https://github.com/urfave/cli/pull/2322)

#### New Contributors

- [@&#8203;lihan3238](https://github.com/lihan3238) made their first contribution in [#&#8203;2322](https://github.com/urfave/cli/pull/2322)

**Full Changelog**: <https://github.com/urfave/cli/compare/v3.10.0...v3.10.1>

</details>

---

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

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/1051
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
2026-07-15 01:41:25 +00:00
Willem Kokke 3b5703177d fix(theme): don't query the terminal at start-up (#1054)
Fixes https://gitea.com/gitea/tea/issues/1053

## What

tea hangs forever on Windows when it is started by something that owns a console but redirects
tea's stdio: a Windows service, a CI runner, an automation harness. Every command is affected,
`tea --version` included.

## Root cause

`modules/theme` imported `charm.land/lipgloss/v2/compat` for one struct type,
`compat.AdaptiveColor`. But compat detects the terminal background from package-level vars:

```go
var (
    HasDarkBackground = lipgloss.HasDarkBackground(os.Stdin, os.Stdout)
    Profile           = colorprofile.Detect(os.Stdout, os.Environ())
)
```

Go initialises every package in the import graph before `main()` runs, and every file in package
`cmd` imports `modules/context`, which imports `modules/theme`. So tea queried the terminal on
every invocation, before urfave/cli had even looked at the arguments — which is why `--version`
hung.

On Windows that query opens `CONIN$`/`CONOUT$` and asks the console directly instead of giving up
when stdio is redirected, then waits for a reply that never comes. The read has a 2 second timeout,
but it does not fire, because the cancel it relies on is a no-op for that handle. Full trace in the
issue.

## Changes

- `modules/theme/theme.go` — drop the `compat` import and use `lipgloss.LightDark`, a plain
  function that touches no terminal. `TeaTheme.Theme` is already handed the `isDark` it needs.

  This also fixes a bug hiding in plain sight: `compat.AdaptiveColor` resolves against a
  process-wide value detected at init, so the title color ignored the `isDark` argument huh passed
  in. `Theme(true)` and `Theme(false)` returned the same color.

- `modules/theme/background.go` — new `HasDarkBackground()` helper that only asks the terminal when
  stdin and stdout are both terminals, and otherwise assumes dark, which is the default lipgloss
  itself falls back to. This is the same rule lipgloss already applies on Unix.

- `modules/interact/print.go` — `printTitleAndContent` called
  `lipgloss.HasDarkBackground(os.Stdin, os.Stdout)` directly, so it hit the same wait on the
  interactive paths, `tea login add` among them. It now goes through the helper.

## Why fix this in tea

The underlying bug is upstream and I have opened PRs for both halves of it: https://github.com/charmbracelet/ultraviolet/pull/138 and
https://github.com/charmbracelet/lipgloss/pull/713. But it is not fixed in any released version — lipgloss v2.0.5 is byte-identical to
v2.0.4 in the relevant files — so upgrading dependencies does not help, and tea would stay broken on
Windows until Charm cuts a release and tea bumps `go.mod`.

Separately, these changes stand on their own. tea should not query the terminal in order to print a
version string, whatever upstream does.

Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Willem Kokke <mail@willem.net>
2026-07-13 04:31:26 +00:00
Lunny Xiao 2a9c8ff6fd upgrade go sdk and add test (#1048)
Fix #1046Reviewed-on: https://gitea.com/gitea/tea/pulls/1048
2026-07-01 18:06:19 +00:00
Brien Coffield 12947f068a feat(comments): accept -d/--description for comment body (#1043)
Closes #1042.

### What

`tea issue create`, `tea issue edit` and `tea pr create` all take the body via
`-d` / `--description`, but `tea comments add` and `tea comments edit` only
accepted it positionally — passing `-d` errored with
`flag provided but not defined: -d`.

This adds `-d` / `--description` to both `comments` subcommands so every
body-bearing command shares one flag.

### Behaviour / precedence

Unchanged for existing usage. Body resolution is now:

1. positional argument (kept first for back-compat),
2. `-d` / `--description`,
3. piped stdin,
4. `$EDITOR` (interactive only).

### Testing

- `go build`, `go vet ./...`, `go test -short ./cmd/... ./modules/...` all pass.
- Manually verified `tea comments add --help` / `edit --help` list the flag,
  and that `-d` resolves the body (reaches the API, no parse error, no editor
  prompt, no stdin block).

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/1043
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Brien Coffield <coffbr01@gmail.com>
Co-committed-by: Brien Coffield <coffbr01@gmail.com>
2026-06-29 00:52:24 +00:00
Lunny Xiao d4545d8ed7 Add reply to code review (#978)
Follow https://gitea.com/gitea/go-sdk/pulls/784

Reviewed-on: https://gitea.com/gitea/tea/pulls/978
2026-06-26 21:40:31 +00:00
dinsmoor 885381e3e4 fix(http): add transport timeouts so tea fails fast on stalled servers (#1020)
Fixes #1018

Co-authored-by: dinsmoor <204368+dinsmoor@noreply.gitea.com>
Co-committed-by: dinsmoor <204368+dinsmoor@noreply.gitea.com>
2026-06-26 19:59:05 +00:00
techknowlogick 6a57af24ad fix(config): write to keychain before config (#1044)
Reviewed-on: https://gitea.com/gitea/tea/pulls/1044
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
2026-06-26 19:53:59 +00:00
Minjie Fang 88f5cdcafa fix(labels): add org label for ls and pr (#1017)
Fix https://gitea.com/gitea/tea/issues/634, https://gitea.com/gitea/tea/issues/669

Reviewed-on: https://gitea.com/gitea/tea/pulls/1017
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Minjie Fang <wingsallen@gmail.com>
Co-committed-by: Minjie Fang <wingsallen@gmail.com>
2026-06-24 17:46:12 +00:00
Renovate Bot 5cfee362c8 chore(deps): update actions/checkout action to v7 (#1041)
Reviewed-on: https://gitea.com/gitea/tea/pulls/1041
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-06-23 00:18:41 +00:00
Renovate Bot fff82ac723 fix(deps): update module github.com/urfave/cli/v3 to v3.10.0 (#1040)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [github.com/urfave/cli/v3](https://github.com/urfave/cli) | `v3.9.1` → `v3.10.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2furfave%2fcli%2fv3/v3.10.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2furfave%2fcli%2fv3/v3.9.1/v3.10.0?slim=true) |

---

### Release Notes

<details>
<summary>urfave/cli (github.com/urfave/cli/v3)</summary>

### [`v3.10.0`](https://github.com/urfave/cli/releases/tag/v3.10.0)

[Compare Source](https://github.com/urfave/cli/compare/v3.9.1...v3.10.0)

#### What's Changed

- feat: add Flag.SchemaType() and Flag.SchemaItemsType() for JSON Schema introspection by [@&#8203;dearchap](https://github.com/dearchap) in [#&#8203;2368](https://github.com/urfave/cli/pull/2368)
- feat: add Command.Walk() and Command.Path() convenience methods by [@&#8203;dearchap](https://github.com/dearchap) in [#&#8203;2353](https://github.com/urfave/cli/pull/2353)
- fix: let --help flag take precedence over parse errors by [@&#8203;dearchap](https://github.com/dearchap) in [#&#8203;2360](https://github.com/urfave/cli/pull/2360)
- fix: resolve remaining help invocation inconsistencies by [@&#8203;dearchap](https://github.com/dearchap) in [#&#8203;2361](https://github.com/urfave/cli/pull/2361)
- fix: skip After hook when help is displayed on subcommand by [@&#8203;dearchap](https://github.com/dearchap) in [#&#8203;2363](https://github.com/urfave/cli/pull/2363)
- fix: show GLOBAL OPTIONS in SubcommandHelpTemplate by [@&#8203;dearchap](https://github.com/dearchap) in [#&#8203;2359](https://github.com/urfave/cli/pull/2359)
- fix: resolve broken links in CHANGELOG.md by [@&#8203;dearchap](https://github.com/dearchap) in [#&#8203;2357](https://github.com/urfave/cli/pull/2357)
- docs: document what happened to build tags in v3 migration guide by [@&#8203;dearchap](https://github.com/dearchap) in [#&#8203;2358](https://github.com/urfave/cli/pull/2358)
- Various docs updates and dependency bumps

**Full Changelog**: <https://github.com/urfave/cli/compare/v3.9.1...v3.10.0>

</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:eyJjcmVhdGVkSW5WZXIiOiI0My4xOTEuMiIsInVwZGF0ZWRJblZlciI6IjQzLjE5MS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/1040
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-06-20 20:04:37 +00:00
Daniel Bankmann b11d991d1e fix(oauth): pass resolved redirect_uri to token exchange (#1019)
When --redirect-url is omitted, the local callback listener binds to a
free port and opts.RedirectURL is rewritten with it. oauth2Config.RedirectURL
was never updated, so Exchange() sent the stale http://127.0.0.1:0 while
the authorize step had sent the real port. RFC-6749-compliant servers
(Gitea >= #37704, current Forgejo) reject the mismatch.

Propagate the resolved URL back into oauth2Config before Exchange. Add a
regression test using httptest that drives the flow end-to-end and asserts
the redirect_uri values match.

---------

Co-authored-by: dbankmann <204984+dbankmann@users.noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/1019
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Daniel Bankmann <204984+dbankmann@noreply.gitea.com>
Co-committed-by: Daniel Bankmann <204984+dbankmann@noreply.gitea.com>
2026-06-20 20:03:34 +00:00
Renovate Bot 4b209d68de fix(deps): update go dependencies (#1039)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [charm.land/glamour/v2](https://github.com/charmbracelet/glamour) | `v2.0.0` → `v2.0.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/charm.land%2fglamour%2fv2/v2.0.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/charm.land%2fglamour%2fv2/v2.0.0/v2.0.1?slim=true) |
| [charm.land/lipgloss/v2](https://github.com/charmbracelet/lipgloss) | `v2.0.3` → `v2.0.4` | ![age](https://developer.mend.io/api/mc/badges/age/go/charm.land%2flipgloss%2fv2/v2.0.4?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/charm.land%2flipgloss%2fv2/v2.0.3/v2.0.4?slim=true) |

---

### Release Notes

<details>
<summary>charmbracelet/glamour (charm.land/glamour/v2)</summary>

### [`v2.0.1`](https://github.com/charmbracelet/glamour/releases/tag/v2.0.1)

[Compare Source](https://github.com/charmbracelet/glamour/compare/v2.0.0...v2.0.1)

#### Changelog

##### Bug fixes

- [`975a0f3`](https://github.com/charmbracelet/glamour/commit/975a0f30cc6519fde4481ab084987222c8f06c34): fix: close wrap writer before its downstream chain ([#&#8203;575](https://github.com/charmbracelet/glamour/issues/575)) ([@&#8203;taciturnaxolotl](https://github.com/taciturnaxolotl))

##### Other work

- [`c293649`](https://github.com/charmbracelet/glamour/commit/c29364924407add0616fc067e4c70ddf9a5da0dd): chore: fix pre-existing lint failures ([#&#8203;575](https://github.com/charmbracelet/glamour/issues/575)) ([@&#8203;taciturnaxolotl](https://github.com/taciturnaxolotl))
- [`95c93db`](https://github.com/charmbracelet/glamour/commit/95c93db04489cebf252ec856584445c7e85ee276): chore: bump lipgloss to v2.0.4 ([@&#8203;taciturnaxolotl](https://github.com/taciturnaxolotl))
- [`600b93a`](https://github.com/charmbracelet/glamour/commit/600b93a40bb9754522c7e3496b8b41989cf1dec7): chore: remove CODEOWNERS ([@&#8203;aymanbagabas](https://github.com/aymanbagabas))
- [`253da61`](https://github.com/charmbracelet/glamour/commit/253da61fcb275fcd28296956ffae5f7d98ff618d): fix(ci): use local golangci config ([@&#8203;aymanbagabas](https://github.com/aymanbagabas))

***

<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg"
width="400"></a>

Thoughts? Questions? We love hearing from you. Feel free to reach out on [Twitter](https://twitter.com/charmcli), [The Fediverse](https://mastodon.social/@&#8203;charm), or on [Discord](https://charm.sh/chat).

</details>

<details>
<summary>charmbracelet/lipgloss (charm.land/lipgloss/v2)</summary>

### [`v2.0.4`](https://github.com/charmbracelet/lipgloss/releases/tag/v2.0.4)

[Compare Source](https://github.com/charmbracelet/lipgloss/compare/v2.0.3...v2.0.4)

### Mini Crash Patch

Hi! This is a small patch to fix a writer-related panic. Thanks for using Lip Gloss!

#### Changelog

##### Fixed

- [`fefa41d`](https://github.com/charmbracelet/lipgloss/commit/fefa41d): fix: prevent crash when writing to a closed wrap writer ([#&#8203;699](https://github.com/charmbracelet/lipgloss/issues/699)) ([@&#8203;taciturnaxolotl](https://github.com/taciturnaxolotl))

##### Docs

- [`40ec0e6`](https://github.com/charmbracelet/lipgloss/commit/40ec0e6): docs: fix typo in table comment ([#&#8203;641](https://github.com/charmbracelet/lipgloss/issues/641)) ([@&#8203;aymanbagabas](https://github.com/aymanbagabas))
- [`a4d0b40`](https://github.com/charmbracelet/lipgloss/commit/a4d0b40): docs: restore missing diaereses ([#&#8203;664](https://github.com/charmbracelet/lipgloss/issues/664)) ([@&#8203;meowgorithm](https://github.com/meowgorithm))

##### Chore

- [`aa91b99`](https://github.com/charmbracelet/lipgloss/commit/aa91b99): chore: remove CODEOWNERS ([@&#8203;aymanbagabas](https://github.com/aymanbagabas))
- [`9cbfe8b`](https://github.com/charmbracelet/lipgloss/commit/9cbfe8b): chore(lint): exclude revive naming linter ([@&#8203;aymanbagabas](https://github.com/aymanbagabas))

***

<a href="https://charm.land/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-banner-next.jpg" width="400"></a>

Thoughts? Questions? We love hearing from you. Feel free to reach out on [X](https://x.com/charmcli), [Discord](https://charm.land/discord), [Slack](https://charm.land/slack), [The Fediverse](https://mastodon.social/@&#8203;charmcli), [Bluesky](https://bsky.app/profile/charm.land).

</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.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- 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:eyJjcmVhdGVkSW5WZXIiOiI0My4xOTEuMiIsInVwZGF0ZWRJblZlciI6IjQzLjE5MS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/tea/pulls/1039
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-06-18 02:32:16 +00:00
Renovate Bot 2b844786f0 fix(deps): update go dependencies (#1037)
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.12.0` → `v0.14.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fgo-authgate%2fsdk-go/v0.14.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fgo-authgate%2fsdk-go/v0.12.0/v0.14.0?slim=true) |
| [github.com/urfave/cli/v3](https://github.com/urfave/cli) | `v3.9.0` → `v3.9.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2furfave%2fcli%2fv3/v3.9.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2furfave%2fcli%2fv3/v3.9.0/v3.9.1?slim=true) |

---

### Release Notes

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

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

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

#### Changelog

##### Features

- [`1e9404e`](https://github.com/go-authgate/sdk-go/commit/1e9404eea69d8f6de17177a98a8ca33a915dee01): feat(credstore): add DefaultStorePath path helper ([@&#8203;appleboy](https://github.com/appleboy))
- [`3498105`](https://github.com/go-authgate/sdk-go/commit/34981059ec8287317d5160a292d05a749d17f888): feat(credstore): add DefaultTokenStorePath shorthand ([@&#8203;appleboy](https://github.com/appleboy))

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

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

#### Changelog

##### Others

- [`fc432e3`](https://github.com/go-authgate/sdk-go/commit/fc432e3da28dbca06767d1be5ace784092974527): feat(credstore)!: add encrypted file store with keyring master key ([#&#8203;35](https://github.com/go-authgate/sdk-go/issues/35)) ([@&#8203;appleboy](https://github.com/appleboy))

</details>

<details>
<summary>urfave/cli (github.com/urfave/cli/v3)</summary>

### [`v3.9.1`](https://github.com/urfave/cli/releases/tag/v3.9.1)

[Compare Source](https://github.com/urfave/cli/compare/v3.9.0...v3.9.1)

#### What's Changed

- fix: inherit Reader/Writer/ErrWriter from parent on subcommand setup by [@&#8203;c-tonneslan](https://github.com/c-tonneslan) in [#&#8203;2329](https://github.com/urfave/cli/pull/2329)
- fix: correct greedy colon parsing in bash completion script by [@&#8203;TimSoethout](https://github.com/TimSoethout) in [#&#8203;2336](https://github.com/urfave/cli/pull/2336)
- fix: Honor env sources on duplicated flags by [@&#8203;ibobgunardi](https://github.com/ibobgunardi) in [#&#8203;2355](https://github.com/urfave/cli/pull/2355)
- fix: Remove incorrect flag check by [@&#8203;MohitPanchariya](https://github.com/MohitPanchariya) in [#&#8203;2280](https://github.com/urfave/cli/pull/2280)
- fix: let completion command use normal flag parsing pipeline by [@&#8203;suzuki-shunsuke](https://github.com/suzuki-shunsuke) in [#&#8203;2279](https://github.com/urfave/cli/pull/2279)
- fix: bash completion spacing by [@&#8203;cyphercodes](https://github.com/cyphercodes) in [#&#8203;2337](https://github.com/urfave/cli/pull/2337)
- fix: fish custom completion arguments by [@&#8203;puneetdixit200](https://github.com/puneetdixit200) in [#&#8203;2338](https://github.com/urfave/cli/pull/2338)
- fix: empty positional arg after a flag regression test by [@&#8203;c-tonneslan](https://github.com/c-tonneslan) in [#&#8203;2328](https://github.com/urfave/cli/pull/2328)
- docs: document build tags in v3 migration guide by [@&#8203;dearchap](https://github.com/dearchap) in [#&#8203;2358](https://github.com/urfave/cli/pull/2358)
- chore(deps): bump mkdocs-git-revision-date-localized-plugin by [@&#8203;dependabot](https://github.com/dependabot) in [#&#8203;2347](https://github.com/urfave/cli/pull/2347)
- Fix typos by [@&#8203;myfloss](https://github.com/myfloss) in [#&#8203;2339](https://github.com/urfave/cli/pull/2339)

**Full Changelog**: <https://github.com/urfave/cli/compare/v3.9.0...v3.9.1>

</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.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- 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:eyJjcmVhdGVkSW5WZXIiOiI0My4xOTEuMiIsInVwZGF0ZWRJblZlciI6IjQzLjE5MS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/tea/pulls/1037
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-06-16 21:09:11 +00:00
LeonM 67bb0e7789 feat(pulls): show PR URL in detail view (#1036)
Fixes #1035.

## Summary
- include the pull request URL in `tea pr <number>` detail output
- keep `tea pr create` output to one URL by relying on the shared detail renderer
- add a focused regression test

## Tests
- `go test ./modules/print`
- `go test ./...`

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Leon Müller <mueller4leon@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/1036
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: LeonM <206012+leonm@noreply.gitea.com>
Co-committed-by: LeonM <206012+leonm@noreply.gitea.com>
2026-06-15 20:35:21 +00:00
Renovate Bot 6de58b2223 fix(deps): update go dependencies (#1034)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) | [`v0.52.0` → `v0.53.0`](https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.52.0...refs/tags/v0.53.0) | ![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2fcrypto/v0.53.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2fcrypto/v0.52.0/v0.53.0?slim=true) |
| [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) | [`v0.43.0` → `v0.44.0`](https://cs.opensource.google/go/x/term/+/refs/tags/v0.43.0...refs/tags/v0.44.0) | ![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2fterm/v0.44.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2fterm/v0.43.0/v0.44.0?slim=true) |

---

### 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.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- 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:eyJjcmVhdGVkSW5WZXIiOiI0My4xOTEuMiIsInVwZGF0ZWRJblZlciI6IjQzLjE5MS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/tea/pulls/1034
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-06-15 20:30:27 +00:00
Renovate Bot 27bcbf80ae fix(deps): update module golang.org/x/sys to v0.46.0 (#1028)
Reviewed-on: https://gitea.com/gitea/tea/pulls/1028
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-06-09 16:02:18 +00:00
Renovate Bot dfbbda83f3 fix(deps): update module gitea.dev/sdk to v1.1.0 (#1027)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| gitea.dev/sdk | `v1.0.1` → `v1.1.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/gitea.dev%2fsdk/v1.1.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/gitea.dev%2fsdk/v1.0.1/v1.1.0?slim=true) |

---

### 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:eyJjcmVhdGVkSW5WZXIiOiI0My4xOTEuMiIsInVwZGF0ZWRJblZlciI6IjQzLjE5MS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/tea/pulls/1027
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-06-08 22:42:38 +00:00
Renovate Bot df320c74ca fix(deps): update module github.com/go-authgate/sdk-go to v0.12.0 (#1024)
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.11.0` → `v0.12.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fgo-authgate%2fsdk-go/v0.12.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fgo-authgate%2fsdk-go/v0.11.0/v0.12.0?slim=true) |

---

### Release Notes

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

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

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

#### Changelog

##### Bug fixes

- [`506ff28`](https://github.com/go-authgate/sdk-go/commit/506ff28e68b10f348fb937c9d734b20ea72245a6): fix: authenticate confidential clients and harden token handling ([#&#8203;33](https://github.com/go-authgate/sdk-go/issues/33)) ([@&#8203;appleboy](https://github.com/appleboy))

##### Refactor

- [`81b51e2`](https://github.com/go-authgate/sdk-go/commit/81b51e244f1ce5f069d6e2a0ac18a1ffb6d25706): refactor: simplify control flow across sdk packages ([#&#8203;30](https://github.com/go-authgate/sdk-go/issues/30)) ([@&#8203;appleboy](https://github.com/appleboy))
- [`7fcdb3c`](https://github.com/go-authgate/sdk-go/commit/7fcdb3cde0b80754262e81bd2e2d86baf8f00f64): refactor(oauth): use error-code constants at all sites ([#&#8203;31](https://github.com/go-authgate/sdk-go/issues/31)) ([@&#8203;appleboy](https://github.com/appleboy))
- [`4407ea6`](https://github.com/go-authgate/sdk-go/commit/4407ea621e8f2f14cb8629dfb80cb4d1e536099e): refactor(middleware): split token scope once for required-scope checks ([#&#8203;32](https://github.com/go-authgate/sdk-go/issues/32)) ([@&#8203;appleboy](https://github.com/appleboy))

##### Documentation updates

- [`f8b8b28`](https://github.com/go-authgate/sdk-go/commit/f8b8b28fd70f30799933e0592878f7e1146a71ea): docs(jwksauth): document UID across remaining godoc and test comments ([@&#8203;appleboy](https://github.com/appleboy))

##### Others

- [`a90443e`](https://github.com/go-authgate/sdk-go/commit/a90443e6226c4f401f40ed9917af2b19cd2f1747): fix(discovery,middleware): coalesce concurrent discovery fetches via singleflight with per-caller cancellation; route scope errors through the error handler ([#&#8203;16](https://github.com/go-authgate/sdk-go/issues/16)) ([@&#8203;appleboy](https://github.com/appleboy))

</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:eyJjcmVhdGVkSW5WZXIiOiI0My4xOTEuMiIsInVwZGF0ZWRJblZlciI6IjQzLjE5MS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/tea/pulls/1024
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-06-05 03:55:19 +00:00
dinsmoor 09fc09c2f7 feat(comments): add list/edit/delete subcommands to tea comment (#1015)
## Why

Today `tea comment` can only *add* a comment. Editing or deleting requires falling back to `tea api`. This came up while I was iterating on PRs in this same repo earlier today and had to correct a couple of comments by hand. Every comparable forge CLI (gh, glab, etc.) exposes these operations as first-class commands.

## What

Restructures `tea comment` from a single-action command into a parent with four subcommands. The parent's default action remains the existing "add" behavior, so the historical shorthand keeps working.

| Command | Purpose |
|---|---|
| `tea comment add <idx> [<body>]` | Add a comment (explicit subcommand) |
| `tea comment list <idx>` | Tabular listing including comment IDs |
| `tea comment edit <id> [<body>]` | Replace the body of one comment |
| `tea comment delete <id> [<id>...]` | Delete one or more comments |
| `tea comment <idx> [<body>]` | Unchanged — still routes to `add` |

The `list` command exists specifically so users can discover the IDs that `edit` and `delete` accept.

## Backward compatibility

The whole point of routing the parent's default `Action` through `add` is to preserve every existing invocation. `tea comment 1 "body"` still does what it did before. No flag or arg names change.

## Input forms (for add and edit)

Same pattern as the original `tea comment`:

1. Positional body (`tea comment edit <id> "new body"`) — wins if present.
2. Piped stdin if no positional body is given.
3. External `$EDITOR` (pre-populated with the current body, on `edit`) if neither.

This matches the stdin-handling fix in #1011 — positional body wins over a non-TTY stdin so the command doesn't hang in CI/subshells.

## Verification

All four subcommands were exercised live against `https://gitea.com/dinsmoor/tea-testing` issue #1. The test artifacts and a summary log are visible on that issue right now. Specifically:

- The annotated summary comment lists every operation tested and the comment IDs each one acted on.
- Comments 1197162 (legacy add), 1197163 (subcommand add, later edited), 1197164 (stdin add) are still there to be inspected.
- Comment 1197166 was created and then deleted; its absence from `tea comment list` output is evidence that delete works.

## New files

- `cmd/comments/add.go` — extracted from the old `cmd/comment.go`
- `cmd/comments/list.go`
- `cmd/comments/edit.go`
- `cmd/comments/delete.go`
- `modules/print/comment.go` — adds `CommentsList` helper for the tabular output

`cmd/comment.go` is rewritten as a thin parent that wires these together.

## Open questions for the reviewer

- **Naming**: should the top-level command be `comments` (plural) or stay `comment` (singular)? I kept it singular with `comments` as an alias to match the existing user-visible name.
- **Delete confirmation**: I did not add a confirmation prompt — `delete` just deletes. Some projects gate this behind `--yes` / interactive `[y/N]`. I'd rather follow whatever convention the maintainers prefer.
- **Output format on list**: currently uses the existing `print.tableWithHeader` helper, matching `tea organizations list` etc. Other tea listings support `--output json` / `--output csv` via the shared `--output` flag, which works here automatically through the same helper.

---

This patch was authored interactively with an AI assistant, driven and reviewed by a human (Tyler / @dinsmoor) every step.

*pull request created by Tyler's lovingly wrangled demon machine <3*

Reviewed-on: https://gitea.com/gitea/tea/pulls/1015
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: dinsmoor <204368+dinsmoor@noreply.gitea.com>
Co-committed-by: dinsmoor <204368+dinsmoor@noreply.gitea.com>
2026-05-31 22:20:48 +00:00
Tyler 5fa24b9a65 feat(pulls): add --draft to create and --draft/--ready to edit (#1008)
Closes #979. An alternative to the approach in #980 — this one is purely client-side title-mangling, no SDK changes needed.

Gitea already treats any PR with a `WIP:` or `[WIP]` title prefix (case-insensitive) as a draft. This patch wires three flags around that behavior:

- `tea pulls create --draft` — prepend `WIP: ` to the title at creation time
- `tea pulls edit --draft <idx>` — add `WIP: ` to an existing PR's title
- `tea pulls edit --ready <idx>` — strip any recognized draft prefix

All three are idempotent. `--draft` and `--ready` on edit are mutually exclusive. If the user also passes `--title` on edit, the toggle applies to the supplied title; otherwise the current title is fetched from the server first.

Why this approach over a server-payload-based one: Gitea's draft state is *defined* as the title-prefix convention (see the Gitea source for `HasWIPPrefix`). Modeling it server-side would either duplicate or fight that. A small string helper covers it without needing the SDK to add a `Draft` field.

Verified against `gitea.com` (1.26.0+dev) with a throwaway repo:
- create with `--draft` → server reports `draft: true` ✓
- `edit --ready` strips → `draft: false` ✓
- `edit --draft` adds back → `draft: true` ✓
- second `edit --draft` is idempotent ✓
- `edit --draft --ready` errors ✓

Unit tests for the prefix detection live in `modules/utils/draft_test.go`.

---

This patch was authored interactively with an AI assistant, driven and reviewed by a human (Tyler / @dinsmoor) every step. Reproduction, design decisions, and the choice not to follow #980's payload approach were mine — happy to discuss any of it.

*pull request created by Tyler's lovingly wrangled demon machine <3*

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/1008
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Tyler <tyler@dinsmoor.us>
Co-committed-by: Tyler <tyler@dinsmoor.us>
2026-05-31 02:50:32 +00:00
Tyler 18274f1ebc fix(pulls): restore standard fork-flow PR creation (#1010)
Closes #1009.

## The standard fork-flow

The textbook workflow for opening a PR with any git CLI (gh, glab, hub, tea, etc.) is:

1. Fork `upstream/repo` on the server.
2. Clone the fork locally; add `upstream` as a second remote.
3. Branch off, commit, push the branch to your fork.
4. Tell the tool: "open a PR on `upstream/repo`, source is `fork:branch`".

In tea that's:

```
tea pulls create --repo upstream/repo --head fork:branch --base main
```

This is the flow tea supported before #850 and the flow this repo's own contribution model assumes.

## What broke

#850 ("Enable git worktree support and improve pr create error handling", Nov 2025) correctly addressed worktree detection by adding `LocalRepo: true` to `runPullsCreate`'s `CtxRequirement`. But that requirement is checked before `--repo` is interpreted, and a slug-style `--repo gitea/tea` doesn't satisfy `LocalRepo` — only a path does.

Result: the standard fork-flow invocation above started failing with:

```
Error: local repository required: execute from a repo dir, or specify a path with --repo
```

— which is misleading, because the user IS in a repo dir and `--repo` IS set.

## The fix

Only require `LocalRepo` when the command genuinely needs the working tree:

- **Interactive mode** (no flags) — uses tree info for prompts.
- **`--head` omitted** — defaults head from the current branch.

Otherwise the command runs with just `RemoteRepo`, and `task.CreatePull` takes the explicit `--repo` and `--head` as given. The #850 worktree fix is preserved for the in-tree path.

## Behavior table

| Invocation | Before | After |
|---|---|---|
| `tea pulls create` (interactive) | works | works (unchanged) |
| `tea pulls create --head my-branch --base main` (in-tree, same repo) | works | works (unchanged) |
| `tea pulls create --repo upstream/repo --head fork:branch --base main` (fork-flow) | **fails with misleading error** | **works** |
| `tea pulls create --repo upstream/repo --base main` (no `--head`, no working tree) | fails | still fails with the same error (correctly — head can't be defaulted without a working tree or explicit flag) |

## Verification

Smoke-tested by opening a PR from `/tmp` (deliberately not a git repo):

```
cd /tmp
tea-dev pulls create --login gitea.com --repo dinsmoor/tea-testing \
                     --head dinsmoor:xfork-smoke --base main \
                     --title "xfork smoke test" --description "..."
→ https://gitea.com/dinsmoor/tea-testing/pulls/4
```

And in fact, **this very PR was opened using the patched binary** invoked from `/tmp`, since the released `tea` still has the bug.

---

This patch was authored interactively with an AI assistant, driven and reviewed by a human (Tyler / @dinsmoor) every step.

*pull request created by Tyler's lovingly wrangled demon machine <3*

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/1010
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Tyler <tyler@dinsmoor.us>
Co-committed-by: Tyler <tyler@dinsmoor.us>
2026-05-28 18:29:11 +00:00
Tyler d1b9b7735e fix(comment): don't block on stdin when body is given positionally (#1011)
## The bug

`tea comment <idx> "body"` hangs forever in any non-TTY context — CI pipelines, subshells, agent harnesses, scripts — unless the user explicitly redirects stdin with `< /dev/null`.

### Reproduction (released `tea` 0.14.1)

```
# Plain positional body — hangs forever
$ tea comment --repo owner/repo 1 "body text"

# Heredoc body — hangs forever
$ tea comment --repo owner/repo 1 "$(cat <<'EOM'
body text
EOM
)"

# Workaround that works (but shouldn't be necessary)
$ tea comment --repo owner/repo 1 "body text" < /dev/null
```

Verified on `gitea.com` (server 1.26.0+dev) with `tea 0.14.1`. Both hanging cases hit a 30s timeout in testing; in normal shell use they hang indefinitely.

## Root cause

`cmd/comment.go:58-65`:

```go
body := strings.Join(ctx.Args().Tail(), " ")
if interact.IsStdinPiped() {
    if bodyStdin, err := io.ReadAll(ctx.Reader); err != nil {
        return err
    } else if len(bodyStdin) != 0 {
        body = strings.Join([]string{body, string(bodyStdin)}, "\n\n")
    }
}
```

`interact.IsStdinPiped()` is implemented as `!term.IsTerminal(os.Stdin)` — true for *any* non-TTY stdin, not just for piped data. When tea enters this branch in a subshell where stdin is open but no producer ever writes to it (the typical case for shell scripts and automation), `io.ReadAll` blocks waiting for an EOF that never arrives.

## Fix

Only consume stdin when the user did **not** supply a positional body. If they passed a body via args, that's their content — ignore stdin entirely.

```go
if len(body) == 0 && interact.IsStdinPiped() {
    // ... read stdin ...
    body = string(bodyStdin)
}
```

## Behavior matrix

| Invocation | Before | After |
|---|---|---|
| `tea comment <idx> "body"` (TTY) | works | works |
| `tea comment <idx> "body"` (non-TTY, no redirect) | **hangs forever** | **works** |
| `tea comment <idx> "body" < /dev/null` | works | works |
| `echo body \| tea comment <idx>` | works | works |
| `tea comment <idx> "prefix"` + piped stdin | "prefix" + stdin concatenated | positional body wins, stdin ignored |
| `tea comment <idx>` (TTY, no body, no pipe) | opens editor | opens editor |

The one behavior change is the prefix-plus-stdin case (5th row). I couldn't find anyone relying on that pattern and it wasn't documented; defaulting to "positional body wins" matches the principle of least surprise.

## Verification

Confirmed each row against `dinsmoor/tea-testing` issue #1 on `gitea.com` with the patched binary. Previously-hanging invocations now post in <0.5s. Piped-stdin path unchanged.

---

This patch was authored interactively with an AI assistant, driven and reviewed by a human (Tyler / @dinsmoor) every step.

*pull request created by Tyler's lovingly wrangled demon machine <3*

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/1011
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Tyler <tyler@dinsmoor.us>
Co-committed-by: Tyler <tyler@dinsmoor.us>
2026-05-28 17:45:44 +00:00
Tyler 6dd33b5f4f docs(login): make the git credential helper discoverable (#1014)
Closes #1013.

## Background

While trying to use `tea` from a non-interactive context I hit a friction point: after `tea login add` succeeded, plain `git push` over HTTPS still prompted for credentials. I filed #1013 as a feature request to add credential helper integration — then found, on reading the source, that the integration **already exists**:

- `tea login add` accepts `--helper` (alias `-j`), which calls `task.SetupHelper` to register a credential helper in `~/.gitconfig`.
- `tea login helper get` correctly implements git's credential protocol, reading the request from stdin and returning `protocol`/`host`/`username`/`password` lines.
- `tea login helper setup` does the same for every configured login.

I verified end-to-end that this works as advertised: after `tea login helper setup`, an HTTPS `git push` against a configured Gitea host authenticates silently using the stored token, no prompts, with `GIT_TERMINAL_PROMPT=0` set as a safety check.

So the feature is fine. The problem is that nobody can find it:

| Surface | Before | Issue |
|---|---|---|
| Flag name on `tea login add` | `--helper` (alias `-j`) | Generic; nothing tying it to git or credentials |
| Flag usage text | `"Add helper"` | Says nothing |
| `tea login helper` command | `Hidden: true` | Not in `tea login --help` |
| `tea login helper` usage | `"Git helper"` | Says nothing |
| `tea login helper` description | `"Git helper"` | Same string again |
| `store/erase` subcommand description | `"Command drops"` | Sentence fragment, no meaning |
| `setup` subcommand description | `"Setup helper to tea authenticate"` | Awkward, doesn't explain what it touches |
| `get` subcommand description | `"Get token to auth"` | Doesn't mention git, stdin, or the credential protocol |
| Mention in `tea login add --help` | None | Feature is invisible |

## What this patch does

Purely cosmetic / documentation changes — **no behavior changes**:

1. Renames `--helper` to `--git-credentials`, keeping `--helper` and `-j` as aliases so existing scripts and muscle memory keep working.
2. Removes `Hidden: true` from `tea login helper` so it appears in `tea login --help`.
3. Rewrites every placeholder `Usage` and `Description` string in the helper command tree to describe what the thing actually does.
4. Expands the top-level `Description` of `tea login add` to mention the option and explain what it does.
5. Prints a one-line hint after a successful non-helper login: `Tip: pass --git-credentials (or run 'tea login helper setup') to authenticate 'git push' and 'git clone' over HTTPS with this token.`

The credential helper protocol implementation, `SetupHelper`'s gitconfig writes, and the `get`/`store`/`setup` action functions are all unchanged.

## Help output after the patch

```
$ tea login --help
COMMANDS:
   ...
   helper, git-credential  Act as a git credential helper for stored Gitea logins
   ...

$ tea login helper --help
NAME:
   tea logins helper - Act as a git credential helper for stored Gitea logins

DESCRIPTION:
   Speaks git's credential helper protocol so that HTTPS push and clone
   operations against your configured Gitea instances authenticate silently
   using the tokens tea already stores.

   Typical use is automatic: 'tea login add --git-credentials' (or 'tea login
   helper setup' for existing logins) registers '!tea login helper' as a
   credential helper in ~/.gitconfig. Git then invokes the 'get' subcommand
   when it needs credentials for a configured host.

COMMANDS:
   store, erase  No-op (git credential protocol store/erase)
   setup         Register tea as a git credential helper for every configured login
   get           Return the stored token for a URL (git credential protocol)
```

## Open questions for the reviewer

A few choices in here that are subjective — happy to change any of them:

- **Flag name**: `--git-credentials` was the first clear name I tried. `--credential-helper` and `--git-helper` are also reasonable. Or keep `--helper` as canonical and just fix its usage text.
- **Canonical subcommand name**: I kept `helper` as canonical with `git-credential` as alias, matching what was already there. Could flip this — `gh` uses `gh auth git-credential` as canonical with no `helper` form.
- **Should `--git-credentials` default to true?** Most users probably want it on; the current opt-in design surprises them. But flipping the default is a behavior change so I left it alone here.
- **Should the hint be silenced by an env var or a config flag?** I left it always-on for the people who need to see it; can gate it if it bothers automation users.
- **Teardown on `tea login delete`** would parallel the setup behavior, but is genuinely a separate change. Not in this PR.

---

This patch was authored interactively with an AI assistant, driven and reviewed by a human (Tyler / @dinsmoor) every step.

*pull request created by Tyler's lovingly wrangled demon machine <3*

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/1014
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Tyler <tyler@dinsmoor.us>
Co-committed-by: Tyler <tyler@dinsmoor.us>
2026-05-28 17:38:58 +00:00
Tyler 23a3967e15 fix(print): distinguish draft PRs from conflicting PRs (#1012)
## The bug

`tea pulls create` and `tea pulls edit` print `• **Conflicting files**` for any open PR that the server reports as `mergeable: false`. But Gitea's API returns `mergeable: false` for **draft PRs by design** — drafts cannot be merged regardless of conflict state. The current code conflates "not mergeable for any reason" with "has file conflicts."

### Reproduction (gitea.com 1.26.0+dev)

```
$ tea pulls create --base main --head my-branch --title "WIP: clean diff" --description "..."
  # #N WIP: clean diff (open)
  ...
  • **Conflicting files**           ← misleading
  • Maintainers are allowed to edit
```

The PR has no actual conflicts. The web UI shows it as draft + cleanly diffable. `tea pulls edit --title "WIP: ..."` shows the same misleading line.

API confirms the root signal:

```
$ curl /api/v1/repos/owner/repo/pulls/N
{ "draft": true, "mergeable": false, ... }
```

Strip the WIP prefix, and `mergeable` flips back to `true`. So `mergeable=false` here means "blocked because draft," not "conflicts."

## Fix

Distinguish the two reasons in `modules/print/pull.go`:

```go
switch {
case pr.Mergeable:
    out += "- No Conflicts\n"
case pr.Draft:
    out += "- Draft (not mergeable until marked ready)\n"
default:
    out += "- **Conflicting files**\n"
}
```

Real conflicts still fall through to the existing "Conflicting files" message.

## Behavior matrix

| PR state | API `mergeable` | Before | After |
|---|---|---|---|
| Open, clean, ready | true | No Conflicts | No Conflicts |
| Open, draft, clean | false | **Conflicting files** (wrong) | **Draft (not mergeable until marked ready)** |
| Open, conflicting | false | Conflicting files | Conflicting files |
| Closed/merged | — | (no line shown) | (no line shown) |

## Verification

Against `dinsmoor/tea-testing` PR #6 on gitea.com:

- Set title to `WIP: ...` → API `mergeable=false`, `draft=true` → tea prints `Draft (not mergeable until marked ready)` ✓
- Strip WIP → API `mergeable=true`, `draft=false` → tea prints `No Conflicts` ✓

---

This patch was authored interactively with an AI assistant, driven and reviewed by a human (Tyler / @dinsmoor) every step.

*pull request created by Tyler's lovingly wrangled demon machine <3*

Reviewed-on: https://gitea.com/gitea/tea/pulls/1012
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Tyler <tyler@dinsmoor.us>
Co-committed-by: Tyler <tyler@dinsmoor.us>
2026-05-28 17:29:00 +00:00
kuil09 06e4d16bf3 feat: add wiki CLI commands (#998)
## Summary

Add first-class `tea wiki` commands backed by the existing Gitea wiki API and SDK support.

## What this adds

- `tea wiki list`
- `tea wiki view <page>`
- `tea wiki revisions <page>`
- `tea wiki create`
- `tea wiki edit <page>`
- `tea wiki delete <page>`

## Implementation details

- registers a new top-level `wiki` entity command
- keeps command logic under `cmd/wiki/`
- adds wiki-specific renderers in `modules/print/wiki.go`
- adds wiki task helpers in `modules/task/wiki.go`
- reuses existing repo/login/output/pagination patterns used elsewhere in `tea`
- base64-encodes wiki content for create/edit API calls
- requires explicit `--confirm` for delete
- preserves the current page title during edit when `--title` is omitted

## Test coverage

The PR is intentionally split into two commits:

1. `feat: add wiki CLI commands`
2. `test: add wiki integration coverage`

Validation performed:

- focused command, task, and print tests for the new wiki functionality
- integration coverage for the wiki command lifecycle
- `make lint`
- `make fmt-check`
- `make docs-check`
- `make build`
- upstream PR CI passed:
  - `check-and-test / Integration Test`
  - `check-and-test / Lint Build And Unit Coverage`

## Motivation

This makes `tea` a better interface for both human and agent-driven workflows by exposing wiki operations as stable first-class CLI commands instead of requiring ad-hoc API calls or custom wrappers.

---
Generated by Hermes Agent with GPT-5.4

---------

Co-authored-by: nitro <nitro@nitroui-Macmini.local>
Reviewed-on: https://gitea.com/gitea/tea/pulls/998
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: kuil09 <202447+kuil09@noreply.gitea.com>
Co-committed-by: kuil09 <202447+kuil09@noreply.gitea.com>
2026-05-26 21:14:47 +00:00
Lunny Xiao 28ba9b915b Move sdk from code.gitea.io/sdk/gitea to gitea.dev/sdk (#1006)
Reviewed-on: https://gitea.com/gitea/tea/pulls/1006
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
2026-05-26 04:51:09 +00:00
Minjie Fang 579099f9d9 fix(context): improve local repo detection logic and test (#999)
Fix https://gitea.com/gitea/tea/issues/995

Reviewed-on: https://gitea.com/gitea/tea/pulls/999
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Minjie Fang <wingsallen@gmail.com>
Co-committed-by: Minjie Fang <wingsallen@gmail.com>
2026-05-25 22:12:19 +00:00
Lunny Xiao a664449282 Use git command instead of go git (#1005)
Remove go git library because it doesn't support sha256 repository but have an interface so that we could have other backend for the future.

Reviewed-on: https://gitea.com/gitea/tea/pulls/1005
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
2026-05-23 20:24:47 +00:00
techknowlogick 8e0666ab85 update import path to use gitea.dev (#1003)
Reviewed-on: https://gitea.com/gitea/tea/pulls/1003
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
2026-05-23 17:26:43 +00:00
Renovate Bot bbe97a5e23 fix(deps): update module golang.org/x/crypto to v0.52.0 [security] (#1002)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) | [`v0.51.0` → `v0.52.0`](https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.51.0...refs/tags/v0.52.0) | ![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2fcrypto/v0.52.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2fcrypto/v0.51.0/v0.52.0?slim=true) |

---

### Invoking key constraints not enforced in golang.org/x/crypto/ssh/agent
[CVE-2026-39833](https://nvd.nist.gov/vuln/detail/CVE-2026-39833) / [GO-2026-5005](https://pkg.go.dev/vuln/GO-2026-5005)

<details>
<summary>More information</summary>

#### Details
The in-memory keyring returned by NewKeyring() silently accepted keys with the ConfirmBeforeUse constraint but never enforced it. The key would sign without any confirmation prompt, with no indication to the caller that the constraint was not in effect. NewKeyring() now returns an error when unsupported constraints are requested.

#### Severity
Unknown

#### References
- [https://go.dev/issue/79436](https://go.dev/issue/79436)
- [https://go.dev/cl/778640](https://go.dev/cl/778640)
- [https://go.dev/cl/778641](https://go.dev/cl/778641)
- [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5005) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Invoking agent constraints dropped when forwarding keys in golang.org/x/crypto/ssh/agent
[CVE-2026-39832](https://nvd.nist.gov/vuln/detail/CVE-2026-39832) / [GO-2026-5006](https://pkg.go.dev/vuln/GO-2026-5006)

<details>
<summary>More information</summary>

#### Details
When adding a key to a remote agent constraint extensions such as restrict-destination-v00@&#8203;openssh.com were not serialized in the request. Destination restrictions were silently stripped when forwarding keys, allowing unrestricted use of the key on the remote host. The client now serializes all constraint extensions. Additionally, the in-memory keyring returned by NewKeyring() now rejects keys with unsupported constraint extensions instead of silently ignoring them.

#### Severity
Unknown

#### References
- [https://go.dev/issue/79435](https://go.dev/issue/79435)
- [https://go.dev/cl/778642](https://go.dev/cl/778642)
- [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5006) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Invoking byte arithmetic causes underflow and panic in golang.org/x/crypto/ssh
[CVE-2026-46597](https://nvd.nist.gov/vuln/detail/CVE-2026-46597) / [GO-2026-5013](https://pkg.go.dev/vuln/GO-2026-5013)

<details>
<summary>More information</summary>

#### Details
An incorrectly placed cast from bytes to int allowed for server-side panic in the AES-GCM packet decoder for well-crafted inputs.

#### Severity
Unknown

#### References
- [https://go.dev/issue/79561](https://go.dev/issue/79561)
- [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI)
- [https://go.dev/cl/781620](https://go.dev/cl/781620)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5013) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Invoking bypass of certificate restrictions in golang.org/x/crypto/ssh
[CVE-2026-39828](https://nvd.nist.gov/vuln/detail/CVE-2026-39828) / [GO-2026-5014](https://pkg.go.dev/vuln/GO-2026-5014)

<details>
<summary>More information</summary>

#### Details
When an SSH server authentication callback returned PartialSuccessError with non-nil Permissions, those permissions were silently discarded, potentially dropping certificate restrictions such as force-command after a second factor succeeded. Returning non-nil Permissions with PartialSuccessError now results in a connection error.

#### Severity
Unknown

#### References
- [https://go.dev/issue/79562](https://go.dev/issue/79562)
- [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI)
- [https://go.dev/cl/781621](https://go.dev/cl/781621)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5014) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Invoking server panic during CheckHostKey/Authenticate in golang.org/x/crypto/ssh
[CVE-2026-39835](https://nvd.nist.gov/vuln/detail/CVE-2026-39835) / [GO-2026-5015](https://pkg.go.dev/vuln/GO-2026-5015)

<details>
<summary>More information</summary>

#### Details
SSH servers which use CertChecker as a public key callback without setting IsUserAuthority or IsHostAuthority could be caused to panic by a client presenting a certificate. CertChecker now returns an error instead of panicking when these callbacks are nil.

#### Severity
Unknown

#### References
- [https://go.dev/issue/79563](https://go.dev/issue/79563)
- [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI)
- [https://go.dev/cl/781660](https://go.dev/cl/781660)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5015) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Invoking memory leak when rejecting channels can lead to DoS in golang.org/x/crypto/ssh
[CVE-2026-39827](https://nvd.nist.gov/vuln/detail/CVE-2026-39827) / [GO-2026-5016](https://pkg.go.dev/vuln/GO-2026-5016)

<details>
<summary>More information</summary>

#### Details
An authenticated SSH client that repeatedly opened channels which were rejected by the server caused unbounded memory growth, eventually crashing the server process and affecting all connected users. Rejected channels are now properly removed from the connection's internal state and released for garbage collection.

#### Severity
Unknown

#### References
- [https://go.dev/issue/35127](https://go.dev/issue/35127)
- [https://go.dev/cl/781320](https://go.dev/cl/781320)
- [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5016) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Invoking client can cause server deadlock on unexpected responses in golang.org/x/crypto/ssh
[CVE-2026-39830](https://nvd.nist.gov/vuln/detail/CVE-2026-39830) / [GO-2026-5017](https://pkg.go.dev/vuln/GO-2026-5017)

<details>
<summary>More information</summary>

#### Details
A malicious SSH peer could send unsolicited global request responses to fill an internal buffer, blocking the connection's read loop. The blocked goroutine could not be released by calling Close(), resulting in a resource leak per connection. Unsolicited global responses are now discarded.

#### Severity
Unknown

#### References
- [https://go.dev/issue/79564](https://go.dev/issue/79564)
- [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI)
- [https://go.dev/cl/781640](https://go.dev/cl/781640)
- [https://go.dev/cl/781664](https://go.dev/cl/781664)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5017) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Invoking pathological RSA/DSA parameters may cause DoS in golang.org/x/crypto/ssh
[CVE-2026-39829](https://nvd.nist.gov/vuln/detail/CVE-2026-39829) / [GO-2026-5018](https://pkg.go.dev/vuln/GO-2026-5018)

<details>
<summary>More information</summary>

#### Details
The RSA and DSA public key parsers did not enforce size limits on key parameters. A crafted public key with an excessively large modulus or DSA parameter could cause several minutes of CPU consumption during signature verification. This could be triggered by unauthenticated clients during public key authentication. RSA moduli are now limited to 8192 bits, and DSA parameters are validated per FIPS 186-2.

#### Severity
Unknown

#### References
- [https://go.dev/issue/79565](https://go.dev/issue/79565)
- [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI)
- [https://go.dev/cl/781641](https://go.dev/cl/781641)
- [https://go.dev/cl/781661](https://go.dev/cl/781661)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5018) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Invoking bypass of FIDO/U2F security keys physical interaction in golang.org/x/crypto/ssh
[CVE-2026-39831](https://nvd.nist.gov/vuln/detail/CVE-2026-39831) / [GO-2026-5019](https://pkg.go.dev/vuln/GO-2026-5019)

<details>
<summary>More information</summary>

#### Details
The Verify() method for FIDO/U2F security key types (sk-ecdsa-sha2-nistp256@&#8203;openssh.com, sk-ssh-ed25519@&#8203;openssh.com) did not check the User Presence flag. Signatures generated without physical touch were accepted, allowing unattended use of a hardware security key. To restore the previous behavior, return a "no-touch-required" extension in Permissions.Extensions from PublicKeyCallback.

#### Severity
Unknown

#### References
- [https://go.dev/issue/79566](https://go.dev/issue/79566)
- [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI)
- [https://go.dev/cl/781662](https://go.dev/cl/781662)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5019) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Invoking infinite loop on large channel writes in golang.org/x/crypto/ssh
[CVE-2026-39834](https://nvd.nist.gov/vuln/detail/CVE-2026-39834) / [GO-2026-5020](https://pkg.go.dev/vuln/GO-2026-5020)

<details>
<summary>More information</summary>

#### Details
When writing data larger than 4GB in a single Write call on an SSH channel, an integer overflow in the internal payload size calculation caused the write loop to spin indefinitely, sending empty packets without making progress. The size comparison now uses int64 to prevent truncation.

#### Severity
Unknown

#### References
- [https://go.dev/issue/79567](https://go.dev/issue/79567)
- [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI)
- [https://go.dev/cl/781663](https://go.dev/cl/781663)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5020) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Invoking auth bypass via unenforced @&#8203;revoked status in golang.org/x/crypto/ssh/knownhosts
[CVE-2026-42508](https://nvd.nist.gov/vuln/detail/CVE-2026-42508) / [GO-2026-5021](https://pkg.go.dev/vuln/GO-2026-5021)

<details>
<summary>More information</summary>

#### Details
Previously, a revoked 'SignatureKey' belonging to a CA was not correctly checked for revocation. Now, both the 'key' and 'key.SignatureKey' are checked for @&#8203;revoked.

#### Severity
Unknown

#### References
- [https://go.dev/issue/79568](https://go.dev/issue/79568)
- [https://go.dev/cl/781220](https://go.dev/cl/781220)
- [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5021) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Invoking VerifiedPublicKeyCallback permissions skip enforcement in golang.org/x/crypto/ssh
[CVE-2026-46595](https://nvd.nist.gov/vuln/detail/CVE-2026-46595) / [GO-2026-5023](https://pkg.go.dev/vuln/GO-2026-5023)

<details>
<summary>More information</summary>

#### Details
Previously, CVE-2024-45337 fixed an authorization bypass for misused ssh server configurations; if any other type of callback is passed other than public key, then the source-address validation would be skipped.

#### Severity
Unknown

#### References
- [https://go.dev/issue/79570](https://go.dev/issue/79570)
- [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI)
- [https://go.dev/cl/781642](https://go.dev/cl/781642)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5023) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Invoking pathological inputs can lead to client panic in golang.org/x/crypto/ssh/agent
[CVE-2026-46598](https://nvd.nist.gov/vuln/detail/CVE-2026-46598) / [GO-2026-5033](https://pkg.go.dev/vuln/GO-2026-5033)

<details>
<summary>More information</summary>

#### Details
For certain crafted inputs, a 'ed25519.PrivateKey' was created by casting malformed wire bytes, leading to a panic when used.

#### Severity
Unknown

#### References
- [https://go.dev/issue/79596](https://go.dev/issue/79596)
- [https://go.dev/cl/781360](https://go.dev/cl/781360)
- [https://groups.google.com/g/golang-announce/c/a082jnz-LvI](https://groups.google.com/g/golang-announce/c/a082jnz-LvI)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5033) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - ""
- 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:eyJjcmVhdGVkSW5WZXIiOiI0My4xOTEuMiIsInVwZGF0ZWRJblZlciI6IjQzLjE5MS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

---------

Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/1002
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-05-22 21:21:18 +00:00
Renovate Bot 82323c7270 chore(deps): update module golang.org/x/net to v0.55.0 [security] (#1001)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) | [`v0.54.0` → `v0.55.0`](https://cs.opensource.google/go/x/net/+/refs/tags/v0.54.0...refs/tags/v0.55.0) | ![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2fnet/v0.55.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2fnet/v0.54.0/v0.55.0?slim=true) |

---

### Invoking incorrect handling of namespaced elements in foreign content in golang.org/x/net/html
[CVE-2026-42506](https://nvd.nist.gov/vuln/detail/CVE-2026-42506) / [GO-2026-5025](https://pkg.go.dev/vuln/GO-2026-5025)

<details>
<summary>More information</summary>

#### Details
Parsing arbitrary HTML which is then rendered using Render can result in an unexpected HTML tree. This can be leveraged to execute XSS attacks in applications that attempt to sanitize input HTML before rendering.

#### Severity
Unknown

#### References
- [https://go.dev/issue/79571](https://go.dev/issue/79571)
- [https://groups.google.com/g/golang-announce/c/iI-mYSI0lu8](https://groups.google.com/g/golang-announce/c/iI-mYSI0lu8)
- [https://go.dev/cl/781700](https://go.dev/cl/781700)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5025) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Invoking failure to reject ASCII-only Punycode-encoded labels in golang.org/x/net/idna
[CVE-2026-39821](https://nvd.nist.gov/vuln/detail/CVE-2026-39821) / [GO-2026-5026](https://pkg.go.dev/vuln/GO-2026-5026)

<details>
<summary>More information</summary>

#### Details
The ToASCII and ToUnicode functions incorrectly accept Punycode-encoded labels that decode to an ASCII-only label. For example, ToUnicode("xn--example-.com") incorrectly returns the name "example.com" rather than an error.

This behavior can lead to privilege escalation in programs using the idna package. For example, a program which performs privilege checks on the ASCII hostname may reject "example.com" but permit "xn--example-.com". If that program subsequently converts the ASCII hostname to Unicode, it will inadvertently permits access to the Unicode name "example.com".

#### Severity
Unknown

#### References
- [https://go.dev/cl/767220](https://go.dev/cl/767220)
- [https://go.dev/issue/78760](https://go.dev/issue/78760)
- [https://groups.google.com/g/golang-announce/c/iI-mYSI0lu8](https://groups.google.com/g/golang-announce/c/iI-mYSI0lu8)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5026) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Invoking incorrect handling of HTML elements in foreign content in golang.org/x/net/html
[CVE-2026-42502](https://nvd.nist.gov/vuln/detail/CVE-2026-42502) / [GO-2026-5027](https://pkg.go.dev/vuln/GO-2026-5027)

<details>
<summary>More information</summary>

#### Details
Parsing arbitrary HTML which is then rendered using Render can result in an unexpected HTML tree. This can be leveraged to execute XSS attacks in applications that attempt to sanitize input HTML before rendering.

#### Severity
Unknown

#### References
- [https://go.dev/issue/79572](https://go.dev/issue/79572)
- [https://groups.google.com/g/golang-announce/c/iI-mYSI0lu8](https://groups.google.com/g/golang-announce/c/iI-mYSI0lu8)
- [https://go.dev/cl/781701](https://go.dev/cl/781701)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5027) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Invoking denial of service when parsing arbitrary HTML in golang.org/x/net/html
[CVE-2026-25680](https://nvd.nist.gov/vuln/detail/CVE-2026-25680) / [GO-2026-5028](https://pkg.go.dev/vuln/GO-2026-5028)

<details>
<summary>More information</summary>

#### Details
Parsing arbitrary HTML can consume excessive CPU time, possibly leading to denial of service.

#### Severity
Unknown

#### References
- [https://go.dev/cl/781702](https://go.dev/cl/781702)
- [https://go.dev/issue/79573](https://go.dev/issue/79573)
- [https://groups.google.com/g/golang-announce/c/iI-mYSI0lu8](https://groups.google.com/g/golang-announce/c/iI-mYSI0lu8)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5028) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Invoking incorrect handling of character references in DOCTYPE nodes in golang.org/x/net/html
[CVE-2026-25681](https://nvd.nist.gov/vuln/detail/CVE-2026-25681) / [GO-2026-5029](https://pkg.go.dev/vuln/GO-2026-5029)

<details>
<summary>More information</summary>

#### Details
Parsing arbitrary HTML which is then rendered using Render can result in an unexpected HTML tree. This can be leveraged to execute XSS attacks in applications that attempt to sanitize input HTML before rendering.

#### Severity
Unknown

#### References
- [https://go.dev/issue/79574](https://go.dev/issue/79574)
- [https://groups.google.com/g/golang-announce/c/iI-mYSI0lu8](https://groups.google.com/g/golang-announce/c/iI-mYSI0lu8)
- [https://go.dev/cl/781703](https://go.dev/cl/781703)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5029) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Invoking duplicate attributes can cause XSS in golang.org/x/net/html
[CVE-2026-27136](https://nvd.nist.gov/vuln/detail/CVE-2026-27136) / [GO-2026-5030](https://pkg.go.dev/vuln/GO-2026-5030)

<details>
<summary>More information</summary>

#### Details
Parsing arbitrary HTML which is then rendered using Render can result in an unexpected HTML tree. This can be leveraged to execute XSS attacks in applications that attempt to sanitize input HTML before rendering.

#### Severity
Unknown

#### References
- [https://go.dev/issue/79575](https://go.dev/issue/79575)
- [https://groups.google.com/g/golang-announce/c/iI-mYSI0lu8](https://groups.google.com/g/golang-announce/c/iI-mYSI0lu8)
- [https://go.dev/cl/781685](https://go.dev/cl/781685)

This data is provided by [OSV](https://osv.dev/vulnerability/GO-2026-5030) and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license)).
</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - ""
- 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:eyJjcmVhdGVkSW5WZXIiOiI0My4xOTEuMiIsInVwZGF0ZWRJblZlciI6IjQzLjE5MS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/tea/pulls/1001
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-05-22 20:51:54 +00:00
Renovate Bot 861201541d fix(deps): update module golang.org/x/sys to v0.45.0 (#1000)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) | [`v0.44.0` → `v0.45.0`](https://cs.opensource.google/go/x/sys/+/refs/tags/v0.44.0...refs/tags/v0.45.0) | ![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2fsys/v0.45.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2fsys/v0.44.0/v0.45.0?slim=true) |

---

### 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:eyJjcmVhdGVkSW5WZXIiOiI0My4xOTAuMSIsInVwZGF0ZWRJblZlciI6IjQzLjE5MC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/tea/pulls/1000
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-22 07:35:59 +00:00
Renovate Bot 3d667c1594 chore(deps): update docker.gitea.com/gitea docker tag to v1.26.2 (#997)
This PR contains the following updates:

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

---

### Release Notes

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

### [`v1.26.2`](https://github.com/go-gitea/gitea/blob/HEAD/CHANGELOG.md#1262---2026-05-20)

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

- SECURITY
  - fix(permissions): Fix reading permission ([#&#8203;37769](https://github.com/go-gitea/gitea/issues/37769))
  - fix(actions): make artifact signature payloads unambiguous ([#&#8203;37707](https://github.com/go-gitea/gitea/issues/37707))
  - fix: Unify public-only token filtering in API queries and repo access checks ([#&#8203;37118](https://github.com/go-gitea/gitea/issues/37118))
  - fix: Add missed token scope checking ([#&#8203;37735](https://github.com/go-gitea/gitea/issues/37735))
  - fix(oauth): bind token exchanges to the original client request ([#&#8203;37704](https://github.com/go-gitea/gitea/issues/37704))
  - fix(oauth): strengthen PKCE validation and refresh token replay protection ([#&#8203;37706](https://github.com/go-gitea/gitea/issues/37706))
  - fix(web): enforce token scopes on raw, media, and attachment downloads ([#&#8203;37698](https://github.com/go-gitea/gitea/issues/37698))
  - fix(security): enforce wiki git writes and LFS token access at request time ([#&#8203;37695](https://github.com/go-gitea/gitea/issues/37695))
  - feat(api): encrypt AWS creds ([#&#8203;37679](https://github.com/go-gitea/gitea/issues/37679))
  - fix(deps): update dependency mermaid to v11.15.0 \[security], add e2e test
  - fix(packages): Add label for private and internal package and fix composor package source permission check ([#&#8203;37610](https://github.com/go-gitea/gitea/issues/37610))
  - fix(git): Fix smart http request scope bug ([#&#8203;37583](https://github.com/go-gitea/gitea/issues/37583))
  - Fix basic auth bug ([#&#8203;37503](https://github.com/go-gitea/gitea/issues/37503))
  - Fix allow maintainer edit permission check ([#&#8203;37479](https://github.com/go-gitea/gitea/issues/37479)) ([#&#8203;37484](https://github.com/go-gitea/gitea/issues/37484))
  - Fix URL sanitization to handle schemeless credentials ([#&#8203;37440](https://github.com/go-gitea/gitea/issues/37440)) ([#&#8203;37471](https://github.com/go-gitea/gitea/issues/37471))
  - Fix attachment Content-Security-Policy ([#&#8203;37455](https://github.com/go-gitea/gitea/issues/37455)) ([#&#8203;37464](https://github.com/go-gitea/gitea/issues/37464))
  - chore(deps): bump go-git/go-git/v5 to 5.19.0 ([#&#8203;37608](https://github.com/go-gitea/gitea/issues/37608))

- BUGFIXES
  - fix(pull): handle empty pull request files view to allow reviews ([#&#8203;37783](https://github.com/go-gitea/gitea/issues/37783))
  - fix(markup): make RenderString never fail ([#&#8203;37779](https://github.com/go-gitea/gitea/issues/37779))
  - fix: add natural sort to sortTreeViewNodes ([#&#8203;37772](https://github.com/go-gitea/gitea/issues/37772))
  - fix: package creation unique conflict ([#&#8203;37774](https://github.com/go-gitea/gitea/issues/37774))
  - fix!: add DEFAULT\_TITLE\_SOURCE setting for pull request title default behavior ([#&#8203;37465](https://github.com/go-gitea/gitea/issues/37465))
  - fix: Allow direct commits for unprotected files with push restrictions ([#&#8203;37657](https://github.com/go-gitea/gitea/issues/37657))
  - fix(actions): wrong assumption that run id always >= job id ([#&#8203;37737](https://github.com/go-gitea/gitea/issues/37737))
  - fix(auth): set User-Agent on avatar fetch and sync avatar on link-account register ([#&#8203;37564](https://github.com/go-gitea/gitea/issues/37564)) ([#&#8203;37588](https://github.com/go-gitea/gitea/issues/37588))
  - fix(actions): deadlock between PrepareRunAndInsert and UpdateTaskByState ([#&#8203;37692](https://github.com/go-gitea/gitea/issues/37692))
  - fix(repo): /generate must sync the branch table for the new repo ([#&#8203;37693](https://github.com/go-gitea/gitea/issues/37693))
  - build: Fix snap build (1.26)
  - fix(actions): run TransferLogs on UpdateLog{Rows:\[], NoMore:true} ([#&#8203;37631](https://github.com/go-gitea/gitea/issues/37631))
  - fix show correct mergebase
  - fix: make clone URL respect public URL detection setting ([#&#8203;37615](https://github.com/go-gitea/gitea/issues/37615))
  - fix: "run as root" check ([#&#8203;37622](https://github.com/go-gitea/gitea/issues/37622))
  - chore(deps): update dependency go to v1.26.3 ([#&#8203;37601](https://github.com/go-gitea/gitea/issues/37601))
  - Compare dropdown fails when selecting branch with no common merge-base ([#&#8203;37470](https://github.com/go-gitea/gitea/issues/37470))
  - fix: treat email addresses case-insensitively ([#&#8203;37600](https://github.com/go-gitea/gitea/issues/37600))
  - fix(actions): fix blank lines after ::endgroup:: ([#&#8203;37597](https://github.com/go-gitea/gitea/issues/37597))
  - fix(actions): report individual step status in workflow job API response ([#&#8203;37592](https://github.com/go-gitea/gitea/issues/37592))
  - fix: Invalid UTF-8 commit messages in JSON API responses ([#&#8203;37542](https://github.com/go-gitea/gitea/issues/37542))
  - fix: use consistent GetUser family functions ([#&#8203;37553](https://github.com/go-gitea/gitea/issues/37553))
  - fix(api): return 409 message instead of empty JSON for wrong commit id ([#&#8203;37572](https://github.com/go-gitea/gitea/issues/37572))
  - fix(actions): prevent panic when workflow contains null jobs ([#&#8203;37570](https://github.com/go-gitea/gitea/issues/37570))
  - Make ServeSetHeaders default to download attachment if filename exists ([#&#8203;37552](https://github.com/go-gitea/gitea/issues/37552)) ([#&#8203;37555](https://github.com/go-gitea/gitea/issues/37555))
  - Fix(actions): validate workflow param to prevent 500 error ([#&#8203;37546](https://github.com/go-gitea/gitea/issues/37546)) ([#&#8203;37554](https://github.com/go-gitea/gitea/issues/37554))
  - Don't unblock run-level-concurrency-blocked runs in the resolver ([#&#8203;37461](https://github.com/go-gitea/gitea/issues/37461)) ([#&#8203;37538](https://github.com/go-gitea/gitea/issues/37538))
  - Fix(packages): use file names for generic web downloads ([#&#8203;37514](https://github.com/go-gitea/gitea/issues/37514)) ([#&#8203;37520](https://github.com/go-gitea/gitea/issues/37520))
  - Fix merge autodetect can't close other PRs but only the last one when multiple PRs are pushed at once ([#&#8203;37512](https://github.com/go-gitea/gitea/issues/37512)) ([#&#8203;37516](https://github.com/go-gitea/gitea/issues/37516))
  - Fix update branch protection order ([#&#8203;37508](https://github.com/go-gitea/gitea/issues/37508)) ([#&#8203;37513](https://github.com/go-gitea/gitea/issues/37513))
  - Fix mCaptcha broken after Vite migration ([#&#8203;37492](https://github.com/go-gitea/gitea/issues/37492)) ([#&#8203;37509](https://github.com/go-gitea/gitea/issues/37509))
  - Fix review submission from single-commit PR view ([#&#8203;37475](https://github.com/go-gitea/gitea/issues/37475)) ([#&#8203;37485](https://github.com/go-gitea/gitea/issues/37485))
  - Fix scheduled action panic with null event payload ([#&#8203;37459](https://github.com/go-gitea/gitea/issues/37459)) ([#&#8203;37466](https://github.com/go-gitea/gitea/issues/37466))
  - Make GetPossibleUserByID can handle deleted user ([#&#8203;37430](https://github.com/go-gitea/gitea/issues/37430)) ([#&#8203;37431](https://github.com/go-gitea/gitea/issues/37431))
  - Remove excessive quote from terraform instructions ([#&#8203;37424](https://github.com/go-gitea/gitea/issues/37424)) ([#&#8203;37426](https://github.com/go-gitea/gitea/issues/37426))
  - Fix color regressions, add `priority` color ([#&#8203;37417](https://github.com/go-gitea/gitea/issues/37417)) ([#&#8203;37421](https://github.com/go-gitea/gitea/issues/37421))

- MISC
  - Add CurrentURL template variable back ([#&#8203;37444](https://github.com/go-gitea/gitea/issues/37444)) ([#&#8203;37449](https://github.com/go-gitea/gitea/issues/37449))

</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:eyJjcmVhdGVkSW5WZXIiOiI0My4xODYuMSIsInVwZGF0ZWRJblZlciI6IjQzLjE4Ni4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/tea/pulls/997
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-21 01:27:33 +00:00
Renovate Bot ef0dc62dd6 fix(deps): update module github.com/go-git/go-git/v5 to v5.19.1 (#996)
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-git/go-git/v5](https://github.com/go-git/go-git) | `v5.19.0` → `v5.19.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fgo-git%2fgo-git%2fv5/v5.19.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fgo-git%2fgo-git%2fv5/v5.19.0/v5.19.1?slim=true) |

---

### Release Notes

<details>
<summary>go-git/go-git (github.com/go-git/go-git/v5)</summary>

### [`v5.19.1`](https://github.com/go-git/go-git/releases/tag/v5.19.1)

[Compare Source](https://github.com/go-git/go-git/compare/v5.19.0...v5.19.1)

#### What's Changed

- v5: plumbing: transport/ssh, Shell-quote path by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2068](https://github.com/go-git/go-git/pull/2068)
- v5: git: submodule, Fix relative URL resolution by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2070](https://github.com/go-git/go-git/pull/2070)
- v5: git: submodule, canonical remote for relative URLs by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2074](https://github.com/go-git/go-git/pull/2074)
- v5: git: submodule, error on remote without URLs by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2078](https://github.com/go-git/go-git/pull/2078)
- v5: plumbing: format/idxfile, Validate offset64 indices by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2084](https://github.com/go-git/go-git/pull/2084)
- v5: \*: Reject malformed variable-length integers by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2092](https://github.com/go-git/go-git/pull/2092)
- v5: plumbing: format/packfile, Tighten delta validation by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2091](https://github.com/go-git/go-git/pull/2091)
- v5: Add `worktreeFilesystem` wrapper for worktree and hardening by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2100](https://github.com/go-git/go-git/pull/2100)
- v5: config: validate submodule names by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2082](https://github.com/go-git/go-git/pull/2082)
- build: Update module github.com/go-git/go-git/v5 to v5.19.0 \[SECURITY] (releases/v5.x) by [@&#8203;go-git-renovate](https://github.com/go-git-renovate)\[bot] in [#&#8203;2111](https://github.com/go-git/go-git/pull/2111)
- v5: git: Allow MkdirAll on worktree-root paths by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2117](https://github.com/go-git/go-git/pull/2117)
- v5: git: Stop validating symlink target paths by [@&#8203;pjbgf](https://github.com/pjbgf) in [#&#8203;2116](https://github.com/go-git/go-git/pull/2116)
- v5: plumbing: format decoder input bounds and contracts by [@&#8203;hiddeco](https://github.com/hiddeco) in [#&#8203;2125](https://github.com/go-git/go-git/pull/2125)
- plumbing: format/packfile, cap delta chain depth in parser by [@&#8203;pjbgf](https://github.com/pjbgf) in [#&#8203;2137](https://github.com/go-git/go-git/pull/2137)

**Full Changelog**: <https://github.com/go-git/go-git/compare/v5.19.0...v5.19.1>

</details>
---

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

Reviewed-on: https://gitea.com/gitea/tea/pulls/996
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-19 04:53:00 +00:00
techknowlogick 70c7ee11f2 update renovate config 2026-05-14 16:24:31 +00:00
techknowlogick 6134351048 bump go deps 2026-05-14 12:01:28 -04:00
Renovate Bot 8be4dae66e fix(deps): update module github.com/go-authgate/sdk-go to v0.11.0 (#988)
Reviewed-on: https://gitea.com/gitea/tea/pulls/988
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-05-14 16:00:02 +00:00
Renovate Bot b8dcb8a442 fix(deps): update module golang.org/x/term to v0.43.0 (#989)
Reviewed-on: https://gitea.com/gitea/tea/pulls/989
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-05-14 15:59:53 +00:00
Renovate Bot 01632d927e fix(deps): update module code.gitea.io/sdk/gitea to v0.25.1 (#991)
Reviewed-on: https://gitea.com/gitea/tea/pulls/991
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-05-14 15:59:44 +00:00
Renovate Bot 1b79be7cea fix(deps): update module github.com/urfave/cli/v3 to v3.9.0 (#992)
Reviewed-on: https://gitea.com/gitea/tea/pulls/992
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-05-14 15:59:34 +00:00
Minjie Fang 2cc45f1cce fix(deps): update github.com/urfave/cli to v3.9.0 (#993)
Fix https://gitea.com/gitea/tea/issues/975

Reviewed-on: https://gitea.com/gitea/tea/pulls/993
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Minjie Fang <wingsallen@gmail.com>
Co-committed-by: Minjie Fang <wingsallen@gmail.com>
2026-05-14 05:15:33 +00:00
Minjie Fang 2b64762a32 Fix login edit to check config existence (#987)
Fix https://gitea.com/gitea/tea/issues/561

Reviewed-on: https://gitea.com/gitea/tea/pulls/987
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Minjie Fang <wingsallen@gmail.com>
Co-committed-by: Minjie Fang <wingsallen@gmail.com>
2026-05-10 01:28:06 +00:00
Carlos Grillet 19dd8b1b4b fix(deps): update module code.gitea.io/sdk/gitea to v0.25.0 (#984)
Bumping gitea SDK to version v0.25.0

Currently there is an issue when users try to use SSH to authenticate to a gitea server. The issue is already reported here #983

The problem was that `*gitea.HTTPSign` embeds `ssh.Signer` (not `ssh.AlgorithmSigner`).

`httpsig v1.2.4` type-asserts the signer to `ssh.AlgorithmSigner` for RSA keys and panics because `*HTTPSign` doesn't expose `SignWithAlgorithm`.

Fix: SDK v0.25.0 adds `SignWithAlgorithm` to `HTTPSign`, satisfying `ssh.AlgorithmSigner`.
Reviewed-on: https://gitea.com/gitea/tea/pulls/984
Reviewed-by: techknowlogick <9+techknowlogick@noreply.gitea.com>
Co-authored-by: Carlos Grillet <carlosbeta5000@gmail.com>
Co-committed-by: Carlos Grillet <carlosbeta5000@gmail.com>
2026-05-07 17:29:39 +00:00
cpamayo f617f26da0 fix: pass the name flag value as the organization FullName (#832)
This change proposes that, when creating an organization using the CLI, the value provided in the `--name` parameter is used as the organization `FullName`.

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/832
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: cpamayo <anderson.carl3@mayo.edu>
Co-committed-by: cpamayo <anderson.carl3@mayo.edu>
2026-05-07 00:14:22 +00:00
Minjie Fang a5ecf06c2a Fix login edit to open one editor only (#977)
Fix https://gitea.com/gitea/tea/issues/906

Reviewed-on: https://gitea.com/gitea/tea/pulls/977
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Minjie Fang <wingsallen@gmail.com>
Co-committed-by: Minjie Fang <wingsallen@gmail.com>
2026-05-07 00:12:12 +00:00
Renovate Bot e686e8d0bd fix(deps): update module github.com/go-authgate/sdk-go to v0.10.0 (#976)
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.9.0` → `v0.10.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fgo-authgate%2fsdk-go/v0.10.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fgo-authgate%2fsdk-go/v0.9.0/v0.10.0?slim=true) |

---

### Release Notes

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

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

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

#### Changelog

##### Others

- [`5b43693`](https://github.com/go-authgate/sdk-go/commit/5b436935ca0c587301754ee8e43dc04329b34623): feat(jwksauth)!: align with upstream JWT\_PRIVATE\_CLAIM\_PREFIX ([#&#8203;27](https://github.com/go-authgate/sdk-go/issues/27)) ([@&#8203;appleboy](https://github.com/appleboy))

</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:eyJjcmVhdGVkSW5WZXIiOiI0My4xNjAuNiIsInVwZGF0ZWRJblZlciI6IjQzLjE2MC42IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Reviewed-on: https://gitea.com/gitea/tea/pulls/976
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-05-04 02:06:58 +00:00
ghainer 22ff601988 feat: add additional admin users subcommands (#842)
## Summary

Adds admin user management commands to the tea CLI, enabling admins to create, edit, and delete user accounts.

## Features Added

### Admin User Management Commands

- **Create users**: `tea admin users create` - Create new user accounts with configurable options
- **Edit users**: `tea admin users edit <username>` - Update user properties including password, permissions, and profile settings
- **Delete users**: `tea admin users delete <username>` - Remove user accounts with confirmation prompt

### Implementation Details

#### Create Command (`admin users create`)
- Required: username
- Optional: email, full name, password
- Flags: admin, restricted, prohibit-login, visibility
- Password input: command-line flag, file, stdin, or interactive prompt with confirmation
- Default: users must change password on first login (use `--no-must-change-password` to skip)
- Post-creation updates for admin/restricted/prohibit-login (not available during creation)

#### Edit Command (`admin users edit`)
- Updates only explicitly provided fields (partial updates)
- Password change support with the same input methods as create
- Editable fields:
  - Profile: email, full name, description, website, location
  - Permissions: admin/restricted/active status
  - Settings: visibility, max repo creation limits
  - Advanced: git hooks, local imports, organization creation
- Default: password changes require password change on next login (use `--no-must-change-password` to skip)

#### Delete Command (`admin users delete`)
- Confirmation prompt by default
- `--confirm` flag to skip confirmation
- Displays user details before deletion

### Security Features

- Secure password input via interactive prompts (hidden input)
- Multiple password input methods: flag, file, stdin, interactive
- Password confirmation for interactive mode
- Whitespace trimming for file/stdin inputs

### Password Input Methods

1. **Command-line flag**: `--password <value>`
2. **File input**: `--password-file <file>` - Read from file
3. **Stdin input**: `--password-stdin` - Read from stdin
4. **Interactive prompt**: Automatically prompts if password not provided (with confirmation)

For edit command: Use `--password=""` to trigger interactive prompt.

## Usage Examples

```bash
# Create a new user
tea admin users create --username john --email john@example.com --admin --no-must-change-password

# Create with interactive password prompt
tea admin users create jane --email jane@example.com

# Edit user properties
tea admin users edit john --email newemail@example.com --restricted

# Change user password (will prompt if not provided)
tea admin users edit john --password=""
tea admin users edit john --password-file /path/to/password.txt

# Delete a user (with confirmation)
tea admin users delete olduser

# Delete without confirmation
tea admin users delete olduser --confirm
```

## Related Issue

Resolves #161

## Testing

- Unit tests for all commands
- Flag validation and default value tests
- Password input method tests (file, stdin, interactive)
- Test coverage for all user option structures
- Confirmation logic tests for delete command

## Technical Details

- Uses Gitea SDK `AdminCreateUser`, `AdminEditUser`, and `AdminDeleteUser` APIs
- Follows existing tea CLI patterns and conventions
- Handles fields not available during creation via post-creation updates
- Partial update support for edit command (only updates explicitly set fields)
- Consistent with other tea commands (webhooks, secrets) in password handling and confirmation patterns

All tests pass and the implementation integrates with existing tea CLI infrastructure.

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/842
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: ghainer <gehainer@gmail.com>
Co-committed-by: ghainer <gehainer@gmail.com>
2026-05-02 23:50:36 +00:00
Brandon Fryslie 9d6ae4bf02 feat(ssh-keys): add ssh-keys command to manage SSH public keys (#940)
## Summary

- Adds `tea ssh-keys` command group (aliases: `ssh-key`, `keys`) under the SETUP category
- Mirrors the interface of `gh ssh-key add/list/delete`
- Three subcommands: `add <keyfile>`, `list`, `delete <id>`

## Commands

\`\`\`sh
tea ssh-keys add ~/.ssh/id_ed25519.pub                     # title defaults to filename stem
tea ssh-keys add ~/.ssh/id_rsa.pub --title "work laptop"
tea ssh-keys add ~/.ssh/deploy.pub --read-only             # authentication-only key
tea ssh-keys list
tea ssh-keys list --output json
tea ssh-keys delete 42                                     # prompts for confirmation
tea ssh-keys delete 42 --force                             # skip prompt
\`\`\`

## Test plan

- [x] `make lint` — 0 issues
- [x] `make fmt-check` — passes
- [x] `go test ./cmd/sshkeys/... -run TestKeyTitle` — unit tests pass (no server needed)
- [ ] Integration tests with live Gitea instance:
  \`\`\`sh
  GITEA_TEA_TEST_URL=https://your-gitea \
  GITEA_TEA_TEST_TOKEN=<token> \
  go test ./cmd/sshkeys/... -v -run TestSSHKey
  \`\`\`
  Exercises full add → SDK-verify → delete → 404-verify lifecycle.

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Brandon Fryslie <530235+brandon-fryslie@users.noreply.github.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/940
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Brandon Fryslie <186614+brandroid@noreply.gitea.com>
Co-committed-by: Brandon Fryslie <186614+brandroid@noreply.gitea.com>
2026-05-02 18:24:08 +00:00
Matěj Cepl 2985824ab0 Multiple PRs (#848)
This is an effort to allow tea pr review to work with multiple reviews.

Fixes: #847
Reviewed-on: https://gitea.com/gitea/tea/pulls/848
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Matěj Cepl <mcepl@cepl.eu>
Co-committed-by: Matěj Cepl <mcepl@cepl.eu>
2026-05-02 17:01:40 +00: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`](https://github.com/go-authgate/sdk-go/commit/86d33f315c3eddfe92f37e4d8b3ac30afbc0ef72): docs(jwksauth): tighten readme table column widths ([@&#8203;appleboy](https://github.com/appleboy))

##### Others

- [`545d96f`](https://github.com/go-authgate/sdk-go/commit/545d96fd43d8a6e6bc76a3c6b28683ffa3eace06): 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`](https://github.com/go-authgate/sdk-go/commit/1e73580c87f2be874101c4ccc02f3dd1ceb17c53): 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`](https://github.com/go-authgate/sdk-go/commit/7af1bc463714a2c4e6aea1741c87b20fdbba21ce): 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`](https://github.com/go-authgate/sdk-go/commit/62ccff06c837abe9c9cd6d8411525e3d25344cf1): 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`](https://github.com/go-authgate/sdk-go/commit/088ee3bd2d5f891c03d27212a6ed5283b1434282): 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`](https://github.com/go-authgate/sdk-go/commit/aa17bc2373b675b0f0882672706c66a0f523b05f): 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
Nicolas a58c35c3e2 fix(cmd): Update CmdRepos description and usage in repos.go (#946)
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/946
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Co-committed-by: Nicolas <bircni@icloud.com>
2026-04-20 19:50:28 +00:00
Matěj Cepl 783ac7684a fix(context): skip local repo detection for repo slugs (#960)
Treat explicit --repo slugs as remote targets so commands do not probe
the current worktree. This avoids SHA256 repository failures when local
git autodetection is unnecessary.

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/960
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Matěj Cepl <mcepl@cepl.eu>
Co-committed-by: Matěj Cepl <mcepl@cepl.eu>
2026-04-20 19:39:42 +00:00
Renovate Bot d0b7ea09e8 fix(deps): update module charm.land/lipgloss/v2 to v2.0.3 (#959)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [charm.land/lipgloss/v2](https://github.com/charmbracelet/lipgloss) | `v2.0.2` → `v2.0.3` | ![age](https://developer.mend.io/api/mc/badges/age/go/charm.land%2flipgloss%2fv2/v2.0.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/charm.land%2flipgloss%2fv2/v2.0.2/v2.0.3?slim=true) |

---

### Release Notes

<details>
<summary>charmbracelet/lipgloss (charm.land/lipgloss/v2)</summary>

### [`v2.0.3`](https://github.com/charmbracelet/lipgloss/releases/tag/v2.0.3)

[Compare Source](https://github.com/charmbracelet/lipgloss/compare/v2.0.2...v2.0.3)

#### Changelog

##### Fixed

- [`472d718`](https://github.com/charmbracelet/lipgloss/commit/472d718e2314596549bee2c0c8ccf8beea5f25ae): fix: Avoid background color query hang ([#&#8203;636](https://github.com/charmbracelet/lipgloss/issues/636)) ([@&#8203;jedevc](https://github.com/jedevc))

##### Docs

- [`9e39a0a`](https://github.com/charmbracelet/lipgloss/commit/9e39a0ad4f4fc779d620f17783cee3494da6ae29): docs: fix README typo ([#&#8203;629](https://github.com/charmbracelet/lipgloss/issues/629)) ([@&#8203;Rohan5commit](https://github.com/Rohan5commit))
- [`cd93a9f`](https://github.com/charmbracelet/lipgloss/commit/cd93a9f5d2e3cb151da83150db29751d92585d23): docs: fix tree comment typo ([#&#8203;634](https://github.com/charmbracelet/lipgloss/issues/634)) ([@&#8203;Rohan5commit](https://github.com/Rohan5commit))

***

<a href="https://charm.land/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-banner-next.jpg" width="400"></a>

Thoughts? Questions? We love hearing from you. Feel free to reach out on [X](https://x.com/charmcli), [Discord](https://charm.land/discord), [Slack](https://charm.land/slack), [The Fediverse](https://mastodon.social/@&#8203;charmcli), [Bluesky](https://bsky.app/profile/charm.land).

</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 [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMTEuMCIsInVwZGF0ZWRJblZlciI6IjQzLjExMS4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/959
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-04-20 19:34:25 +00:00
Renovate Bot 20914a1375 fix(deps): update module github.com/go-git/go-git/v5 to v5.18.0 (#961)
Reviewed-on: https://gitea.com/gitea/tea/pulls/961
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-04-20 01:11:50 +00:00
Renovate Bot 3c1c9b2904 chore(deps): update docker.gitea.com/gitea docker tag to v1.26.0 (#962)
Reviewed-on: https://gitea.com/gitea/tea/pulls/962
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-04-20 01:11:09 +00:00
Matěj Cepl 63bc90ea52 feat(branches): add rename subcommand (#939)
Implements the 'branches rename' command to rename a branch in a repository.
This wraps the Gitea API endpoint PATCH /repos/{owner}/{repo}/branches/{branch}.

Usage: tea branches rename <old_branch_name> <new_branch_name>

Example: tea branches rename -r owner/repo main factory

This resolves issue #938.

Reviewed-on: https://gitea.com/gitea/tea/pulls/939
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Matěj Cepl <mcepl@cepl.eu>
Co-committed-by: Matěj Cepl <mcepl@cepl.eu>
2026-04-15 17:27:47 +00:00
Bo-Yi Wu 9e0a6203ae feat(pulls): add ci status field to pull request list (#956)
## Summary

- Add `"ci"` as a new selectable field for `tea pr list --fields`, allowing users to see CI status across multiple PRs at a glance
- Fetch CI status via `GetCombinedStatus` API **only when the `ci` field is explicitly requested** via `--fields`, avoiding unnecessary API calls in default usage
- Improve CI status display in both detail and list views:
  - **Detail view** (`tea pr <index>`): show each CI check with symbol, context name, description, and clickable link to CI run
  - **List view** (`tea pr list --fields ci`): show symbol + context name per CI check (e.g., `✓ lint,  build,  test`)
  - **Machine-readable output**: return raw state string (e.g., `success`, `pending`)
- Replace pending CI symbol from `⭮` to `` for better readability
- Extract `formatCIStatus` helper and reuse it in `PullDetails` to reduce code duplication
- Add comprehensive tests for CI status formatting and PR list integration

## Detail View Example

```
- CI:
  - ✓ [**lint**](https://ci.example.com/lint): Lint passed
  -  [**build**](https://ci.example.com/build): Build is running
  -  [**test**](https://ci.example.com/test): 3 tests failed
```

## List View Example

```
INDEX  TITLE       STATE  CI
123    Fix bug     open   ✓ lint,  build,  test
```

## Usage

```bash
# Show CI status column in list
tea pr list --fields index,title,state,ci

# Default output is unchanged (no CI column, no extra API calls)
tea pr list
```

## Files Changed

- `cmd/pulls/list.go` — conditionally fetch CI status per PR when `ci` field is selected
- `modules/print/pull.go` — add `ci` field, `formatCIStatus` helper, improve detail/list CI display
- `modules/print/pull_test.go` — comprehensive tests for CI status formatting

## Test plan

- [x] `go build ./...` passes
- [x] `go test ./...` passes (11 new tests)
- [x] `tea pr list` — default output unchanged, no extra API calls
- [x] `tea pr list --fields index,title,state,ci` — CI column with context names
- [x] `tea pr <index>` — CI section shows each check with name, description, and link
- [x] `tea pr list --fields ci -o csv` — machine-readable output shows raw state strings

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

Reviewed-on: https://gitea.com/gitea/tea/pulls/956
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-04-10 17:29:15 +00:00
Bo-Yi Wu 84ecd16f9c fix(deps): update Go dependencies to latest versions (#955)
## Summary
- Upgrade all Go module dependencies to their latest versions
- Includes updates to charm.land, golang.org/x, goldmark, go-crypto, and other indirect dependencies
- Project builds cleanly with all updates

## Test plan
- [x] `go build ./...` passes
- [x] CI pipeline passes

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

Reviewed-on: https://gitea.com/gitea/tea/pulls/955
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-04-10 01:40:40 +00:00
Bo-Yi Wu 53e53e1067 feat(workflows): add dispatch, view, enable and disable subcommands (#952)
## Summary

- Add `tea actions workflows dispatch` to trigger `workflow_dispatch` events with `--ref`, `--input key=value`, and `--follow` for log tailing
- Add `tea actions workflows view` to show workflow details
- Add `tea actions workflows enable` and `disable` to toggle workflow state
- Rewrite `workflows list` to use the Workflow API instead of file listing
- Remove dead `WorkflowsList` print function that used `ContentsResponse`
- Update `CLI.md` and `example-workflows.md` with usage documentation and examples

## Motivation

Enable re-triggering specific workflows from the CLI, which is essential for AI-driven PR flows where a specific workflow needs to be re-run after pushing changes.

Leverages the 5 workflow API endpoints already supported by the Go SDK (v0.24.1) from go-gitea/gitea#33545:
- `ListRepoActionWorkflows`
- `GetRepoActionWorkflow`
- `DispatchRepoActionWorkflow` (with `returnRunDetails` support)
- `EnableRepoActionWorkflow`
- `DisableRepoActionWorkflow`

## New commands

\`\`\`
tea actions workflows
├── list          (rewritten to use Workflow API)
├── view <id>     (new)
├── dispatch <id> (new)
├── enable <id>   (new)
└── disable <id>  (new)
\`\`\`

### Usage examples

\`\`\`bash
# Dispatch workflow on current branch
tea actions workflows dispatch deploy.yml

# Dispatch with specific ref and inputs
tea actions workflows dispatch deploy.yml --ref main --input env=staging --input version=1.2.3

# Dispatch and follow logs
tea actions workflows dispatch ci.yml --ref feature/my-pr --follow

# View workflow details
tea actions workflows view deploy.yml

# Enable/disable workflows
tea actions workflows enable deploy.yml
tea actions workflows disable deploy.yml --confirm
\`\`\`

## Test plan

- [x] `go build ./...` passes
- [x] `go test ./...` passes
- [x] `go vet ./...` passes
- [x] `make lint` — 0 issues
- [x] `make docs-check` — CLI.md is up to date
- [x] Manual test: `tea actions workflows list` shows workflows from API
- [x] Manual test: `tea actions workflows dispatch <workflow> --ref main` triggers a run
- [x] Manual test: `tea actions workflows view <workflow>` shows details

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/952
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-04-09 20:03:33 +00:00
Renovate Bot 0489d8c275 fix(deps): update module golang.org/x/sys to v0.43.0 (#951)
Reviewed-on: https://gitea.com/gitea/tea/pulls/951
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-04-09 14:16:37 +00:00
Nicolas f538c05282 refactor: code cleanup across codebase (#947)
## Summary

- Extract duplicate \`getReleaseByTag\` into shared \`cmd/releases/utils.go\`
- Replace \`log.Fatal\` calls with proper error returns in config and login commands; \`GetLoginByToken\`/\`GetLoginsByHost\`/\`GetLoginByHost\` now return errors
- Remove dead \`portChan\` channel in \`modules/auth/oauth.go\`
- Fix YAML integer detection to use \`strconv.ParseInt\` (correctly handles negatives and large ints)
- Fix \`path.go\` error handling to use \`errors.As\` + \`syscall.ENOTDIR\` instead of string comparison
- Extract repeated credential helper key into local variable in \`SetupHelper\`
- Use existing \`isRemoteDeleted()\` in \`pull_clean.go\` instead of duplicating the logic
- Fix ~30 error message casing violations to follow Go conventions
- Use \`fmt.Errorf\` consistently instead of string concatenation in \`generic.go\`

Reviewed-on: https://gitea.com/gitea/tea/pulls/947
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Bo-Yi Wu (吳柏毅) <appleboy.tw@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Co-committed-by: Nicolas <bircni@icloud.com>
2026-04-08 03:38:49 +00:00
Bo-Yi Wu 662e339bf9 feat(pulls): add resolve, unresolve and review-comments subcommands (#948)
## Summary

- Add `tea pulls review-comments <pull-index>` subcommand to list PR review comments with configurable fields (supports table/json/csv/yaml output)
- Add `tea pulls resolve <comment-id>` subcommand to mark a review comment as resolved
- Add `tea pulls unresolve <comment-id>` subcommand to unmark a review comment as resolved
- Follow existing approve/reject pattern with shared `runResolveComment` helper in `review_helpers.go`

## Usage

```bash
# List review comments for PR #42
tea pulls review-comments 42

# Resolve comment #789
tea pulls resolve 789

# Unresolve comment #789
tea pulls unresolve 789

# Custom output fields
tea pulls review-comments 42 --fields id,path,body,resolver --output json
```

## New Files

| File | Description |
|------|-------------|
| `cmd/pulls/review_comments.go` | `review-comments` subcommand |
| `cmd/pulls/resolve.go` | `resolve` subcommand |
| `cmd/pulls/unresolve.go` | `unresolve` subcommand |
| `modules/task/pull_review_comment.go` | Task layer: list, resolve, unresolve via SDK |
| `modules/print/pull_review_comment.go` | Print formatting with `printable` interface |

## Modified Files

| File | Description |
|------|-------------|
| `cmd/pulls.go` | Register 3 new commands |
| `cmd/pulls/review_helpers.go` | Add shared `runResolveComment` helper |

## Test Plan

- [x] `go build ./...` passes
- [x] `go vet ./...` passes
- [x] `tea pulls review-comments <PR-index>` lists comments with IDs
- [x] `tea pulls resolve <comment-id>` resolves successfully
- [x] `tea pulls unresolve <comment-id>` unresolves successfully
- [x] `--output json` produces valid JSON output

Reviewed-on: https://gitea.com/gitea/tea/pulls/948
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-04-08 03:36:09 +00:00
appleboy 5bb73667d1 docs: add v0.13.0 release notes to CHANGELOG (#945)
Add v0.13.0 release notes to CHANGELOG.md covering 21 commits since v0.12.0: 5 new features, 2 enhancements, and dependency updates.

Reviewed-on: https://gitea.com/gitea/tea/pulls/945
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: appleboy <appleboy.tw@gmail.com>
Co-committed-by: appleboy <appleboy.tw@gmail.com>
2026-04-05 16:42:27 +00:00
appleboy f329f6fab2 feat(pulls): add edit subcommand for pull requests (#944)
## Summary

- Add `tea pr edit` subcommand to support editing pull request properties (description, title, milestone, deadline, assignees, labels, reviewers)
- Add `--add-reviewers` / `--remove-reviewers` flags for managing PR reviewers via `CreateReviewRequests` / `DeleteReviewRequests` API
- Extract shared helpers (`ResolveLabelOpts`, `ApplyLabelChanges`, `ApplyReviewerChanges`, `ResolveMilestoneID`) into `modules/task/labels.go` to reduce duplication between issue and PR editing
- Refactor existing `EditIssue` to use the same shared helpers
- Wrap original error in `ResolveMilestoneID` to preserve underlying error context

## Usage

```bash
# Edit PR description
tea pr edit 1 --description "new description"

# Edit PR title
tea pr edit 1 --title "new title"

# Edit multiple fields
tea pr edit 1 --title "new title" --description "new desc" --add-labels "bug"

# Edit multiple PRs
tea pr edit 1 2 3 --add-assignees "user1"

# Add reviewers
tea pr edit 1 --add-reviewers "user1,user2"

# Remove reviewers
tea pr edit 1 --remove-reviewers "user1"
```

## Test plan

- [x] `go build .` succeeds
- [x] `go test ./...` passes
- [x] `make clean && make vet && make lint && make fmt-check && make docs-check && make build` all pass
- [x] `tea pr edit <idx> --description "test"` updates PR description on a Gitea instance
- [x] `tea pr edit <idx> --title "test"` updates PR title
- [x] `tea pr edit <idx> --add-labels "bug"` adds label
- [x] `tea pr edit <idx> --add-reviewers "user"` requests review
- [x] `tea pr edit <idx> --remove-reviewers "user"` removes reviewer
- [x] Existing `tea issues edit` still works correctly after refactor

Reviewed-on: https://gitea.com/gitea/tea/pulls/944
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: appleboy <appleboy.tw@gmail.com>
Co-committed-by: appleboy <appleboy.tw@gmail.com>
2026-04-05 05:35:15 +00:00
Renovate Bot 366069315f fix(deps): update module github.com/go-git/go-git/v5 to v5.17.2 (#943)
Reviewed-on: https://gitea.com/gitea/tea/pulls/943
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-04-01 18:14:14 +00:00
Renovate Bot 1e13681663 fix(deps): update module github.com/go-git/go-git/v5 to v5.17.1 (#942)
Reviewed-on: https://gitea.com/gitea/tea/pulls/942
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-03-30 07:00:31 +00:00
Renovate Bot bfbec3fc00 fix(deps): update module code.gitea.io/sdk/gitea to v0.24.1 (#936)
Reviewed-on: https://gitea.com/gitea/tea/pulls/936
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-03-27 06:04:29 +00:00
Renovate Bot e31a167e54 fix(deps): update module github.com/go-authgate/sdk-go to v0.6.1 (#935)
Reviewed-on: https://gitea.com/gitea/tea/pulls/935
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-03-27 03:47:06 +00:00
Renovate Bot 6a7c3e4efa fix(deps): update module github.com/urfave/cli/v3 to v3.8.0 (#937)
Reviewed-on: https://gitea.com/gitea/tea/pulls/937
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-03-27 03:46:50 +00:00
techknowlogick b05e03416b replace log.Fatal/os.Exit with error returns (#941)
* Use stdlib encoders
* Reduce some duplication
* Remove global pagination state
* Dedupe JSON detail types
* Bump golangci-lint

Reviewed-on: https://gitea.com/gitea/tea/pulls/941
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
2026-03-27 03:36:44 +00:00
Renovate Bot 21881525a8 chore(deps): update docker.gitea.com/gitea docker tag to v1.25.5 (#934)
Reviewed-on: https://gitea.com/gitea/tea/pulls/934
Reviewed-by: techknowlogick <9+techknowlogick@noreply.gitea.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-03-15 23:04:40 +00:00
Renovate Bot 9a462247bd fix(deps): update module github.com/olekukonko/tablewriter to v1.1.4 (#933)
Reviewed-on: https://gitea.com/gitea/tea/pulls/933
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-03-13 00:18:26 +00:00
Renovate Bot 5f74fb37df chore(deps): update mcr.microsoft.com/devcontainers/go docker tag to v2.1 (#930)
Reviewed-on: https://gitea.com/gitea/tea/pulls/930
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-03-12 17:13:25 +00:00
Bo-Yi Wu ec658cfc33 chore(deps): update Go dependencies and CI workflow action versions (#932)
## Summary

- Run `go get -u ./...` and `go mod tidy` to update all Go dependencies
- Update CI workflow action versions:
  - `crazy-max/ghaction-import-gpg`: v6 → v7
  - `goreleaser/goreleaser-action`: v6 → v7
  - `docker/setup-qemu-action`: v3 → v4
  - `docker/setup-buildx-action`: v3 → v4
  - `docker/login-action`: v3 → v4
  - `docker/build-push-action`: v6 → v7

## Notable Go dependency updates

- `github.com/urfave/cli/v3`: v3.6.2 → v3.7.0
- `github.com/ProtonMail/go-crypto`: v1.3.0 → v1.4.0
- `charm.land/huh/v2`: v2.0.1 → v2.0.3
- `golang.org/x/crypto`: v0.48.0 → v0.49.0
- `golang.org/x/net`: v0.49.0 → v0.52.0

Reviewed-on: https://gitea.com/gitea/tea/pulls/932
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-03-12 05:28:47 +00:00
Bo-Yi Wu cb9824b451 feat(repos): support owner-based repository listing with robust lookup (#931)
- Add an owner flag to the repos list command to list repositories for a specific user or organization
- Implement owner-based repository listing by detecting whether the owner is an organization or a user and calling the appropriate API
- Improve error handling for owner lookup by checking HTTP status codes instead of relying on error string matching
- Align repository search logic with the updated owner lookup behavior using HTTP response validation

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

Reviewed-on: https://gitea.com/gitea/tea/pulls/931
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-03-12 04:22:28 +00:00
Bo-Yi Wu a531faa626 feat(repos): add repo edit subcommand (#928)
## Summary
- Add `tea repo edit` subcommand to update repository properties via the Gitea API
- Support flags: `--name`, `--description`/`--desc`, `--website`, `--private`, `--template`, `--archived`, `--default-branch`
- Boolean-like flags use string type to distinguish "not set" from "false", following the pattern in `releases/edit.go`

## Test plan
- [x] `go build ./...` passes
- [x] `go vet ./...` passes
- [x] `tea repo edit --help` shows all flags correctly
- [x] Manual test: `tea repo edit --private true` on a test repo
- [x] Manual test: `tea repo edit --name new-name --description "new desc"` on a test repo

Reviewed-on: https://gitea.com/gitea/tea/pulls/928
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-03-12 03:06:44 +00:00
Bo-Yi Wu 302c946cb8 feat: store OAuth tokens in OS keyring via credstore (#926)
## Summary

- Introduce `github.com/go-authgate/sdk-go/credstore` to store OAuth tokens securely in the OS keyring (macOS Keychain / Linux Secret Service / Windows Credential Manager), with automatic fallback to an encrypted JSON file
- Add `AuthMethod` field to `Login` struct; new OAuth logins are marked `auth_method: oauth` and no longer write `token`/`refresh_token`/`token_expiry` to `config.yml`
- Add `GetAccessToken()` / `GetRefreshToken()` / `GetTokenExpiry()` accessors that transparently read from credstore for OAuth logins, with fallback to YAML fields for legacy logins
- Update all token reference sites across the codebase to use the new accessors
- Non-OAuth logins (token, SSH) are completely unaffected; no migration of existing tokens

## Key files

| File | Role |
|------|------|
| `modules/config/credstore.go` | **New** — credstore wrapper (Load/Save/Delete) |
| `modules/config/login.go` | Login struct, token accessors, refresh logic |
| `modules/auth/oauth.go` | OAuth flow, token creation / re-authentication |
| `modules/api/client.go`, `cmd/login/helper.go`, `cmd/login/oauth_refresh.go` | Token reference updates |
| `modules/task/pull_*.go`, `modules/task/repo_clone.go` | Git operation token reference updates |

## Test plan

- [x] `go build ./...` compiles successfully
- [x] `go test ./...` all tests pass
- [x] `tea login add --oauth` completes OAuth flow; verify config.yml has `auth_method: oauth` but no token/refresh_token/token_expiry
- [x] `tea repos ls` API calls work (token read from credstore)
- [x] `tea login delete <name>` credstore token is also removed
- [x] Existing non-OAuth logins continue to work unchanged

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

Reviewed-on: https://gitea.com/gitea/tea/pulls/926
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-03-12 02:49:14 +00:00
techknowlogick 0346e1cbb5 add function comment 2026-03-10 10:00:39 -04:00
techknowlogick cd4051ed38 make vet&fmt pass 2026-03-10 09:55:10 -04:00
Michal Suchanek c797624fcf Update to charm libraries v2 (#923)
Reviewed-on: https://gitea.com/gitea/tea/pulls/923
Reviewed-by: techknowlogick <9+techknowlogick@noreply.gitea.com>
Co-authored-by: Michal Suchanek <msuchanek@suse.de>
Co-committed-by: Michal Suchanek <msuchanek@suse.de>
2026-03-09 16:36:00 +00:00
Renovate Bot 3372c9ec59 fix(deps): update module golang.org/x/oauth2 to v0.36.0 (#919)
Reviewed-on: https://gitea.com/gitea/tea/pulls/919
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-03-09 16:19:28 +00:00
techknowlogick 1ac8492ac7 go 1.26 2026-03-09 15:57:54 +00:00
Renovate Bot d019f0dd72 fix(deps): update module github.com/go-git/go-git/v5 to v5.17.0 (#910)
Reviewed-on: https://gitea.com/gitea/tea/pulls/910
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-02-26 17:59:28 +00:00
techknowlogick c031db2413 Parse multiple values in api subcommand (#911)
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
2026-02-26 17:43:46 +00:00
Nikolaos Karaolidis e3c550ff22 fix: authentication via env variables repo argument (#809)
---------

Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/809
Co-authored-by: Nikolaos Karaolidis <nick@karaolidis.com>
Co-committed-by: Nikolaos Karaolidis <nick@karaolidis.com>
2026-02-19 19:23:44 +00:00
Lunny Xiao fab70f83c1 Fix issue detail view ignoring --owner flag (#899)
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/899
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-02-19 18:57:23 +00:00
techknowlogick 0b1147bfc0 build for windows aarch64 too 2026-02-19 18:41:21 +00:00
Lunny Xiao 93d4d3cc55 Skip token uniqueness check when using SSH authentication (#898)
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-authored-by: silverwind <silverwind@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/898
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-02-19 15:19:45 +00:00
Alain Thiffault bdf15a57be feat(pulls): add JSON output support for single PR view (#864)
Reviewed-on: https://gitea.com/gitea/tea/pulls/864
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Alain Thiffault <athiffau@effectivemomentum.com>
Co-committed-by: Alain Thiffault <athiffau@effectivemomentum.com>
2026-02-19 15:16:21 +00:00
Lunny Xiao 87c8c3d6e0 Fix new tty prompt (#897)
Fix #827

---------

Co-authored-by: silverwind <silverwind@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/897
Reviewed-by: silverwind <silverwind@noreply.gitea.com>
2026-02-16 03:37:44 +00:00
Michal Suchanek dfd400f15b Fix termenv OSC RGBA handling (#907)
Fixes: #889
Reviewed-on: https://gitea.com/gitea/tea/pulls/907
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Michal Suchanek <msuchanek@suse.de>
Co-committed-by: Michal Suchanek <msuchanek@suse.de>
2026-02-12 16:16:53 +00:00
yousfi saad 2152d99f2d Add tea actions runs and workflows commands (#880)
Implements comprehensive workflow execution tracking for Gitea Actions using tea CLI

## Features

### tea actions runs list
- List workflow runs with filtering (status, branch, event, actor, time)
- Time filters: relative (24h, 7d) and absolute dates
- Status symbols: ✓ success, ✘ failure, ⭮ pending, ⊘ skipped/cancelled, ⚠ blocked
- Multiple output formats: table, json, yaml, csv, tsv

### tea actions runs view
- View run details with metadata (ID, status, workflow, branch, event, trigger info)
- Shows jobs table with status, runner, duration
- Optional --jobs flag to toggle jobs display

### tea actions runs delete
- Delete/cancel workflow runs with confirmation prompt
- Supports --confirm/-y to skip prompt

### tea actions runs logs
- View job logs for all jobs or specific job (--job <id>)
- **New: --follow/-f flag for real-time log following** (like tail -f)
- Polls API every 2 seconds, only shows new content
- Auto-detects completion and exits

### tea actions workflows list
- List workflow files (.yml and .yaml) in repository
- Searches in .gitea/workflows and .github/workflows
- Shows active (✓) or inactive (✗) status based on recent runs
- Displays workflow name, path, and file size

## Commands

`tea actions runs list --status success --since 24h`
`tea actions runs view 123`
`tea actions runs delete 123 --confirm`
`tea actions runs logs 123 --job 456 --follow`
`tea actions workflows list`

## Tests
- 19 unit tests across all commands
- Full test suite passing
- Manual testing successful

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/880
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: yousfi saad <yousfi.saad@gmail.com>
Co-committed-by: yousfi saad <yousfi.saad@gmail.com>
2026-02-11 00:40:06 +00:00
Renovate Bot ea795775af fix(deps): update module golang.org/x/crypto to v0.48.0 (#905)
Reviewed-on: https://gitea.com/gitea/tea/pulls/905
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-02-10 00:43:00 +00:00
Renovate Bot 1093ef1524 fix(deps): update module github.com/go-git/go-git/v5 to v5.16.5 (#904)
Reviewed-on: https://gitea.com/gitea/tea/pulls/904
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-02-10 00:42:45 +00:00
Renovate Bot 873a44f897 fix(deps): update module golang.org/x/sys to v0.41.0 (#901)
Reviewed-on: https://gitea.com/gitea/tea/pulls/901
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-02-09 05:22:44 +00:00
Renovate Bot 47f74ea696 fix(deps): update module golang.org/x/oauth2 to v0.35.0 (#900)
Reviewed-on: https://gitea.com/gitea/tea/pulls/900
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-02-09 04:56:34 +00:00
Michal Suchanek 59656dfcd2 Require non-empty token in GetLoginByToken (#895)
Fixes: #893
Reviewed-on: https://gitea.com/gitea/tea/pulls/895
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Michal Suchanek <msuchanek@suse.de>
Co-committed-by: Michal Suchanek <msuchanek@suse.de>
2026-02-08 18:11:54 +00:00
Michal Suchanek e644cc49d4 Revert "Login requires a http/https login URL and revmoe SSH as a login method. SSH will be optional (#826)" (#891)
This reverts commit 90f8624ae7.

Fixes: #890

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/891
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Michal Suchanek <msuchanek@suse.de>
Co-committed-by: Michal Suchanek <msuchanek@suse.de>
2026-02-08 00:21:47 +00:00
boozedog 3595f8f89d fixed minor typo and grammar issue (#892)
Reviewed-on: https://gitea.com/gitea/tea/pulls/892
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: boozedog <boozedog@noreply.gitea.com>
Co-committed-by: boozedog <boozedog@noreply.gitea.com>
2026-02-07 16:02:20 +00:00
techknowlogick 49a9032d8a Move versions/filelocker into dedicated subpackages, and consistent headers in http requests (#888)
- move filelocker logic into dedicated subpackage
- consistent useragent in requests

Reviewed-on: https://gitea.com/gitea/tea/pulls/888
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
2026-02-05 18:05:43 +00:00
techknowlogick 982adb4d02 Update README.md 2026-02-04 19:37:39 +00:00
techknowlogick 29488a1f46 build w/ go1.25 (#886)
Reviewed-on: https://gitea.com/gitea/tea/pulls/886
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
2026-02-04 19:27:25 +00:00
Renovate Bot a47ac265d2 chore(deps): update mcr.microsoft.com/devcontainers/go docker tag to v2 (#884)
Reviewed-on: https://gitea.com/gitea/tea/pulls/884
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-02-04 00:56:27 +00:00
Renovate Bot 037d1aad23 fix(deps): update module github.com/charmbracelet/lipgloss to v2 (#885)
Reviewed-on: https://gitea.com/gitea/tea/pulls/885
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-02-04 00:56:04 +00:00
Renovate Bot e5342660fa chore(deps): update actions/checkout action to v6 (#882)
Reviewed-on: https://gitea.com/gitea/tea/pulls/882
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-02-04 00:16:06 +00:00
Renovate Bot 233ffe4508 chore(deps): update actions/setup-go action to v6 (#883)
Reviewed-on: https://gitea.com/gitea/tea/pulls/883
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-02-04 00:15:53 +00:00
techknowlogick ae9eb4f2c0 Add locking to ensure safe concurrent access to config file (#881)
Reviewed-on: https://gitea.com/gitea/tea/pulls/881
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
2026-02-03 23:48:18 +00:00
a1012112796 0d5bf60632 support create agit flow pull request (#867)
while looks the alibaba has not maintain
[`git-repo-go`](https://github.com/alibaba/git-repo-go/)
tool, to make agit flow pull requst can be create quickly.
add creating agit flow pull request feature
in tea tool

example:

```SHELL
tea pulls create --agit --remote=origin --topic=test-topic
--title="hello world" --description="test1
test 2
test 3"
```

Signed-off-by: a1012112796 <1012112796@qq.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/867
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-committed-by: a1012112796 <1012112796@qq.com>
2026-02-03 20:36:04 +00:00
techknowlogick 82d8a14c73 Add api subcommand for arbitrary api calls not covered by existing subcommands (#879)
Reviewed-on: https://gitea.com/gitea/tea/pulls/879
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
2026-02-03 20:24:21 +00:00
Renovate Bot 6414a5e00e chore(deps): update docker.gitea.com/gitea docker tag to v1.25.4 (#877)
Reviewed-on: https://gitea.com/gitea/tea/pulls/877
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-02-03 01:06:13 +00:00
Renovate Bot 864face284 fix(deps): update module golang.org/x/oauth2 to v0.34.0 (#878)
Reviewed-on: https://gitea.com/gitea/tea/pulls/878
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-02-03 01:06:03 +00:00
Renovate Bot 383c5fdc03 fix(deps): update module github.com/urfave/cli/v3 to v3.6.2 (#876)
Reviewed-on: https://gitea.com/gitea/tea/pulls/876
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-02-03 01:00:21 +00:00
Renovate Bot 7801310a18 fix(deps): update module github.com/olekukonko/tablewriter to v1.1.3 (#875)
Reviewed-on: https://gitea.com/gitea/tea/pulls/875
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2026-02-03 01:00:03 +00:00
techknowlogick c2180048a0 Split up Context (#873)
Reviewed-on: https://gitea.com/gitea/tea/pulls/873
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
2026-02-02 23:16:39 +00:00
techknowlogick 629872d1e9 nix flake update (#872)
Reviewed-on: https://gitea.com/gitea/tea/pulls/872
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
2026-02-02 23:05:45 +00:00
techknowlogick 0be14de5c2 bump devcontainer 2026-02-02 23:02:00 +00:00
techknowlogick 4f8cb7ef19 helpful error messages (#871)
Reviewed-on: https://gitea.com/gitea/tea/pulls/871
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
2026-02-02 22:59:22 +00:00
techknowlogick f638dba99b More improvements (#870)
- no duplicate logins
- link to html page rather than api in output
- client side pagination of watched repos

Reviewed-on: https://gitea.com/gitea/tea/pulls/870
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
2026-02-02 22:58:25 +00:00
techknowlogick 20da414145 Code Cleanup (#869)
- switch to golangci-lint for linting
- switch to gofmpt for formatting
- fix lint and fmt issues that came up from switch to new tools
- upgrade go-sdk to 0.23.2
- support pagination for listing tracked times
- remove `FixPullHeadSha` workaround (upstream fix has been merged for 5+ years at this point)
- standardize on US spelling (previously a mix of US&UK spelling)
- remove some unused code
- reduce some duplication in parsing state and issue type
- reduce some duplication in reading input for secrets and variables
- reduce some duplication with PR Review code
- report error for when yaml parsing fails
- various other misc cleanup

Reviewed-on: https://gitea.com/gitea/tea/pulls/869
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
2026-02-02 22:39:26 +00:00
techknowlogick ae740a66e8 update sdk version (#868)
Reviewed-on: https://gitea.com/gitea/tea/pulls/868
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
2026-02-02 19:54:44 +00:00
techknowlogick c2e9265dae bump more CI actions 2026-02-02 19:53:20 +00:00
techknowlogick 45260e1a1f bump action versions in CI for PRs
disable govulncheck temporarily
2026-02-02 19:50:25 +00:00
Alain Thiffault 7ab3366220 fix(labels): improve delete command and fix --id flag type (#865)
## Summary

Fix the `tea labels delete` and `tea labels update` commands which were silently ignoring the `--id` flag.

## Problem

Both commands used `IntFlag` for the `--id` parameter but called `ctx.Int64("id")` to retrieve the value. This type mismatch caused the ID to always be read as `0`, making the commands useless.

**Before (bug):**
```bash
$ tea labels delete --id 36 --debug
DELETE: .../labels/0   # Wrong! ID ignored
```

**After (fix):**
```bash
$ tea labels delete --id 36 --debug
GET: .../labels/36     # Verify exists
DELETE: .../labels/36  # Correct ID
Label 'my-label' (id: 36) deleted successfully
```

## Changes

### labels/delete.go
- Change `IntFlag` to `Int64Flag` to match `ctx.Int64()` usage
- Make `--id` flag required
- Verify label exists before attempting deletion
- Provide clear error messages with label name and ID context
- Print success message after deletion

### labels/update.go
- Change `IntFlag` to `Int64Flag` to fix the same bug

## Test plan

- [x] `go test ./...` passes
- [x] `go vet ./...` passes
- [x] `gofmt` check passes
- [x] Manual testing confirms ID is now correctly passed to API
- [ ] CI passes

Reviewed-on: https://gitea.com/gitea/tea/pulls/865
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Alain Thiffault <athiffau@effectivemomentum.com>
Co-committed-by: Alain Thiffault <athiffau@effectivemomentum.com>
2026-01-25 23:36:42 +00:00
Alain Thiffault 68b9620b8c fix: expose pagination flags for secrets list command (#853)
The command uses flags.GetListOptions() internally but didn't expose --page and --limit flags to users, making pagination inaccessible.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/853
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Alain Thiffault <athiffau@effectivemomentum.com>
Co-committed-by: Alain Thiffault <athiffau@effectivemomentum.com>
2025-12-05 06:14:41 +00:00
Alain Thiffault e961a8f01d fix: expose pagination flags for webhooks list command (#852)
The command uses flags.GetListOptions() internally but didn't expose --page and --limit flags to users, making pagination inaccessible.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/852
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Alain Thiffault <athiffau@effectivemomentum.com>
Co-committed-by: Alain Thiffault <athiffau@effectivemomentum.com>
2025-12-05 06:14:34 +00:00
Alain Thiffault f59430a42a fix: pass pagination options to ListRepoPullRequests (#851)
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/851
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Alain Thiffault <athiffau@effectivemomentum.com>
Co-committed-by: Alain Thiffault <athiffau@effectivemomentum.com>
2025-12-05 06:14:01 +00:00
Lunny Xiao 7e2e7ee809 Fix delete repo description (#858)
Fix #857

Reviewed-on: https://gitea.com/gitea/tea/pulls/858
2025-12-05 06:11:38 +00:00
Riccardo Förster 1d1d9197ee feat(issue): Add JSON output and file redirection (#841)
This change enhances the 'issue' command functionality by enabling structured JSON
output for single issue views and introducing a method for output redirection.

**Changes Implemented:**

1. Enables the existing `--output json` flag for single issue commands (e.g., 'tea issue 17'). This flag was previously ignored in this context.
2. Introduces the new `--out <filename>` flag, which redirects the marshaled JSON output from stdout to the specified file.

Feeback more then welcome.

Co-authored-by: Jonas Toth <development@jonas-toth.eu>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/841
Reviewed-by: TheFox0x7 <thefox0x7@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Riccardo Förster <riccardo.foerster@sarad.de>
Co-committed-by: Riccardo Förster <riccardo.foerster@sarad.de>
2025-11-29 05:05:30 +00:00
TheFox0x7 f6d4b5fa4f remove group readwrite permission (#856)
closes: https://gitea.com/gitea/tea/issues/855
Reviewed-on: https://gitea.com/gitea/tea/pulls/856
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-committed-by: TheFox0x7 <thefox0x7@gmail.com>
2025-11-27 22:45:25 +00:00
Brandon Martin 016e068c60 Fix: Enable git worktree support and improve pr create error handling (#850)
## Problem

Tea commands fail when run from git worktrees with the error:
Remote repository required: Specify ID via --repo or execute from a
local git repo.

Even though the worktree is in a valid git repository with remotes
configured.

Additionally, `tea pr create` was missing context validation, showing
cryptic errors like `"path segment [0]
is empty"` instead of helpful messages.

## Root Cause

1. **Worktree issue**: go-git's `PlainOpenWithOptions` was not
configured to read the `commondir` file that
git worktrees use. This file points to the main repository's `.git`
directory where remotes are actually
stored (worktrees don't have their own remotes).

2. **PR create issue**: Missing `ctx.Ensure()` validation meant errors
weren't caught early with clear
messages.

## Solution

### 1. Enable worktree support (`modules/git/repo.go`)
```go
EnableDotGitCommonDir: true, // Enable commondir support for worktrees

This tells go-git to:
- Read the commondir file in .git/worktrees/<name>/commondir
- Follow the reference (typically ../..) to the main repository
- Load remotes from the main repo's config

2. Add context validation (cmd/pulls/create.go)

ctx.Ensure(context.CtxRequirement{
LocalRepo:  true,
RemoteRepo: true,
})

Provides clear error messages and matches the pattern used in pr
checkout (fixed in commit 0970b945 from
2020).

3. Add test coverage (modules/git/repo_test.go)

- Creates a real git repository with a worktree
- Verifies that RepoFromPath() can open the worktree
- Confirms that Config() correctly reads remotes from main repo

Test Results

Without fix:
 FAIL: Should NOT be empty, but was map[]

With fix:
 PASS: TestRepoFromPath_Worktree (0.12s)

Manual test in worktree:
cd /path/to/worktree
tea pr create --title "test"
# Now works! 

Checklist

- Tested manually in a git worktree
- Added test case that fails without the fix
- All existing tests pass

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/850
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Brandon Martin <brandon@codedmart.com>
Co-committed-by: Brandon Martin <brandon@codedmart.com>
2025-11-24 22:21:19 +00:00
Lunny Xiao 587b31503d Upgrade dependencies (#849)
Reviewed-on: https://gitea.com/gitea/tea/pulls/849
2025-11-24 19:21:55 +00:00
qwerty287 4877f181fb Only prompt for SSH passphrase if necessary (#844)
Since one of the last updates (I cannot tell you exactly which one, but likely 0.10 or 0.11), tea always asks me for my ssh passphrase without actually needing it. I do not have anything configured regarding SSH keys.

The passphrase is not even verified, you can enter anything there. But as this is quite annoying, I fixed this by moving the prompt to only be used when a ssh key/cert is configured.

Would be nice to get this in. Thanks!

Reviewed-on: https://gitea.com/gitea/tea/pulls/844
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: qwerty287 <qwerty287@posteo.de>
Co-committed-by: qwerty287 <qwerty287@posteo.de>
2025-11-20 01:32:28 +00:00
Ross Golder 81481f8f9d Fix: Only prompt for login confirmation when no default login is set (#839)
When running tea commands outside of a repository context, tea falls back to using the default login but always prompted for confirmation, even when a default was set. This fix only prompts when no default is configured.

Reviewed-on: https://gitea.com/gitea/tea/pulls/839
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Ross Golder <ross@golder.org>
Co-committed-by: Ross Golder <ross@golder.org>
2025-10-27 17:52:04 +00:00
Ross Golder 3495ec5ed4 feat: add repository webhook management (#798)
## Summary

This PR adds support for organization-level and global webhooks in the tea CLI tool.

## Changes Made

### Organization Webhooks
- Added `--org` flag to webhook commands to operate on organization-level webhooks
- Implemented full CRUD operations for org webhooks (create, list, update, delete)
- Extended TeaContext to support organization scope

### Global Webhooks
- Added `--global` flag with placeholder implementation
- Ready for when Gitea SDK adds global webhook API methods

### Technical Details
- Updated context handling to support org/global scopes
- Modified all webhook subcommands (create, list, update, delete)
- Maintained backward compatibility for repository webhooks
- Updated tests and documentation

## Usage Examples

```bash
# Repository webhooks (existing)
tea webhooks list
tea webhooks create https://example.com/hook --events push

# Organization webhooks (new)
tea webhooks list --org myorg
tea webhooks create https://example.com/hook --org myorg --events push,pull_request

# Global webhooks (future)
tea webhooks list --global
```

## Testing
- All existing tests pass
- Updated test expectations for new descriptions
- Manual testing of org webhook operations completed

Closes: webhook management feature request
Reviewed-on: https://gitea.com/gitea/tea/pulls/798
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Ross Golder <ross@golder.org>
Co-committed-by: Ross Golder <ross@golder.org>
2025-10-19 03:40:23 +00:00
Ross Golder 7a5c260268 feat: add actions management commands (#796)
## Summary

This PR adds comprehensive Actions secrets and variables management functionality to the tea CLI, enabling users to manage their repository's CI/CD configuration directly from the command line.

## Features Added

### Actions Secrets Management
- **List secrets**: `tea actions secrets list` - Display all repository action secrets
- **Create secrets**: `tea actions secrets create <name>` - Create new secrets with interactive prompts
- **Delete secrets**: `tea actions secrets delete <name>` - Remove existing secrets

### Actions Variables Management
- **List variables**: `tea actions variables list` - Display all repository action variables
- **Set variables**: `tea actions variables set <name> <value>` - Create or update variables
- **Delete variables**: `tea actions variables delete <name>` - Remove existing variables

## Implementation Details

- **Interactive prompts**: Secure input handling for sensitive secret values
- **Input validation**: Proper validation for secret/variable names and values
- **Table formatting**: Consistent output formatting with existing tea commands
- **Error handling**: Comprehensive error handling and user feedback
- **Test coverage**: Full test suite for all functionality

## Usage Examples

```bash
# Secrets management
tea actions secrets list
tea actions secrets create API_KEY    # Will prompt securely for value
tea actions secrets delete OLD_SECRET

# Variables management
tea actions variables list
tea actions variables set API_URL https://api.example.com
tea actions variables delete UNUSED_VAR
```

## Related Issue

Resolves #797

## Testing

- All new functionality includes comprehensive unit tests
- Integration with existing tea CLI patterns and conventions
- Validated against Gitea Actions API

Reviewed-on: https://gitea.com/gitea/tea/pulls/796
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Ross Golder <ross@golder.org>
Co-committed-by: Ross Golder <ross@golder.org>
2025-10-19 02:53:17 +00:00
Lunny Xiao 90f8624ae7 Login requires a http/https login URL and revmoe SSH as a login method. SSH will be optional (#826)
Fix #825

Reviewed-on: https://gitea.com/gitea/tea/pulls/826
2025-10-18 23:09:27 +00:00
Lunny Xiao 61d4e571a7 Fix Pr Create crash (#823)
Fix #822

Reviewed-on: https://gitea.com/gitea/tea/pulls/823
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-10-08 14:43:38 +00:00
Lunny Xiao 4f33146b70 add test for matching logins (#820)
Reviewed-on: https://gitea.com/gitea/tea/pulls/820
2025-10-03 18:05:51 +00:00
Lunny Xiao 08b83986dd Update README.md (#819)
Use official docker images on README

Reviewed-on: https://gitea.com/gitea/tea/pulls/819
Reviewed-by: TheFox0x7 <thefox0x7@noreply.gitea.com>
2025-09-25 07:08:21 +00:00
Lunny Xiao 6acb29efd7 Fix yaml output single quote (#814)
Fix #659

Reviewed-on: https://gitea.com/gitea/tea/pulls/814
2025-09-14 00:23:12 +00:00
Valentin Brandl 4f513ca3e3 generate man page (#811)
[CLI.md](src/branch/main/docs/CLI.md) already gets generated using `urfave/cli-docs`. `cli-docs` can also generate man pages.

This change extends the doc generator to also generate a man page for `tea`.

* Add a subcommand to the doc generator to print the generated man page to stdout

Closes #777.

Co-authored-by: Valentin Brandl <mail@vbrandl.net>
Reviewed-on: https://gitea.com/gitea/tea/pulls/811
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: TheFox0x7 <thefox0x7@noreply.gitea.com>
Co-authored-by: Valentin Brandl <vbrandl@noreply.gitea.com>
Co-committed-by: Valentin Brandl <vbrandl@noreply.gitea.com>
2025-09-14 00:17:28 +00:00
Lunny Xiao cc20b52ab3 feat: add validation for object-format flag in repo create command (#741)
This PR adds validation for the `--object-format` flag in the `repo create` command. The flag now accepts only `sha1` or `sha256` as valid values, and returns an error for any other value.

Changes:
- Added validation in `runRepoCreate` to check for valid object format values
- Added unit tests to verify the validation logic
- Fixed the field name from `ObjectFormat` to `ObjectFormatName` to match the SDK

The changes ensure that users get clear error messages when using invalid object format values, improving the user experience.

Fix #727
Fix #660
Fix #767

Co-authored-by: techknowlogick <techknowlogick@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/741
Reviewed-by: TheFox0x7 <thefox0x7@noreply.gitea.com>
2025-09-12 16:51:43 +00:00
Lunny Xiao 2ca114e309 Fix release version (#815)
Reviewed-on: https://gitea.com/gitea/tea/pulls/815
Reviewed-by: TheFox0x7 <thefox0x7@noreply.gitea.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-09-11 19:18:19 +00:00
TheFox0x7 45771265c4 update gitea sdk to v0.22 (#813)
needed because of: https://gitea.com/gitea/go-sdk/commit/25b5fb0ff757f95731b5f3240153a4da59cf6c68
closes: https://gitea.com/gitea/tea/issues/812

Reviewed-on: https://gitea.com/gitea/tea/pulls/813
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-committed-by: TheFox0x7 <thefox0x7@gmail.com>
2025-09-10 23:18:05 +00:00
Lunny Xiao 8faa1d33f4 don't fallback login directly (#806)
Fix #499

Reviewed-on: https://gitea.com/gitea/tea/pulls/806
2025-09-10 21:30:15 +00:00
Lunny Xiao ddf5c0a5bb Check duplicated login name in interact mode when creating new login (#803)
Reviewed-on: https://gitea.com/gitea/tea/pulls/803
2025-09-10 20:42:49 +00:00
Lunny Xiao d3c73cd5dc Fix bug when output json with special chars (#801)
Fix #800

Reviewed-on: https://gitea.com/gitea/tea/pulls/801
2025-09-10 20:36:27 +00:00
Lunny Xiao 6c958eec99 add debug mode and update readme (#805)
Fix #456
Fix #207

Reviewed-on: https://gitea.com/gitea/tea/pulls/805
2025-09-10 19:10:02 +00:00
Lunny Xiao d531c6fdb0 update go.mod to retract the wrong tag v1.3.3 (#802)
Fix #674

Reviewed-on: https://gitea.com/gitea/tea/pulls/802
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-08-27 15:38:58 +00:00
TheFox0x7 cd58296995 revert completion scripts removal (#808)
partial revert of: https://gitea.com/gitea/tea/pulls/782
closes: https://gitea.com/gitea/tea/issues/784

Old versions of tea have hardcoded completion fetched from main branch

Those should not be used from v0.10 onward.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/808
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-committed-by: TheFox0x7 <thefox0x7@gmail.com>
2025-08-26 23:15:36 +00:00
TheFox0x7 b74405530a Remove pagination from context (#807)
Pagination related flags now write directly to ListOption struct and enforce non negative numbers.
Flag tests were added to cover the validation

Reviewed-on: https://gitea.com/gitea/tea/pulls/807
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-committed-by: TheFox0x7 <thefox0x7@gmail.com>
2025-08-26 23:13:27 +00:00
Chen Linxuan 8876fe3cb8 Continue auth when failed to open browser (#794)
When users login gitea on a headless server via ssh, xdg-open might not be installed on that machine. So tea may fail to open URL itself. In this case, users can use the other machine to open the URL for authentication.

Github CLI act like this, too.

Signed-off-by: Chen Linxuan <me@black-desk.cn>

Reviewed-on: https://gitea.com/gitea/tea/pulls/794
Reviewed-by: blumia <blumia@noreply.gitea.com>
Co-authored-by: Chen Linxuan <me@black-desk.cn>
Co-committed-by: Chen Linxuan <me@black-desk.cn>
2025-08-18 03:12:25 +00:00
Lunny Xiao 07ca1ba106 Fix bug (#793)
Partially fix #791

Reviewed-on: https://gitea.com/gitea/tea/pulls/793
Reviewed-by: hiifong <i@hiif.ong>
2025-08-15 02:38:45 +00:00
Lunny Xiao d643e94a69 Fix tea login add with ssh public key bug (#789)
Fix #705

Reviewed-on: https://gitea.com/gitea/tea/pulls/789
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
2025-08-11 19:23:28 +00:00
Tim Riedl d2ccead88b Add temporary authentication via environment variables (#639)
#633

Co-authored-by: Tim Riedl <mail@tim-riedl.de>
Co-authored-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Lunny Xiao <lunny@noreply.gitea.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/639
Co-authored-by: Tim Riedl <uvulpos@noreply.gitea.com>
Co-committed-by: Tim Riedl <uvulpos@noreply.gitea.com>
2025-08-11 18:53:09 +00:00
Lunny Xiao 449b2e3117 Fix attachment size (#787)
Fix `tea releases assets <tag_name>` displayed wrong attachment size.

Reviewed-on: https://gitea.com/gitea/tea/pulls/787
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-08-11 18:45:12 +00:00
Lunny Xiao 9e8c71e13e deploy image when tagging (#792)
Reviewed-on: https://gitea.com/gitea/tea/pulls/792
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-08-11 18:44:34 +00:00
Lunny Xiao 2ddb3bd4a1 Add Zip URL for release list (#788)
Fix #780

Reviewed-on: https://gitea.com/gitea/tea/pulls/788
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-08-11 18:43:34 +00:00
Lunny Xiao 4c00b8b571 Use bubbletea instead of survey for interacting with TUI (#786)
Fix #772

Reviewed-on: https://gitea.com/gitea/tea/pulls/786
Reviewed-by: Bo-Yi Wu (吳柏毅) <appleboy.tw@gmail.com>
2025-08-11 18:23:52 +00:00
techknowlogick c0eb30af03 capitalize a few items 2025-08-11 15:29:19 +00:00
techknowlogick e462acfcd6 rm out of date comparison file 2025-08-11 15:27:04 +00:00
Michal Suchanek ee111d7c12 README: Document logging in to gitea (#790)
References: #569 #675 #697 #767 #775
Reviewed-on: https://gitea.com/gitea/tea/pulls/790
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Michal Suchanek <msuchanek@suse.de>
Co-committed-by: Michal Suchanek <msuchanek@suse.de>
2025-08-11 15:25:42 +00:00
TheFox0x7 5f35cebcf1 remove autocomplete command (#782)
Add a note in readme about adding shell completions

Closes: https://gitea.com/gitea/tea/issues/781
Co-authored-by: techknowlogick <techknowlogick@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/782
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-committed-by: TheFox0x7 <thefox0x7@gmail.com>
2025-07-21 21:24:28 +00:00
Renovate Bot a010c9bc7f chore(deps): update ghcr.io/devcontainers/features/git-lfs docker tag to v1.2.5 (#773)
Reviewed-on: https://gitea.com/gitea/tea/pulls/773
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2025-07-21 21:22:04 +00:00
TheFox0x7 ab4ad92d40 replace arch package url (#783)
Direct users to main repository instead of AUR package

Reviewed-on: https://gitea.com/gitea/tea/pulls/783
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-committed-by: TheFox0x7 <thefox0x7@gmail.com>
2025-07-21 21:21:25 +00:00
Kirill Müller 15052b4dcc fix: Reenable -p and --limit switches (#778)
Reduced version of #776, without the new tests.

Fixes #771.

Reviewed-on: https://gitea.com/gitea/tea/pulls/778
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Kirill Müller <kirill@cynkra.com>
Co-committed-by: Kirill Müller <kirill@cynkra.com>
2025-07-14 14:28:35 +00:00
Lunny Xiao 8212d5f527 Update release ci (#768)
Reviewed-on: https://gitea.com/gitea/tea/pulls/768
2025-06-16 19:23:55 +00:00
Renovate Bot d536242aa9 chore(deps): update crazy-max/ghaction-import-gpg action to v6 (#736)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [crazy-max/ghaction-import-gpg](https://github.com/crazy-max/ghaction-import-gpg) | action | major | `v5` -> `v6` |

---

### Release Notes

<details>
<summary>crazy-max/ghaction-import-gpg (crazy-max/ghaction-import-gpg)</summary>

### [`v6`](https://github.com/crazy-max/ghaction-import-gpg/compare/v5...v6)

[Compare Source](https://github.com/crazy-max/ghaction-import-gpg/compare/v5...v6)

</details>

---

### Configuration

📅 **Schedule**: 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 [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yMDcuMyIsInVwZGF0ZWRJblZlciI6IjM5LjIwNy4zIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/736
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>
2025-06-16 17:10:55 +00:00
Renovate Bot ffff540aa7 fix(deps): update module github.com/urfave/cli/v3 to v3.3.8 (#766)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [github.com/urfave/cli/v3](https://github.com/urfave/cli) | require | patch | `v3.3.3` -> `v3.3.8` |

---

### Release Notes

<details>
<summary>urfave/cli (github.com/urfave/cli/v3)</summary>

### [`v3.3.8`](https://github.com/urfave/cli/releases/tag/v3.3.8)

[Compare Source](https://github.com/urfave/cli/compare/v3.3.7...v3.3.8)

#### What's Changed

-   Remove "alpha" wording around `v3` series by [@&#8203;meatballhat](https://github.com/meatballhat) in https://github.com/urfave/cli/pull/2155

**Full Changelog**: https://github.com/urfave/cli/compare/v3.3.7...v3.3.8

### [`v3.3.7`](https://github.com/urfave/cli/releases/tag/v3.3.7)

[Compare Source](https://github.com/urfave/cli/compare/v3.3.6...v3.3.7)

#### What's Changed

-   fix: add missing `IsLocal` for BoolWithInverseFlag by [@&#8203;huiyifyj](https://github.com/huiyifyj) in https://github.com/urfave/cli/pull/2151
-   Fix OnUsageError Trigger When Error Is Caused by Mutually Exclusive Flags by [@&#8203;Ali-Doustkani](https://github.com/Ali-Doustkani) in https://github.com/urfave/cli/pull/2152

#### New Contributors

-   [@&#8203;Ali-Doustkani](https://github.com/Ali-Doustkani) made their first contribution in https://github.com/urfave/cli/pull/2152

**Full Changelog**: https://github.com/urfave/cli/compare/v3.3.6...v3.3.7

### [`v3.3.6`](https://github.com/urfave/cli/releases/tag/v3.3.6)

[Compare Source](https://github.com/urfave/cli/compare/v3.3.5...v3.3.6)

#### What's Changed

-   Fish completions with identically named sub-commands now work by [@&#8203;bittrance](https://github.com/bittrance) in https://github.com/urfave/cli/pull/2130

**Full Changelog**: https://github.com/urfave/cli/compare/v3.3.5...v3.3.6

### [`v3.3.5`](https://github.com/urfave/cli/releases/tag/v3.3.5)

[Compare Source](https://github.com/urfave/cli/compare/v3.3.4...v3.3.5)

#### What's Changed

-   Fix:(issue\_2137) Ensure default value for bool with inverse flag is h… by [@&#8203;dearchap](https://github.com/dearchap) in https://github.com/urfave/cli/pull/2138
-   Fix:(issue\_2131) Show help text for BoolWithInverseFlag by [@&#8203;Juneezee](https://github.com/Juneezee) in https://github.com/urfave/cli/pull/2142

**Full Changelog**: https://github.com/urfave/cli/compare/v3.3.4...v3.3.5

### [`v3.3.4`](https://github.com/urfave/cli/releases/tag/v3.3.4)

[Compare Source](https://github.com/urfave/cli/compare/v3.3.3...v3.3.4)

#### What's Changed

-   Fix Docs(issue\_2125) Add PathFlag to StringFlag migration by [@&#8203;dearchap](https://github.com/dearchap) in https://github.com/urfave/cli/pull/2136
-   fix: remove extraneous space from subcommand help template by [@&#8203;G-Rath](https://github.com/G-Rath) in https://github.com/urfave/cli/pull/2140
-   Fix:(issue\_2135) Correct formatting of default subcommand USAGE text by [@&#8203;zzspoon](https://github.com/zzspoon) in https://github.com/urfave/cli/pull/2139

#### New Contributors

-   [@&#8203;zzspoon](https://github.com/zzspoon) made their first contribution in https://github.com/urfave/cli/pull/2139

**Full Changelog**: https://github.com/urfave/cli/compare/v3.3.3...v3.3.4

</details>

---

### Configuration

📅 **Schedule**: 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 [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC40OS42IiwidXBkYXRlZEluVmVyIjoiNDAuNDkuNiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Reviewed-on: https://gitea.com/gitea/tea/pulls/766
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
2025-06-16 17:07:00 +00:00
300 changed files with 21331 additions and 3212 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
{
"name": "Tea DevContainer",
"image": "mcr.microsoft.com/devcontainers/go:1.24-bullseye",
"image": "mcr.microsoft.com/devcontainers/go:2.1-trixie",
"features": {
"ghcr.io/devcontainers/features/git-lfs:1.2.4": {}
"ghcr.io/devcontainers/features/git-lfs:1.2.5": {}
},
"customizations": {
"vscode": {
+16 -11
View File
@@ -8,35 +8,40 @@ jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0
- run: git fetch --force --tags
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
- name: import gpg
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v5
uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
- name: get SDK version
id: sdk_version
run: echo "version=$(go list -f '{{.Version}}' -m gitea.dev/sdk)" >> "$GITHUB_OUTPUT"
- name: goreleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser-pro
version: "~> v1"
args: release --nightly
env:
SDK_VERSION: ${{ steps.sdk_version.outputs.version }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
AWS_REGION: ${{ secrets.AWS_REGION }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
S3_REGION: ${{ secrets.AWS_REGION }}
S3_BUCKET: ${{ secrets.AWS_BUCKET }}
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
GORELEASER_FORCE_TOKEN: 'gitea'
GPGSIGN_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
release-image:
runs-on: ubuntu-latest
env:
@@ -44,24 +49,24 @@ jobs:
DOCKER_LATEST: nightly
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0 # all history for all branches and tags
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Login to DockerHub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
env:
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
with:
+48 -5
View File
@@ -9,27 +9,30 @@ jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0
- run: git fetch --force --tags
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- name: import gpg
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v6
uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
fingerprint: CC64B1DB67ABBEECAB24B6455FC346329753F4B0
- name: get SDK version
id: sdk_version
run: echo "version=$(go list -f '{{.Version}}' -m gitea.dev/sdk)" >> "$GITHUB_OUTPUT"
- name: goreleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@v7
with:
distribution: goreleaser-pro
version: "~> v1"
args: release
env:
SDK_VERSION: ${{ steps.sdk_version.outputs.version }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
AWS_REGION: ${{ secrets.AWS_REGION }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
@@ -40,3 +43,43 @@ jobs:
GPGSIGN_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
release-image:
runs-on: ubuntu-latest
env:
DOCKER_ORG: gitea
DOCKER_LATEST: nightly
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0 # all history for all branches and tags
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v4
- name: Login to DockerHub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Get tag version without v
id: get_version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Build and push
uses: docker/build-push-action@v7
env:
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
with:
context: .
file: ./Dockerfile
platforms: |
linux/amd64
linux/arm64
push: true
tags: |
gitea/tea:${{ env.VERSION }}
+55 -6
View File
@@ -4,11 +4,20 @@ on:
- pull_request
jobs:
check-and-test:
#govulncheck_job:
# runs-on: ubuntu-latest
# name: Run govulncheck
# steps:
# - id: govulncheck
# uses: golang/govulncheck-action@v1
# with:
# go-version-file: 'go.mod'
check-and-unit:
name: Lint Build And Unit Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- uses: actions/checkout@v7
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- name: lint and build
@@ -17,10 +26,50 @@ jobs:
make vet
make lint
make fmt-check
make misspell-check
make docs-check
make build
- name: test and coverage
- name: unit test and coverage
run: |
make test
make unit-test-coverage
integration-test:
name: Integration Test
runs-on: ubuntu-latest
env:
HTTP_PROXY: ""
GITEA_TEA_TEST_URL: "http://gitea:3000"
GITEA_TEA_TEST_USERNAME: "test01"
GITEA_TEA_TEST_PASSWORD: "test01"
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- run: curl --noproxy "*" http://gitea:3000/api/v1/version # verify connection to instance
- name: integration test
run: |
make integration-test
services:
gitea:
image: docker.gitea.com/gitea:1.26.2
cmd:
- bash
- -c
- >-
mkdir -p /tmp/conf/
&& mkdir -p /tmp/data/
&& echo "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT = true" > /tmp/conf/app.ini
&& echo "[security]" >> /tmp/conf/app.ini
&& echo "INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE1NTg4MzY4ODB9.LoKQyK5TN_0kMJFVHWUW0uDAyoGjDP6Mkup4ps2VJN4" >> /tmp/conf/app.ini
&& echo "INSTALL_LOCK = true" >> /tmp/conf/app.ini
&& echo "SECRET_KEY = 2crAW4UANgvLipDS6U5obRcFosjSJHQANll6MNfX7P0G3se3fKcCwwK3szPyGcbo" >> /tmp/conf/app.ini
&& echo "PASSWORD_COMPLEXITY = off" >> /tmp/conf/app.ini
&& echo "[database]" >> /tmp/conf/app.ini
&& echo "DB_TYPE = sqlite3" >> /tmp/conf/app.ini
&& echo "[repository]" >> /tmp/conf/app.ini
&& echo "ROOT = /tmp/data/" >> /tmp/conf/app.ini
&& echo "[server]" >> /tmp/conf/app.ini
&& echo "ROOT_URL = http://gitea:3000" >> /tmp/conf/app.ini
&& gitea migrate -c /tmp/conf/app.ini
&& gitea admin user create --username=test01 --password=test01 --email=test01@gitea.io --admin=true --must-change-password=false --access-token -c /tmp/conf/app.ini
&& gitea web -c /tmp/conf/app.ini
+2
View File
@@ -17,3 +17,5 @@ dist/
.direnv/
result
result-*
.DS_Store
+45
View File
@@ -0,0 +1,45 @@
version: "2"
formatters:
enable:
- gofumpt
linters:
default: none
enable:
- govet
- revive
- misspell
- ineffassign
- unused
settings:
revive:
rules:
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
- name: if-return
- name: increment-decrement
- name: var-declaration
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
misspell:
locale: US
ignore-words:
- unknwon
- destory
issues:
max-issues-per-linter: 0
max-same-issues: 0
+1 -3
View File
@@ -38,8 +38,6 @@ builds:
- goos: windows
goarch: arm
goarm: "7"
- goos: windows
goarch: arm64
- goos: freebsd
goarch: ppc64le
- goos: freebsd
@@ -58,7 +56,7 @@ builds:
flags:
- -trimpath
ldflags:
- -s -w -X main.Version={{ .Version }}
- -s -w -X "gitea.dev/tea/modules/version.Version={{ trimprefix .Summary "v" }}" -X "gitea.dev/tea/modules/version.Tags=" -X "gitea.dev/tea/modules/version.SDK={{ .Env.SDK_VERSION }}"
binary: >-
{{ .ProjectName }}-
{{- .Version }}-
+15
View File
@@ -1,5 +1,20 @@
# Changelog
## [v0.13.0](https://gitea.com/gitea/tea/releases/tag/v0.13.0) - 2026-04-05
* FEATURES
* Add `tea pr edit` subcommand for pull requests (#944)
* Add `tea repo edit` subcommand (#928)
* Support owner-based repository listing in `tea repo ls` (#931)
* Store OAuth tokens in OS keyring via credstore (#926)
* Support parsing multiple values in `tea api` subcommand (#911)
* ENHANCEMENTS
* Replace log.Fatal/os.Exit with proper error returns (#941)
* Update to charm libraries v2 (#923)
* MISC
* Bump Go version to 1.26
* Update dependencies: go-git/v5 v5.17.2, gitea SDK v0.24.1, urfave/cli/v3 v3.8.0, oauth2 v0.36.0, tablewriter v1.1.4, go-authgate/sdk-go v0.6.1
## [v0.9.1](https://gitea.com/gitea/tea/releases/tag/v0.9.1) - 2023-02-15
* BUGFIXES
+1 -1
View File
@@ -124,7 +124,7 @@ import (
// local packages
"code.gitea.io/gitea/models"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
// external packages
"github.com/foo/bar"
-63
View File
@@ -1,63 +0,0 @@
# comparing git forge commandline interfaces
[tea]: https://gitea.com/gitea/tea
[sip]: https://gitea.com/jolheiser/sip
[gitlab]: https://github.com/makkes/gitlab-cli
[glab]: https://github.com/profclems/glab
[gh]: https://cli.github.com
last update: 2020-12-11
## general
/ | [tea][tea] | [sip][sip] | [gitlab][gitlab] | [gh][gh]
-----------------------|:-----:|:-----:|:-----:|:-----:
forge|gitea|gitea|gitlab|github
official forge support|✓|✘|✘|✓
dev status|adding features|maintenance||
platform|any|any|any|any
## philosophy
/ | [tea][tea] | [sip][sip] | [gitlab][gitlab] | [gh][gh]
-----------------------|:-----:|:-----:|:-----:|:-----:
aims to replace git cli|✘|||✓
works with decentralization in mind|✓|✓|✓|✘
per-repo setup needed|✘||✓|✘
workflow helpers|✓|||
interactive mode |[(✓)](https://gitea.com/gitea/tea/issues?type=all&state=open&labels=&milestone=0&assignee=0&q=interactive)|✘| |✓
programmatic mode|✓|||✓
machine readable output|✓|||
follows XDG spec|✓|||
## features
/ | [tea][tea] | [sip][sip] | [gitlab][gitlab] | [gh][gh]
-----------------------|:-----:|:-----:|:-----:|:-----:
open web UI|✓|||
search repos|✓|||
search issues|✘|✓||
textual item search filter syntax|✘|✓||
CRUD repos|[(✓)](https://gitea.com/gitea/tea/issues/239)|||
CRUD issues|[(✓)](https://gitea.com/gitea/tea/issues/229)|||
CRUD milestones|[(✓)](https://gitea.com/gitea/tea/issues/246)|||
CRUD releases|✓|||
CRUD labels|✓|||
CRUD PRs|✓|||
CRUD time tracking|✓|||x
CRUD orgs|[(✓)](https://gitea.com/gitea/tea/issues/287)|||
create PRs from local repo|✓|||
create PRs from remote repo|✓|||
code review|[u](https://gitea.com/gitea/tea/issues/131)|||
merge PRs||||
read comments|[u](https://gitea.com/gitea/tea/issues/172)|||
post comments||||
manage CI|✘|✘|✓|
manage notifications|[(✓)]()|||
administration|[u](https://gitea.com/gitea/tea/issues/161)|✘||✘
markdown rendering|✓|||✓
issue import/export|[u](https://gitea.com/gitea/tea/issues/132)|||
checkout PRs|✓|||
- ✓: supported
- (✓): partial support
- u: upcoming
- ✘: not supported
- ?: unknown
+33 -27
View File
@@ -5,7 +5,10 @@ SHASUM ?= shasum -a 256
export PATH := $($(GO) env GOPATH)/bin:$(PATH)
GOFILES := $(shell find . -name "*.go" -type f ! -path "*/bindata.go")
GOFMT ?= gofmt -s
# Tool packages with pinned versions
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4
ifneq ($(DRONE_TAG),)
VERSION ?= $(subst v,,$(DRONE_TAG))
@@ -21,13 +24,17 @@ endif
TEA_VERSION_TAG ?= $(shell sed 's/+/_/' <<< $(TEA_VERSION))
TAGS ?=
SDK ?= $(shell $(GO) list -f '{{.Version}}' -m code.gitea.io/sdk/gitea)
LDFLAGS := -X "code.gitea.io/tea/cmd.Version=$(TEA_VERSION)" -X "code.gitea.io/tea/cmd.Tags=$(TAGS)" -X "code.gitea.io/tea/cmd.SDK=$(SDK)" -s -w
SDK ?= $(shell $(GO) list -f '{{.Version}}' -m gitea.dev/sdk)
LDFLAGS := -X "gitea.dev/tea/modules/version.Version=$(TEA_VERSION)" -X "gitea.dev/tea/modules/version.Tags=$(TAGS)" -X "gitea.dev/tea/modules/version.SDK=$(SDK)" -s -w
# override to allow passing additional goflags via make CLI
override GOFLAGS := $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)'
PACKAGES ?= $(shell $(GO) list ./...)
PACKAGES ?= $(shell $(GO) list ./... | grep -v '^gitea.dev/tea/tests')
UNIT_PACKAGES ?= $(PACKAGES)
INTEGRATION_PACKAGES ?= $(shell $(GO) list ./tests/... 2>/dev/null)
INTEGRATION_TEST_TAGS ?= testtools
INTEGRATION_TEST_GOFLAGS ?= -v
SOURCES ?= $(shell find . -name "*.go" -type f)
# OS specific vars.
@@ -49,7 +56,7 @@ clean:
.PHONY: fmt
fmt:
$(GOFMT) -w $(GOFILES)
$(GO) run $(GOFUMPT_PACKAGE) -w $(GOFILES)
.PHONY: vet
vet:
@@ -60,21 +67,17 @@ vet:
$(GO) vet -vettool=$(VET_TOOL) $(PACKAGES)
.PHONY: lint
lint: install-lint-tools
$(GO) run github.com/mgechev/revive@v1.3.2 -config .revive.toml ./... || exit 1
lint:
$(GO) run $(GOLANGCI_LINT_PACKAGE) run --build-tags testtools
.PHONY: misspell-check
misspell-check: install-lint-tools
$(GO) run github.com/client9/misspell/cmd/misspell@latest -error -i unknwon,destory $(GOFILES)
.PHONY: misspell
misspell: install-lint-tools
$(GO) run github.com/client9/misspell/cmd/misspell@latest -w -i unknwon $(GOFILES)
.PHONY: lint-fix
lint-fix:
$(GO) run $(GOLANGCI_LINT_PACKAGE) run --build-tags testtools --fix
.PHONY: fmt-check
fmt-check:
# get all go files and run go fmt on them
@diff=$$($(GOFMT) -d $(GOFILES)); \
# get all go files and run gofumpt on them
@diff=$$($(GO) run $(GOFUMPT_PACKAGE) -d $(GOFILES)); \
if [ -n "$$diff" ]; then \
echo "Please run 'make fmt' and commit the result:"; \
echo "$${diff}"; \
@@ -94,13 +97,24 @@ docs-check:
exit 1; \
fi;
.PHONY: unit-test
unit-test:
$(GO) test $(UNIT_PACKAGES)
.PHONY: integration-test
integration-test:
@if [ -n "$(INTEGRATION_PACKAGES)" ]; then \
$(GO) test $(INTEGRATION_TEST_GOFLAGS) -tags='$(INTEGRATION_TEST_TAGS)' $(INTEGRATION_PACKAGES); \
else \
echo "No integration test packages found"; \
fi
.PHONY: test
test:
$(GO) test -tags='sqlite sqlite_unlock_notify' $(PACKAGES)
test: unit-test integration-test
.PHONY: unit-test-coverage
unit-test-coverage:
$(GO) test -tags='sqlite sqlite_unlock_notify' -cover -coverprofile coverage.out $(PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1
$(GO) test -cover -coverprofile coverage.out $(UNIT_PACKAGES) && echo "\n==>\033[32m Ok\033[m\n" || exit 1
.PHONY: tidy
tidy:
@@ -123,11 +137,3 @@ $(EXECUTABLE): $(SOURCES)
.PHONY: build-image
build-image:
docker build --build-arg VERSION=$(TEA_VERSION) -t gitea/tea:$(TEA_VERSION_TAG) .
install-lint-tools:
@hash revive > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) install github.com/mgechev/revive@v1.3.2; \
fi
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) install github.com/client9/misspell/cmd/misspell@latest; \
fi
+87 -21
View File
@@ -3,7 +3,7 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![Release](https://raster.shields.io/badge/dynamic/json.svg?label=release&url=https://gitea.com/api/v1/repos/gitea/tea/releases&query=$[0].tag_name)](https://gitea.com/gitea/tea/releases)
[![Join the chat at https://img.shields.io/discord/322538954119184384.svg](https://img.shields.io/discord/322538954119184384.svg)](https://discord.gg/Gitea)
[![Go Report Card](https://goreportcard.com/badge/code.gitea.io/tea)](https://goreportcard.com/report/code.gitea.io/tea) [![GoDoc](https://pkg.go.dev/badge/code.gitea.io/tea?status.svg)](https://godoc.org/code.gitea.io/tea)
[![Go Report Card](https://goreportcard.com/badge/gitea.dev/tea)](https://goreportcard.com/report/gitea.dev/tea) [![GoDoc](https://pkg.go.dev/badge/gitea.dev/tea?status.svg)](https://godoc.org/gitea.dev/tea)
![Tea Release Status](https://gitea.com/gitea/tea/actions/workflows/release-nightly.yml/badge.svg)
## The official CLI for Gitea
@@ -11,13 +11,16 @@
![demo gif](./demo.gif)
```
NAME:
tea - command line tool to interact with Gitea
version 0.8.0-preview
USAGE
tea command [subcommand] [command options] [arguments...]
USAGE:
tea [global options] [command [command options]]
DESCRIPTION
VERSION:
Version: 0.10.1+15-g8876fe3 golang: 1.25.0 go-sdk: v0.21.0
DESCRIPTION:
tea is a productivity helper for Gitea. It can be used to manage most entities on
one or multiple Gitea instances & provides local helpers like 'tea pr checkout'.
@@ -26,34 +29,42 @@
upstream repo. tea assumes that local git state is published on the remote before
doing operations with tea. Configuration is persisted in $XDG_CONFIG_HOME/tea.
COMMANDS
COMMANDS:
help, h Shows a list of commands or help for one command
ENTITIES:
issues, issue, i List, create and update issues
pulls, pull, pr Manage and checkout pull requests
labels, label Manage issue labels
milestones, milestone, ms List and create milestones
releases, release, r Manage releases
release assets, release asset, r a Manage release attachments
times, time, t Operate on tracked times of a repository's issues & pulls
organizations, organization, org List, create, delete organizations
repos, repo Show repository details
branches, branch, b Consult branches
actions Manage repository actions (secrets, variables)
comment, c Add a comment to an issue / pr
webhooks, webhook Manage repository webhooks
HELPERS:
open, o Open something of the repository in web browser
notifications, notification, n Show notifications
clone, C Clone a repository locally
MISCELLANEOUS:
whoami Show current logged in user
admin, a Operations requiring admin access on the Gitea instance
SETUP:
logins, login Log in to a Gitea server
logout Log out from a Gitea server
shellcompletion, autocomplete Install shell completion for tea
whoami Show current logged in user
OPTIONS
--help, -h show help (default: false)
--version, -v print the version (default: false)
GLOBAL OPTIONS:
--debug, --vvv Enable debug mode (default: false)
--help, -h show help
--version, -v print the version
EXAMPLES
EXAMPLES
tea login add # add a login once to get started
tea pulls # list open pulls for the repo in $PWD
@@ -68,17 +79,25 @@
tea open 189 # open web ui for issue 189
tea open milestones # open web ui for milestones
tea actions secrets list # list all repository action secrets
tea actions secrets create API_KEY # create a new secret (will prompt for value)
tea actions variables list # list all repository action variables
tea actions variables set API_URL https://api.example.com
tea webhooks list # list repository webhooks
tea webhooks list --org myorg # list organization webhooks
tea webhooks create https://example.com/hook --events push,pull_request
# send gitea desktop notifications every 5 minutes (bash + libnotify)
while :; do tea notifications --mine -o simple | xargs -i notify-send {}; sleep 300; done
ABOUT
ABOUT
Written & maintained by The Gitea Authors.
If you find a bug or want to contribute, we'll welcome you at https://gitea.com/gitea/tea.
More info about Gitea itself on https://about.gitea.com.
```
- [Compare features with other git forge CLIs](./FEATURE-COMPARISON.md)
- tea uses [code.gitea.io/sdk](https://code.gitea.io/sdk) and interacts with the Gitea API.
- tea uses [gitea.dev/sdk](https://gitea.dev/sdk) and interacts with the Gitea API.
## Installation
@@ -89,7 +108,7 @@ There are different ways to get `tea`:
```sh
brew install tea
```
- arch linux ([gitea-tea-git](https://aur.archlinux.org/packages/gitea-tea-git), thirdparty)
- arch linux ([tea](https://archlinux.org/packages/extra/x86_64/tea/), thirdparty)
- alpine linux ([tea](https://pkgs.alpinelinux.org/packages?name=tea&branch=edge), thirdparty)
- Windows via `MSYS2` ([tea](https://packages.msys2.org/base/mingw-w64-tea), thirdparty)
@@ -97,13 +116,60 @@ There are different ways to get `tea`:
3. Install from source: [see *Compilation*](#compilation)
4. Docker (thirdparty): [tgerczei/tea](https://hub.docker.com/r/tgerczei/tea)
4. Docker: [Tea at docker hub](https://hub.docker.com/r/gitea/tea)
5. asdf (thirdparty): [mvaldes14/asdf-tea](https://github.com/mvaldes14/asdf-tea)
### Log in to Gitea from tea
Gitea can use many different authentication schemes, and not every authentication method will work with every Gitea deployment. When you are a Gitea instance administrator you can tweak your settings to fit your use case. For the method that is most likely to work with any Gitea deployment use the following steps:
1. Open your Gitea instance in a web browser
2. Log in to Gitea in your web browser. Any MFA, IDP, or whatever else should be available this way.
3. In your "user settings", generate an application token with at least **user read** permissions. If you want to do anything useful with the token add additional permissions/scopes.
4. Run `tea login add`, select **application token** authentication when asked for authentication type, and answer **yes** to the question if you have a token. Paste the generated token when asked for one.
You should now be logged in to your gitea instance from tea.
Since 0.10 Gitea supports the much simpler oauth workflow but oauth may not be available on all Gitea deployments, and gets much more complex when running tea on a remote system.
### Shell completion
If you installed from source or the package does not provide the completions with it you can add them yourself with `tea completion <shell>` command which is not visible in help. To generate the completions run one of the following commands depending on your shell.
```shell
# .bashrc
source <(tea completion bash)
# .zshrc
source <(tea completion zsh)
# fish
tea completion fish > ~/.config/fish/completions/tea.fish
# Powershell
Output the script to path/to/autocomplete/tea.ps1 an run it.
```
### Man Page
The hidden command `tea man` can be used to generate the `tea` man page.
```shell
# for bash or zsh
man <(tea man)
# for fish
man (tea man | psub)
# write man page to a file
tea man --out ./tea.man
```
## Compilation
Make sure you have a current go version installed (1.13 or newer).
Make sure you have a current Go version installed (1.26 or newer).
- To compile the source yourself with the recommended flags & tags:
```sh
@@ -120,7 +186,7 @@ Make sure you have a current go version installed (1.13 or newer).
- For a quick installation without `git` & `make`, set $version and exec:
```sh
go install code.gitea.io/tea@${version}
go install gitea.dev/tea@${version}
```
## Contributing
+47
View File
@@ -0,0 +1,47 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
stdctx "context"
"gitea.dev/tea/cmd/actions"
"github.com/urfave/cli/v3"
)
// CmdActions represents the actions command for managing Gitea Actions
var CmdActions = cli.Command{
Name: "actions",
Aliases: []string{"action"},
Category: catEntities,
Usage: "Manage repository actions",
Description: "Manage repository actions including secrets, variables, and workflow runs",
Action: runActionsDefault,
Commands: []*cli.Command{
&actions.CmdActionsSecrets,
&actions.CmdActionsVariables,
&actions.CmdActionsRuns,
&actions.CmdActionsWorkflows,
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "repo",
Usage: "repository to operate on",
},
&cli.StringFlag{
Name: "login",
Usage: "gitea login instance to use",
},
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "output format [table, csv, simple, tsv, yaml, json]",
},
},
}
func runActionsDefault(_ stdctx.Context, cmd *cli.Command) error {
return cli.ShowSubcommandHelp(cmd)
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
stdctx "context"
"gitea.dev/tea/cmd/actions/runs"
"github.com/urfave/cli/v3"
)
// CmdActionsRuns represents the actions runs command
var CmdActionsRuns = cli.Command{
Name: "runs",
Aliases: []string{"run"},
Usage: "Manage workflow runs",
Description: "List, view, and manage workflow runs for repository actions",
Action: runRunsDefault,
Commands: []*cli.Command{
&runs.CmdRunsList,
&runs.CmdRunsView,
&runs.CmdRunsDelete,
&runs.CmdRunsLogs,
},
}
func runRunsDefault(ctx stdctx.Context, cmd *cli.Command) error {
return runs.RunRunsList(ctx, cmd)
}
+71
View File
@@ -0,0 +1,71 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package runs
import (
stdctx "context"
"fmt"
"strconv"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"github.com/urfave/cli/v3"
)
// CmdRunsDelete represents a sub command to delete/cancel workflow runs
var CmdRunsDelete = cli.Command{
Name: "delete",
Aliases: []string{"remove", "rm", "cancel"},
Usage: "Delete or cancel a workflow run",
Description: "Delete (cancel) a workflow run from the repository",
ArgsUsage: "<run-id>",
Action: runRunsDelete,
Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "confirm",
Aliases: []string{"y"},
Usage: "confirm deletion without prompting",
},
}, flags.AllDefaultFlags...),
}
func runRunsDelete(ctx stdctx.Context, cmd *cli.Command) error {
if cmd.Args().Len() == 0 {
return fmt.Errorf("run ID is required")
}
c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client()
runIDStr := cmd.Args().First()
runID, err := strconv.ParseInt(runIDStr, 10, 64)
if err != nil {
return fmt.Errorf("invalid run ID: %s", runIDStr)
}
if !cmd.Bool("confirm") {
fmt.Printf("Are you sure you want to delete run %d? [y/N] ", runID)
var response string
fmt.Scanln(&response)
if response != "y" && response != "Y" && response != "yes" {
fmt.Println("Deletion canceled.")
return nil
}
}
_, err = client.Actions.DeleteRepoRun(ctx, c.Owner, c.Repo, runID)
if err != nil {
return fmt.Errorf("failed to delete run: %w", err)
}
fmt.Printf("Run %d deleted successfully\n", runID)
return nil
}
+148
View File
@@ -0,0 +1,148 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package runs
import (
stdctx "context"
"fmt"
"time"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"github.com/urfave/cli/v3"
)
// CmdRunsList represents a sub command to list workflow runs
var CmdRunsList = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List workflow runs",
Description: "List workflow runs for repository actions with optional filtering",
Action: RunRunsList,
Flags: append([]cli.Flag{
&flags.PaginationPageFlag,
&flags.PaginationLimitFlag,
&cli.StringFlag{
Name: "status",
Usage: "Filter by status (success, failure, pending, queued, in_progress, skipped, canceled)",
},
&cli.StringFlag{
Name: "branch",
Usage: "Filter by branch name",
},
&cli.StringFlag{
Name: "event",
Usage: "Filter by event type (push, pull_request, etc.)",
},
&cli.StringFlag{
Name: "actor",
Usage: "Filter by actor username (who triggered the run)",
},
&cli.StringFlag{
Name: "since",
Usage: "Show runs started after this time (e.g., '24h', '2024-01-01')",
},
&cli.StringFlag{
Name: "until",
Usage: "Show runs started before this time (e.g., '2024-01-01')",
},
}, flags.AllDefaultFlags...),
}
// parseTimeFlag parses time flags like "24h" or "2024-01-01"
func parseTimeFlag(value string) (time.Time, error) {
if value == "" {
return time.Time{}, nil
}
// Try parsing as duration (e.g., "24h", "168h")
if duration, err := time.ParseDuration(value); err == nil {
return time.Now().Add(-duration), nil
}
// Try parsing as date
formats := []string{
"2006-01-02",
"2006-01-02 15:04",
"2006-01-02T15:04:05",
time.RFC3339,
}
for _, format := range formats {
if t, err := time.Parse(format, value); err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("unable to parse time: %s", value)
}
// RunRunsList lists workflow runs
func RunRunsList(ctx stdctx.Context, cmd *cli.Command) error {
c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client()
// Parse time filters
since, err := parseTimeFlag(cmd.String("since"))
if err != nil {
return fmt.Errorf("invalid --since value: %w", err)
}
until, err := parseTimeFlag(cmd.String("until"))
if err != nil {
return fmt.Errorf("invalid --until value: %w", err)
}
// Build list options
listOpts := flags.GetListOptions(cmd)
runs, _, err := client.Actions.ListRepoRuns(ctx, c.Owner, c.Repo, gitea.ListRepoActionRunsOptions{
ListOptions: listOpts,
Status: cmd.String("status"),
Branch: cmd.String("branch"),
Event: cmd.String("event"),
Actor: cmd.String("actor"),
})
if err != nil {
return err
}
if runs == nil {
return print.ActionRunsList(nil, c.Output)
}
// Filter by time if specified
filteredRuns := filterRunsByTime(runs.WorkflowRuns, since, until)
return print.ActionRunsList(filteredRuns, c.Output)
}
// filterRunsByTime filters runs based on time range
func filterRunsByTime(runs []*gitea.ActionWorkflowRun, since, until time.Time) []*gitea.ActionWorkflowRun {
if since.IsZero() && until.IsZero() {
return runs
}
var filtered []*gitea.ActionWorkflowRun
for _, run := range runs {
if !since.IsZero() && run.StartedAt.Before(since) {
continue
}
if !until.IsZero() && run.StartedAt.After(until) {
continue
}
filtered = append(filtered, run)
}
return filtered
}
+111
View File
@@ -0,0 +1,111 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package runs
import (
"os"
"testing"
"time"
gitea "gitea.dev/sdk"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
"gitea.dev/tea/modules/config"
)
func TestFilterRunsByTime(t *testing.T) {
now := time.Now()
runs := []*gitea.ActionWorkflowRun{
{ID: 1, StartedAt: now.Add(-1 * time.Hour)},
{ID: 2, StartedAt: now.Add(-2 * time.Hour)},
{ID: 3, StartedAt: now.Add(-3 * time.Hour)},
{ID: 4, StartedAt: now.Add(-4 * time.Hour)},
{ID: 5, StartedAt: now.Add(-5 * time.Hour)},
}
tests := []struct {
name string
since time.Time
until time.Time
expected []int64
}{
{
name: "no filter",
since: time.Time{},
until: time.Time{},
expected: []int64{1, 2, 3, 4, 5},
},
{
name: "since 2.5 hours ago",
since: now.Add(-150 * time.Minute),
until: time.Time{},
expected: []int64{1, 2},
},
{
name: "until 2.5 hours ago",
since: time.Time{},
until: now.Add(-150 * time.Minute),
expected: []int64{3, 4, 5},
},
{
name: "between 2 and 4 hours ago",
since: now.Add(-4 * time.Hour),
until: now.Add(-2 * time.Hour),
expected: []int64{2, 3, 4},
},
{
name: "filter excludes all",
since: now.Add(-30 * time.Minute),
until: time.Time{},
expected: []int64{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := filterRunsByTime(runs, tt.since, tt.until)
if len(result) != len(tt.expected) {
t.Errorf("filterRunsByTime() returned %d runs, want %d", len(result), len(tt.expected))
return
}
for i, run := range result {
if run.ID != tt.expected[i] {
t.Errorf("filterRunsByTime()[%d].ID = %d, want %d", i, run.ID, tt.expected[i])
}
}
})
}
}
func TestRunRunsListRequiresRepoContext(t *testing.T) {
oldWd, err := os.Getwd()
require.NoError(t, err)
require.NoError(t, os.Chdir(t.TempDir()))
t.Cleanup(func() {
require.NoError(t, os.Chdir(oldWd))
})
config.SetConfigForTesting(config.LocalConfig{
Logins: []config.Login{{
Name: "test",
URL: "https://gitea.example.com",
Token: "token",
User: "tester",
Default: true,
}},
})
cmd := &cli.Command{
Name: CmdRunsList.Name,
Flags: CmdRunsList.Flags,
}
require.NoError(t, cmd.Set("login", "test"))
err = RunRunsList(t.Context(), cmd)
require.ErrorContains(t, err, "remote repository required")
}
+175
View File
@@ -0,0 +1,175 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package runs
import (
stdctx "context"
"fmt"
"strconv"
"time"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"github.com/urfave/cli/v3"
)
// CmdRunsLogs represents a sub command to view workflow run logs
var CmdRunsLogs = cli.Command{
Name: "logs",
Aliases: []string{"log"},
Usage: "View workflow run logs",
Description: "View logs for a workflow run or specific job",
ArgsUsage: "<run-id>",
Action: runRunsLogs,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "job",
Usage: "specific job ID to view logs for (if omitted, shows all jobs)",
},
&cli.BoolFlag{
Name: "follow",
Aliases: []string{"f"},
Usage: "follow log output (like tail -f), requires job to be in progress",
},
}, flags.AllDefaultFlags...),
}
func runRunsLogs(ctx stdctx.Context, cmd *cli.Command) error {
if cmd.Args().Len() == 0 {
return fmt.Errorf("run ID is required")
}
c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client()
runIDStr := cmd.Args().First()
runID, err := strconv.ParseInt(runIDStr, 10, 64)
if err != nil {
return fmt.Errorf("invalid run ID: %s", runIDStr)
}
// Check if follow mode is enabled
follow := cmd.Bool("follow")
// If specific job ID provided, fetch only that job's logs
jobIDStr := cmd.String("job")
if jobIDStr != "" {
jobID, err := strconv.ParseInt(jobIDStr, 10, 64)
if err != nil {
return fmt.Errorf("invalid job ID: %s", jobIDStr)
}
if follow {
return followJobLogs(ctx, client, c, jobID, "")
}
logs, _, err := client.Actions.GetRepoRunJobLogs(ctx, c.Owner, c.Repo, jobID)
if err != nil {
return fmt.Errorf("failed to get logs for job %d: %w", jobID, err)
}
fmt.Printf("Logs for job %d:\n", jobID)
fmt.Printf("---\n%s\n", string(logs))
return nil
}
// Otherwise, fetch all jobs and their logs
jobs, _, err := client.Actions.ListRepoJobsByRun(ctx, c.Owner, c.Repo, runID, gitea.ListRepoActionJobsOptions{
ListOptions: flags.GetListOptions(cmd),
})
if err != nil {
return fmt.Errorf("failed to get jobs: %w", err)
}
if len(jobs.Jobs) == 0 {
fmt.Printf("No jobs found for run %d\n", runID)
return nil
}
// If following and multiple jobs, require --job flag
if follow && len(jobs.Jobs) > 1 {
return fmt.Errorf("--follow requires --job when run has multiple jobs (found %d jobs)", len(jobs.Jobs))
}
// If following with single job, follow it
if follow && len(jobs.Jobs) == 1 {
return followJobLogs(ctx, client, c, jobs.Jobs[0].ID, jobs.Jobs[0].Name)
}
// Fetch logs for each job
for i, job := range jobs.Jobs {
if i > 0 {
fmt.Println()
}
fmt.Printf("Job: %s (ID: %d)\n", job.Name, job.ID)
fmt.Printf("Status: %s\n", job.Status)
fmt.Println("---")
logs, _, err := client.Actions.GetRepoRunJobLogs(ctx, c.Owner, c.Repo, job.ID)
if err != nil {
fmt.Printf("Error fetching logs: %v\n", err)
continue
}
fmt.Println(string(logs))
}
return nil
}
// followJobLogs continuously fetches and displays logs for a running job
func followJobLogs(requestCtx stdctx.Context, client *gitea.Client, c *context.TeaContext, jobID int64, jobName string) error {
var lastLogLength int
if jobName != "" {
fmt.Printf("Following logs for job '%s' (ID: %d) - press Ctrl+C to stop...\n", jobName, jobID)
} else {
fmt.Printf("Following logs for job %d (press Ctrl+C to stop)...\n", jobID)
}
fmt.Println("---")
for {
// Fetch job status
job, _, err := client.Actions.GetRepoRunJob(requestCtx, c.Owner, c.Repo, jobID)
if err != nil {
return fmt.Errorf("failed to get job: %w", err)
}
// Check if job is still running
isRunning := job.Status == "in_progress" || job.Status == "queued" || job.Status == "pending"
// Fetch logs
logs, _, err := client.Actions.GetRepoRunJobLogs(requestCtx, c.Owner, c.Repo, jobID)
if err != nil {
return fmt.Errorf("failed to get logs: %w", err)
}
// Display new content only
if len(logs) > lastLogLength {
newLogs := string(logs)[lastLogLength:]
fmt.Print(newLogs)
lastLogLength = len(logs)
}
// If job is complete, exit
if !isRunning {
fmt.Printf("\n---\nJob completed with status: %s\n", job.Status)
break
}
// Wait before next poll
time.Sleep(2 * time.Second)
}
return nil
}
+82
View File
@@ -0,0 +1,82 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package runs
import (
stdctx "context"
"fmt"
"strconv"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"github.com/urfave/cli/v3"
)
// CmdRunsView represents a sub command to view workflow run details
var CmdRunsView = cli.Command{
Name: "view",
Aliases: []string{"show", "get"},
Usage: "View workflow run details",
Description: "View details of a specific workflow run including jobs",
ArgsUsage: "<run-id>",
Action: runRunsView,
Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "jobs",
Usage: "show jobs table",
Value: true,
},
}, flags.AllDefaultFlags...),
}
func runRunsView(ctx stdctx.Context, cmd *cli.Command) error {
if cmd.Args().Len() == 0 {
return fmt.Errorf("run ID is required")
}
c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client()
runIDStr := cmd.Args().First()
runID, err := strconv.ParseInt(runIDStr, 10, 64)
if err != nil {
return fmt.Errorf("invalid run ID: %s", runIDStr)
}
// Fetch run details
run, _, err := client.Actions.GetRepoRun(ctx, c.Owner, c.Repo, runID)
if err != nil {
return fmt.Errorf("failed to get run: %w", err)
}
// Print run details
print.ActionRunDetails(run)
// Fetch and print jobs if requested
if cmd.Bool("jobs") {
jobs, _, err := client.Actions.ListRepoJobsByRun(ctx, c.Owner, c.Repo, runID, gitea.ListRepoActionJobsOptions{
ListOptions: flags.GetListOptions(cmd),
})
if err != nil {
return fmt.Errorf("failed to get jobs: %w", err)
}
if jobs != nil && len(jobs.Jobs) > 0 {
fmt.Printf("\nJobs:\n\n")
if err := print.ActionWorkflowJobsList(jobs.Jobs, c.Output); err != nil {
return err
}
}
}
return nil
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
stdctx "context"
"gitea.dev/tea/cmd/actions/secrets"
"github.com/urfave/cli/v3"
)
// CmdActionsSecrets represents the actions secrets command
var CmdActionsSecrets = cli.Command{
Name: "secrets",
Aliases: []string{"secret"},
Usage: "Manage repository action secrets",
Description: "Manage secrets used by repository actions and workflows",
Action: runSecretsDefault,
Commands: []*cli.Command{
&secrets.CmdSecretsList,
&secrets.CmdSecretsCreate,
&secrets.CmdSecretsDelete,
},
}
func runSecretsDefault(ctx stdctx.Context, cmd *cli.Command) error {
return secrets.RunSecretsList(ctx, cmd)
}
+74
View File
@@ -0,0 +1,74 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package secrets
import (
stdctx "context"
"fmt"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/utils"
"github.com/urfave/cli/v3"
)
// CmdSecretsCreate represents a sub command to create action secrets
var CmdSecretsCreate = cli.Command{
Name: "create",
Aliases: []string{"add", "set"},
Usage: "Create an action secret",
Description: "Create a secret for use in repository actions and workflows",
ArgsUsage: "<secret-name> [secret-value]",
Action: runSecretsCreate,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "file",
Usage: "read secret value from file",
},
&cli.BoolFlag{
Name: "stdin",
Usage: "read secret value from stdin",
},
}, flags.AllDefaultFlags...),
}
func runSecretsCreate(ctx stdctx.Context, cmd *cli.Command) error {
if cmd.Args().Len() == 0 {
return fmt.Errorf("secret name is required")
}
c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client()
secretName := cmd.Args().First()
// Read secret value using the utility
secretValue, err := utils.ReadValue(cmd, utils.ReadValueOptions{
ResourceName: "secret",
PromptMsg: fmt.Sprintf("Enter secret value for '%s'", secretName),
Hidden: true,
AllowEmpty: false,
})
if err != nil {
return err
}
_, err = client.Actions.CreateRepoSecret(ctx, c.Owner, c.Repo, secretName, gitea.CreateOrUpdateSecretOption{
Data: secretValue,
})
if err != nil {
return err
}
fmt.Printf("Secret '%s' created successfully\n", secretName)
return nil
}
+56
View File
@@ -0,0 +1,56 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package secrets
import (
"testing"
)
func TestGetSecretSourceArgs(t *testing.T) {
tests := []struct {
name string
args []string
wantErr bool
}{
{
name: "valid args",
args: []string{"VALID_SECRET", "secret_value"},
wantErr: false,
},
{
name: "missing name",
args: []string{},
wantErr: true,
},
{
name: "too many args",
args: []string{"SECRET_NAME", "value", "extra"},
wantErr: true,
},
{
name: "invalid secret name",
args: []string{"invalid_secret", "value"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test argument validation only
if len(tt.args) == 0 {
if !tt.wantErr {
t.Error("Expected error for empty args")
}
return
}
if len(tt.args) > 2 {
if !tt.wantErr {
t.Error("Expected error for too many args")
}
return
}
})
}
}
+66
View File
@@ -0,0 +1,66 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package secrets
import (
stdctx "context"
"fmt"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"github.com/urfave/cli/v3"
)
// CmdSecretsDelete represents a sub command to delete action secrets
var CmdSecretsDelete = cli.Command{
Name: "delete",
Aliases: []string{"remove", "rm"},
Usage: "Delete an action secret",
Description: "Delete a secret used by repository actions",
ArgsUsage: "<secret-name>",
Action: runSecretsDelete,
Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "confirm",
Aliases: []string{"y"},
Usage: "confirm deletion without prompting",
},
}, flags.AllDefaultFlags...),
}
func runSecretsDelete(ctx stdctx.Context, cmd *cli.Command) error {
if cmd.Args().Len() == 0 {
return fmt.Errorf("secret name is required")
}
c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client()
secretName := cmd.Args().First()
if !cmd.Bool("confirm") {
fmt.Printf("Are you sure you want to delete secret '%s'? [y/N] ", secretName)
var response string
fmt.Scanln(&response)
if response != "y" && response != "Y" && response != "yes" {
fmt.Println("Deletion canceled.")
return nil
}
}
_, err = client.Actions.DeleteRepoSecret(ctx, c.Owner, c.Repo, secretName)
if err != nil {
return err
}
fmt.Printf("Secret '%s' deleted successfully\n", secretName)
return nil
}
+93
View File
@@ -0,0 +1,93 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package secrets
import (
"fmt"
"testing"
)
func TestSecretsDeleteValidation(t *testing.T) {
tests := []struct {
name string
args []string
wantErr bool
}{
{
name: "valid secret name",
args: []string{"VALID_SECRET"},
wantErr: false,
},
{
name: "no args",
args: []string{},
wantErr: true,
},
{
name: "too many args",
args: []string{"SECRET1", "SECRET2"},
wantErr: true,
},
{
name: "invalid secret name but client does not validate",
args: []string{"invalid_secret"},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateDeleteArgs(tt.args)
if (err != nil) != tt.wantErr {
t.Errorf("validateDeleteArgs() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestSecretsDeleteFlags(t *testing.T) {
cmd := CmdSecretsDelete
// Test command properties
if cmd.Name != "delete" {
t.Errorf("Expected command name 'delete', got %s", cmd.Name)
}
// Check that rm is one of the aliases
hasRmAlias := false
for _, alias := range cmd.Aliases {
if alias == "rm" {
hasRmAlias = true
break
}
}
if !hasRmAlias {
t.Error("Expected 'rm' to be one of the aliases for delete command")
}
if cmd.ArgsUsage != "<secret-name>" {
t.Errorf("Expected ArgsUsage '<secret-name>', got %s", cmd.ArgsUsage)
}
if cmd.Usage == "" {
t.Error("Delete command should have usage text")
}
if cmd.Description == "" {
t.Error("Delete command should have description")
}
}
// validateDeleteArgs validates arguments for the delete command
func validateDeleteArgs(args []string) error {
if len(args) == 0 {
return fmt.Errorf("secret name is required")
}
if len(args) > 1 {
return fmt.Errorf("only one secret name allowed")
}
return nil
}
+49
View File
@@ -0,0 +1,49 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package secrets
import (
stdctx "context"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"github.com/urfave/cli/v3"
)
// CmdSecretsList represents a sub command to list action secrets
var CmdSecretsList = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List action secrets",
Description: "List secrets configured for repository actions",
Action: RunSecretsList,
Flags: append([]cli.Flag{
&flags.PaginationPageFlag,
&flags.PaginationLimitFlag,
}, flags.AllDefaultFlags...),
}
// RunSecretsList list action secrets
func RunSecretsList(ctx stdctx.Context, cmd *cli.Command) error {
c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client()
secrets, _, err := client.Actions.ListRepoSecrets(ctx, c.Owner, c.Repo, gitea.ListRepoActionSecretOption{
ListOptions: flags.GetListOptions(cmd),
})
if err != nil {
return err
}
return print.ActionSecretsList(secrets, c.Output)
}
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package secrets
import (
"os"
"testing"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
"gitea.dev/tea/modules/config"
)
func TestSecretsListFlags(t *testing.T) {
cmd := CmdSecretsList
// Test that required flags exist
expectedFlags := []string{"output", "remote", "login", "repo"}
for _, flagName := range expectedFlags {
found := false
for _, flag := range cmd.Flags {
if flag.Names()[0] == flagName {
found = true
break
}
}
if !found {
t.Errorf("Expected flag %s not found in CmdSecretsList", flagName)
}
}
// Test command properties
if cmd.Name != "list" {
t.Errorf("Expected command name 'list', got %s", cmd.Name)
}
if len(cmd.Aliases) == 0 || cmd.Aliases[0] != "ls" {
t.Errorf("Expected alias 'ls' for list command")
}
if cmd.Usage == "" {
t.Error("List command should have usage text")
}
if cmd.Description == "" {
t.Error("List command should have description")
}
}
func TestSecretsListValidation(t *testing.T) {
// Basic validation that the command accepts the expected arguments
// More detailed testing would require mocking the Gitea client
// Test that list command doesn't require arguments
args := []string{}
if len(args) > 0 {
t.Error("List command should not require arguments")
}
// Test that extra arguments are ignored
extraArgs := []string{"extra", "args"}
if len(extraArgs) > 0 {
// This is fine - list commands typically ignore extra args
}
}
func TestRunSecretsListRequiresRepoContext(t *testing.T) {
oldWd, err := os.Getwd()
require.NoError(t, err)
require.NoError(t, os.Chdir(t.TempDir()))
t.Cleanup(func() {
require.NoError(t, os.Chdir(oldWd))
})
config.SetConfigForTesting(config.LocalConfig{
Logins: []config.Login{{
Name: "test",
URL: "https://gitea.example.com",
Token: "token",
User: "tester",
Default: true,
}},
})
cmd := &cli.Command{
Name: CmdSecretsList.Name,
Flags: CmdSecretsList.Flags,
}
require.NoError(t, cmd.Set("login", "test"))
err = RunSecretsList(t.Context(), cmd)
require.ErrorContains(t, err, "remote repository required")
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
stdctx "context"
"gitea.dev/tea/cmd/actions/variables"
"github.com/urfave/cli/v3"
)
// CmdActionsVariables represents the actions variables command
var CmdActionsVariables = cli.Command{
Name: "variables",
Aliases: []string{"variable", "vars", "var"},
Usage: "Manage repository action variables",
Description: "Manage variables used by repository actions and workflows",
Action: runVariablesDefault,
Commands: []*cli.Command{
&variables.CmdVariablesList,
&variables.CmdVariablesSet,
&variables.CmdVariablesDelete,
},
}
func runVariablesDefault(ctx stdctx.Context, cmd *cli.Command) error {
return variables.RunVariablesList(ctx, cmd)
}
+66
View File
@@ -0,0 +1,66 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package variables
import (
stdctx "context"
"fmt"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"github.com/urfave/cli/v3"
)
// CmdVariablesDelete represents a sub command to delete action variables
var CmdVariablesDelete = cli.Command{
Name: "delete",
Aliases: []string{"remove", "rm"},
Usage: "Delete an action variable",
Description: "Delete a variable used by repository actions",
ArgsUsage: "<variable-name>",
Action: runVariablesDelete,
Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "confirm",
Aliases: []string{"y"},
Usage: "confirm deletion without prompting",
},
}, flags.AllDefaultFlags...),
}
func runVariablesDelete(ctx stdctx.Context, cmd *cli.Command) error {
if cmd.Args().Len() == 0 {
return fmt.Errorf("variable name is required")
}
c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client()
variableName := cmd.Args().First()
if !cmd.Bool("confirm") {
fmt.Printf("Are you sure you want to delete variable '%s'? [y/N] ", variableName)
var response string
fmt.Scanln(&response)
if response != "y" && response != "Y" && response != "yes" {
fmt.Println("Deletion canceled.")
return nil
}
}
_, err = client.Actions.DeleteRepoVariable(ctx, c.Owner, c.Repo, variableName)
if err != nil {
return err
}
fmt.Printf("Variable '%s' deleted successfully\n", variableName)
return nil
}
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package variables
import (
"fmt"
"testing"
)
func TestVariablesDeleteValidation(t *testing.T) {
tests := []struct {
name string
args []string
wantErr bool
}{
{
name: "valid variable name",
args: []string{"VALID_VARIABLE"},
wantErr: false,
},
{
name: "valid lowercase name",
args: []string{"valid_variable"},
wantErr: false,
},
{
name: "no args",
args: []string{},
wantErr: true,
},
{
name: "too many args",
args: []string{"VARIABLE1", "VARIABLE2"},
wantErr: true,
},
{
name: "invalid variable name",
args: []string{"invalid-variable"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateVariableDeleteArgs(tt.args)
if (err != nil) != tt.wantErr {
t.Errorf("validateVariableDeleteArgs() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestVariablesDeleteFlags(t *testing.T) {
cmd := CmdVariablesDelete
// Test command properties
if cmd.Name != "delete" {
t.Errorf("Expected command name 'delete', got %s", cmd.Name)
}
// Check that rm is one of the aliases
hasRmAlias := false
for _, alias := range cmd.Aliases {
if alias == "rm" {
hasRmAlias = true
break
}
}
if !hasRmAlias {
t.Error("Expected 'rm' to be one of the aliases for delete command")
}
if cmd.ArgsUsage != "<variable-name>" {
t.Errorf("Expected ArgsUsage '<variable-name>', got %s", cmd.ArgsUsage)
}
if cmd.Usage == "" {
t.Error("Delete command should have usage text")
}
if cmd.Description == "" {
t.Error("Delete command should have description")
}
}
// validateVariableDeleteArgs validates arguments for the delete command
func validateVariableDeleteArgs(args []string) error {
if len(args) == 0 {
return fmt.Errorf("variable name is required")
}
if len(args) > 1 {
return fmt.Errorf("only one variable name allowed")
}
return validateVariableName(args[0])
}
+61
View File
@@ -0,0 +1,61 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package variables
import (
stdctx "context"
"fmt"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"github.com/urfave/cli/v3"
)
// CmdVariablesList represents a sub command to list action variables
var CmdVariablesList = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List action variables",
Description: "List variables configured for repository actions",
Action: RunVariablesList,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "name",
Usage: "show specific variable by name",
},
}, flags.AllDefaultFlags...),
}
// RunVariablesList list action variables
func RunVariablesList(ctx stdctx.Context, cmd *cli.Command) error {
c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client()
if name := cmd.String("name"); name != "" {
// Get specific variable
variable, _, err := client.Actions.GetRepoVariable(ctx, c.Owner, c.Repo, name)
if err != nil {
return err
}
print.ActionVariableDetails(variable)
return nil
}
// List all variables - Note: SDK doesn't have ListRepoActionVariables yet
// This is a limitation of the current SDK
fmt.Println("Note: Listing all variables is not yet supported by the Gitea SDK.")
fmt.Println("Use 'tea actions variables list --name <variable-name>' to get a specific variable.")
fmt.Println("You can also check your repository's Actions settings in the web interface.")
return nil
}
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package variables
import (
"os"
"testing"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
"gitea.dev/tea/modules/config"
)
func TestVariablesListFlags(t *testing.T) {
cmd := CmdVariablesList
// Test that required flags exist
expectedFlags := []string{"output", "remote", "login", "repo"}
for _, flagName := range expectedFlags {
found := false
for _, flag := range cmd.Flags {
if flag.Names()[0] == flagName {
found = true
break
}
}
if !found {
t.Errorf("Expected flag %s not found in CmdVariablesList", flagName)
}
}
// Test command properties
if cmd.Name != "list" {
t.Errorf("Expected command name 'list', got %s", cmd.Name)
}
if len(cmd.Aliases) == 0 || cmd.Aliases[0] != "ls" {
t.Errorf("Expected alias 'ls' for list command")
}
if cmd.Usage == "" {
t.Error("List command should have usage text")
}
if cmd.Description == "" {
t.Error("List command should have description")
}
}
func TestVariablesListValidation(t *testing.T) {
// Basic validation that the command accepts the expected arguments
// More detailed testing would require mocking the Gitea client
// Test that list command doesn't require arguments
args := []string{}
if len(args) > 0 {
t.Error("List command should not require arguments")
}
// Test that extra arguments are ignored
extraArgs := []string{"extra", "args"}
if len(extraArgs) > 0 {
// This is fine - list commands typically ignore extra args
}
}
func TestRunVariablesListRequiresRepoContext(t *testing.T) {
oldWd, err := os.Getwd()
require.NoError(t, err)
require.NoError(t, os.Chdir(t.TempDir()))
t.Cleanup(func() {
require.NoError(t, os.Chdir(oldWd))
})
config.SetConfigForTesting(config.LocalConfig{
Logins: []config.Login{{
Name: "test",
URL: "https://gitea.example.com",
Token: "token",
User: "tester",
Default: true,
}},
})
cmd := &cli.Command{
Name: CmdVariablesList.Name,
Flags: CmdVariablesList.Flags,
}
require.NoError(t, cmd.Set("login", "test"))
err = RunVariablesList(t.Context(), cmd)
require.ErrorContains(t, err, "remote repository required")
}
+108
View File
@@ -0,0 +1,108 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package variables
import (
stdctx "context"
"fmt"
"regexp"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/utils"
"github.com/urfave/cli/v3"
)
// CmdVariablesSet represents a sub command to set action variables
var CmdVariablesSet = cli.Command{
Name: "set",
Aliases: []string{"create", "update"},
Usage: "Set an action variable",
Description: "Set a variable for use in repository actions and workflows",
ArgsUsage: "<variable-name> [variable-value]",
Action: runVariablesSet,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "file",
Usage: "read variable value from file",
},
&cli.BoolFlag{
Name: "stdin",
Usage: "read variable value from stdin",
},
}, flags.AllDefaultFlags...),
}
func runVariablesSet(ctx stdctx.Context, cmd *cli.Command) error {
if cmd.Args().Len() == 0 {
return fmt.Errorf("variable name is required")
}
c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client()
variableName := cmd.Args().First()
if err := validateVariableName(variableName); err != nil {
return err
}
// Read variable value using the utility
variableValue, err := utils.ReadValue(cmd, utils.ReadValueOptions{
ResourceName: "variable",
PromptMsg: fmt.Sprintf("Enter variable value for '%s'", variableName),
Hidden: false,
AllowEmpty: false,
})
if err != nil {
return err
}
if err := validateVariableValue(variableValue); err != nil {
return err
}
_, err = client.Actions.CreateRepoVariable(ctx, c.Owner, c.Repo, variableName, variableValue)
if err != nil {
return err
}
fmt.Printf("Variable '%s' set successfully\n", variableName)
return nil
}
// validateVariableName validates that a variable name follows the required format
func validateVariableName(name string) error {
if name == "" {
return fmt.Errorf("variable name cannot be empty")
}
// Variable names can contain letters (upper/lower), numbers, and underscores
// Cannot start with a number
// Cannot contain spaces or special characters (except underscore)
validPattern := regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
if !validPattern.MatchString(name) {
return fmt.Errorf("variable name must contain only letters, numbers, and underscores, and cannot start with a number")
}
return nil
}
// validateVariableValue validates that a variable value is acceptable
func validateVariableValue(value string) error {
// Variables can be empty or contain whitespace, unlike secrets
// Check for maximum size (64KB limit)
if len(value) > 65536 {
return fmt.Errorf("variable value cannot exceed 64KB")
}
return nil
}
+213
View File
@@ -0,0 +1,213 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package variables
import (
"strings"
"testing"
)
func TestValidateVariableName(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{
name: "valid name",
input: "VALID_VARIABLE_NAME",
wantErr: false,
},
{
name: "valid name with numbers",
input: "VARIABLE_123",
wantErr: false,
},
{
name: "valid lowercase",
input: "valid_variable",
wantErr: false,
},
{
name: "valid mixed case",
input: "Mixed_Case_Variable",
wantErr: false,
},
{
name: "invalid - spaces",
input: "INVALID VARIABLE",
wantErr: true,
},
{
name: "invalid - special chars",
input: "INVALID-VARIABLE!",
wantErr: true,
},
{
name: "invalid - starts with number",
input: "1INVALID",
wantErr: true,
},
{
name: "invalid - empty",
input: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateVariableName(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("validateVariableName(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
})
}
}
func TestGetVariableSourceArgs(t *testing.T) {
tests := []struct {
name string
args []string
wantErr bool
}{
{
name: "valid args",
args: []string{"VALID_VARIABLE", "variable_value"},
wantErr: false,
},
{
name: "valid lowercase",
args: []string{"valid_variable", "value"},
wantErr: false,
},
{
name: "missing name",
args: []string{},
wantErr: true,
},
{
name: "too many args",
args: []string{"VARIABLE_NAME", "value", "extra"},
wantErr: true,
},
{
name: "invalid variable name",
args: []string{"invalid-variable", "value"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test argument validation only
if len(tt.args) == 0 {
if !tt.wantErr {
t.Error("Expected error for empty args")
}
return
}
if len(tt.args) > 2 {
if !tt.wantErr {
t.Error("Expected error for too many args")
}
return
}
// Test variable name validation
err := validateVariableName(tt.args[0])
if (err != nil) != tt.wantErr {
t.Errorf("validateVariableName() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestVariableNameValidation(t *testing.T) {
// Test that variable names follow GitHub Actions/Gitea Actions conventions
validNames := []string{
"VALID_VARIABLE",
"API_URL",
"DATABASE_HOST",
"VARIABLE_123",
"mixed_Case_Variable",
"lowercase_variable",
"UPPERCASE_VARIABLE",
}
invalidNames := []string{
"Invalid-Dashes",
"INVALID SPACES",
"123_STARTS_WITH_NUMBER",
"", // Empty
"INVALID!@#", // Special chars
}
for _, name := range validNames {
t.Run("valid_"+name, func(t *testing.T) {
err := validateVariableName(name)
if err != nil {
t.Errorf("validateVariableName(%q) should be valid, got error: %v", name, err)
}
})
}
for _, name := range invalidNames {
t.Run("invalid_"+name, func(t *testing.T) {
err := validateVariableName(name)
if err == nil {
t.Errorf("validateVariableName(%q) should be invalid, got no error", name)
}
})
}
}
func TestVariableValueValidation(t *testing.T) {
tests := []struct {
name string
value string
wantErr bool
}{
{
name: "valid value",
value: "variable123",
wantErr: false,
},
{
name: "valid complex value",
value: "https://api.example.com/v1",
wantErr: false,
},
{
name: "valid multiline value",
value: "line1\nline2\nline3",
wantErr: false,
},
{
name: "empty value allowed",
value: "",
wantErr: false, // Variables can be empty unlike secrets
},
{
name: "whitespace only allowed",
value: " \t\n ",
wantErr: false, // Variables can contain whitespace
},
{
name: "very long value",
value: strings.Repeat("a", 65537), // Over 64KB
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateVariableValue(tt.value)
if (err != nil) != tt.wantErr {
t.Errorf("validateVariableValue() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
+32
View File
@@ -0,0 +1,32 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
stdctx "context"
"gitea.dev/tea/cmd/actions/workflows"
"github.com/urfave/cli/v3"
)
// CmdActionsWorkflows represents the actions workflows command
var CmdActionsWorkflows = cli.Command{
Name: "workflows",
Aliases: []string{"workflow"},
Usage: "Manage repository workflows",
Description: "List and manage repository action workflows",
Action: runWorkflowsDefault,
Commands: []*cli.Command{
&workflows.CmdWorkflowsList,
&workflows.CmdWorkflowsView,
&workflows.CmdWorkflowsDispatch,
&workflows.CmdWorkflowsEnable,
&workflows.CmdWorkflowsDisable,
},
}
func runWorkflowsDefault(ctx stdctx.Context, cmd *cli.Command) error {
return workflows.RunWorkflowsList(ctx, cmd)
}
+65
View File
@@ -0,0 +1,65 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package workflows
import (
stdctx "context"
"fmt"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"github.com/urfave/cli/v3"
)
// CmdWorkflowsDisable represents a sub command to disable a workflow
var CmdWorkflowsDisable = cli.Command{
Name: "disable",
Usage: "Disable a workflow",
Description: "Disable a workflow in the repository",
ArgsUsage: "<workflow-id>",
Action: runWorkflowsDisable,
Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "confirm",
Aliases: []string{"y"},
Usage: "confirm disable without prompting",
},
}, flags.AllDefaultFlags...),
}
func runWorkflowsDisable(ctx stdctx.Context, cmd *cli.Command) error {
if cmd.Args().Len() == 0 {
return fmt.Errorf("workflow ID is required")
}
c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client()
workflowID := cmd.Args().First()
if !cmd.Bool("confirm") {
fmt.Printf("Are you sure you want to disable workflow %s? [y/N] ", workflowID)
var response string
fmt.Scanln(&response)
if response != "y" && response != "Y" && response != "yes" {
fmt.Println("Disable canceled.")
return nil
}
}
_, err = client.Actions.DisableRepoWorkflow(ctx, c.Owner, c.Repo, workflowID)
if err != nil {
return fmt.Errorf("failed to disable workflow: %w", err)
}
fmt.Printf("Workflow %s disabled successfully\n", workflowID)
return nil
}
+174
View File
@@ -0,0 +1,174 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package workflows
import (
stdctx "context"
"fmt"
"strings"
"time"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"github.com/urfave/cli/v3"
)
// CmdWorkflowsDispatch represents a sub command to dispatch a workflow
var CmdWorkflowsDispatch = cli.Command{
Name: "dispatch",
Aliases: []string{"trigger", "run"},
Usage: "Dispatch a workflow run",
Description: "Trigger a workflow_dispatch event for a workflow",
ArgsUsage: "<workflow-id>",
Action: runWorkflowsDispatch,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "ref",
Aliases: []string{"r"},
Usage: "branch or tag to dispatch on (default: current branch)",
},
&cli.StringSliceFlag{
Name: "input",
Aliases: []string{"i"},
Usage: "workflow input in key=value format (can be specified multiple times)",
},
&cli.BoolFlag{
Name: "follow",
Aliases: []string{"f"},
Usage: "follow log output after dispatching",
},
}, flags.AllDefaultFlags...),
}
func runWorkflowsDispatch(ctx stdctx.Context, cmd *cli.Command) error {
if cmd.Args().Len() == 0 {
return fmt.Errorf("workflow ID is required")
}
c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client()
workflowID := cmd.Args().First()
ref := cmd.String("ref")
if ref == "" {
if c.LocalRepo != nil {
branchName, _, localErr := c.LocalRepo.TeaGetCurrentBranchNameAndSHA()
if localErr == nil && branchName != "" {
ref = branchName
}
}
if ref == "" {
return fmt.Errorf("--ref is required (no local branch detected)")
}
}
inputs := make(map[string]string)
for _, input := range cmd.StringSlice("input") {
key, value, ok := strings.Cut(input, "=")
if !ok {
return fmt.Errorf("invalid input format %q, expected key=value", input)
}
inputs[key] = value
}
opt := gitea.CreateActionWorkflowDispatchOption{
Ref: ref,
Inputs: inputs,
}
details, _, err := client.Actions.DispatchRepoWorkflow(ctx, c.Owner, c.Repo, workflowID, opt, true)
if err != nil {
return fmt.Errorf("failed to dispatch workflow: %w", err)
}
print.ActionWorkflowDispatchResult(details)
if cmd.Bool("follow") && details != nil && details.WorkflowRunID > 0 {
return followDispatchedRun(ctx, client, c, details.WorkflowRunID)
}
return nil
}
const (
followPollInterval = 2 * time.Second
followMaxDuration = 30 * time.Minute
)
// followDispatchedRun waits for the dispatched run to start, then follows its logs
func followDispatchedRun(ctx stdctx.Context, client *gitea.Client, c *context.TeaContext, runID int64) error {
fmt.Printf("\nWaiting for run %d to start...\n", runID)
var jobs *gitea.ActionWorkflowJobsResponse
for range 30 {
time.Sleep(followPollInterval)
var err error
jobs, _, err = client.Actions.ListRepoJobsByRun(ctx, c.Owner, c.Repo, runID, gitea.ListRepoActionJobsOptions{})
if err != nil {
return fmt.Errorf("failed to get jobs: %w", err)
}
if len(jobs.Jobs) > 0 {
break
}
}
if jobs == nil || len(jobs.Jobs) == 0 {
return fmt.Errorf("timed out waiting for jobs to appear")
}
jobID := jobs.Jobs[0].ID
jobName := jobs.Jobs[0].Name
fmt.Printf("Following logs for job '%s' (ID: %d) - press Ctrl+C to stop...\n", jobName, jobID)
fmt.Println("---")
deadline := time.Now().Add(followMaxDuration)
var lastLogLength int
for time.Now().Before(deadline) {
job, _, err := client.Actions.GetRepoRunJob(ctx, c.Owner, c.Repo, jobID)
if err != nil {
return fmt.Errorf("failed to get job: %w", err)
}
isRunning := job.Status == "in_progress" || job.Status == "queued" || job.Status == "pending"
logs, _, logErr := client.Actions.GetRepoRunJobLogs(ctx, c.Owner, c.Repo, jobID)
if logErr != nil && isRunning {
time.Sleep(followPollInterval)
continue
}
if logErr == nil && len(logs) > lastLogLength {
fmt.Print(string(logs[lastLogLength:]))
lastLogLength = len(logs)
}
if !isRunning {
if logErr != nil {
fmt.Printf("\n---\nJob completed with status: %s (failed to fetch final logs: %v)\n", job.Status, logErr)
} else {
fmt.Printf("\n---\nJob completed with status: %s\n", job.Status)
}
break
}
time.Sleep(followPollInterval)
}
if time.Now().After(deadline) {
return fmt.Errorf("timed out after %s following logs", followMaxDuration)
}
return nil
}
+48
View File
@@ -0,0 +1,48 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package workflows
import (
stdctx "context"
"fmt"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"github.com/urfave/cli/v3"
)
// CmdWorkflowsEnable represents a sub command to enable a workflow
var CmdWorkflowsEnable = cli.Command{
Name: "enable",
Usage: "Enable a workflow",
Description: "Enable a disabled workflow in the repository",
ArgsUsage: "<workflow-id>",
Action: runWorkflowsEnable,
Flags: flags.AllDefaultFlags,
}
func runWorkflowsEnable(ctx stdctx.Context, cmd *cli.Command) error {
if cmd.Args().Len() == 0 {
return fmt.Errorf("workflow ID is required")
}
c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client()
workflowID := cmd.Args().First()
_, err = client.Actions.EnableRepoWorkflow(ctx, c.Owner, c.Repo, workflowID)
if err != nil {
return fmt.Errorf("failed to enable workflow: %w", err)
}
fmt.Printf("Workflow %s enabled successfully\n", workflowID)
return nil
}
+50
View File
@@ -0,0 +1,50 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package workflows
import (
stdctx "context"
"fmt"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"github.com/urfave/cli/v3"
)
// CmdWorkflowsList represents a sub command to list workflows
var CmdWorkflowsList = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List repository workflows",
Description: "List workflows in the repository with their status",
Action: RunWorkflowsList,
Flags: flags.AllDefaultFlags,
}
// RunWorkflowsList lists workflows in the repository using the workflow API
func RunWorkflowsList(ctx stdctx.Context, cmd *cli.Command) error {
c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client()
resp, _, err := client.Actions.ListRepoWorkflows(ctx, c.Owner, c.Repo)
if err != nil {
return fmt.Errorf("failed to list workflows: %w", err)
}
var workflows []*gitea.ActionWorkflow
if resp != nil {
workflows = resp.Workflows
}
return print.ActionWorkflowsList(workflows, c.Output)
}
+50
View File
@@ -0,0 +1,50 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package workflows
import (
stdctx "context"
"fmt"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"github.com/urfave/cli/v3"
)
// CmdWorkflowsView represents a sub command to view workflow details
var CmdWorkflowsView = cli.Command{
Name: "view",
Aliases: []string{"show", "get"},
Usage: "View workflow details",
Description: "View details of a specific workflow",
ArgsUsage: "<workflow-id>",
Action: runWorkflowsView,
Flags: flags.AllDefaultFlags,
}
func runWorkflowsView(ctx stdctx.Context, cmd *cli.Command) error {
if cmd.Args().Len() == 0 {
return fmt.Errorf("workflow ID is required")
}
c, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := c.Login.Client()
workflowID := cmd.Args().First()
wf, _, err := client.Actions.GetRepoWorkflow(ctx, c.Owner, c.Repo, workflowID)
if err != nil {
return fmt.Errorf("failed to get workflow: %w", err)
}
print.ActionWorkflowDetails(wf)
return nil
}
+14 -7
View File
@@ -6,10 +6,11 @@ package cmd
import (
stdctx "context"
"code.gitea.io/tea/cmd/admin/users"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"github.com/urfave/cli/v3"
"gitea.dev/tea/cmd/admin/users"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
)
// CmdAdmin represents the namespace of admin commands.
@@ -19,7 +20,7 @@ var CmdAdmin = cli.Command{
Usage: "Operations requiring admin access on the Gitea instance",
Aliases: []string{"a"},
Category: catMisc,
Action: func(_ stdctx.Context, cmd *cli.Command) error {
Action: func(requestCtx stdctx.Context, cmd *cli.Command) error {
return cli.ShowSubcommandHelp(cmd)
},
Commands: []*cli.Command{
@@ -39,14 +40,20 @@ var cmdAdminUsers = cli.Command{
},
Commands: []*cli.Command{
&users.CmdUserList,
&users.CmdUserCreate,
&users.CmdUserEdit,
&users.CmdUserDelete,
},
Flags: users.CmdUserList.Flags,
}
func runAdminUserDetail(_ stdctx.Context, cmd *cli.Command, u string) error {
ctx := context.InitCommand(cmd)
func runAdminUserDetail(requestCtx stdctx.Context, cmd *cli.Command, u string) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
client := ctx.Login.Client()
user, _, err := client.GetUserInfo(u)
user, _, err := client.Users.GetUserInfo(requestCtx, u)
if err != nil {
return err
}
+220
View File
@@ -0,0 +1,220 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package users
import (
stdctx "context"
"fmt"
"io"
"os"
"strings"
"syscall"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"github.com/urfave/cli/v3"
"golang.org/x/term"
)
// CmdUserCreate represents a sub command of users to create a user
var CmdUserCreate = cli.Command{
Name: "create",
Aliases: []string{"add", "new"},
Usage: "Create a new user",
Description: "Create a new user account",
ArgsUsage: " ", // command does not accept arguments
Action: RunUserCreate,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"u"},
Usage: "Username for the new user (required)",
Required: true,
},
&cli.StringFlag{
Name: "password",
Aliases: []string{"p"},
Usage: "Password for the new user (will prompt if not provided)",
},
&cli.StringFlag{
Name: "password-file",
Usage: "Read password from file",
},
&cli.BoolFlag{
Name: "password-stdin",
Usage: "Read password from stdin",
},
&cli.StringFlag{
Name: "email",
Aliases: []string{"e"},
Usage: "Email address for the new user (required)",
Required: true,
},
&cli.StringFlag{
Name: "full-name",
Usage: "Full name for the new user",
},
&cli.BoolFlag{
Name: "admin",
Usage: "Make the user an administrator",
},
&cli.BoolFlag{
Name: "restricted",
Usage: "Make the user restricted",
},
&cli.BoolFlag{
Name: "prohibit-login",
Usage: "Prohibit the user from logging in",
},
&cli.BoolFlag{
Name: "no-must-change-password",
Usage: "Don't require the user to change password on first login (default: password change required)",
},
&cli.StringFlag{
Name: "visibility",
Usage: "Visibility of the user profile (public, limited, private)",
Value: "public",
},
}, flags.AllDefaultFlags...),
}
// RunUserCreate creates a new user
func RunUserCreate(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
username := ctx.String("username")
password := ctx.String("password")
email := ctx.String("email")
fullName := ctx.String("full-name")
isAdmin := ctx.Bool("admin")
restricted := ctx.Bool("restricted")
prohibitLogin := ctx.Bool("prohibit-login")
noMustChangePassword := ctx.Bool("no-must-change-password")
visibility := ctx.String("visibility")
// Get password from various sources in priority order
if password == "" {
if ctx.String("password-file") != "" {
// Read from file
content, err := os.ReadFile(ctx.String("password-file"))
if err != nil {
return fmt.Errorf("failed to read password file: %w", err)
}
password = strings.TrimSpace(string(content))
} else if ctx.Bool("password-stdin") {
// Read from stdin
content, err := io.ReadAll(os.Stdin)
if err != nil {
return fmt.Errorf("failed to read password from stdin: %w", err)
}
password = strings.TrimSpace(string(content))
} else {
// Interactive prompt (hidden input)
fmt.Printf("Enter password for '%s': ", username)
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return fmt.Errorf("failed to read password: %w", err)
}
fmt.Println() // Add newline after hidden input
password = string(bytePassword)
if password == "" {
return fmt.Errorf("password cannot be empty")
}
// Confirm password (only for interactive mode)
fmt.Printf("Confirm password for '%s': ", username)
bytePasswordConfirm, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return fmt.Errorf("failed to read password confirmation: %w", err)
}
fmt.Println() // Add newline after hidden input
passwordConfirm := string(bytePasswordConfirm)
if password != passwordConfirm {
return fmt.Errorf("passwords do not match")
}
}
}
if password == "" {
return fmt.Errorf("password cannot be empty")
}
if email == "" {
return fmt.Errorf("email is required")
}
client := ctx.Login.Client()
// Build create options
createOpts := gitea.CreateUserOption{
LoginName: username,
Username: username,
Password: password,
Email: email,
FullName: fullName,
SendNotify: false,
}
// Set must change password flag (pointer to bool required)
// By default, require user to change password on first login
// Only set to false if --no-must-change-password flag is explicitly set
mustChangePassword := !noMustChangePassword
createOpts.MustChangePassword = &mustChangePassword
vis, err := parseUserVisibility(visibility)
if err != nil {
return err
}
createOpts.Visibility = vis
// Create the user
user, _, err := client.Admin.CreateUser(requestCtx, createOpts)
if err != nil {
return err
}
// Admin, Restricted, and ProhibitLogin cannot be set during user creation
// We need to update them via AdminEditUser after creation if any of these flags are set
if isAdmin || restricted || prohibitLogin {
editOpts := gitea.EditUserOption{
LoginName: username, // Required field
}
if isAdmin {
editOpts.Admin = &isAdmin
}
if restricted {
editOpts.Restricted = &restricted
}
if prohibitLogin {
editOpts.ProhibitLogin = &prohibitLogin
}
// Update user with admin/restricted/prohibit-login settings
_, err = client.Admin.EditUser(requestCtx, username, editOpts)
if err != nil {
return fmt.Errorf("user created but failed to update admin/restricted/prohibit-login status: %w", err)
}
// Refresh user info to reflect the changes
user, _, err = client.Users.GetUserInfo(requestCtx, username)
if err != nil {
return fmt.Errorf("user updated but failed to retrieve updated user info: %w", err)
}
}
print.UserDetails(user)
return nil
}
+77
View File
@@ -0,0 +1,77 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package users
import (
stdctx "context"
"fmt"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"github.com/urfave/cli/v3"
)
// CmdUserDelete represents a sub command of users to delete a user
var CmdUserDelete = cli.Command{
Name: "delete",
Aliases: []string{"rm", "remove"},
Usage: "Delete a user",
Description: "Delete a user account",
ArgsUsage: "<username>",
Action: RunUserDelete,
Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: "confirm",
Aliases: []string{"y"},
Usage: "confirm deletion without prompting",
},
}, flags.AllDefaultFlags...),
}
// RunUserDelete deletes a user
func RunUserDelete(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if ctx.Args().Len() == 0 {
return fmt.Errorf("username is required")
}
client := ctx.Login.Client()
username := ctx.Args().First()
// Get user details first to show what we're deleting
user, _, err := client.Users.GetUserInfo(requestCtx, username)
if err != nil {
return fmt.Errorf("failed to get user info: %w", err)
}
if !ctx.Bool("confirm") {
userInfo := fmt.Sprintf("%s (ID: %d)", user.UserName, user.ID)
if user.Email != "" {
userInfo += fmt.Sprintf(" - %s", user.Email)
}
if user.IsAdmin {
userInfo += " [admin]"
}
fmt.Printf("Are you sure you want to delete user %s? [y/N] ", userInfo)
var response string
fmt.Scanln(&response)
if !isConfirmationAccepted(response) {
fmt.Println("Deletion canceled.")
return nil
}
}
_, err = client.Admin.DeleteUser(requestCtx, username)
if err != nil {
return fmt.Errorf("failed to delete user: %w", err)
}
fmt.Printf("User '%s' deleted successfully\n", username)
return nil
}
+373
View File
@@ -0,0 +1,373 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package users
import (
stdctx "context"
"fmt"
"io"
"os"
"strings"
"syscall"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"github.com/urfave/cli/v3"
"golang.org/x/term"
)
// CmdUserEdit represents a sub command of users to edit a user
var CmdUserEdit = cli.Command{
Name: "edit",
Aliases: []string{"update", "e", "u"},
Usage: "Edit a user",
Description: "Edit user account properties",
ArgsUsage: "<username>",
Action: RunUserEdit,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "password",
Usage: "New password (use empty value --password=\"\" to trigger interactive prompt)",
Value: "",
},
&cli.StringFlag{
Name: "password-file",
Usage: "Read password from file",
},
&cli.BoolFlag{
Name: "password-stdin",
Usage: "Read password from stdin",
},
&cli.StringFlag{
Name: "email",
Aliases: []string{"e"},
Usage: "Email address",
},
&cli.StringFlag{
Name: "full-name",
Usage: "Full name",
},
&cli.StringFlag{
Name: "description",
Usage: "User description",
},
&cli.StringFlag{
Name: "website",
Usage: "Website URL",
},
&cli.StringFlag{
Name: "location",
Usage: "Location",
},
&cli.BoolFlag{
Name: "admin",
Usage: "Make the user an administrator",
},
&cli.BoolFlag{
Name: "no-admin",
Usage: "Remove administrator status",
},
&cli.BoolFlag{
Name: "restricted",
Usage: "Make the user restricted",
},
&cli.BoolFlag{
Name: "no-restricted",
Usage: "Remove restricted status",
},
&cli.BoolFlag{
Name: "prohibit-login",
Usage: "Prohibit the user from logging in",
},
&cli.BoolFlag{
Name: "allow-login",
Usage: "Allow the user to log in",
},
&cli.BoolFlag{
Name: "active",
Usage: "Activate the user",
},
&cli.BoolFlag{
Name: "inactive",
Usage: "Deactivate the user",
},
&cli.BoolFlag{
Name: "no-must-change-password",
Usage: "Don't require the user to change password on next login (default: password change required)",
},
&cli.StringFlag{
Name: "visibility",
Usage: "Visibility of the user profile (public, limited, private)",
},
&cli.IntFlag{
Name: "max-repo-creation",
Usage: "Maximum number of repositories the user can create (-1 for unlimited)",
},
&cli.BoolFlag{
Name: "allow-git-hook",
Usage: "Allow the user to use git hooks",
},
&cli.BoolFlag{
Name: "no-allow-git-hook",
Usage: "Disallow the user from using git hooks",
},
&cli.BoolFlag{
Name: "allow-import-local",
Usage: "Allow the user to import local repositories",
},
&cli.BoolFlag{
Name: "no-allow-import-local",
Usage: "Disallow the user from importing local repositories",
},
&cli.BoolFlag{
Name: "allow-create-organization",
Usage: "Allow the user to create organizations",
},
&cli.BoolFlag{
Name: "no-allow-create-organization",
Usage: "Disallow the user from creating organizations",
},
}, flags.AllDefaultFlags...),
}
// RunUserEdit edits an existing user
func RunUserEdit(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if ctx.Args().Len() == 0 {
return fmt.Errorf("username is required")
}
client := ctx.Login.Client()
username := ctx.Args().First()
// Verify the user exists before attempting an update.
_, _, err = client.Users.GetUserInfo(requestCtx, username)
if err != nil {
return fmt.Errorf("failed to get user info: %w", err)
}
// Build edit options, starting with required LoginName
editOpts := gitea.EditUserOption{
LoginName: username,
}
// Update email if set
if ctx.IsSet("email") {
email := ctx.String("email")
editOpts.Email = &email
}
// Update full name if set
if ctx.IsSet("full-name") {
fullName := ctx.String("full-name")
editOpts.FullName = &fullName
}
// Update description if set
if ctx.IsSet("description") {
description := ctx.String("description")
editOpts.Description = &description
}
// Update website if set
if ctx.IsSet("website") {
website := ctx.String("website")
editOpts.Website = &website
}
// Update location if set
if ctx.IsSet("location") {
location := ctx.String("location")
editOpts.Location = &location
}
// Handle admin status
if ctx.IsSet("admin") {
admin := ctx.Bool("admin")
editOpts.Admin = &admin
} else if ctx.IsSet("no-admin") {
admin := false
editOpts.Admin = &admin
}
// Handle restricted status
if ctx.IsSet("restricted") {
restricted := ctx.Bool("restricted")
editOpts.Restricted = &restricted
} else if ctx.IsSet("no-restricted") {
restricted := false
editOpts.Restricted = &restricted
}
// Handle prohibit login status
if ctx.IsSet("prohibit-login") {
prohibitLogin := ctx.Bool("prohibit-login")
editOpts.ProhibitLogin = &prohibitLogin
} else if ctx.IsSet("allow-login") {
prohibitLogin := false
editOpts.ProhibitLogin = &prohibitLogin
}
// Handle active status
if ctx.IsSet("active") {
active := ctx.Bool("active")
editOpts.Active = &active
} else if ctx.IsSet("inactive") {
active := false
editOpts.Active = &active
}
// Handle must change password - will be set when password is changed unless flag is set
// Handle visibility
if ctx.IsSet("visibility") {
vis, err := parseUserVisibility(ctx.String("visibility"))
if err != nil {
return err
}
editOpts.Visibility = vis
}
// Handle max repo creation
if ctx.IsSet("max-repo-creation") {
maxRepoCreation := ctx.Int("max-repo-creation")
editOpts.MaxRepoCreation = &maxRepoCreation
}
// Handle allow git hook
if ctx.IsSet("allow-git-hook") {
allowGitHook := ctx.Bool("allow-git-hook")
editOpts.AllowGitHook = &allowGitHook
} else if ctx.IsSet("no-allow-git-hook") {
allowGitHook := false
editOpts.AllowGitHook = &allowGitHook
}
// Handle allow import local
if ctx.IsSet("allow-import-local") {
allowImportLocal := ctx.Bool("allow-import-local")
editOpts.AllowImportLocal = &allowImportLocal
} else if ctx.IsSet("no-allow-import-local") {
allowImportLocal := false
editOpts.AllowImportLocal = &allowImportLocal
}
// Handle allow create organization
if ctx.IsSet("allow-create-organization") {
allowCreateOrg := ctx.Bool("allow-create-organization")
editOpts.AllowCreateOrganization = &allowCreateOrg
} else if ctx.IsSet("no-allow-create-organization") {
allowCreateOrg := false
editOpts.AllowCreateOrganization = &allowCreateOrg
}
// Handle password if any password flag is set or if password flag was provided (even without value)
shouldChangePassword := ctx.IsSet("password") || ctx.IsSet("password-file") || ctx.Bool("password-stdin")
if shouldChangePassword {
password := ctx.String("password")
// Get password from various sources in priority order
if password == "" {
if ctx.IsSet("password-file") && ctx.String("password-file") != "" {
// Read from file
content, err := os.ReadFile(ctx.String("password-file"))
if err != nil {
return fmt.Errorf("failed to read password file: %w", err)
}
password = strings.TrimSpace(string(content))
} else if ctx.Bool("password-stdin") {
// Read from stdin
content, err := io.ReadAll(os.Stdin)
if err != nil {
return fmt.Errorf("failed to read password from stdin: %w", err)
}
password = strings.TrimSpace(string(content))
} else {
// Interactive prompt (hidden input) - triggered when --password is used without value
fmt.Printf("Enter new password for '%s': ", username)
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return fmt.Errorf("failed to read password: %w", err)
}
fmt.Println() // Add newline after hidden input
password = string(bytePassword)
if password == "" {
return fmt.Errorf("password cannot be empty")
}
// Confirm password (only for interactive mode)
fmt.Printf("Confirm new password for '%s': ", username)
bytePasswordConfirm, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return fmt.Errorf("failed to read password confirmation: %w", err)
}
fmt.Println() // Add newline after hidden input
passwordConfirm := string(bytePasswordConfirm)
if password != passwordConfirm {
return fmt.Errorf("passwords do not match")
}
}
}
if password == "" {
return fmt.Errorf("password cannot be empty")
}
editOpts.Password = password
// When password is changed, require user to change password on next login by default
// Only set to false if --no-must-change-password flag is explicitly set
if !ctx.IsSet("no-must-change-password") {
mustChangePassword := true
editOpts.MustChangePassword = &mustChangePassword
} else {
mustChangePassword := false
editOpts.MustChangePassword = &mustChangePassword
}
}
// Only proceed with update if at least one field is being modified
hasChanges := editOpts.Email != nil ||
editOpts.FullName != nil ||
editOpts.Description != nil ||
editOpts.Website != nil ||
editOpts.Location != nil ||
editOpts.Admin != nil ||
editOpts.Restricted != nil ||
editOpts.ProhibitLogin != nil ||
editOpts.Active != nil ||
editOpts.Visibility != nil ||
editOpts.MaxRepoCreation != nil ||
editOpts.AllowGitHook != nil ||
editOpts.AllowImportLocal != nil ||
editOpts.AllowCreateOrganization != nil ||
editOpts.Password != ""
if !hasChanges {
return fmt.Errorf("no changes specified")
}
// Update the user
_, err = client.Admin.EditUser(requestCtx, username, editOpts)
if err != nil {
return fmt.Errorf("failed to update user: %w", err)
}
// Refresh user info to reflect the changes
updatedUser, _, err := client.Users.GetUserInfo(requestCtx, username)
if err != nil {
return fmt.Errorf("user updated but failed to retrieve updated user info: %w", err)
}
print.UserDetails(updatedUser)
return nil
}
+12 -11
View File
@@ -6,11 +6,11 @@ package users
import (
stdctx "context"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
gitea "gitea.dev/sdk"
"code.gitea.io/sdk/gitea"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"github.com/urfave/cli/v3"
)
@@ -33,8 +33,11 @@ var CmdUserList = cli.Command{
}
// RunUserList list users
func RunUserList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
func RunUserList(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
fields, err := userFieldsFlag.GetValues(cmd)
if err != nil {
@@ -42,14 +45,12 @@ func RunUserList(_ stdctx.Context, cmd *cli.Command) error {
}
client := ctx.Login.Client()
users, _, err := client.AdminListUsers(gitea.AdminListUsersOptions{
ListOptions: ctx.GetListOptions(),
users, _, err := client.Admin.ListUsers(requestCtx, gitea.AdminListUsersOptions{
ListOptions: flags.GetListOptions(cmd),
})
if err != nil {
return err
}
print.UserList(users, ctx.Output, fields)
return nil
return print.UserList(users, ctx.Output, fields)
}
+32
View File
@@ -0,0 +1,32 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package users
import (
"fmt"
"strings"
"gitea.dev/sdk"
)
func parseUserVisibility(visibility string) (*gitea.VisibleType, error) {
switch visibility {
case "public":
vis := gitea.VisibleTypePublic
return &vis, nil
case "limited":
vis := gitea.VisibleTypeLimited
return &vis, nil
case "private":
vis := gitea.VisibleTypePrivate
return &vis, nil
default:
return nil, fmt.Errorf("invalid visibility: %s (must be public, limited, or private)", visibility)
}
}
func isConfirmationAccepted(response string) bool {
trimmed := strings.TrimSpace(response)
return strings.EqualFold(trimmed, "y") || strings.EqualFold(trimmed, "yes")
}
+408
View File
@@ -0,0 +1,408 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"bytes"
stdctx "context"
"encoding/json"
"fmt"
"io"
"os"
"strconv"
"strings"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/api"
"gitea.dev/tea/modules/context"
"github.com/urfave/cli/v3"
"golang.org/x/term"
)
// apiFlags returns a fresh set of flag instances for the api command.
// This is a factory function so that each invocation gets independent flag
// objects, avoiding shared hasBeenSet state across tests.
func apiFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "method",
Aliases: []string{"X"},
Usage: "HTTP method (GET, POST, PUT, PATCH, DELETE)",
Value: "GET",
},
&cli.StringSliceFlag{
Name: "field",
Aliases: []string{"f"},
Usage: "Add a string field to the request body (key=value)",
},
&cli.StringSliceFlag{
Name: "Field",
Aliases: []string{"F"},
Usage: "Add a typed field to the request body (key=value, @file, or @- for stdin)",
},
&cli.StringSliceFlag{
Name: "header",
Aliases: []string{"H"},
Usage: "Add a custom header (key:value)",
},
&cli.StringFlag{
Name: "data",
Aliases: []string{"d"},
Usage: "Raw JSON request body (use @file to read from file, @- for stdin)",
},
&cli.BoolFlag{
Name: "include",
Aliases: []string{"i"},
Usage: "Include HTTP status and response headers in output (written to stderr)",
},
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Write response body to file instead of stdout (use '-' for stdout)",
},
}
}
// CmdApi represents the api command
var CmdApi = cli.Command{
Name: "api",
Category: catHelpers,
DisableSliceFlagSeparator: true,
Usage: "Make an authenticated API request",
Description: `Makes an authenticated HTTP request to the Gitea API and prints the response.
The endpoint argument is the path to the API endpoint, which will be prefixed
with /api/v1/ if it doesn't start with /api/ or http(s)://.
Placeholders like {owner} and {repo} in the endpoint will be replaced with
values from the current repository context.
Use -f for string fields and -F for typed fields (numbers, booleans, null).
With -F, prefix value with @ to read from file (@- for stdin). Values starting
with [ or { are parsed as JSON arrays/objects. Wrap values in quotes to force
string type (e.g., -F key="null" for literal string "null").
Use -d/--data to send a raw JSON body. Use @file to read from a file, or @-
to read from stdin. The -d flag cannot be combined with -f or -F.
When a request body is provided via -f, -F, or -d, the method defaults to POST
unless explicitly set with -X/--method.
Note: if your endpoint contains ? or &, quote it to prevent shell expansion
(e.g., '/repos/{owner}/{repo}/issues?state=open').`,
ArgsUsage: "<endpoint>",
Action: runApi,
Flags: append(apiFlags(), flags.LoginRepoFlags...),
}
type preparedAPIRequest struct {
Method string
Endpoint string
Headers map[string]string
Body []byte
}
func runApi(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
request, err := prepareAPIRequest(cmd, ctx)
if err != nil {
return err
}
var body io.Reader
if request.Body != nil {
body = bytes.NewReader(request.Body)
}
// Create API client and make request
client := api.NewClient(ctx.Login)
resp, err := client.Do(request.Method, request.Endpoint, body, request.Headers)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer func() {
if closeErr := resp.Body.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", closeErr)
}
}()
// Print headers to stderr if requested (so redirects/pipes work correctly)
if cmd.Bool("include") {
fmt.Fprintf(os.Stderr, "%s %s\n", resp.Proto, resp.Status)
for key, values := range resp.Header {
for _, value := range values {
fmt.Fprintf(os.Stderr, "%s: %s\n", key, value)
}
}
fmt.Fprintln(os.Stderr)
}
// Determine output destination
outputPath := cmd.String("output")
forceStdout := outputPath == "-"
outputToStdout := outputPath == "" || forceStdout
// Check for binary output to terminal (skip warning if user explicitly forced stdout)
if outputToStdout && !forceStdout && term.IsTerminal(int(os.Stdout.Fd())) && !isTextContentType(resp.Header.Get("Content-Type")) {
fmt.Fprintln(os.Stderr, "Warning: Binary output detected. Use '-o <file>' to save to a file,")
fmt.Fprintln(os.Stderr, "or '-o -' to force output to terminal.")
return nil
}
var output io.Writer = os.Stdout
if !outputToStdout {
file, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer func() {
if closeErr := file.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr, "warning: failed to close output file: %v\n", closeErr)
}
}()
output = file
}
// Copy response body to output
_, err = io.Copy(output, resp.Body)
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}
// Add newline for better terminal display
if outputToStdout && term.IsTerminal(int(os.Stdout.Fd())) {
fmt.Println()
}
return nil
}
func prepareAPIRequest(cmd *cli.Command, ctx *context.TeaContext) (*preparedAPIRequest, error) {
var err error
// Get the endpoint argument
if cmd.NArg() < 1 {
return nil, fmt.Errorf("endpoint argument required")
}
endpoint := cmd.Args().First()
// Expand placeholders in endpoint
endpoint = expandPlaceholders(endpoint, ctx)
// Parse headers
headers := make(map[string]string)
for _, h := range cmd.StringSlice("header") {
parts := strings.SplitN(h, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid header format: %q (expected key:value)", h)
}
headers[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
// Build request body from fields
var bodyBytes []byte
stringFields := cmd.StringSlice("field")
typedFields := cmd.StringSlice("Field")
dataRaw := cmd.String("data")
if dataRaw != "" && (len(stringFields) > 0 || len(typedFields) > 0) {
return nil, fmt.Errorf("--data/-d cannot be combined with --field/-f or --Field/-F")
}
if dataRaw != "" {
var dataBytes []byte
var dataSource string
if strings.HasPrefix(dataRaw, "@") {
filename := dataRaw[1:]
if filename == "-" {
dataBytes, err = io.ReadAll(os.Stdin)
dataSource = "stdin"
} else {
dataBytes, err = os.ReadFile(filename)
dataSource = filename
}
if err != nil {
return nil, fmt.Errorf("failed to read %q: %w", dataRaw, err)
}
} else {
dataBytes = []byte(dataRaw)
}
if !json.Valid(dataBytes) {
if dataSource != "" {
return nil, fmt.Errorf("--data/-d value from %s is not valid JSON", dataSource)
}
return nil, fmt.Errorf("--data/-d value is not valid JSON")
}
bodyBytes = dataBytes
} else if len(stringFields) > 0 || len(typedFields) > 0 {
bodyMap := make(map[string]any)
// Process string fields (-f)
for _, f := range stringFields {
parts := strings.SplitN(f, "=", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid field format: %q (expected key=value)", f)
}
key := parts[0]
if key == "" {
return nil, fmt.Errorf("field key cannot be empty in %q", f)
}
if _, exists := bodyMap[key]; exists {
return nil, fmt.Errorf("duplicate field key %q", key)
}
bodyMap[key] = parts[1]
}
// Process typed fields (-F)
for _, f := range typedFields {
parts := strings.SplitN(f, "=", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid field format: %q (expected key=value)", f)
}
key := parts[0]
if key == "" {
return nil, fmt.Errorf("field key cannot be empty in %q", f)
}
if _, exists := bodyMap[key]; exists {
return nil, fmt.Errorf("duplicate field key %q", key)
}
value := parts[1]
parsedValue, err := parseTypedValue(value)
if err != nil {
return nil, fmt.Errorf("failed to parse field %q: %w", key, err)
}
bodyMap[key] = parsedValue
}
bodyBytes, err = json.Marshal(bodyMap)
if err != nil {
return nil, fmt.Errorf("failed to encode request body: %w", err)
}
}
method := strings.ToUpper(cmd.String("method"))
if !cmd.IsSet("method") {
if bodyBytes != nil {
method = "POST"
} else {
method = "GET"
}
}
return &preparedAPIRequest{
Method: method,
Endpoint: endpoint,
Headers: headers,
Body: bodyBytes,
}, nil
}
// parseTypedValue parses a value for -F flag, handling:
// - @filename: read content from file
// - @-: read content from stdin
// - "quoted": literal string (prevents type parsing)
// - true/false: boolean
// - null: nil
// - numbers: int or float
// - []/{}: JSON arrays/objects
// - otherwise: string
func parseTypedValue(value string) (any, error) {
// Handle file references.
// Note: if multiple fields use @- (stdin), only the first will get data;
// subsequent reads will return empty since stdin is consumed once.
if strings.HasPrefix(value, "@") {
filename := value[1:]
var content []byte
var err error
if filename == "-" {
content, err = io.ReadAll(os.Stdin)
} else {
content, err = os.ReadFile(filename)
}
if err != nil {
return nil, fmt.Errorf("failed to read %q: %w", value, err)
}
return strings.TrimSuffix(string(content), "\n"), nil
}
// Handle quoted strings (literal strings, no type parsing).
// Uses strconv.Unquote so escape sequences like \" are handled correctly.
if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' {
unquoted, err := strconv.Unquote(value)
if err != nil {
return nil, fmt.Errorf("invalid quoted string %s: %w", value, err)
}
return unquoted, nil
}
// Handle null
if value == "null" {
return nil, nil
}
// Handle booleans
if value == "true" {
return true, nil
}
if value == "false" {
return false, nil
}
// Handle integers
if i, err := strconv.ParseInt(value, 10, 64); err == nil {
return i, nil
}
// Handle floats
if f, err := strconv.ParseFloat(value, 64); err == nil {
return f, nil
}
// Handle JSON arrays and objects
if len(value) > 0 && (value[0] == '[' || value[0] == '{') {
var jsonVal any
if err := json.Unmarshal([]byte(value), &jsonVal); err == nil {
return jsonVal, nil
}
}
// Default to string
return value, nil
}
// isTextContentType returns true if the content type indicates text data
func isTextContentType(contentType string) bool {
if contentType == "" {
return true // assume text if unknown
}
contentType = strings.ToLower(strings.Split(contentType, ";")[0]) // strip charset
return strings.HasPrefix(contentType, "text/") ||
strings.Contains(contentType, "json") ||
strings.Contains(contentType, "xml") ||
strings.Contains(contentType, "javascript") ||
strings.Contains(contentType, "yaml") ||
strings.Contains(contentType, "toml")
}
// expandPlaceholders replaces {owner}, {repo}, and {branch} in the endpoint
func expandPlaceholders(endpoint string, ctx *context.TeaContext) string {
endpoint = strings.ReplaceAll(endpoint, "{owner}", ctx.Owner)
endpoint = strings.ReplaceAll(endpoint, "{repo}", ctx.Repo)
// Get current branch if available
if ctx.LocalRepo != nil {
if branch, err := ctx.LocalRepo.Head(); err == nil {
branchName := branch.Name().Short()
endpoint = strings.ReplaceAll(endpoint, "{branch}", branchName)
}
}
return endpoint
}
+632
View File
@@ -0,0 +1,632 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
stdctx "context"
"encoding/json"
"io"
"os"
"os/exec"
"path/filepath"
"testing"
"gitea.dev/tea/modules/config"
"gitea.dev/tea/modules/context"
tea_git "gitea.dev/tea/modules/git"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
)
func TestParseTypedValue(t *testing.T) {
t.Run("null", func(t *testing.T) {
v, err := parseTypedValue("null")
require.NoError(t, err)
assert.Nil(t, v)
})
t.Run("bool true", func(t *testing.T) {
v, err := parseTypedValue("true")
require.NoError(t, err)
assert.Equal(t, true, v)
})
t.Run("bool false", func(t *testing.T) {
v, err := parseTypedValue("false")
require.NoError(t, err)
assert.Equal(t, false, v)
})
t.Run("integer", func(t *testing.T) {
v, err := parseTypedValue("42")
require.NoError(t, err)
assert.Equal(t, int64(42), v)
})
t.Run("float", func(t *testing.T) {
v, err := parseTypedValue("3.14")
require.NoError(t, err)
assert.Equal(t, 3.14, v)
})
t.Run("string", func(t *testing.T) {
v, err := parseTypedValue("hello")
require.NoError(t, err)
assert.Equal(t, "hello", v)
})
t.Run("JSON array", func(t *testing.T) {
v, err := parseTypedValue("[1,2,3]")
require.NoError(t, err)
assert.Equal(t, []any{float64(1), float64(2), float64(3)}, v)
})
t.Run("JSON object", func(t *testing.T) {
v, err := parseTypedValue(`{"key":"val"}`)
require.NoError(t, err)
assert.Equal(t, map[string]any{"key": "val"}, v)
})
t.Run("invalid JSON array falls back to string", func(t *testing.T) {
v, err := parseTypedValue("[not json")
require.NoError(t, err)
assert.Equal(t, "[not json", v)
})
t.Run("invalid JSON object falls back to string", func(t *testing.T) {
v, err := parseTypedValue("{not json")
require.NoError(t, err)
assert.Equal(t, "{not json", v)
})
t.Run("file reference", func(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), "test.txt")
require.NoError(t, os.WriteFile(tmpFile, []byte("file content\n"), 0o644))
v, err := parseTypedValue("@" + tmpFile)
require.NoError(t, err)
assert.Equal(t, "file content", v)
})
t.Run("file reference without trailing newline", func(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), "test.txt")
require.NoError(t, os.WriteFile(tmpFile, []byte("no newline"), 0o644))
v, err := parseTypedValue("@" + tmpFile)
require.NoError(t, err)
assert.Equal(t, "no newline", v)
})
t.Run("empty file reference", func(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), "empty.txt")
require.NoError(t, os.WriteFile(tmpFile, []byte(""), 0o644))
v, err := parseTypedValue("@" + tmpFile)
require.NoError(t, err)
assert.Equal(t, "", v)
})
t.Run("nonexistent file reference", func(t *testing.T) {
_, err := parseTypedValue("@/nonexistent/file.txt")
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to read")
})
t.Run("negative integer", func(t *testing.T) {
v, err := parseTypedValue("-42")
require.NoError(t, err)
assert.Equal(t, int64(-42), v)
})
t.Run("negative float", func(t *testing.T) {
v, err := parseTypedValue("-3.14")
require.NoError(t, err)
assert.Equal(t, -3.14, v)
})
t.Run("scientific notation", func(t *testing.T) {
v, err := parseTypedValue("1.5e10")
require.NoError(t, err)
assert.Equal(t, 1.5e10, v)
})
t.Run("empty string", func(t *testing.T) {
v, err := parseTypedValue("")
require.NoError(t, err)
assert.Equal(t, "", v)
})
t.Run("string starting with number", func(t *testing.T) {
v, err := parseTypedValue("123abc")
require.NoError(t, err)
assert.Equal(t, "123abc", v)
})
t.Run("nested JSON object", func(t *testing.T) {
v, err := parseTypedValue(`{"user":{"name":"alice","id":1}}`)
require.NoError(t, err)
expected := map[string]any{
"user": map[string]any{
"name": "alice",
"id": float64(1),
},
}
assert.Equal(t, expected, v)
})
t.Run("complex JSON array", func(t *testing.T) {
v, err := parseTypedValue(`[{"id":1},{"id":2}]`)
require.NoError(t, err)
expected := []any{
map[string]any{"id": float64(1)},
map[string]any{"id": float64(2)},
}
assert.Equal(t, expected, v)
})
t.Run("quoted string prevents type parsing", func(t *testing.T) {
v, err := parseTypedValue(`"null"`)
require.NoError(t, err)
assert.Equal(t, "null", v)
})
t.Run("quoted true becomes string", func(t *testing.T) {
v, err := parseTypedValue(`"true"`)
require.NoError(t, err)
assert.Equal(t, "true", v)
})
t.Run("quoted false becomes string", func(t *testing.T) {
v, err := parseTypedValue(`"false"`)
require.NoError(t, err)
assert.Equal(t, "false", v)
})
t.Run("quoted number becomes string", func(t *testing.T) {
v, err := parseTypedValue(`"123"`)
require.NoError(t, err)
assert.Equal(t, "123", v)
})
t.Run("quoted empty string", func(t *testing.T) {
v, err := parseTypedValue(`""`)
require.NoError(t, err)
assert.Equal(t, "", v)
})
t.Run("quoted string with spaces", func(t *testing.T) {
v, err := parseTypedValue(`"hello world"`)
require.NoError(t, err)
assert.Equal(t, "hello world", v)
})
t.Run("single quote not treated as quote", func(t *testing.T) {
v, err := parseTypedValue(`'hello'`)
require.NoError(t, err)
assert.Equal(t, "'hello'", v)
})
t.Run("unmatched quote at start only", func(t *testing.T) {
v, err := parseTypedValue(`"hello`)
require.NoError(t, err)
assert.Equal(t, `"hello`, v)
})
t.Run("unmatched quote at end only", func(t *testing.T) {
v, err := parseTypedValue(`hello"`)
require.NoError(t, err)
assert.Equal(t, `hello"`, v)
})
t.Run("quoted string with escaped quote", func(t *testing.T) {
v, err := parseTypedValue(`"hello \"world\""`)
require.NoError(t, err)
assert.Equal(t, `hello "world"`, v)
})
t.Run("quoted string with backslash-n", func(t *testing.T) {
v, err := parseTypedValue(`"line1\nline2"`)
require.NoError(t, err)
assert.Equal(t, "line1\nline2", v)
})
t.Run("quoted string with tab escape", func(t *testing.T) {
v, err := parseTypedValue(`"col1\tcol2"`)
require.NoError(t, err)
assert.Equal(t, "col1\tcol2", v)
})
t.Run("quoted string with backslash", func(t *testing.T) {
v, err := parseTypedValue(`"path\\to\\file"`)
require.NoError(t, err)
assert.Equal(t, `path\to\file`, v)
})
t.Run("invalid escape sequence in quoted string", func(t *testing.T) {
_, err := parseTypedValue(`"bad \z escape"`)
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid quoted string")
})
}
// runApiWithArgs configures a test login, parses the command line, and captures
// the prepared request without opening sockets or making HTTP requests.
func runApiWithArgs(t *testing.T, args []string) (method string, body []byte, err error) {
t.Helper()
var capturedMethod string
var capturedBody []byte
config.SetConfigForTesting(config.LocalConfig{
Logins: []config.Login{{
Name: "testLogin",
URL: "https://gitea.example.com",
Token: "test-token",
User: "testUser",
Default: true,
}},
})
// Use the apiFlags factory to get fresh flag instances, avoiding shared
// hasBeenSet state between tests. Append minimal login/repo flags needed
// for the test harness.
cmd := cli.Command{
Name: "api",
DisableSliceFlagSeparator: true,
Action: func(_ stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
request, err := prepareAPIRequest(cmd, ctx)
if err != nil {
return err
}
capturedMethod = request.Method
capturedBody = append([]byte(nil), request.Body...)
return nil
},
Flags: append(apiFlags(), []cli.Flag{
&cli.StringFlag{Name: "login", Aliases: []string{"l"}},
&cli.StringFlag{Name: "repo", Aliases: []string{"r"}},
&cli.StringFlag{Name: "remote", Aliases: []string{"R"}},
}...),
Writer: io.Discard,
ErrWriter: io.Discard,
}
fullArgs := append([]string{"api", "--login", "testLogin"}, args...)
runErr := cmd.Run(t.Context(), fullArgs)
return capturedMethod, capturedBody, runErr
}
func TestApiCommaInFieldValue(t *testing.T) {
_, body, err := runApiWithArgs(t, []string{"-f", "body=hello, world", "-X", "POST", "/test"})
require.NoError(t, err)
var parsed map[string]any
require.NoError(t, json.Unmarshal(body, &parsed))
assert.Equal(t, "hello, world", parsed["body"])
}
func TestApiRawDataFlag(t *testing.T) {
_, body, err := runApiWithArgs(t, []string{"-d", `{"title":"test","body":"hello"}`, "/test"})
require.NoError(t, err)
var parsed map[string]any
require.NoError(t, json.Unmarshal(body, &parsed))
assert.Equal(t, "test", parsed["title"])
assert.Equal(t, "hello", parsed["body"])
}
func TestApiDataFieldMutualExclusion(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{"-d", `{"title":"test"}`, "-f", "key=val", "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "--data/-d cannot be combined with --field/-f or --Field/-F")
}
func TestApiMethodAutoDefault(t *testing.T) {
t.Run("POST when body provided without explicit method", func(t *testing.T) {
method, _, err := runApiWithArgs(t, []string{"-d", `{"title":"test"}`, "/test"})
require.NoError(t, err)
assert.Equal(t, "POST", method)
})
t.Run("explicit method overrides auto-POST", func(t *testing.T) {
method, _, err := runApiWithArgs(t, []string{"-d", `{"title":"test"}`, "-X", "PATCH", "/test"})
require.NoError(t, err)
assert.Equal(t, "PATCH", method)
})
t.Run("GET when no body", func(t *testing.T) {
method, _, err := runApiWithArgs(t, []string{"/test"})
require.NoError(t, err)
assert.Equal(t, "GET", method)
})
}
func TestApiMultipleFields(t *testing.T) {
t.Run("multiple -f flags", func(t *testing.T) {
_, body, err := runApiWithArgs(t, []string{
"-f", "title=Test Issue",
"-f", "body=Description here",
"-X", "POST",
"/test",
})
require.NoError(t, err)
var parsed map[string]any
require.NoError(t, json.Unmarshal(body, &parsed))
assert.Equal(t, "Test Issue", parsed["title"])
assert.Equal(t, "Description here", parsed["body"])
})
t.Run("multiple -F flags with different types", func(t *testing.T) {
_, body, err := runApiWithArgs(t, []string{
"-F", "milestone=5",
"-F", "closed=true",
"-F", "title=Test",
"-X", "POST",
"/test",
})
require.NoError(t, err)
var parsed map[string]any
require.NoError(t, json.Unmarshal(body, &parsed))
assert.Equal(t, float64(5), parsed["milestone"])
assert.Equal(t, true, parsed["closed"])
assert.Equal(t, "Test", parsed["title"])
})
t.Run("combining -f and -F flags", func(t *testing.T) {
_, body, err := runApiWithArgs(t, []string{
"-f", "title=Test",
"-F", "milestone=3",
"-F", "closed=false",
"-X", "POST",
"/test",
})
require.NoError(t, err)
var parsed map[string]any
require.NoError(t, json.Unmarshal(body, &parsed))
assert.Equal(t, "Test", parsed["title"])
assert.Equal(t, float64(3), parsed["milestone"])
assert.Equal(t, false, parsed["closed"])
})
t.Run("-F with JSON array", func(t *testing.T) {
_, body, err := runApiWithArgs(t, []string{
"-F", `labels=["bug","enhancement"]`,
"-X", "POST",
"/test",
})
require.NoError(t, err)
var parsed map[string]any
require.NoError(t, json.Unmarshal(body, &parsed))
assert.Equal(t, []any{"bug", "enhancement"}, parsed["labels"])
})
t.Run("-F with JSON object", func(t *testing.T) {
_, body, err := runApiWithArgs(t, []string{
"-F", `assignee={"login":"alice","id":123}`,
"-X", "POST",
"/test",
})
require.NoError(t, err)
var parsed map[string]any
require.NoError(t, json.Unmarshal(body, &parsed))
assignee, ok := parsed["assignee"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "alice", assignee["login"])
assert.Equal(t, float64(123), assignee["id"])
})
t.Run("-F with quoted string to prevent type parsing", func(t *testing.T) {
_, body, err := runApiWithArgs(t, []string{
"-F", `status="null"`,
"-F", `enabled="true"`,
"-F", `count="42"`,
"-X", "POST",
"/test",
})
require.NoError(t, err)
var parsed map[string]any
require.NoError(t, json.Unmarshal(body, &parsed))
assert.Equal(t, "null", parsed["status"])
assert.Equal(t, "true", parsed["enabled"])
assert.Equal(t, "42", parsed["count"])
})
}
func TestApiDataFromFile(t *testing.T) {
t.Run("read JSON from file", func(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), "data.json")
jsonData := `{"title":"From File","body":"File content"}`
require.NoError(t, os.WriteFile(tmpFile, []byte(jsonData), 0o644))
_, body, err := runApiWithArgs(t, []string{"-d", "@" + tmpFile, "/test"})
require.NoError(t, err)
var parsed map[string]any
require.NoError(t, json.Unmarshal(body, &parsed))
assert.Equal(t, "From File", parsed["title"])
assert.Equal(t, "File content", parsed["body"])
})
t.Run("invalid JSON in --data flag", func(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{"-d", `{invalid json}`, "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "not valid JSON")
})
t.Run("invalid JSON from file includes filename", func(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), "bad.json")
require.NoError(t, os.WriteFile(tmpFile, []byte("not json"), 0o644))
_, _, err := runApiWithArgs(t, []string{"-d", "@" + tmpFile, "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "not valid JSON")
assert.Contains(t, err.Error(), "bad.json")
})
}
func TestApiErrorHandling(t *testing.T) {
t.Run("missing endpoint argument", func(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{})
require.Error(t, err)
assert.Contains(t, err.Error(), "endpoint argument required")
})
t.Run("invalid field format", func(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{"-f", "invalidformat", "-X", "POST", "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid field format")
})
t.Run("invalid Field format", func(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{"-F", "noequalsign", "-X", "POST", "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid field format")
})
t.Run("empty field key with -f", func(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{"-f", "=value", "-X", "POST", "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "field key cannot be empty")
})
t.Run("empty field key with -F", func(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{"-F", "=123", "-X", "POST", "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "field key cannot be empty")
})
t.Run("duplicate field key in -f flags", func(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{"-f", "key=first", "-f", "key=second", "-X", "POST", "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "duplicate field key")
})
t.Run("duplicate field key in -F flags", func(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{"-F", "key=1", "-F", "key=2", "-X", "POST", "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "duplicate field key")
})
t.Run("duplicate field key across -f and -F flags", func(t *testing.T) {
_, _, err := runApiWithArgs(t, []string{"-f", "key=string", "-F", "key=123", "-X", "POST", "/test"})
require.Error(t, err)
assert.Contains(t, err.Error(), "duplicate field key")
})
}
func TestExpandPlaceholders(t *testing.T) {
t.Run("replaces owner and repo", func(t *testing.T) {
ctx := &context.TeaContext{
Owner: "myorg",
Repo: "myrepo",
}
result := expandPlaceholders("/repos/{owner}/{repo}/issues", ctx)
assert.Equal(t, "/repos/myorg/myrepo/issues", result)
})
t.Run("replaces multiple occurrences", func(t *testing.T) {
ctx := &context.TeaContext{
Owner: "alice",
Repo: "proj",
}
result := expandPlaceholders("/repos/{owner}/{repo}/branches?owner={owner}", ctx)
assert.Equal(t, "/repos/alice/proj/branches?owner=alice", result)
})
t.Run("no placeholders returns unchanged", func(t *testing.T) {
ctx := &context.TeaContext{
Owner: "alice",
Repo: "proj",
}
result := expandPlaceholders("/api/v1/version", ctx)
assert.Equal(t, "/api/v1/version", result)
})
t.Run("empty owner and repo produce empty replacements", func(t *testing.T) {
ctx := &context.TeaContext{}
result := expandPlaceholders("/repos/{owner}/{repo}", ctx)
assert.Equal(t, "/repos//", result)
})
t.Run("branch left unreplaced when no local repo", func(t *testing.T) {
ctx := &context.TeaContext{
Owner: "alice",
Repo: "proj",
}
result := expandPlaceholders("/repos/{owner}/{repo}/branches/{branch}", ctx)
assert.Equal(t, "/repos/alice/proj/branches/{branch}", result)
})
t.Run("replaces branch from local repo HEAD", func(t *testing.T) {
tmpDir := t.TempDir()
runGit := func(args ...string) {
cmd := exec.Command("git", args...)
cmd.Dir = tmpDir
require.NoError(t, cmd.Run())
}
runGit("init")
runGit("config", "user.email", "test@test.com")
runGit("config", "user.name", "test")
// Create an initial commit so HEAD points to a branch.
tmpFile := filepath.Join(tmpDir, "init.txt")
require.NoError(t, os.WriteFile(tmpFile, []byte("init"), 0o644))
runGit("add", "init.txt")
runGit("commit", "-m", "initial commit")
// Create and checkout a feature branch.
runGit("checkout", "-b", "feature/my-branch")
repo, err := tea_git.RepoFromPath(tmpDir)
require.NoError(t, err)
ctx := &context.TeaContext{
Owner: "alice",
Repo: "proj",
LocalRepo: repo,
}
result := expandPlaceholders("/repos/{owner}/{repo}/branches/{branch}", ctx)
assert.Equal(t, "/repos/alice/proj/branches/feature/my-branch", result)
})
}
func TestIsTextContentType(t *testing.T) {
tests := []struct {
name string
contentType string
want bool
}{
{"empty string defaults to text", "", true},
{"plain text", "text/plain", true},
{"html", "text/html", true},
{"json", "application/json", true},
{"json with charset", "application/json; charset=utf-8", true},
{"xml", "application/xml", true},
{"javascript", "application/javascript", true},
{"yaml", "application/yaml", true},
{"toml", "application/toml", true},
{"binary", "application/octet-stream", false},
{"image", "image/png", false},
{"pdf", "application/pdf", false},
{"zip", "application/zip", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isTextContentType(tt.contentType)
assert.Equal(t, tt.want, got)
})
}
}
+2 -2
View File
@@ -4,8 +4,8 @@
package cmd
import (
"code.gitea.io/tea/cmd/attachments"
"code.gitea.io/tea/cmd/flags"
"gitea.dev/tea/cmd/attachments"
"gitea.dev/tea/cmd/flags"
"github.com/urfave/cli/v3"
)
+15 -9
View File
@@ -9,8 +9,9 @@ import (
"os"
"path/filepath"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/cmd/releases"
"gitea.dev/tea/modules/context"
"github.com/urfave/cli/v3"
)
@@ -26,21 +27,26 @@ var CmdReleaseAttachmentCreate = cli.Command{
Flags: flags.AllDefaultFlags,
}
func runReleaseAttachmentCreate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
func runReleaseAttachmentCreate(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client()
if ctx.Args().Len() < 2 {
return fmt.Errorf("No release tag or assets specified.\nUsage:\t%s", ctx.Command.UsageText)
return fmt.Errorf("no release tag or assets specified.\nUsage:\t%s", ctx.Command.UsageText)
}
tag := ctx.Args().First()
if len(tag) == 0 {
return fmt.Errorf("Release tag needed to create attachment")
return fmt.Errorf("release tag needed to create attachment")
}
release, err := getReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
release, err := releases.GetReleaseByTag(requestCtx, ctx.Owner, ctx.Repo, tag, client)
if err != nil {
return err
}
@@ -53,7 +59,7 @@ func runReleaseAttachmentCreate(_ stdctx.Context, cmd *cli.Command) error {
filePath := filepath.Base(asset)
if _, _, err = ctx.Login.Client().CreateReleaseAttachment(ctx.Owner, ctx.Repo, release.ID, file, filePath); err != nil {
if _, _, err = ctx.Login.Client().Releases.CreateReleaseAttachment(requestCtx, ctx.Owner, ctx.Repo, release.ID, file, filePath); err != nil {
file.Close()
return err
}
+27 -31
View File
@@ -7,10 +7,11 @@ import (
stdctx "context"
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
gitea "gitea.dev/sdk"
"code.gitea.io/sdk/gitea"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/cmd/releases"
"gitea.dev/tea/modules/context"
"github.com/urfave/cli/v3"
)
@@ -31,18 +32,23 @@ var CmdReleaseAttachmentDelete = cli.Command{
}, flags.AllDefaultFlags...),
}
func runReleaseAttachmentDelete(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
func runReleaseAttachmentDelete(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client()
if ctx.Args().Len() < 2 {
return fmt.Errorf("No release tag or attachment names specified.\nUsage:\t%s", ctx.Command.UsageText)
return fmt.Errorf("no release tag or attachment names specified.\nUsage:\t%s", ctx.Command.UsageText)
}
tag := ctx.Args().First()
if len(tag) == 0 {
return fmt.Errorf("Release tag needed to delete attachment")
return fmt.Errorf("release tag needed to delete attachment")
}
if !ctx.Bool("confirm") {
@@ -50,17 +56,25 @@ func runReleaseAttachmentDelete(_ stdctx.Context, cmd *cli.Command) error {
return nil
}
release, err := getReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
release, err := releases.GetReleaseByTag(requestCtx, ctx.Owner, ctx.Repo, tag, client)
if err != nil {
return err
}
existing, _, err := client.ListReleaseAttachments(ctx.Owner, ctx.Repo, release.ID, gitea.ListReleaseAttachmentsOptions{
ListOptions: gitea.ListOptions{Page: -1},
var existing []*gitea.Attachment
for page := 1; ; {
page_attachments, resp, err := client.Releases.ListReleaseAttachments(requestCtx, ctx.Owner, ctx.Repo, release.ID, gitea.ListReleaseAttachmentsOptions{
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
})
if err != nil {
return err
}
existing = append(existing, page_attachments...)
if resp == nil || resp.NextPage == 0 {
break
}
page = resp.NextPage
}
for _, name := range ctx.Args().Slice()[1:] {
var attachment *gitea.Attachment
@@ -70,10 +84,10 @@ func runReleaseAttachmentDelete(_ stdctx.Context, cmd *cli.Command) error {
}
}
if attachment == nil {
return fmt.Errorf("Release does not have attachment named '%s'", name)
return fmt.Errorf("release does not have attachment named '%s'", name)
}
_, err = client.DeleteReleaseAttachment(ctx.Owner, ctx.Repo, release.ID, attachment.ID)
_, err = client.Releases.DeleteReleaseAttachment(requestCtx, ctx.Owner, ctx.Repo, release.ID, attachment.ID)
if err != nil {
return err
}
@@ -81,21 +95,3 @@ func runReleaseAttachmentDelete(_ stdctx.Context, cmd *cli.Command) error {
return nil
}
func getReleaseAttachmentByName(owner, repo string, release int64, name string, client *gitea.Client) (*gitea.Attachment, error) {
al, _, err := client.ListReleaseAttachments(owner, repo, release, gitea.ListReleaseAttachmentsOptions{
ListOptions: gitea.ListOptions{Page: -1},
})
if err != nil {
return nil, err
}
if len(al) == 0 {
return nil, fmt.Errorf("Release does not have any attachments")
}
for _, a := range al {
if a.Name == name {
return a, nil
}
}
return nil, fmt.Errorf("Attachment does not exist")
}
+18 -32
View File
@@ -7,11 +7,11 @@ import (
stdctx "context"
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/sdk/gitea"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/cmd/releases"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"github.com/urfave/cli/v3"
)
@@ -30,46 +30,32 @@ var CmdReleaseAttachmentList = cli.Command{
}
// RunReleaseAttachmentList list release attachments
func RunReleaseAttachmentList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
func RunReleaseAttachmentList(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client()
tag := ctx.Args().First()
if len(tag) == 0 {
return fmt.Errorf("Release tag needed to list attachments")
return fmt.Errorf("release tag needed to list attachments")
}
release, err := getReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
release, err := releases.GetReleaseByTag(requestCtx, ctx.Owner, ctx.Repo, tag, client)
if err != nil {
return err
}
attachments, _, err := ctx.Login.Client().ListReleaseAttachments(ctx.Owner, ctx.Repo, release.ID, gitea.ListReleaseAttachmentsOptions{
ListOptions: ctx.GetListOptions(),
attachments, _, err := ctx.Login.Client().Releases.ListReleaseAttachments(requestCtx, ctx.Owner, ctx.Repo, release.ID, gitea.ListReleaseAttachmentsOptions{
ListOptions: flags.GetListOptions(cmd),
})
if err != nil {
return err
}
print.ReleaseAttachmentsList(attachments, ctx.Output)
return nil
}
func getReleaseByTag(owner, repo, tag string, client *gitea.Client) (*gitea.Release, error) {
rl, _, err := client.ListReleases(owner, repo, gitea.ListReleasesOptions{
ListOptions: gitea.ListOptions{Page: -1},
})
if err != nil {
return nil, err
}
if len(rl) == 0 {
return nil, fmt.Errorf("Repo does not have any release")
}
for _, r := range rl {
if r.TagName == tag {
return r, nil
}
}
return nil, fmt.Errorf("Release tag does not exist")
return print.ReleaseAttachmentsList(attachments, ctx.Output)
}
-138
View File
@@ -1,138 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"github.com/adrg/xdg"
"github.com/urfave/cli/v3"
)
// CmdAutocomplete manages autocompletion
var CmdAutocomplete = cli.Command{
Name: "shellcompletion",
Aliases: []string{"autocomplete"},
Category: catSetup,
Usage: "Install shell completion for tea",
Description: "Install shell completion for tea",
ArgsUsage: "<shell type> (bash, zsh, powershell, fish)",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "install",
Usage: "Persist in shell config instead of printing commands",
},
},
Action: runAutocompleteAdd,
}
func runAutocompleteAdd(_ context.Context, cmd *cli.Command) error {
var remoteFile, localFile, cmds string
shell := cmd.Args().First()
switch shell {
case "zsh":
remoteFile = "contrib/autocomplete.zsh"
localFile = "autocomplete.zsh"
cmds = "echo 'PROG=tea _CLI_ZSH_AUTOCOMPLETE_HACK=1 source \"%s\"' >> ~/.zshrc && source ~/.zshrc"
case "bash":
remoteFile = "contrib/autocomplete.sh"
localFile = "autocomplete.sh"
cmds = "echo 'PROG=tea source \"%s\"' >> ~/.bashrc && source ~/.bashrc"
case "powershell":
remoteFile = "contrib/autocomplete.ps1"
localFile = "tea.ps1"
cmds = "\"& %s\" >> $profile"
case "fish":
// fish is different, in that urfave/cli provides a generator for the shell script needed.
// this also means that the fish completion can become out of sync with the tea binary!
// writing to this directory suffices, as fish reads files there on startup, no cmds needed.
return writeFishAutoCompleteFile(cmd)
default:
return fmt.Errorf("Must specify valid %s", cmd.ArgsUsage)
}
localPath, err := xdg.ConfigFile("tea/" + localFile)
if err != nil {
return err
}
cmds = fmt.Sprintf(cmds, localPath)
if err = writeRemoteAutoCompleteFile(remoteFile, localPath); err != nil {
return err
}
if cmd.Bool("install") {
fmt.Println("Installing in your shellrc")
installer := exec.Command(shell, "-c", cmds)
if shell == "powershell" {
installer = exec.Command("powershell.exe", "-Command", cmds)
}
out, err := installer.CombinedOutput()
if err != nil {
return fmt.Errorf("Couldn't run the commands: %s %s", err, out)
}
} else {
fmt.Println("\n# Run the following commands to install autocompletion (or use --install)")
fmt.Println(cmds)
}
return nil
}
func writeRemoteAutoCompleteFile(file, destPath string) error {
url := fmt.Sprintf("https://gitea.com/gitea/tea/raw/branch/master/%s", file)
fmt.Println("Fetching " + url)
res, err := http.Get(url)
if err != nil {
return err
}
defer res.Body.Close()
writer, err := os.Create(destPath)
if err != nil {
return err
}
defer writer.Close()
_, err = io.Copy(writer, res.Body)
return err
}
func writeFishAutoCompleteFile(cmd *cli.Command) error {
// NOTE: to make sure this file is in sync with tea commands, we'd need to
// - check if the file exists
// - if it does, check if the tea version that wrote it is the currently running version
// - if not, rewrite the file
// on each application run
// NOTE: this generates a completion that also suggests file names, which looks kinda messy..
script, err := cmd.ToFishCompletion()
if err != nil {
return err
}
localPath, err := xdg.ConfigFile("fish/conf.d/tea_completion.fish")
if err != nil {
return err
}
writer, err := os.Create(localPath)
if err != nil {
return err
}
if _, err = io.WriteString(writer, script); err != nil {
return err
}
fmt.Printf("Installed tab completion to %s\n", localPath)
return nil
}
+2 -1
View File
@@ -6,7 +6,7 @@ package cmd
import (
"context"
"code.gitea.io/tea/cmd/branches"
"gitea.dev/tea/cmd/branches"
"github.com/urfave/cli/v3"
)
@@ -24,6 +24,7 @@ var CmdBranches = cli.Command{
&branches.CmdBranchesList,
&branches.CmdBranchesProtect,
&branches.CmdBranchesUnprotect,
&branches.CmdBranchesRename,
},
Flags: append([]cli.Flag{
&cli.BoolFlag{
+17 -16
View File
@@ -6,11 +6,11 @@ package branches
import (
stdctx "context"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
gitea "gitea.dev/sdk"
"code.gitea.io/sdk/gitea"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"github.com/urfave/cli/v3"
)
@@ -37,9 +37,14 @@ var CmdBranchesList = cli.Command{
}
// RunBranchesList list branches
func RunBranchesList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
func RunBranchesList(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
owner := ctx.Owner
if ctx.IsSet("owner") {
@@ -48,19 +53,16 @@ func RunBranchesList(_ stdctx.Context, cmd *cli.Command) error {
var branches []*gitea.Branch
var protections []*gitea.BranchProtection
var err error
branches, _, err = ctx.Login.Client().ListRepoBranches(owner, ctx.Repo, gitea.ListRepoBranchesOptions{
ListOptions: ctx.GetListOptions(),
branches, _, err = ctx.Login.Client().Repositories.ListRepoBranches(requestCtx, owner, ctx.Repo, gitea.ListRepoBranchesOptions{
ListOptions: flags.GetListOptions(cmd),
})
if err != nil {
return err
}
protections, _, err = ctx.Login.Client().ListBranchProtections(owner, ctx.Repo, gitea.ListBranchProtectionsOptions{
ListOptions: ctx.GetListOptions(),
protections, _, err = ctx.Login.Client().Repositories.ListBranchProtections(requestCtx, owner, ctx.Repo, gitea.ListBranchProtectionsOptions{
ListOptions: flags.GetListOptions(cmd),
})
if err != nil {
return err
}
@@ -70,6 +72,5 @@ func RunBranchesList(_ stdctx.Context, cmd *cli.Command) error {
return err
}
print.BranchesList(branches, protections, ctx.Output, fields)
return nil
return print.BranchesList(branches, protections, ctx.Output, fields)
}
+13 -8
View File
@@ -7,10 +7,10 @@ import (
stdctx "context"
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
gitea "gitea.dev/sdk"
"code.gitea.io/sdk/gitea"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"github.com/urfave/cli/v3"
)
@@ -44,9 +44,14 @@ var CmdBranchesUnprotect = cli.Command{
}
// RunBranchesProtect function to protect/unprotect a list of branches
func RunBranchesProtect(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
func RunBranchesProtect(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if !cmd.Args().Present() {
return fmt.Errorf("must specify at least one branch")
@@ -62,7 +67,7 @@ func RunBranchesProtect(_ stdctx.Context, cmd *cli.Command) error {
var err error
command := ctx.Command.Name
if command == "protect" {
_, _, err = ctx.Login.Client().CreateBranchProtection(owner, ctx.Repo, gitea.CreateBranchProtectionOption{
_, _, err = ctx.Login.Client().Repositories.CreateBranchProtection(requestCtx, owner, ctx.Repo, gitea.CreateBranchProtectionOption{
BranchName: branch,
RuleName: "",
EnablePush: false,
@@ -88,7 +93,7 @@ func RunBranchesProtect(_ stdctx.Context, cmd *cli.Command) error {
UnprotectedFilePatterns: "",
})
} else if command == "unprotect" {
_, err = ctx.Login.Client().DeleteBranchProtection(owner, ctx.Repo, branch)
_, err = ctx.Login.Client().Repositories.DeleteBranchProtection(requestCtx, owner, ctx.Repo, branch)
} else {
return fmt.Errorf("command %s is not supported", command)
}
+78
View File
@@ -0,0 +1,78 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package branches
import (
stdctx "context"
"fmt"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"github.com/urfave/cli/v3"
)
// CmdBranchesRenameFlags Flags for command rename
var CmdBranchesRenameFlags = append([]cli.Flag{
branchFieldsFlag,
&flags.PaginationPageFlag,
&flags.PaginationLimitFlag,
}, flags.AllDefaultFlags...)
// CmdBranchesRename represents a sub command of branches to rename a branch
var CmdBranchesRename = cli.Command{
Name: "rename",
Aliases: []string{"rn"},
Usage: "Rename a branch",
Description: `Rename a branch in a repository`,
ArgsUsage: "<old_branch_name> <new_branch_name>",
Action: RunBranchesRename,
Flags: CmdBranchesRenameFlags,
}
// RunBranchesRename function to rename a branch
func RunBranchesRename(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if err := ValidateRenameArgs(ctx.Args().Slice()); err != nil {
return err
}
oldBranchName := ctx.Args().Get(0)
newBranchName := ctx.Args().Get(1)
owner := ctx.Owner
if ctx.IsSet("owner") {
owner = ctx.String("owner")
}
successful, _, err := ctx.Login.Client().Repositories.RenameRepoBranch(requestCtx, owner, ctx.Repo, oldBranchName, gitea.RenameRepoBranchOption{
Name: newBranchName,
})
if err != nil {
return fmt.Errorf("failed to rename branch: %w", err)
}
if !successful {
return fmt.Errorf("failed to rename branch")
}
fmt.Printf("Successfully renamed branch '%s' to '%s'\n", oldBranchName, newBranchName)
return nil
}
// ValidateRenameArgs validates arguments for the rename command
func ValidateRenameArgs(args []string) error {
if len(args) != 2 {
return fmt.Errorf("must specify exactly two arguments: <old_branch_name> <new_branch_name>")
}
return nil
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package branches
import (
"testing"
)
func TestBranchesRenameArgs(t *testing.T) {
tests := []struct {
name string
args []string
wantErr bool
}{
{
name: "valid args",
args: []string{"main", "develop"},
wantErr: false,
},
{
name: "missing both args",
args: []string{},
wantErr: true,
},
{
name: "missing new branch name",
args: []string{"main"},
wantErr: true,
},
{
name: "too many args",
args: []string{"main", "develop", "extra"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateRenameArgs(tt.args)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateRenameArgs() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
+24 -12
View File
@@ -7,13 +7,14 @@ import (
stdctx "context"
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/git"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/utils"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/config"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/debug"
"gitea.dev/tea/modules/git"
"gitea.dev/tea/modules/interact"
"gitea.dev/tea/modules/task"
"gitea.dev/tea/modules/utils"
"github.com/urfave/cli/v3"
)
@@ -47,7 +48,10 @@ When a host is specified in the repo-slug, it will override the login specified
}
func runRepoClone(ctx stdctx.Context, cmd *cli.Command) error {
teaCmd := context.InitCommand(cmd)
teaCmd, err := context.InitCommand(cmd)
if err != nil {
return err
}
args := teaCmd.Args()
if args.Len() < 1 {
@@ -57,7 +61,7 @@ func runRepoClone(ctx stdctx.Context, cmd *cli.Command) error {
var (
login *config.Login = teaCmd.Login
owner string = teaCmd.Login.User
owner string
repo string
)
@@ -68,15 +72,23 @@ func runRepoClone(ctx stdctx.Context, cmd *cli.Command) error {
return err
}
debug.Printf("Cloning repository %s into %s", url.String(), dir)
owner, repo = utils.GetOwnerAndRepo(url.Path, login.User)
if url.Host != "" {
login = config.GetLoginByHost(url.Host)
if login == nil {
return fmt.Errorf("No login configured matching host '%s', run `tea login add` first", url.Host)
var lookupErr error
login, lookupErr = config.GetLoginByHost(url.Host)
if lookupErr != nil {
return lookupErr
}
if login == nil {
return fmt.Errorf("no login configured matching host '%s', run 'tea login add' first", url.Host)
}
debug.Printf("Matched login '%s' for host '%s'", login.Name, url.Host)
}
_, err = task.RepoClone(
ctx,
dir,
login,
owner,
+14 -36
View File
@@ -2,25 +2,16 @@
// SPDX-License-Identifier: MIT
// Tea is command line tool for Gitea.
package cmd // import "code.gitea.io/tea"
package cmd // import "gitea.dev/tea"
import (
"fmt"
"runtime"
"strings"
"github.com/urfave/cli/v3"
"gitea.dev/tea/modules/version"
)
// Version holds the current tea version
var Version = "development"
// Tags holds the build tags used
var Tags = ""
// SDK holds the sdk version from go.mod
var SDK = ""
// App creates and returns a tea Command with all subcommands set
// it was separated from main so docs can be generated for it
func App() *cli.Command {
@@ -32,11 +23,10 @@ func App() *cli.Command {
Usage: "command line tool to interact with Gitea",
Description: appDescription,
CustomHelpTemplate: helpTemplate,
Version: formatVersion(),
Version: version.Format(),
Commands: []*cli.Command{
&CmdLogin,
&CmdLogout,
&CmdAutocomplete,
&CmdWhoami,
&CmdIssues,
@@ -48,34 +38,26 @@ func App() *cli.Command {
&CmdOrgs,
&CmdRepos,
&CmdBranches,
&CmdAddComment,
&CmdActions,
&CmdWiki,
&CmdWebhooks,
&CmdComments,
&CmdOpen,
&CmdNotifications,
&CmdRepoClone,
&CmdSSHKeys,
&CmdAdmin,
&CmdApi,
&CmdGenerateManPage,
},
EnableShellCompletion: true,
}
}
func formatVersion() string {
version := fmt.Sprintf("Version: %s\tgolang: %s",
bold(Version),
strings.ReplaceAll(runtime.Version(), "go", ""))
if len(Tags) != 0 {
version += fmt.Sprintf("\tbuilt with: %s", strings.Replace(Tags, " ", ", ", -1))
}
if len(SDK) != 0 {
version += fmt.Sprintf("\tgo-sdk: %s", SDK)
}
return version
}
var appDescription = `tea is a productivity helper for Gitea. It can be used to manage most entities on
one or multiple Gitea instances & provides local helpers like 'tea pr checkout'.
@@ -85,7 +67,7 @@ upstream repo. tea assumes that local git state is published on the remote befor
doing operations with tea. Configuration is persisted in $XDG_CONFIG_HOME/tea.
`
var helpTemplate = bold(`
var helpTemplate = fmt.Sprintf("\033[1m%s\033[0m", `
{{.Name}}{{if .Usage}} - {{.Usage}}{{end}}`) + `
{{if .Version}}{{if not .HideVersion}}version {{.Version}}{{end}}{{end}}
@@ -127,7 +109,3 @@ var helpTemplate = bold(`
If you find a bug or want to contribute, we'll welcome you at https://gitea.com/gitea/tea.
More info about Gitea itself on https://about.gitea.com.
`
func bold(t string) string {
return fmt.Sprintf("\033[1m%s\033[0m", t)
}
+18 -70
View File
@@ -4,80 +4,28 @@
package cmd
import (
stdctx "context"
"fmt"
"io"
"strings"
"gitea.dev/tea/cmd/comments"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea"
"github.com/AlecAivazis/survey/v2"
"github.com/urfave/cli/v3"
)
// CmdAddComment is the main command to operate with notifications
var CmdAddComment = cli.Command{
Name: "comment",
Aliases: []string{"c"},
// CmdComments is the top-level command for managing comments on issues and pull requests.
var CmdComments = cli.Command{
Name: "comments",
Aliases: []string{"comment", "c"},
Category: catEntities,
Usage: "Add a comment to an issue / pr",
Description: "Add a comment to an issue / pr",
Usage: "Manage comments on issues and pull requests",
Description: `Manage comments on issues and pull requests.
When invoked with an issue/PR index and an optional body, behaves like 'tea comments add'
(this preserves the historical 'tea comment <idx> "<body>"' shorthand).`,
ArgsUsage: "<issue / pr index> [<comment body>]",
Action: runAddComment,
Flags: flags.AllDefaultFlags,
}
func runAddComment(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
args := ctx.Args()
if args.Len() == 0 {
return fmt.Errorf("Please specify issue / pr index")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
body := strings.Join(ctx.Args().Tail(), " ")
if interact.IsStdinPiped() {
// custom solution until https://github.com/AlecAivazis/survey/issues/328 is fixed
if bodyStdin, err := io.ReadAll(ctx.Reader); err != nil {
return err
} else if len(bodyStdin) != 0 {
body = strings.Join([]string{body, string(bodyStdin)}, "\n\n")
}
} else if len(body) == 0 {
if err = survey.AskOne(interact.NewMultiline(interact.Multiline{
Message: "Comment:",
Syntax: "md",
UseEditor: config.GetPreferences().Editor,
}), &body); err != nil {
return err
}
}
if len(body) == 0 {
return fmt.Errorf("No comment body provided")
}
client := ctx.Login.Client()
comment, _, err := client.CreateIssueComment(ctx.Owner, ctx.Repo, idx, gitea.CreateIssueCommentOption{
Body: body,
})
if err != nil {
return err
}
print.Comment(comment)
return nil
Action: comments.RunCommentsAdd,
Flags: comments.CmdCommentsAdd.Flags,
Commands: []*cli.Command{
&comments.CmdCommentsAdd,
&comments.CmdCommentsList,
&comments.CmdCommentsEdit,
&comments.CmdCommentsDelete,
},
}
+96
View File
@@ -0,0 +1,96 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package comments
import (
stdctx "context"
"errors"
"fmt"
"strings"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/config"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/interact"
"gitea.dev/tea/modules/print"
"gitea.dev/tea/modules/theme"
"gitea.dev/tea/modules/utils"
"charm.land/huh/v2"
"github.com/urfave/cli/v3"
)
// CmdCommentsAdd adds a comment to an issue or pull request.
var CmdCommentsAdd = cli.Command{
Name: "add",
Aliases: []string{"a"},
Usage: "Add a comment to an issue or pull request",
Description: "Add a comment to an issue or pull request.",
ArgsUsage: "<issue / pr index> [<comment body>]",
Action: RunCommentsAdd,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "description",
Aliases: []string{"d"},
Usage: "comment body (alternative to the positional argument)",
},
}, flags.AllDefaultFlags...),
}
// RunCommentsAdd creates a new comment.
func RunCommentsAdd(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if ctx.Args().Len() == 0 {
return fmt.Errorf("please specify issue / pr index")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
stdinPiped := interact.IsStdinPiped()
body, err := resolveBody(strings.Join(ctx.Args().Tail(), " "), ctx.String("description"), stdinPiped, ctx.Reader)
if err != nil {
return err
}
if len(body) == 0 && !stdinPiped {
if err := huh.NewForm(
huh.NewGroup(
huh.NewText().
Title("Comment(markdown):").
ExternalEditor(config.GetPreferences().Editor).
EditorExtension("md").
Value(&body),
),
).WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
}
if len(body) == 0 {
return errors.New("no comment content provided")
}
client := ctx.Login.Client()
comment, _, err := client.Issues.CreateIssueComment(requestCtx, ctx.Owner, ctx.Repo, idx, gitea.CreateIssueCommentOption{
Body: body,
})
if err != nil {
return err
}
print.Comment(comment)
return nil
}
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package comments
import "io"
// resolveBody picks the comment body from the non-interactive sources, in
// precedence order:
//
// 1. the positional argument (kept first for back-compat with the historical
// 'tea comment <idx> "<body>"' shorthand),
// 2. the -d/--description flag (mirrors the body flag on 'issue create',
// 'issue edit' and 'pr create'),
// 3. piped stdin.
//
// stdin is only read when stdinPiped is true (a non-TTY stdin, e.g. CI,
// subshells or agent harnesses) and no body was supplied otherwise, so the
// command never blocks reading an interactive terminal when a body is already
// given. An empty result means the caller should fall back to the editor (when
// interactive) or error out.
func resolveBody(positional, description string, stdinPiped bool, stdin io.Reader) (string, error) {
if len(positional) != 0 {
return positional, nil
}
if len(description) != 0 {
return description, nil
}
if stdinPiped {
stdinBytes, err := io.ReadAll(stdin)
if err != nil {
return "", err
}
return string(stdinBytes), nil
}
return "", nil
}
+101
View File
@@ -0,0 +1,101 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package comments
import (
"io"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestResolveBody(t *testing.T) {
testCases := []struct {
name string
positional string
description string
stdinPiped bool
stdin string
expected string
}{
{
name: "positional only",
positional: "from positional",
expected: "from positional",
},
{
name: "description flag only",
description: "from -d",
expected: "from -d",
},
{
name: "positional wins over description for back-compat",
positional: "from positional",
description: "from -d",
expected: "from positional",
},
{
name: "description wins over piped stdin",
description: "from -d",
stdinPiped: true,
stdin: "from stdin",
expected: "from -d",
},
{
name: "piped stdin used when nothing else given",
stdinPiped: true,
stdin: "from stdin",
expected: "from stdin",
},
{
name: "stdin ignored when not piped (interactive terminal)",
stdinPiped: false,
stdin: "should never be read",
expected: "",
},
{
name: "empty when no source provided",
stdinPiped: false,
expected: "",
},
{
name: "piped but empty stdin yields empty body",
stdinPiped: true,
stdin: "",
expected: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
body, err := resolveBody(tc.positional, tc.description, tc.stdinPiped, strings.NewReader(tc.stdin))
require.NoError(t, err)
assert.Equal(t, tc.expected, body)
})
}
}
// TestResolveBodyDoesNotReadStdinWhenBodyGiven guards the original bug: when a
// body is supplied positionally (or via -d), stdin must not be consumed, so the
// command can never block on a non-TTY stdin under CI / agent harnesses.
func TestResolveBodyDoesNotReadStdinWhenBodyGiven(t *testing.T) {
reader := &trackingReader{}
body, err := resolveBody("positional body", "", true, reader)
require.NoError(t, err)
assert.Equal(t, "positional body", body)
assert.False(t, reader.read, "stdin must not be read when a body is supplied")
}
// trackingReader records whether Read was ever called.
type trackingReader struct {
read bool
}
func (r *trackingReader) Read(p []byte) (int, error) {
r.read = true
return 0, io.EOF
}
+55
View File
@@ -0,0 +1,55 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package comments
import (
stdctx "context"
"fmt"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/utils"
"github.com/urfave/cli/v3"
)
// CmdCommentsDelete deletes one or more comments by ID.
var CmdCommentsDelete = cli.Command{
Name: "delete",
Aliases: []string{"rm"},
Usage: "Delete one or more comments by ID",
Description: "Delete one or more comments by their comment ID. Use 'tea comments list <issue>' to find IDs.",
ArgsUsage: "<comment id> [<comment id>...]",
Action: RunCommentsDelete,
Flags: flags.AllDefaultFlags,
}
// RunCommentsDelete removes one or more comments.
func RunCommentsDelete(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if ctx.Args().Len() == 0 {
return fmt.Errorf("please specify at least one comment id")
}
ids, err := utils.ArgsToIndices(ctx.Args().Slice())
if err != nil {
return fmt.Errorf("invalid comment id: %s", err)
}
client := ctx.Login.Client()
for _, id := range ids {
if _, err := client.Issues.DeleteIssueComment(requestCtx, ctx.Owner, ctx.Repo, id); err != nil {
return fmt.Errorf("could not delete comment %d: %s", id, err)
}
fmt.Printf("Deleted comment %d\n", id)
}
return nil
}
+105
View File
@@ -0,0 +1,105 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package comments
import (
stdctx "context"
"errors"
"fmt"
"strings"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/config"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/interact"
"gitea.dev/tea/modules/print"
"gitea.dev/tea/modules/theme"
"gitea.dev/tea/modules/utils"
"charm.land/huh/v2"
"github.com/urfave/cli/v3"
)
// CmdCommentsEdit edits an existing comment.
var CmdCommentsEdit = cli.Command{
Name: "edit",
Aliases: []string{"e"},
Usage: "Edit the body of an existing comment",
Description: `Edit the body of an existing comment by its comment ID. Use 'tea comments list <issue>' to find IDs.
The new body can be supplied as a positional argument, via -d/--description, piped on stdin, or (if none is given and stdin is a terminal) entered in your $EDITOR.`,
ArgsUsage: "<comment id> [<new body>]",
Action: RunCommentsEdit,
Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "description",
Aliases: []string{"d"},
Usage: "new comment body (alternative to the positional argument)",
},
}, flags.AllDefaultFlags...),
}
// RunCommentsEdit updates the body of an existing comment.
func RunCommentsEdit(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if ctx.Args().Len() == 0 {
return fmt.Errorf("please specify comment id")
}
id, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return fmt.Errorf("invalid comment id %q: %s", ctx.Args().First(), err)
}
stdinPiped := interact.IsStdinPiped()
body, err := resolveBody(strings.Join(ctx.Args().Tail(), " "), ctx.String("description"), stdinPiped, ctx.Reader)
if err != nil {
return err
}
if len(body) == 0 && !stdinPiped {
// Fetch current body to pre-populate the editor.
client := ctx.Login.Client()
current, _, fetchErr := client.Issues.GetIssueComment(requestCtx, ctx.Owner, ctx.Repo, id)
if fetchErr != nil {
return fmt.Errorf("could not fetch comment %d: %s", id, fetchErr)
}
body = current.Body
if err := huh.NewForm(
huh.NewGroup(
huh.NewText().
Title("Comment(markdown):").
ExternalEditor(config.GetPreferences().Editor).
EditorExtension("md").
Value(&body),
),
).WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
}
if len(body) == 0 {
return errors.New("no comment content provided")
}
client := ctx.Login.Client()
comment, _, err := client.Issues.EditIssueComment(requestCtx, ctx.Owner, ctx.Repo, id, gitea.EditIssueCommentOption{
Body: body,
})
if err != nil {
return err
}
print.Comment(comment)
return nil
}
+62
View File
@@ -0,0 +1,62 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package comments
import (
stdctx "context"
"fmt"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"gitea.dev/tea/modules/utils"
"github.com/urfave/cli/v3"
)
// CmdCommentsList lists comments on an issue or pull request.
var CmdCommentsList = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List comments on an issue or pull request",
Description: "List comments on an issue or pull request. Comment IDs returned here are the IDs accepted by 'tea comments edit' and 'tea comments delete'.",
ArgsUsage: "<issue / pr index>",
Action: RunCommentsList,
Flags: append([]cli.Flag{
&flags.PaginationPageFlag,
&flags.PaginationLimitFlag,
}, flags.AllDefaultFlags...),
}
// RunCommentsList lists comments on the given issue/PR.
func RunCommentsList(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if ctx.Args().Len() == 0 {
return fmt.Errorf("please specify issue / pr index")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
client := ctx.Login.Client()
comments, _, err := client.Issues.ListIssueComments(requestCtx, ctx.Owner, ctx.Repo, idx, gitea.ListIssueCommentOptions{
ListOptions: flags.GetListOptions(cmd),
})
if err != nil {
return err
}
return print.CommentsList(comments, ctx.Output)
}
+93
View File
@@ -0,0 +1,93 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"encoding/json"
"io"
"time"
"gitea.dev/sdk"
)
type detailLabelData struct {
Name string `json:"name"`
Color string `json:"color"`
Description string `json:"description"`
}
type detailCommentData struct {
ID int64 `json:"id"`
Author string `json:"author"`
Created time.Time `json:"created"`
Body string `json:"body"`
}
type detailReviewData struct {
ID int64 `json:"id"`
Reviewer string `json:"reviewer"`
State gitea.ReviewStateType `json:"state"`
Body string `json:"body"`
Created time.Time `json:"created"`
}
func buildDetailLabels(labels []*gitea.Label) []detailLabelData {
labelSlice := make([]detailLabelData, 0, len(labels))
for _, label := range labels {
labelSlice = append(labelSlice, detailLabelData{
Name: label.Name,
Color: label.Color,
Description: label.Description,
})
}
return labelSlice
}
func buildDetailAssignees(assignees []*gitea.User) []string {
assigneeSlice := make([]string, 0, len(assignees))
for _, assignee := range assignees {
assigneeSlice = append(assigneeSlice, username(assignee))
}
return assigneeSlice
}
func buildDetailComments(comments []*gitea.Comment) []detailCommentData {
commentSlice := make([]detailCommentData, 0, len(comments))
for _, comment := range comments {
commentSlice = append(commentSlice, detailCommentData{
ID: comment.ID,
Author: username(comment.Poster),
Body: comment.Body,
Created: comment.Created,
})
}
return commentSlice
}
func buildDetailReviews(reviews []*gitea.PullReview) []detailReviewData {
reviewSlice := make([]detailReviewData, 0, len(reviews))
for _, review := range reviews {
reviewSlice = append(reviewSlice, detailReviewData{
ID: review.ID,
Reviewer: username(review.Reviewer),
State: review.State,
Body: review.Body,
Created: review.Submitted,
})
}
return reviewSlice
}
func username(user *gitea.User) string {
if user == nil {
return "ghost"
}
return user.UserName
}
func writeIndentedJSON(w io.Writer, data any) error {
encoder := json.NewEncoder(w)
encoder.SetIndent("", "\t")
return encoder.Encode(data)
}
+3 -2
View File
@@ -7,8 +7,9 @@ import (
"fmt"
"strings"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v3"
"gitea.dev/tea/modules/utils"
)
// CsvFlag is a wrapper around cli.StringFlag, with an added GetValues() method
@@ -44,7 +45,7 @@ func (f CsvFlag) GetValues(cmd *cli.Command) ([]string, error) {
if f.AvailableFields != nil && val != "" {
for _, field := range selection {
if !utils.Contains(f.AvailableFields, field) {
return nil, fmt.Errorf("Invalid field '%s'", field)
return nil, fmt.Errorf("invalid field '%s'", field)
}
}
}
+89 -4
View File
@@ -1,9 +1,13 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package flags
import (
"errors"
"fmt"
gitea "gitea.dev/sdk"
"github.com/urfave/cli/v3"
)
@@ -35,18 +39,68 @@ var OutputFlag = cli.StringFlag{
Usage: "Output format. (simple, table, csv, tsv, yaml, json)",
}
var (
// ErrPage indicates that the provided page value is invalid (less than -1 or equal to 0).
ErrPage = errors.New("page cannot be smaller than 1")
// ErrLimit indicates that the provided limit value is invalid (negative).
ErrLimit = errors.New("limit cannot be negative")
)
const (
defaultPageValue = 1
defaultLimitValue = 30
)
// GetListOptions returns list options derived from the active command.
func GetListOptions(cmd *cli.Command) gitea.ListOptions {
page := cmd.Int("page")
if page == 0 {
page = defaultPageValue
}
pageSize := cmd.Int("limit")
if pageSize == 0 {
pageSize = defaultLimitValue
}
return gitea.ListOptions{
Page: page,
PageSize: pageSize,
}
}
// PaginationFlags provides all pagination related flags
var PaginationFlags = []cli.Flag{
&PaginationPageFlag,
&PaginationLimitFlag,
}
// PaginationPageFlag provides flag for pagination options
var PaginationPageFlag = cli.StringFlag{
var PaginationPageFlag = cli.IntFlag{
Name: "page",
Aliases: []string{"p"},
Usage: "specify page, default is 1",
Usage: "specify page",
Value: defaultPageValue,
Validator: func(i int) error {
if i < 1 && i != -1 {
return ErrPage
}
return nil
},
}
// PaginationLimitFlag provides flag for pagination options
var PaginationLimitFlag = cli.StringFlag{
var PaginationLimitFlag = cli.IntFlag{
Name: "limit",
Aliases: []string{"lm"},
Usage: "specify limit of items per page",
Value: defaultLimitValue,
Validator: func(i int) error {
if i < 0 {
return ErrLimit
}
return nil
},
}
// LoginOutputFlags defines login and output flags that should
@@ -103,3 +157,34 @@ var NotificationStateFlag = NewCsvFlag(
func FieldsFlag(availableFields, defaultFields []string) *CsvFlag {
return NewCsvFlag("fields", "fields to print", []string{"f"}, availableFields, defaultFields)
}
// ParseState parses a state string and returns the corresponding gitea.StateType
func ParseState(stateStr string) (gitea.StateType, error) {
switch stateStr {
case "all":
return gitea.StateAll, nil
case "", "open":
return gitea.StateOpen, nil
case "closed":
return gitea.StateClosed, nil
default:
return "", fmt.Errorf("unknown state '%s'", stateStr)
}
}
// ParseIssueKind parses a kind string and returns the corresponding gitea.IssueType.
// If kindStr is empty, returns the provided defaultKind.
func ParseIssueKind(kindStr string, defaultKind gitea.IssueType) (gitea.IssueType, error) {
switch kindStr {
case "":
return defaultKind, nil
case "all":
return gitea.IssueTypeAll, nil
case "issue", "issues":
return gitea.IssueTypeIssue, nil
case "pull", "pulls", "pr":
return gitea.IssueTypePull, nil
default:
return "", fmt.Errorf("unknown kind '%s'", kindStr)
}
}
+152
View File
@@ -0,0 +1,152 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package flags
import (
"context"
"io"
"testing"
gitea "gitea.dev/sdk"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
)
func TestPaginationFlags(t *testing.T) {
var (
defaultPage = PaginationPageFlag.Value
defaultLimit = PaginationLimitFlag.Value
)
cases := []struct {
name string
args []string
expectedPage int
expectedLimit int
}{
{
name: "no flags",
args: []string{"test"},
expectedPage: defaultPage,
expectedLimit: defaultLimit,
},
{
name: "only paging",
args: []string{"test", "--page", "5"},
expectedPage: 5,
expectedLimit: defaultLimit,
},
{
name: "only limit",
args: []string{"test", "--limit", "10"},
expectedPage: defaultPage,
expectedLimit: 10,
},
{
name: "only limit",
args: []string{"test", "--limit", "10"},
expectedPage: defaultPage,
expectedLimit: 10,
},
{
name: "both flags",
args: []string{"test", "--page", "2", "--limit", "20"},
expectedPage: 2,
expectedLimit: 20,
},
{ // TODO: Should no paging be applied as -1 or a separate flag? It's not obvious that page=-1 turns off paging and limit is ignored
name: "no paging",
args: []string{"test", "--limit", "20", "--page", "-1"},
expectedPage: -1,
expectedLimit: 20,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cmd := cli.Command{
Name: "test-paging",
Action: func(_ context.Context, cmd *cli.Command) error {
assert.Equal(t, tc.expectedPage, cmd.Int("page"))
assert.Equal(t, tc.expectedLimit, cmd.Int("limit"))
return nil
},
Flags: PaginationFlags,
}
err := cmd.Run(t.Context(), tc.args)
require.NoError(t, err)
})
}
}
func TestPaginationFailures(t *testing.T) {
cases := []struct {
name string
args []string
expectedError error
}{
{
name: "negative limit",
args: []string{"test", "--limit", "-10"},
expectedError: ErrLimit,
},
{
name: "negative paging",
args: []string{"test", "--page", "-2"},
expectedError: ErrPage,
},
{
name: "zero paging",
args: []string{"test", "--page", "0"},
expectedError: ErrPage,
},
{
// urfave does not validate all flags in one pass
name: "negative paging and paging",
args: []string{"test", "--page", "-2", "--limit", "-10"},
expectedError: ErrPage,
},
}
for _, tc := range cases {
cmd := cli.Command{
Name: "test-paging",
Flags: PaginationFlags,
Writer: io.Discard,
ErrWriter: io.Discard,
}
t.Run(tc.name, func(t *testing.T) {
err := cmd.Run(t.Context(), tc.args)
require.ErrorContains(t, err, tc.expectedError.Error())
// require.ErrorIs(t, err, tc.expectedError)
})
}
}
func TestGetListOptionsDoesNotLeakBetweenCommands(t *testing.T) {
var results []gitea.ListOptions
run := func(args []string) {
t.Helper()
cmd := cli.Command{
Name: "test-paging",
Action: func(_ context.Context, cmd *cli.Command) error {
results = append(results, GetListOptions(cmd))
return nil
},
Flags: PaginationFlags,
}
require.NoError(t, cmd.Run(t.Context(), args))
}
run([]string{"test", "--page", "5", "--limit", "10"})
run([]string{"test"})
require.Len(t, results, 2)
assert.Equal(t, gitea.ListOptions{Page: 5, PageSize: 10}, results[0])
assert.Equal(t, gitea.ListOptions{Page: defaultPageValue, PageSize: defaultLimitValue}, results[1])
}
+9 -7
View File
@@ -4,13 +4,15 @@
package flags
import (
stdctx "context"
"fmt"
"strings"
"time"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/task"
gitea "gitea.dev/sdk"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/task"
"github.com/araddon/dateparse"
"github.com/urfave/cli/v3"
@@ -130,7 +132,7 @@ var IssuePRCreateFlags = append([]cli.Flag{
}, issuePRFlags...)
// GetIssuePRCreateFlags parses all IssuePREditFlags
func GetIssuePRCreateFlags(ctx *context.TeaContext) (*gitea.CreateIssueOption, error) {
func GetIssuePRCreateFlags(requestCtx stdctx.Context, ctx *context.TeaContext) (*gitea.CreateIssueOption, error) {
opts := gitea.CreateIssueOption{
Title: ctx.String("title"),
Body: ctx.String("description"),
@@ -154,7 +156,7 @@ func GetIssuePRCreateFlags(ctx *context.TeaContext) (*gitea.CreateIssueOption, e
if client == nil {
client = ctx.Login.Client()
}
if opts.Labels, err = task.ResolveLabelNames(client, ctx.Owner, ctx.Repo, labelNames); err != nil {
if opts.Labels, err = task.ResolveLabelNames(requestCtx, client, ctx.Owner, ctx.Repo, labelNames); err != nil {
return nil, err
}
}
@@ -163,9 +165,9 @@ func GetIssuePRCreateFlags(ctx *context.TeaContext) (*gitea.CreateIssueOption, e
if client == nil {
client = ctx.Login.Client()
}
ms, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, milestoneName)
ms, _, err := client.Repositories.GetMilestoneByName(requestCtx, ctx.Owner, ctx.Repo, milestoneName)
if err != nil {
return nil, fmt.Errorf("Milestone '%s' not found", milestoneName)
return nil, fmt.Errorf("milestone '%s' not found", milestoneName)
}
opts.Milestone = ms.ID
}
+110 -13
View File
@@ -6,16 +6,48 @@ package cmd
import (
stdctx "context"
"fmt"
"time"
"code.gitea.io/tea/cmd/issues"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/cmd/issues"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/interact"
"gitea.dev/tea/modules/print"
"gitea.dev/tea/modules/utils"
"github.com/urfave/cli/v3"
)
type labelData = detailLabelData
type issueData struct {
ID int64 `json:"id"`
Index int64 `json:"index"`
Title string `json:"title"`
State gitea.StateType `json:"state"`
Created time.Time `json:"created"`
Labels []labelData `json:"labels"`
User string `json:"user"`
Body string `json:"body"`
Assignees []string `json:"assignees"`
URL string `json:"url"`
ClosedAt *time.Time `json:"closedAt"`
Comments []commentData `json:"comments"`
}
type issueDetailClient interface {
GetIssue(ctx stdctx.Context, owner, repo string, index int64) (*gitea.Issue, *gitea.Response, error)
ListIssueReactions(ctx stdctx.Context, owner, repo string, index int64, opt gitea.ListIssueReactionsOptions) ([]*gitea.Reaction, *gitea.Response, error)
}
type issueCommentClient interface {
ListIssueComments(ctx stdctx.Context, owner, repo string, index int64, opt gitea.ListIssueCommentOptions) ([]*gitea.Comment, *gitea.Response, error)
}
type commentData = detailCommentData
// CmdIssues represents to login a gitea server.
var CmdIssues = cli.Command{
Name: "issues",
@@ -47,27 +79,56 @@ func runIssues(ctx stdctx.Context, cmd *cli.Command) error {
return issues.RunIssuesList(ctx, cmd)
}
func runIssueDetail(_ stdctx.Context, cmd *cli.Command, index string) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
func runIssueDetail(requestCtx stdctx.Context, cmd *cli.Command, index string) error {
ctx, idx, err := resolveIssueDetailContext(cmd, index)
if err != nil {
return err
}
return runIssueDetailWithClient(requestCtx, ctx, idx, ctx.Login.Client().Issues)
}
func resolveIssueDetailContext(cmd *cli.Command, index string) (*context.TeaContext, int64, error) {
ctx, err := context.InitCommand(cmd)
if err != nil {
return nil, 0, err
}
if ctx.IsSet("owner") {
ctx.Owner = ctx.String("owner")
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return nil, 0, err
}
idx, err := utils.ArgToIndex(index)
if err != nil {
return err
return nil, 0, err
}
client := ctx.Login.Client()
issue, _, err := client.GetIssue(ctx.Owner, ctx.Repo, idx)
return ctx, idx, nil
}
func runIssueDetailWithClient(requestCtx stdctx.Context, ctx *context.TeaContext, idx int64, client issueDetailClient) error {
issue, _, err := client.GetIssue(requestCtx, ctx.Owner, ctx.Repo, idx)
if err != nil {
return err
}
reactions, _, err := client.GetIssueReactions(ctx.Owner, ctx.Repo, idx)
reactions, _, err := client.ListIssueReactions(requestCtx, ctx.Owner, ctx.Repo, idx, gitea.ListIssueReactionsOptions{})
if err != nil {
return err
}
if ctx.IsSet("output") {
switch ctx.String("output") {
case "json":
return runIssueDetailAsJSON(requestCtx, ctx, issue)
}
}
print.IssueDetails(issue, reactions)
if issue.Comments > 0 {
err = interact.ShowCommentsMaybeInteractive(ctx, idx, issue.Comments)
err = interact.ShowCommentsMaybeInteractive(requestCtx, ctx, idx, issue.Comments)
if err != nil {
return fmt.Errorf("error loading comments: %v", err)
}
@@ -75,3 +136,39 @@ func runIssueDetail(_ stdctx.Context, cmd *cli.Command, index string) error {
return nil
}
func runIssueDetailAsJSON(requestCtx stdctx.Context, ctx *context.TeaContext, issue *gitea.Issue) error {
return runIssueDetailAsJSONWithClient(requestCtx, ctx, issue, ctx.Login.Client().Issues)
}
func runIssueDetailAsJSONWithClient(requestCtx stdctx.Context, ctx *context.TeaContext, issue *gitea.Issue, c issueCommentClient) error {
opts := gitea.ListIssueCommentOptions{ListOptions: flags.GetListOptions(ctx.Command)}
comments := []*gitea.Comment{}
if ctx.Bool("comments") {
var err error
comments, _, err = c.ListIssueComments(requestCtx, ctx.Owner, ctx.Repo, issue.Index, opts)
if err != nil {
return err
}
}
return writeIndentedJSON(ctx.Writer, buildIssueData(issue, comments))
}
func buildIssueData(issue *gitea.Issue, comments []*gitea.Comment) issueData {
return issueData{
ID: issue.ID,
Index: issue.Index,
Title: issue.Title,
State: issue.State,
Created: issue.Created,
User: username(issue.Poster),
Body: issue.Body,
Labels: buildDetailLabels(issue.Labels),
Assignees: buildDetailAssignees(issue.Assignees),
URL: issue.HTMLURL,
ClosedAt: issue.Closed,
Comments: buildDetailComments(comments),
}
}
+16 -11
View File
@@ -7,12 +7,12 @@ import (
stdctx "context"
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils"
gitea "gitea.dev/sdk"
"code.gitea.io/sdk/gitea"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"gitea.dev/tea/modules/utils"
"github.com/urfave/cli/v3"
)
@@ -23,18 +23,23 @@ var CmdIssuesClose = cli.Command{
Description: `Change state of one ore more issues to 'closed'`,
ArgsUsage: "<issue index> [<issue index>...]",
Action: func(ctx stdctx.Context, cmd *cli.Command) error {
var s = gitea.StateClosed
s := gitea.StateClosed
return editIssueState(ctx, cmd, gitea.EditIssueOption{State: &s})
},
Flags: flags.AllDefaultFlags,
}
// editIssueState abstracts the arg parsing to edit the given issue
func editIssueState(_ stdctx.Context, cmd *cli.Command, opts gitea.EditIssueOption) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
func editIssueState(requestCtx stdctx.Context, cmd *cli.Command, opts gitea.EditIssueOption) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if ctx.Args().Len() == 0 {
return fmt.Errorf(ctx.Command.ArgsUsage)
return fmt.Errorf("missing required argument: %s", ctx.Command.ArgsUsage)
}
indices, err := utils.ArgsToIndices(ctx.Args().Slice())
@@ -44,7 +49,7 @@ func editIssueState(_ stdctx.Context, cmd *cli.Command, opts gitea.EditIssueOpti
client := ctx.Login.Client()
for _, index := range indices {
issue, _, err := client.EditIssue(ctx.Owner, ctx.Repo, index, opts)
issue, _, err := client.Issues.EditIssue(requestCtx, ctx.Owner, ctx.Repo, index, opts)
if err != nil {
return err
}
+21 -13
View File
@@ -6,10 +6,10 @@ package issues
import (
stdctx "context"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/interact"
"gitea.dev/tea/modules/task"
"github.com/urfave/cli/v3"
)
@@ -25,21 +25,29 @@ var CmdIssuesCreate = cli.Command{
Flags: flags.IssuePRCreateFlags,
}
func runIssuesCreate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.NumFlags() == 0 {
return interact.CreateIssue(ctx.Login, ctx.Owner, ctx.Repo)
func runIssuesCreate(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
opts, err := flags.GetIssuePRCreateFlags(ctx)
if ctx.IsInteractiveMode() {
err := interact.CreateIssue(requestCtx, ctx.Login, ctx.Owner, ctx.Repo)
if err != nil && !interact.IsQuitting(err) {
return err
}
return nil
}
opts, err := flags.GetIssuePRCreateFlags(requestCtx, ctx)
if err != nil {
return err
}
return task.CreateIssue(
ctx.Login,
return task.CreateIssue(requestCtx, ctx.Login,
ctx.Owner,
ctx.Repo,
*opts,
+21 -12
View File
@@ -8,13 +8,14 @@ import (
stdctx "context"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v3"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/interact"
"gitea.dev/tea/modules/print"
"gitea.dev/tea/modules/task"
"gitea.dev/tea/modules/utils"
)
// CmdIssuesEdit is the subcommand of issues to edit issues
@@ -29,9 +30,14 @@ use an empty string (eg. --milestone "").`,
Flags: flags.IssuePREditFlags,
}
func runIssuesEdit(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
func runIssuesEdit(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if !cmd.Args().Present() {
return fmt.Errorf("must specify at least one issue index")
@@ -49,15 +55,18 @@ func runIssuesEdit(_ stdctx.Context, cmd *cli.Command) error {
client := ctx.Login.Client()
for _, opts.Index = range indices {
if ctx.NumFlags() == 0 {
if ctx.IsInteractiveMode() {
var err error
opts, err = interact.EditIssue(*ctx, opts.Index)
opts, err = interact.EditIssue(requestCtx, *ctx, opts.Index)
if err != nil {
if interact.IsQuitting(err) {
return nil // user quit
}
return err
}
}
issue, err := task.EditIssue(ctx, client, *opts)
issue, err := task.EditIssue(requestCtx, ctx, client, *opts)
if err != nil {
return err
}
+26 -39
View File
@@ -5,14 +5,14 @@ package issues
import (
stdctx "context"
"fmt"
"errors"
"time"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
gitea "gitea.dev/sdk"
"code.gitea.io/sdk/gitea"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"github.com/araddon/dateparse"
"github.com/urfave/cli/v3"
)
@@ -33,34 +33,22 @@ var CmdIssuesList = cli.Command{
}
// RunIssuesList list issues
func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
state := gitea.StateOpen
switch ctx.String("state") {
case "all":
state = gitea.StateAll
case "", "open":
state = gitea.StateOpen
case "closed":
state = gitea.StateClosed
default:
return fmt.Errorf("unknown state '%s'", ctx.String("state"))
func RunIssuesList(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
kind := gitea.IssueTypeIssue
switch ctx.String("kind") {
case "", "issues", "issue":
kind = gitea.IssueTypeIssue
case "pulls", "pull", "pr":
kind = gitea.IssueTypePull
case "all":
kind = gitea.IssueTypeAll
default:
return fmt.Errorf("unknown kind '%s'", ctx.String("kind"))
state, err := flags.ParseState(ctx.String("state"))
if err != nil {
return err
}
kind, err := flags.ParseIssueKind(ctx.String("kind"), gitea.IssueTypeIssue)
if err != nil {
return err
}
var err error
var from, until time.Time
if ctx.IsSet("from") {
from, err = dateparse.ParseLocal(ctx.String("from"))
@@ -84,31 +72,32 @@ func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error {
milestones, _ := flags.MilestoneFilterFlag.GetValues(cmd)
var issues []*gitea.Issue
if ctx.Repo != "" {
issues, _, err = ctx.Login.Client().ListRepoIssues(owner, ctx.Repo, gitea.ListIssueOption{
ListOptions: ctx.GetListOptions(),
issues, _, err = ctx.Login.Client().Issues.ListRepoIssues(requestCtx, owner, ctx.Repo, gitea.ListIssueOption{
ListOptions: flags.GetListOptions(cmd),
State: state,
Type: kind,
KeyWord: ctx.String("keyword"),
CreatedBy: ctx.String("author"),
AssignedBy: ctx.String("assigned-to"),
AssignedBy: ctx.String("assignee"),
MentionedBy: ctx.String("mentions"),
Labels: labels,
Milestones: milestones,
Since: from,
Before: until,
})
if err != nil {
return err
}
} else {
issues, _, err = ctx.Login.Client().ListIssues(gitea.ListIssueOption{
ListOptions: ctx.GetListOptions(),
if ctx.IsSet("assignee") {
return errors.New("--assignee requires --repo (global issue search does not support assignee filter)")
}
issues, _, err = ctx.Login.Client().Issues.ListIssues(requestCtx, gitea.ListIssueOption{
ListOptions: flags.GetListOptions(cmd),
State: state,
Type: kind,
KeyWord: ctx.String("keyword"),
CreatedBy: ctx.String("author"),
AssignedBy: ctx.String("assigned-to"),
MentionedBy: ctx.String("mentions"),
Labels: labels,
Milestones: milestones,
@@ -116,7 +105,6 @@ func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error {
Before: until,
Owner: owner,
})
if err != nil {
return err
}
@@ -127,6 +115,5 @@ func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error {
return err
}
print.IssuesPullsList(issues, ctx.Output, fields)
return nil
return print.IssuesPullsList(issues, ctx.Output, fields)
}
+115
View File
@@ -0,0 +1,115 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issues
import (
stdctx "context"
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
"gitea.dev/tea/modules/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
"golang.org/x/crypto/ssh"
)
func TestRunIssuesListWithSSHPubkeyLoginDoesNotDeadlock(t *testing.T) {
t.Parallel()
sshKeyPath, fingerprint := writeTestSSHKey(t)
var versionRequests atomic.Int32
var issueRequests atomic.Int32
var signedVersionRequests atomic.Int32
var signedIssueRequests atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/version":
versionRequests.Add(1)
if r.Header.Get("Signature") != "" {
signedVersionRequests.Add(1)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"version":"1.26.4"}`))
case "/api/v1/repos/gitea/tea/issues":
issueRequests.Add(1)
if r.Header.Get("Signature") != "" {
signedIssueRequests.Add(1)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[]`))
default:
t.Errorf("unexpected path %s", r.URL.Path)
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
config.SetConfigForTesting(config.LocalConfig{
Logins: []config.Login{{
Name: "ssh-login",
URL: server.URL,
SSHKey: sshKeyPath,
SSHKeyFingerprint: fingerprint,
VersionCheck: true,
Default: true,
}},
})
cmd := cli.Command{
Name: CmdIssuesList.Name,
Flags: CmdIssuesList.Flags,
}
require.NoError(t, cmd.Set("login", "ssh-login"))
require.NoError(t, cmd.Set("repo", "gitea/tea"))
require.NoError(t, cmd.Set("output", "json"))
done := make(chan error, 1)
go func() {
done <- RunIssuesList(stdctx.Background(), &cmd)
}()
select {
case err := <-done:
require.NoError(t, err)
case <-time.After(2 * time.Second):
t.Fatal("RunIssuesList deadlocked while bootstrapping the server version for HTTPSign authentication")
}
assert.EqualValues(t, 1, versionRequests.Load())
assert.EqualValues(t, 0, signedVersionRequests.Load())
assert.EqualValues(t, 1, issueRequests.Load())
assert.EqualValues(t, 1, signedIssueRequests.Load())
}
func writeTestSSHKey(t *testing.T) (string, string) {
t.Helper()
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
pkcs8, err := x509.MarshalPKCS8PrivateKey(privateKey)
require.NoError(t, err)
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8})
sshKeyPath := filepath.Join(t.TempDir(), "id_ed25519")
require.NoError(t, os.WriteFile(sshKeyPath, pemBytes, 0o600))
signer, err := ssh.NewSignerFromKey(privateKey)
require.NoError(t, err)
return sshKeyPath, ssh.FingerprintSHA256(signer.PublicKey())
}
+3 -3
View File
@@ -6,9 +6,9 @@ package issues
import (
"context"
"code.gitea.io/tea/cmd/flags"
"gitea.dev/sdk"
"code.gitea.io/sdk/gitea"
"gitea.dev/tea/cmd/flags"
"github.com/urfave/cli/v3"
)
@@ -20,7 +20,7 @@ var CmdIssuesReopen = cli.Command{
Description: `Change state of one or more issues to 'open'`,
ArgsUsage: "<issue index> [<issue index>...]",
Action: func(ctx context.Context, cmd *cli.Command) error {
var s = gitea.StateOpen
s := gitea.StateOpen
return editIssueState(ctx, cmd, gitea.EditIssueOption{State: &s})
},
Flags: flags.AllDefaultFlags,
+360
View File
@@ -0,0 +1,360 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"bytes"
stdctx "context"
"encoding/json"
"fmt"
"testing"
"time"
gitea "gitea.dev/sdk"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/config"
"gitea.dev/tea/modules/context"
)
const (
testOwner = "testOwner"
testRepo = "testRepo"
)
type fakeIssueCommentClient struct {
owner string
repo string
index int64
comments []*gitea.Comment
}
func (f *fakeIssueCommentClient) ListIssueComments(_ stdctx.Context, owner, repo string, index int64, _ gitea.ListIssueCommentOptions) ([]*gitea.Comment, *gitea.Response, error) {
f.owner = owner
f.repo = repo
f.index = index
return f.comments, nil, nil
}
type fakeIssueDetailClient struct {
owner string
repo string
index int64
issue *gitea.Issue
reactions []*gitea.Reaction
}
func (f *fakeIssueDetailClient) GetIssue(_ stdctx.Context, owner, repo string, index int64) (*gitea.Issue, *gitea.Response, error) {
f.owner = owner
f.repo = repo
f.index = index
return f.issue, nil, nil
}
func (f *fakeIssueDetailClient) ListIssueReactions(_ stdctx.Context, owner, repo string, index int64, _ gitea.ListIssueReactionsOptions) ([]*gitea.Reaction, *gitea.Response, error) {
f.owner = owner
f.repo = repo
f.index = index
return f.reactions, nil, nil
}
func toCommentPointers(comments []gitea.Comment) []*gitea.Comment {
result := make([]*gitea.Comment, 0, len(comments))
for i := range comments {
comment := comments[i]
result = append(result, &comment)
}
return result
}
func createTestIssue(comments int, isClosed bool) gitea.Issue {
issue := gitea.Issue{
ID: 42,
Index: 1,
Title: "Test issue",
State: gitea.StateOpen,
Body: "This is a test",
Created: time.Date(2025, 31, 10, 23, 59, 59, 999999999, time.UTC),
Updated: time.Date(2025, 1, 11, 0, 0, 0, 0, time.UTC),
Labels: []*gitea.Label{
{
Name: "example/Label1",
Color: "very red",
Description: "This is an example label",
},
{
Name: "example/Label2",
Color: "hardly red",
Description: "This is another example label",
},
},
Comments: comments,
Poster: &gitea.User{
UserName: "testUser",
},
Assignees: []*gitea.User{
{UserName: "testUser"},
{UserName: "testUser3"},
},
HTMLURL: "<space holder>",
Closed: nil, // 2025-11-10T21:20:19Z
}
if isClosed {
closed := time.Date(2025, 11, 10, 21, 20, 19, 0, time.UTC)
issue.Closed = &closed
}
if isClosed {
issue.State = gitea.StateClosed
} else {
issue.State = gitea.StateOpen
}
return issue
}
func createTestIssueComments(comments int) []gitea.Comment {
baseID := 900
var result []gitea.Comment
for commentID := 0; commentID < comments; commentID++ {
result = append(result, gitea.Comment{
ID: int64(baseID + commentID),
Poster: &gitea.User{
UserName: "Freddy",
},
Body: fmt.Sprintf("This is a test comment #%v", commentID),
Created: time.Date(2025, 11, 3, 12, 0, 0, 0, time.UTC).
Add(time.Duration(commentID) * time.Hour),
})
}
return result
}
func TestRunIssueDetailAsJSON(t *testing.T) {
type TestCase struct {
name string
issue gitea.Issue
comments []gitea.Comment
flagComments bool
}
cmd := cli.Command{
Name: "t",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "comments",
Value: false,
},
&cli.StringFlag{
Name: "output",
Value: "json",
},
},
}
testContext := context.TeaContext{
Owner: testOwner,
Repo: testRepo,
Login: &config.Login{
Name: "testLogin",
URL: "http://127.0.0.1:8081",
},
Command: &cmd,
}
testCases := []TestCase{
{
name: "Simple issue with no comments, no comments requested",
issue: createTestIssue(0, true),
comments: []gitea.Comment{},
flagComments: false,
},
{
name: "Simple issue with no comments, comments requested",
issue: createTestIssue(0, true),
comments: []gitea.Comment{},
flagComments: true,
},
{
name: "Simple issue with comments, no comments requested",
issue: createTestIssue(2, true),
comments: createTestIssueComments(2),
flagComments: false,
},
{
name: "Simple issue with comments, comments requested",
issue: createTestIssue(2, true),
comments: createTestIssueComments(2),
flagComments: true,
},
{
name: "Simple issue with comments, comments requested, not closed",
issue: createTestIssue(2, false),
comments: createTestIssueComments(2),
flagComments: true,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
client := &fakeIssueCommentClient{
comments: toCommentPointers(testCase.comments),
}
testContext.Login.URL = "https://gitea.example.com"
testCase.issue.HTMLURL = fmt.Sprintf("%s/%s/%s/issues/%d/", testContext.Login.URL, testOwner, testRepo, testCase.issue.Index)
var outBuffer bytes.Buffer
testContext.Writer = &outBuffer
var errBuffer bytes.Buffer
testContext.ErrWriter = &errBuffer
if testCase.flagComments {
require.NoError(t, testContext.Set("comments", "true"))
} else {
require.NoError(t, testContext.Set("comments", "false"))
}
err := runIssueDetailAsJSONWithClient(t.Context(), &testContext, &testCase.issue, client)
require.NoError(t, err, "Failed to run issue detail as JSON")
if testCase.flagComments {
assert.Equal(t, testOwner, client.owner)
assert.Equal(t, testRepo, client.repo)
assert.Equal(t, testCase.issue.Index, client.index)
}
out := outBuffer.String()
require.NotEmpty(t, out, "Unexpected empty output from runIssueDetailAsJSON")
// setting expectations
var expectedLabels []labelData
expectedLabels = []labelData{}
for _, l := range testCase.issue.Labels {
expectedLabels = append(expectedLabels, labelData{
Name: l.Name,
Color: l.Color,
Description: l.Description,
})
}
var expectedAssignees []string
expectedAssignees = []string{}
for _, a := range testCase.issue.Assignees {
expectedAssignees = append(expectedAssignees, a.UserName)
}
var expectedClosedAt *time.Time
if testCase.issue.Closed != nil {
expectedClosedAt = testCase.issue.Closed
}
var expectedComments []commentData
expectedComments = []commentData{}
if testCase.flagComments {
for _, c := range testCase.comments {
expectedComments = append(expectedComments, commentData{
ID: c.ID,
Author: c.Poster.UserName,
Body: c.Body,
Created: c.Created,
})
}
}
expected := issueData{
ID: testCase.issue.ID,
Index: testCase.issue.Index,
Title: testCase.issue.Title,
State: testCase.issue.State,
Created: testCase.issue.Created,
User: testCase.issue.Poster.UserName,
Body: testCase.issue.Body,
URL: testCase.issue.HTMLURL,
ClosedAt: expectedClosedAt,
Labels: expectedLabels,
Assignees: expectedAssignees,
Comments: expectedComments,
}
// validating reality
var actual issueData
dec := json.NewDecoder(bytes.NewReader(outBuffer.Bytes()))
dec.DisallowUnknownFields()
err = dec.Decode(&actual)
require.NoError(t, err, "Failed to unmarshal output into struct")
assert.Equal(t, expected, actual, "Expected structs differ from expected one")
})
}
}
func TestRunIssueDetailUsesOwnerFlag(t *testing.T) {
issueIndex := int64(12)
expectedOwner := "overrideOwner"
expectedRepo := "overrideRepo"
issue := &gitea.Issue{
ID: 99,
Index: issueIndex,
Title: "Owner override test",
State: gitea.StateOpen,
Created: time.Date(2025, 11, 1, 10, 0, 0, 0, time.UTC),
Poster: &gitea.User{
UserName: "tester",
},
HTMLURL: "https://example.test/issues/12",
}
config.SetConfigForTesting(config.LocalConfig{
Logins: []config.Login{{
Name: "testLogin",
URL: "https://gitea.example.com",
Token: "token",
User: "loginUser",
Default: true,
}},
})
cmd := cli.Command{
Name: "issues",
Flags: []cli.Flag{
&flags.LoginFlag,
&flags.RepoFlag,
&flags.RemoteFlag,
&flags.OutputFlag,
&cli.StringFlag{Name: "owner"},
&cli.BoolFlag{Name: "comments"},
},
}
var outBuffer bytes.Buffer
var errBuffer bytes.Buffer
cmd.Writer = &outBuffer
cmd.ErrWriter = &errBuffer
require.NoError(t, cmd.Set("login", "testLogin"))
require.NoError(t, cmd.Set("repo", expectedRepo))
require.NoError(t, cmd.Set("owner", expectedOwner))
require.NoError(t, cmd.Set("comments", "false"))
teaCtx, idx, err := resolveIssueDetailContext(&cmd, fmt.Sprintf("%d", issueIndex))
require.NoError(t, err)
client := &fakeIssueDetailClient{
issue: issue,
reactions: []*gitea.Reaction{},
}
err = runIssueDetailWithClient(t.Context(), teaCtx, idx, client)
require.NoError(t, err, "Expected runIssueDetail to succeed")
assert.Equal(t, expectedOwner, client.owner)
assert.Equal(t, expectedRepo, client.repo)
assert.Equal(t, issueIndex, client.index)
}
+3 -2
View File
@@ -7,8 +7,9 @@ import (
"context"
"fmt"
"code.gitea.io/tea/cmd/labels"
"github.com/urfave/cli/v3"
"gitea.dev/tea/cmd/labels"
)
// CmdLabels represents to operate repositories' labels.
@@ -37,5 +38,5 @@ func runLabels(ctx context.Context, cmd *cli.Command) error {
}
func runLabelsDetails(cmd *cli.Command) error {
return fmt.Errorf("Not yet implemented")
return fmt.Errorf("not yet implemented")
}
+21 -13
View File
@@ -10,10 +10,10 @@ import (
"os"
"strings"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"gitea.dev/sdk"
"code.gitea.io/sdk/gitea"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"github.com/urfave/cli/v3"
)
@@ -45,19 +45,25 @@ var CmdLabelCreate = cli.Command{
}, flags.AllDefaultFlags...),
}
func runLabelCreate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
func runLabelCreate(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
labelFile := ctx.String("file")
var err error
if len(labelFile) == 0 {
_, _, err = ctx.Login.Client().CreateLabel(ctx.Owner, ctx.Repo, gitea.CreateLabelOption{
_, _, err := ctx.Login.Client().Repositories.CreateLabel(requestCtx, ctx.Owner, ctx.Repo, gitea.CreateLabelOption{
Name: ctx.String("name"),
Color: ctx.String("color"),
Description: ctx.String("description"),
})
} else {
return err
}
f, err := os.Open(labelFile)
if err != nil {
return err
@@ -65,25 +71,27 @@ func runLabelCreate(_ stdctx.Context, cmd *cli.Command) error {
defer f.Close()
scanner := bufio.NewScanner(f)
var i = 1
i := 1
for scanner.Scan() {
line := scanner.Text()
color, name, description := splitLabelLine(line)
if color == "" || name == "" {
log.Printf("Line %d ignored because lack of enough fields: %s\n", i, line)
} else {
_, _, err = ctx.Login.Client().CreateLabel(ctx.Owner, ctx.Repo, gitea.CreateLabelOption{
_, _, err = ctx.Login.Client().Repositories.CreateLabel(requestCtx, ctx.Owner, ctx.Repo, gitea.CreateLabelOption{
Name: name,
Color: color,
Description: description,
})
if err != nil {
return err
}
}
i++
}
}
return err
return nil
}
func splitLabelLine(line string) (string, string, string) {
+1 -1
View File
@@ -20,7 +20,7 @@ func TestParseLabelLine(t *testing.T) {
`
scanner := bufio.NewScanner(strings.NewReader(labels))
var i = 1
i := 1
for scanner.Scan() {
line := scanner.Text()
color, name, description := splitLabelLine(line)
+29 -8
View File
@@ -5,9 +5,10 @@ package labels
import (
stdctx "context"
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"github.com/urfave/cli/v3"
)
@@ -21,17 +22,37 @@ var CmdLabelDelete = cli.Command{
ArgsUsage: " ", // command does not accept arguments
Action: runLabelDelete,
Flags: append([]cli.Flag{
&cli.IntFlag{
&cli.Int64Flag{
Name: "id",
Usage: "label id",
Required: true,
},
}, flags.AllDefaultFlags...),
}
func runLabelDelete(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
_, err := ctx.Login.Client().DeleteLabel(ctx.Owner, ctx.Repo, ctx.Int64("id"))
func runLabelDelete(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
labelID := ctx.Int64("id")
client := ctx.Login.Client()
// Verify the label exists first
label, _, err := client.Repositories.GetRepoLabel(requestCtx, ctx.Owner, ctx.Repo, labelID)
if err != nil {
return fmt.Errorf("failed to get label %d: %w", labelID, err)
}
_, err = client.Repositories.DeleteLabel(requestCtx, ctx.Owner, ctx.Repo, labelID)
if err != nil {
return fmt.Errorf("failed to delete label '%s' (id: %d): %w", label.Name, labelID, err)
}
fmt.Printf("Label '%s' (id: %d) deleted successfully\n", label.Name, labelID)
return nil
}
+64 -13
View File
@@ -5,13 +5,14 @@ package labels
import (
stdctx "context"
"log"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/task"
gitea "gitea.dev/sdk"
"code.gitea.io/sdk/gitea"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"gitea.dev/tea/modules/task"
"github.com/urfave/cli/v3"
)
@@ -29,20 +30,37 @@ var CmdLabelsList = cli.Command{
Aliases: []string{"s"},
Usage: "Save all the labels as a file",
},
&cli.StringFlag{
Name: "org",
Usage: "List organization labels",
},
&cli.BoolFlag{
Name: "exclude-org",
Usage: "Exclude organization labels from the list",
},
&flags.PaginationPageFlag,
&flags.PaginationLimitFlag,
}, flags.AllDefaultFlags...),
}
// RunLabelsList list labels.
func RunLabelsList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
func RunLabelsList(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if cmd.String("org") == "" && cmd.String("repo") == "" {
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
}
if cmd.String("org") != "" && cmd.String("repo") != "" {
log.Printf("Warning: Both --org and --repo flags are set. Ignoring --org and using --repo value as owner.\n")
}
client := ctx.Login.Client()
labels, _, err := client.ListRepoLabels(ctx.Owner, ctx.Repo, gitea.ListLabelsOptions{
ListOptions: ctx.GetListOptions(),
})
labels, err := collectLabels(requestCtx, client, ctx, cmd)
if err != nil {
return err
}
@@ -51,6 +69,39 @@ func RunLabelsList(_ stdctx.Context, cmd *cli.Command) error {
return task.LabelsExport(labels, ctx.String("save"))
}
print.LabelsList(labels, ctx.Output)
return nil
return print.LabelsList(labels, ctx.Output)
}
// CollectLabels collects labels based on the provided command flags and context.
func collectLabels(requestCtx stdctx.Context, client *gitea.Client, ctx *context.TeaContext, cmd *cli.Command) ([]*gitea.Label, error) {
var labels []*gitea.Label
var err error
owner := ctx.Owner
// If --org is set but --repo is not, list organization labels only and skip repository labels.
if cmd.String("org") != "" && cmd.String("repo") == "" {
owner = ctx.Org
} else {
labels, _, err = client.Repositories.ListRepoLabels(requestCtx, owner, ctx.Repo, gitea.ListLabelsOptions{
ListOptions: flags.GetListOptions(cmd),
})
if err != nil {
log.Printf("Failed to list repository labels: %v", err)
}
}
// If --exclude is not set and the owner is an organization, also list organization labels and append them to the list.
if !ctx.IsSet("exclude-org") {
if _, _, err = client.Organizations.GetOrg(requestCtx, owner); err == nil {
orgLabels, _, err := client.Organizations.ListOrgLabels(requestCtx, owner, gitea.ListOrgLabelsOptions{
ListOptions: flags.GetListOptions(cmd),
})
if err != nil {
return nil, err
}
labels = append(labels, orgLabels...)
}
}
return labels, nil
}
+13 -10
View File
@@ -6,10 +6,10 @@ package labels
import (
stdctx "context"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"gitea.dev/sdk"
"code.gitea.io/sdk/gitea"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"github.com/urfave/cli/v3"
)
@@ -21,7 +21,7 @@ var CmdLabelUpdate = cli.Command{
ArgsUsage: " ", // command does not accept arguments
Action: runLabelUpdate,
Flags: append([]cli.Flag{
&cli.IntFlag{
&cli.Int64Flag{
Name: "id",
Usage: "label id",
},
@@ -40,9 +40,14 @@ var CmdLabelUpdate = cli.Command{
}, flags.AllDefaultFlags...),
}
func runLabelUpdate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
func runLabelUpdate(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
id := ctx.Int64("id")
var pName, pColor, pDescription *string
@@ -61,13 +66,11 @@ func runLabelUpdate(_ stdctx.Context, cmd *cli.Command) error {
pDescription = &description
}
var err error
_, _, err = ctx.Login.Client().EditLabel(ctx.Owner, ctx.Repo, id, gitea.EditLabelOption{
_, _, err = ctx.Login.Client().Repositories.EditLabel(requestCtx, ctx.Owner, ctx.Repo, id, gitea.EditLabelOption{
Name: pName,
Color: pColor,
Description: pDescription,
})
if err != nil {
return err
}
+7 -4
View File
@@ -7,9 +7,9 @@ import (
"context"
"fmt"
"code.gitea.io/tea/cmd/login"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/print"
"gitea.dev/tea/cmd/login"
"gitea.dev/tea/modules/config"
"gitea.dev/tea/modules/print"
"github.com/urfave/cli/v3"
)
@@ -42,7 +42,10 @@ func runLogins(ctx context.Context, cmd *cli.Command) error {
}
func runLoginDetail(name string) error {
l := config.GetLoginByName(name)
l, err := config.GetLoginByName(name)
if err != nil {
return err
}
if l == nil {
fmt.Printf("Login '%s' do not exist\n\n", name)
return nil
+21 -11
View File
@@ -5,10 +5,11 @@ package login
import (
"context"
"fmt"
"code.gitea.io/tea/modules/auth"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task"
"gitea.dev/tea/modules/auth"
"gitea.dev/tea/modules/interact"
"gitea.dev/tea/modules/task"
"github.com/urfave/cli/v3"
)
@@ -17,7 +18,12 @@ import (
var CmdLoginAdd = cli.Command{
Name: "add",
Usage: "Add a Gitea login",
Description: `Add a Gitea login, without args it will create one interactively`,
Description: `Add a Gitea login, without args it will create one interactively.
By default tea only stores the token for its own API use. Pass --git-credentials
to also register tea as a git credential helper for the login's URL, so that
'git push' and 'git clone' over HTTPS authenticate silently using the stored
token. Equivalent to running 'tea login helper setup' afterwards.`,
ArgsUsage: " ", // command does not accept arguments
Flags: []cli.Flag{
&cli.StringFlag{
@@ -88,9 +94,9 @@ var CmdLoginAdd = cli.Command{
Usage: "Use SSH public key or SSH fingerprint to login (needs a running ssh-agent with ssh key loaded)",
},
&cli.BoolFlag{
Name: "helper",
Aliases: []string{"j"},
Usage: "Add helper",
Name: "git-credentials",
Aliases: []string{"helper", "j"},
Usage: "Register tea as a git credential helper for this login's URL, so 'git push' and 'git clone' over HTTPS authenticate silently using the stored token",
},
&cli.BoolFlag{
Name: "oauth",
@@ -109,10 +115,13 @@ var CmdLoginAdd = cli.Command{
Action: runLoginAdd,
}
func runLoginAdd(_ context.Context, cmd *cli.Command) error {
func runLoginAdd(requestCtx context.Context, cmd *cli.Command) error {
// if no args create login interactive
if cmd.NumFlags() == 0 {
return interact.CreateLogin()
if err := interact.CreateLogin(requestCtx); err != nil && !interact.IsQuitting(err) {
return fmt.Errorf("error adding login: %w", err)
}
return nil
}
// if OAuth flag is provided, use OAuth2 PKCE flow
@@ -133,7 +142,7 @@ func runLoginAdd(_ context.Context, cmd *cli.Command) error {
opts.RedirectURL = cmd.String("redirect-url")
}
return auth.OAuthLoginWithFullOptions(opts)
return auth.OAuthLoginWithFullOptions(requestCtx, opts)
}
sshAgent := false
@@ -143,6 +152,7 @@ func runLoginAdd(_ context.Context, cmd *cli.Command) error {
// else use args to add login
return task.CreateLogin(
requestCtx,
cmd.String("name"),
cmd.String("token"),
cmd.String("user"),
@@ -156,6 +166,6 @@ func runLoginAdd(_ context.Context, cmd *cli.Command) error {
cmd.Bool("insecure"),
sshAgent,
!cmd.Bool("no-version-check"),
cmd.Bool("helper"),
cmd.Bool("git-credentials"),
)
}
+3 -3
View File
@@ -7,8 +7,8 @@ import (
"context"
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/config"
"github.com/urfave/cli/v3"
)
@@ -23,7 +23,7 @@ var CmdLoginSetDefault = cli.Command{
Flags: []cli.Flag{&flags.OutputFlag},
}
func runLoginSetDefault(_ context.Context, cmd *cli.Command) error {
func runLoginSetDefault(requestCtx context.Context, cmd *cli.Command) error {
if cmd.Args().Len() == 0 {
l, err := config.GetDefaultLogin()
if err != nil {
+5 -6
View File
@@ -5,10 +5,9 @@ package login
import (
"context"
"errors"
"log"
"fmt"
"code.gitea.io/tea/modules/config"
"gitea.dev/tea/modules/config"
"github.com/urfave/cli/v3"
)
@@ -24,10 +23,10 @@ var CmdLoginDelete = cli.Command{
}
// RunLoginDelete runs the action of a login delete command
func RunLoginDelete(_ context.Context, cmd *cli.Command) error {
func RunLoginDelete(requestCtx context.Context, cmd *cli.Command) error {
logins, err := config.GetLogins()
if err != nil {
log.Fatal(err)
return err
}
var name string
@@ -37,7 +36,7 @@ func RunLoginDelete(_ context.Context, cmd *cli.Command) error {
} else if len(logins) == 1 {
name = logins[0].Name
} else {
return errors.New("Please specify a login name")
return fmt.Errorf("please specify a login name")
}
return config.DeleteLogin(name)
+12 -8
View File
@@ -5,12 +5,13 @@ package login
import (
"context"
"log"
"fmt"
"os"
"os/exec"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/config"
"gitea.dev/tea/modules/utils"
"github.com/skratchdot/open-golang/open"
"github.com/urfave/cli/v3"
@@ -27,15 +28,18 @@ var CmdLoginEdit = cli.Command{
Flags: []cli.Flag{&flags.OutputFlag},
}
func runLoginEdit(_ context.Context, _ *cli.Command) error {
func runLoginEdit(requestCtx context.Context, _ *cli.Command) error {
ymlPath := config.GetConfigPath()
if e, ok := os.LookupEnv("EDITOR"); ok && e != "" {
cmd := exec.Command(e, config.GetConfigPath())
cmd := exec.Command(e, ymlPath)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err.Error())
return cmd.Run()
}
if exist, _ := utils.FileExist(ymlPath); !exist {
fmt.Printf("Config file does not exist, please run login add first\n")
return nil
}
return open.Start(config.GetConfigPath())
return open.Start(ymlPath)
}
+55 -31
View File
@@ -7,38 +7,44 @@ import (
"bufio"
"context"
"fmt"
"log"
"net/url"
"os"
"strings"
"time"
"code.gitea.io/tea/modules/auth"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/task"
"github.com/urfave/cli/v3"
"gitea.dev/tea/modules/config"
"gitea.dev/tea/modules/task"
)
// CmdLoginHelper represents to login a gitea helper.
var CmdLoginHelper = cli.Command{
Name: "helper",
Aliases: []string{"git-credential"},
Usage: "Git helper",
Description: `Git helper`,
Hidden: true,
Usage: "Act as a git credential helper for stored Gitea logins",
Description: `Speaks git's credential helper protocol so that HTTPS push and clone
operations against your configured Gitea instances authenticate silently
using the tokens tea already stores.
Typical use is automatic: 'tea login add --git-credentials' (or 'tea login
helper setup' for existing logins) registers '!tea login helper' as a
credential helper in ~/.gitconfig. Git then invokes the 'get' subcommand
when it needs credentials for a configured host.`,
Commands: []*cli.Command{
{
Name: "store",
Description: "Command drops",
Aliases: []string{"erase"},
Action: func(_ context.Context, _ *cli.Command) error {
Usage: "No-op (git credential protocol store/erase)",
Description: "No-op invoked by git on credential store/erase; tea persists credentials only in its own config",
Action: func(requestCtx context.Context, _ *cli.Command) error {
return nil
},
},
{
Name: "setup",
Description: "Setup helper to tea authenticate",
Action: func(_ context.Context, _ *cli.Command) error {
Usage: "Register tea as a git credential helper for every configured login",
Description: "Register tea as a git credential helper in ~/.gitconfig for every configured login",
Action: func(requestCtx context.Context, _ *cli.Command) error {
logins, err := config.GetLogins()
if err != nil {
return err
@@ -58,8 +64,16 @@ var CmdLoginHelper = cli.Command{
},
{
Name: "get",
Description: "Get token to auth",
Action: func(_ context.Context, cmd *cli.Command) error {
Usage: "Return the stored token for a URL (git credential protocol)",
Description: "Return the stored token for a given URL in git's credential helper protocol (called by git, reads request from stdin)",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "login",
Aliases: []string{"l"},
Usage: "Use a specific login",
},
},
Action: func(requestCtx context.Context, cmd *cli.Command) error {
wants := map[string]string{}
s := bufio.NewScanner(os.Stdin)
for s.Scan() {
@@ -88,16 +102,35 @@ var CmdLoginHelper = cli.Command{
}
if len(wants["host"]) == 0 {
log.Fatal("Require hostname")
return fmt.Errorf("hostname is required")
} else if len(wants["protocol"]) == 0 {
wants["protocol"] = "http"
}
userConfig := config.GetLoginByHost(wants["host"])
// Use --login flag if provided, otherwise fall back to host lookup
var userConfig *config.Login
if loginName := cmd.String("login"); loginName != "" {
var lookupErr error
userConfig, lookupErr = config.GetLoginByName(loginName)
if lookupErr != nil {
return lookupErr
}
if userConfig == nil {
log.Fatal("host not exists")
} else if len(userConfig.Token) == 0 {
log.Fatal("User no set")
return fmt.Errorf("login '%s' not found", loginName)
}
} else {
var lookupErr error
userConfig, lookupErr = config.GetLoginByHost(wants["host"])
if lookupErr != nil {
return lookupErr
}
if userConfig == nil {
return fmt.Errorf("no login found for host '%s'", wants["host"])
}
}
if len(userConfig.GetAccessToken()) == 0 {
return fmt.Errorf("user not set")
}
host, err := url.Parse(userConfig.URL)
@@ -105,21 +138,12 @@ var CmdLoginHelper = cli.Command{
return err
}
if userConfig.TokenExpiry > 0 && time.Now().Unix() > userConfig.TokenExpiry {
// Token is expired, refresh it
err = auth.RefreshAccessToken(userConfig)
if err != nil {
// Refresh token if expired or near expiry (updates userConfig in place)
if err = userConfig.RefreshOAuthTokenIfNeeded(); err != nil {
return err
}
// Once token is refreshed, get the latest from the updated config
refreshedConfig := config.GetLoginByHost(wants["host"])
if refreshedConfig != nil {
userConfig = refreshedConfig
}
}
_, err = fmt.Fprintf(os.Stdout, "protocol=%s\nhost=%s\nusername=%s\npassword=%s\n", host.Scheme, host.Host, userConfig.User, userConfig.Token)
_, err = fmt.Fprintf(os.Stdout, "protocol=%s\nhost=%s\nusername=%s\npassword=%s\n", host.Scheme, host.Host, userConfig.User, userConfig.GetAccessToken())
if err != nil {
return err
}
+5 -6
View File
@@ -6,9 +6,9 @@ package login
import (
"context"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/print"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/config"
"gitea.dev/tea/modules/print"
"github.com/urfave/cli/v3"
)
@@ -25,11 +25,10 @@ var CmdLoginList = cli.Command{
}
// RunLoginList list all logins
func RunLoginList(_ context.Context, cmd *cli.Command) error {
func RunLoginList(requestCtx context.Context, cmd *cli.Command) error {
logins, err := config.GetLogins()
if err != nil {
return err
}
print.LoginsList(logins, cmd.String("output"))
return nil
return print.LoginsList(logins, cmd.String("output"))
}
+24 -12
View File
@@ -7,8 +7,8 @@ import (
"context"
"fmt"
"code.gitea.io/tea/modules/auth"
"code.gitea.io/tea/modules/config"
"gitea.dev/tea/modules/auth"
"gitea.dev/tea/modules/config"
"github.com/urfave/cli/v3"
)
@@ -17,12 +17,12 @@ import (
var CmdLoginOAuthRefresh = cli.Command{
Name: "oauth-refresh",
Usage: "Refresh an OAuth token",
Description: "Manually refresh an expired OAuth token. Usually only used when troubleshooting authentication.",
Description: "Manually refresh an expired OAuth token. If the refresh token is also expired, opens a browser for re-authentication.",
ArgsUsage: "[<login name>]",
Action: runLoginOAuthRefresh,
}
func runLoginOAuthRefresh(_ context.Context, cmd *cli.Command) error {
func runLoginOAuthRefresh(requestCtx context.Context, cmd *cli.Command) error {
var loginName string
// Get login name from args or use default
@@ -38,22 +38,34 @@ func runLoginOAuthRefresh(_ context.Context, cmd *cli.Command) error {
}
// Get the login from config
login := config.GetLoginByName(loginName)
login, err := config.GetLoginByName(loginName)
if err != nil {
return err
}
if login == nil {
return fmt.Errorf("login '%s' not found", loginName)
}
// Check if the login has a refresh token
if login.RefreshToken == "" {
if login.GetRefreshToken() == "" {
return fmt.Errorf("login '%s' does not have a refresh token. It may have been created using a different authentication method", loginName)
}
// Refresh the token
err := auth.RefreshAccessToken(login)
if err != nil {
return fmt.Errorf("failed to refresh token: %s", err)
}
// Try to refresh the token
err = auth.RefreshAccessToken(login)
if err == nil {
fmt.Printf("Successfully refreshed OAuth token for %s\n", loginName)
return nil
}
// Refresh failed - fall back to browser-based re-authentication
fmt.Printf("Token refresh failed: %s\n", err)
fmt.Println("Opening browser for re-authentication...")
if err := auth.ReauthenticateLogin(requestCtx, login); err != nil {
return fmt.Errorf("re-authentication failed: %s", err)
}
fmt.Printf("Successfully re-authenticated %s\n", loginName)
return nil
}
+1 -1
View File
@@ -4,7 +4,7 @@
package cmd
import (
"code.gitea.io/tea/cmd/login"
"gitea.dev/tea/cmd/login"
"github.com/urfave/cli/v3"
)
+64
View File
@@ -0,0 +1,64 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"fmt"
"os"
"path/filepath"
docs "github.com/urfave/cli-docs/v3"
"github.com/urfave/cli/v3"
)
// DocRenderFlags are the flags for documentation generation, used by `./docs/docs.go` and the `generate-man-page` sub command
var DocRenderFlags = []cli.Flag{
&cli.StringFlag{
Name: "out",
Usage: "Path to output docs to, otherwise prints to stdout",
Aliases: []string{"o"},
},
}
// CmdGenerateManPage is the sub command to generate the `tea` man page
var CmdGenerateManPage = cli.Command{
Name: "man",
Usage: "Generate man page",
Hidden: true,
Flags: DocRenderFlags,
Action: func(ctx context.Context, cmd *cli.Command) error {
return RenderDocs(cmd, cmd.Root(), func(cmd *cli.Command) (string, error) {
return docs.ToManWithSection(cmd, 1)
})
},
}
// RenderDocs renders the documentation for `target` using the supplied `render` function
func RenderDocs(cmd, target *cli.Command, render func(*cli.Command) (string, error)) error {
out, err := render(target)
if err != nil {
return err
}
outPath := cmd.String("out")
if outPath == "" {
fmt.Print(out)
return nil
}
if err = os.MkdirAll(filepath.Dir(outPath), os.ModePerm); err != nil {
return err
}
fi, err := os.Create(outPath)
if err != nil {
return err
}
defer fi.Close()
if _, err = fi.WriteString(out); err != nil {
return err
}
return nil
}
+13 -7
View File
@@ -6,10 +6,11 @@ package cmd
import (
stdctx "context"
"code.gitea.io/tea/cmd/milestones"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"github.com/urfave/cli/v3"
"gitea.dev/tea/cmd/milestones"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
)
// CmdMilestones represents to operate repositories milestones.
@@ -39,12 +40,17 @@ func runMilestones(ctx stdctx.Context, cmd *cli.Command) error {
return milestones.RunMilestonesList(ctx, cmd)
}
func runMilestoneDetail(_ stdctx.Context, cmd *cli.Command, name string) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
func runMilestoneDetail(requestCtx stdctx.Context, cmd *cli.Command, name string) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client()
milestone, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, name)
milestone, _, err := client.Repositories.GetMilestoneByName(requestCtx, ctx.Owner, ctx.Repo, name)
if err != nil {
return err
}
+2 -1
View File
@@ -6,8 +6,9 @@ package milestones
import (
"context"
"code.gitea.io/tea/cmd/flags"
"github.com/urfave/cli/v3"
"gitea.dev/tea/cmd/flags"
)
// CmdMilestonesClose represents a sub command of milestones to close an milestone
+17 -11
View File
@@ -8,13 +8,14 @@ import (
stdctx "context"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task"
gitea "gitea.dev/sdk"
"github.com/araddon/dateparse"
"github.com/urfave/cli/v3"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/interact"
"gitea.dev/tea/modules/task"
)
// CmdMilestonesCreate represents a sub command of milestones to create milestone
@@ -49,8 +50,11 @@ var CmdMilestonesCreate = cli.Command{
}, flags.AllDefaultFlags...),
}
func runMilestonesCreate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
func runMilestonesCreate(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
date := ctx.String("deadline")
deadline := &time.Time{}
@@ -67,12 +71,14 @@ func runMilestonesCreate(_ stdctx.Context, cmd *cli.Command) error {
state = gitea.StateClosed
}
if ctx.NumFlags() == 0 {
return interact.CreateMilestone(ctx.Login, ctx.Owner, ctx.Repo)
if ctx.IsInteractiveMode() {
if err := interact.CreateMilestone(requestCtx, ctx.Login, ctx.Owner, ctx.Repo); err != nil && !interact.IsQuitting(err) {
return err
}
return nil
}
return task.CreateMilestone(
ctx.Login,
return task.CreateMilestone(requestCtx, ctx.Login,
ctx.Owner,
ctx.Repo,
ctx.String("title"),
+11 -6
View File
@@ -6,8 +6,8 @@ package milestones
import (
stdctx "context"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"github.com/urfave/cli/v3"
)
@@ -23,11 +23,16 @@ var CmdMilestonesDelete = cli.Command{
Flags: flags.AllDefaultFlags,
}
func deleteMilestone(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
func deleteMilestone(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client()
_, err := client.DeleteMilestoneByName(ctx.Owner, ctx.Repo, ctx.Args().First())
_, err = client.Repositories.DeleteMilestoneByName(requestCtx, ctx.Owner, ctx.Repo, ctx.Args().First())
return err
}
+70 -55
View File
@@ -8,12 +8,13 @@ import (
stdctx "context"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils"
gitea "gitea.dev/sdk"
"github.com/urfave/cli/v3"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"gitea.dev/tea/modules/utils"
)
var msIssuesFieldsFlag = flags.FieldsFlag(print.IssueFields, []string{
@@ -70,40 +71,39 @@ var CmdMilestoneRemoveIssue = cli.Command{
Flags: flags.AllDefaultFlags,
}
func runMilestoneIssueList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
func runMilestoneIssueList(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client()
state := gitea.StateOpen
switch ctx.String("state") {
case "all":
state = gitea.StateAll
case "closed":
state = gitea.StateClosed
}
kind := gitea.IssueTypeAll
switch ctx.String("kind") {
case "issue":
kind = gitea.IssueTypeIssue
case "pull":
kind = gitea.IssueTypePull
}
if ctx.Args().Len() != 1 {
return fmt.Errorf("Must specify milestone name")
}
milestone := ctx.Args().First()
// make sure milestone exist
_, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, milestone)
state, err := flags.ParseState(ctx.String("state"))
if err != nil {
return err
}
issues, _, err := client.ListRepoIssues(ctx.Owner, ctx.Repo, gitea.ListIssueOption{
ListOptions: ctx.GetListOptions(),
kind, err := flags.ParseIssueKind(ctx.String("kind"), gitea.IssueTypeAll)
if err != nil {
return err
}
if ctx.Args().Len() != 1 {
return fmt.Errorf("milestone name is required")
}
milestone := ctx.Args().First()
// make sure milestone exist
_, _, err = client.Repositories.GetMilestoneByName(requestCtx, ctx.Owner, ctx.Repo, milestone)
if err != nil {
return err
}
issues, _, err := client.Issues.ListRepoIssues(requestCtx, ctx.Owner, ctx.Repo, gitea.ListIssueOption{
ListOptions: flags.GetListOptions(cmd),
Milestones: []string{milestone},
Type: kind,
State: state,
@@ -116,13 +116,17 @@ func runMilestoneIssueList(_ stdctx.Context, cmd *cli.Command) error {
if err != nil {
return err
}
print.IssuesPullsList(issues, ctx.Output, fields)
return nil
return print.IssuesPullsList(issues, ctx.Output, fields)
}
func runMilestoneIssueAdd(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
func runMilestoneIssueAdd(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
client := ctx.Login.Client()
if ctx.Args().Len() != 2 {
return fmt.Errorf("need two arguments")
@@ -136,20 +140,28 @@ func runMilestoneIssueAdd(_ stdctx.Context, cmd *cli.Command) error {
}
// make sure milestone exist
mile, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, mileName)
mile, _, err := client.Repositories.GetMilestoneByName(requestCtx, ctx.Owner, ctx.Repo, mileName)
if err != nil {
return fmt.Errorf("failed to get milestone '%s': %w", mileName, err)
}
_, _, err = client.Issues.EditIssue(requestCtx, ctx.Owner, ctx.Repo, idx, gitea.EditIssueOption{
Milestone: &mile.ID,
})
if err != nil {
return fmt.Errorf("failed to add issue #%d to milestone '%s': %w", idx, mileName, err)
}
return nil
}
func runMilestoneIssueRemove(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
_, _, err = client.EditIssue(ctx.Owner, ctx.Repo, idx, gitea.EditIssueOption{
Milestone: &mile.ID,
})
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
func runMilestoneIssueRemove(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
}
client := ctx.Login.Client()
if ctx.Args().Len() != 2 {
return fmt.Errorf("need two arguments")
@@ -159,25 +171,28 @@ func runMilestoneIssueRemove(_ stdctx.Context, cmd *cli.Command) error {
issueIndex := ctx.Args().Get(1)
idx, err := utils.ArgToIndex(issueIndex)
if err != nil {
return err
return fmt.Errorf("invalid issue index '%s': %w", issueIndex, err)
}
issue, _, err := client.GetIssue(ctx.Owner, ctx.Repo, idx)
issue, _, err := client.Issues.GetIssue(requestCtx, ctx.Owner, ctx.Repo, idx)
if err != nil {
return err
return fmt.Errorf("failed to get issue #%d: %w", idx, err)
}
if issue.Milestone == nil {
return fmt.Errorf("issue is not assigned to a milestone")
return fmt.Errorf("issue #%d is not assigned to a milestone", idx)
}
if issue.Milestone.Title != mileName {
return fmt.Errorf("issue is not assigned to this milestone")
return fmt.Errorf("issue #%d is assigned to milestone '%s', not '%s'", idx, issue.Milestone.Title, mileName)
}
zero := int64(0)
_, _, err = client.EditIssue(ctx.Owner, ctx.Repo, idx, gitea.EditIssueOption{
_, _, err = client.Issues.EditIssue(requestCtx, ctx.Owner, ctx.Repo, idx, gitea.EditIssueOption{
Milestone: &zero,
})
return err
if err != nil {
return fmt.Errorf("failed to remove issue #%d from milestone '%s': %w", idx, mileName, err)
}
return nil
}

Some files were not shown because too many files have changed in this diff Show More