Compare commits

...
10 Commits
Author SHA1 Message Date
magerbeton e0bdf2f57b fix(git): tolerate remotes without a url key
goreleaser / goreleaser (push) Failing after 1m9s
goreleaser / release-image (push) Failing after 2m46s
goreleaser / goreleaser (push) Canceled after 1s
goreleaser / release-image (push) Failing after 14s
git-annex S3 special remotes have no url key; running
`git config --get-all remote.<name>.url` on them exits 1, which made
tea hard-fail in Config() on any repo that has such a remote. Treat a
missing key (exit 1 with empty or 'No such section or key' stderr) as an
empty URL list, mirroring the existing branch-section handling.
2026-08-23 16:07:55 +02:00
Bo-Yi Wu f34697c5ed chore(config): replace authgate SDK with signet (#1081)
## Summary

- Replace `github.com/go-authgate/sdk-go` with `github.com/go-signet/sdk-go v1.1.0`.
- Update the credential-store import while retaining the existing `credstore` API and OAuth token persistence
behavior.

## Related issues

- GitHub/Gitea: fixed https://gitea.com/gitea/tea/issues/1058

Reviewed-on: https://gitea.com/gitea/tea/pulls/1081
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-08-02 14:38:25 +00:00
Ross Golder a613a344de fix(test): disable gpg signing in worktree test repo (#1072)
Fixes #1071

`TestRepoFromPath_Worktree` creates a throwaway temp repo and commits to it. On machines with `commit.gpgsign=true` in global git config, the commit fails with `No secret key`.

Override the global setting by setting `commit.gpgsign=false` in the temp repo's local config so the test is environment-independent.

Reviewed-on: https://gitea.com/gitea/tea/pulls/1072
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Ross Golder <ross@golder.org>
2026-07-30 00:29:29 +00:00
Renovate Bot 6435b12202 chore(deps): pin dependencies (#1064)
chore(deps): pin dependencies (gitea/tea#1064)

Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
2026-07-27 22:02:00 +00:00
Lunny Xiao 61b8536e4a ci(goreleaser): mirror release artifacts to Cloudflare R2 (#1063)
Ports the Cloudflare R2 release mirror from [gitea.com/gitea/runner](https://gitea.com/gitea/runner) to `tea`, so release artifacts land in R2 alongside S3 for the duration of the migration away from S3.

Two commits, meant to be reviewed in order.

## 1. `ci(goreleaser): migrate release config to goreleaser v2`

A purely mechanical migration, no behaviour change intended:

- add `version: 2`
- `blobs.folder` -> `blobs.directory`
- `snapshot.name_template` -> `snapshot.version_template`
- `nightly.name_template` -> `nightly.version_template`
- `version: "~> v1"` -> `"~> v2"` in both release workflows

This is a prefactor rather than scope creep. Under goreleaser v1 the custom-publisher pipe runs *4th*, before the `release` pipe; under v2 it runs *last*. That ordering difference matters for the change below: on v1 a failed R2 upload would abort the publish after every artifact had already gone to S3 but **before** the Gitea release was created, leaving a half-finished release. On v2 the R2 mirror runs after the release exists, which matches the behaviour the runner repo already has in production.

The pre-existing `archives.format` deprecation warning is deliberately left alone; it is orthogonal to this change and the runner repo has not addressed it either.

## 2. `ci(goreleaser): mirror release artifacts to Cloudflare R2`

- **`scripts/upload-r2.sh`** — uploads one local file to one R2 object key using curl's built-in AWS SigV4 signer (R2 is S3-API compatible). Credentials are fed through `curl --config -` so they never appear in `ps` output. Also provides a `--check-config` preflight mode. This file is byte-identical to the runner repo's copy.
- **`.goreleaser.yaml`** — a `publishers:` entry mirroring the existing S3 `blobs:` upload into R2. A second `blobs:` entry is not usable here: the blob pipe authenticates from the global `AWS_*` environment and has no per-entry credentials, whereas `publishers:` supports per-entry `env:`.
- **Both release workflows** — forward the R2 secrets, plus an early `check R2 configuration` step. Custom publishers run as the very last step of goreleaser's publish pipeline, so without a preflight a missing secret would only surface after the release had been created and every artifact already uploaded to S3.

### Deviation from the runner implementation

The publisher here also sets `signature: true` in addition to `checksum: true`. `tea` has a `signs:` block that GPG-signs the checksum file, and the S3 blob pipe uploads the resulting `checksums.txt.sig`; without `signature: true` the R2 mirror would carry the artifacts and their checksums but no signature to verify them against.

The object key prefix is `tea/{{ .Version }}/...`, matching the existing S3 `directory: "tea/{{.Version}}"`.

## Required repository secrets

This PR is inert until these are configured. The preflight step will fail the release loudly if they are missing:

- `R2_ENDPOINT` — e.g. `https://<account>.r2.cloudflarestorage.com`
- `R2_BUCKET`
- `R2_ACCESS_KEY_ID`
- `R2_SECRET_ACCESS_KEY`

## Verification

- `goreleaser check` against the migrated config (with the pro-only `nightly:` block temporarily stripped, since the check ran with the OSS v2 binary): *configuration is valid*, the only deprecation being the pre-existing `archives.format`.
- A real `goreleaser build --snapshot --clean --single-target` against the v2 config: succeeded, including the `xz` and `.goreleaser.checksum.sh` post-hooks.
- `scripts/upload-r2.sh`: clean under `sh -n` and `shellcheck`; all four `--check-config` cases exercised (all vars unset, one missing, all set, wrong argument count).
- The actual upload path was not exercised end to end, since that needs live R2 credentials.

Reviewed-on: https://gitea.com/gitea/tea/pulls/1063
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
2026-07-26 03:21:03 +00:00
73b6bf3e23 fix(context): clarify the fallback login prompt wording (#1061)
The prompt shown when no login matches the current repository reads:

```
NOTE: no gitea login detected, whether falling back to login 'X'?
```

Two problems, both raised by @magistra-aria in #817:

- **"whether"** is a conjunction that needs two stated alternatives, so it doesn't parse in front of a yes/no confirm.
- **"no gitea login detected"** is misleading. The condition is that no *configured login matched this repository's remote* — not that a Gitea instance is missing. Read literally it suggests the CLI expects gitea.com specifically, which is how at least one user (me) first misread it.

Reworded to say what actually happened, for both the interactive prompt and its non-interactive counterpart:

```
NOTE: no login matched this repository. Fall back to login 'X'?
```

Strings only, no logic change.

Refs #817

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zach Winter <contact@zachwinter.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/1061
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zach Winter <222839+zachwinter@noreply.gitea.com>
2026-07-26 01:38:10 +00:00
Lunny Xiao 993eb37b57 Fix notifications --mine outside git repositories (#1056)
Fixes #1055.

## Root cause
`tea notifications --mine` still initialized full repository context before checking the global notification scope, so it probed the current working directory with git and could select or fail on repository-derived context even though repository data is not needed.

## Changes
- Add an InitCommand option to skip local git repository discovery when a command does not need repository context.
- Use that option for notification list and mark-as operations when `--mine` is set.
- Add a regression test that makes `git` fail if invoked and verifies `notifications --mine` still uses the global notifications API.

## Tests
- `go test ./cmd/notifications ./modules/context`

Reviewed-on: https://gitea.com/gitea/tea/pulls/1056
2026-07-26 01:37:33 +00:00
Zach WinterandZach Winter cd93d8561b ci: wait for the gitea service to be ready before integration tests (#1062)
The `Integration Test` job is currently failing repo-wide, on unrelated PRs and on `main`:

```
curl: (7) Failed to connect to gitea port 3000 after 7 ms
```

It fails at the health check, before any Go test runs. Recent examples: #1056 (7/18), #1059, #1060, #1061.

### Cause

The health check was a single bare curl fired as soon as `setup-go` finished — there was never a readiness wait. The job has been depending on gitea binding port 3000 within however long `checkout` + `setup-go` happened to take. Timings from the job logs, container start → curl:

| run | gap | result |
|---|---|---|
| #1021 (2026-06-21) | 7.4s | pass |
| #1056 (2026-07-18) | 6.3s | fail |
| #1060 (2026-07-25) | 6.3s | fail |

Two failures a week apart with an identical gap, and curl giving up in single-digit **milliseconds** with nothing listening, points at the service still starting rather than crashing.

### What I could not determine

Two things landed in the same window and I can't separate them from outside: the service image bump 1.26.2 → 1.27.0 (d664c01) and the hosted runner fleet going v1.0.3 → v2.0.0 (visible in the log header). The service container's own stdout isn't exposed by the job log endpoint, so **"still running migrations" is inferred from timing, not observed.** Happy to be corrected by anyone who can see the container logs.

### Why this fix regardless

The missing readiness wait is the actual bug class, independent of what shifted the timing. If the service is merely slower to boot, this fixes it permanently. If it is genuinely broken, this converts a cryptic 7ms connection refusal into an explicit 60s timeout with a clear error.

Locally I verified the YAML parses and exercised both loop paths in a shell (success exits immediately; exhaustion emits `::error::` and exits 1). I can't run Gitea Actions locally, so this workflow's first real execution is the CI run on this PR — a passing `Integration Test` here is the fix demonstrating itself.

---------

Co-authored-by: Zach Winter <contact@zachwinter.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/1062
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zach Winter <222839+zachwinter@noreply.gitea.com>
2026-07-25 04:47:09 +00:00
Minjie Fang d664c01e18 feat(assignees): add set, add, and remove assignees APIs (#1045)
Implemented set, add, and remove assignees APIs.
Closes https://gitea.com/gitea/tea/issues/965 and https://gitea.com/gitea/tea/issues/966Reviewed-on: https://gitea.com/gitea/tea/pulls/1045
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Minjie Fang <wingsallen@gmail.com>
2026-07-18 00:20:03 +00:00
Renovate Bot 2d6dcd062f fix(deps): update go dependencies (#1057)
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.53.0` → `v0.54.0`](https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.53.0...refs/tags/v0.54.0) | ![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2fcrypto/v0.54.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2fcrypto/v0.53.0/v0.54.0?slim=true) |
| [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) | [`v0.44.0` → `v0.45.0`](https://cs.opensource.google/go/x/term/+/refs/tags/v0.44.0...refs/tags/v0.45.0) | ![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2fterm/v0.45.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2fterm/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.

👻 **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/1057
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
2026-07-16 13:18:16 +00:00
24 changed files with 788 additions and 104 deletions
+26 -10
View File
@@ -8,16 +8,28 @@ jobs:
goreleaser: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with: with:
fetch-depth: 0 fetch-depth: 0
- run: git fetch --force --tags - run: git fetch --force --tags
- uses: actions/setup-go@v6 # Custom publishers (the R2 mirror below) run as the very last
# step of goreleaser's publish pipeline, after the Gitea release
# has already been created and every artifact already uploaded
# to S3. Fail here instead, before anything is built or
# published, if the R2 secrets are missing.
- name: check R2 configuration
run: sh scripts/upload-r2.sh --check-config
env:
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with: with:
go-version-file: "go.mod" go-version-file: "go.mod"
- name: import gpg - name: import gpg
id: import_gpg id: import_gpg
uses: crazy-max/ghaction-import-gpg@v7 uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
with: with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }} gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
@@ -25,10 +37,10 @@ jobs:
id: sdk_version id: sdk_version
run: echo "version=$(go list -f '{{.Version}}' -m gitea.dev/sdk)" >> "$GITHUB_OUTPUT" run: echo "version=$(go list -f '{{.Version}}' -m gitea.dev/sdk)" >> "$GITHUB_OUTPUT"
- name: goreleaser - name: goreleaser
uses: goreleaser/goreleaser-action@v7 uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7
with: with:
distribution: goreleaser-pro distribution: goreleaser-pro
version: "~> v1" version: "~> v2"
args: release --nightly args: release --nightly
env: env:
SDK_VERSION: ${{ steps.sdk_version.outputs.version }} SDK_VERSION: ${{ steps.sdk_version.outputs.version }}
@@ -38,6 +50,10 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
S3_REGION: ${{ secrets.AWS_REGION }} S3_REGION: ${{ secrets.AWS_REGION }}
S3_BUCKET: ${{ secrets.AWS_BUCKET }} S3_BUCKET: ${{ secrets.AWS_BUCKET }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
GORELEASER_FORCE_TOKEN: 'gitea' GORELEASER_FORCE_TOKEN: 'gitea'
GPGSIGN_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }} GPGSIGN_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
@@ -49,24 +65,24 @@ jobs:
DOCKER_LATEST: nightly DOCKER_LATEST: nightly
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v7 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with: with:
fetch-depth: 0 # all history for all branches and tags fetch-depth: 0 # all history for all branches and tags
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v4 uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
- name: Set up Docker BuildX - name: Set up Docker BuildX
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v4 uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v7 uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
env: env:
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119 ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
with: with:
+26 -10
View File
@@ -9,16 +9,28 @@ jobs:
goreleaser: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with: with:
fetch-depth: 0 fetch-depth: 0
- run: git fetch --force --tags - run: git fetch --force --tags
- uses: actions/setup-go@v6 # Custom publishers (the R2 mirror below) run as the very last
# step of goreleaser's publish pipeline, after the Gitea release
# has already been created and every artifact already uploaded
# to S3. Fail here instead, before anything is built or
# published, if the R2 secrets are missing.
- name: check R2 configuration
run: sh scripts/upload-r2.sh --check-config
env:
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with: with:
go-version-file: 'go.mod' go-version-file: 'go.mod'
- name: import gpg - name: import gpg
id: import_gpg id: import_gpg
uses: crazy-max/ghaction-import-gpg@v7 uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
with: with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }} gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
@@ -26,10 +38,10 @@ jobs:
id: sdk_version id: sdk_version
run: echo "version=$(go list -f '{{.Version}}' -m gitea.dev/sdk)" >> "$GITHUB_OUTPUT" run: echo "version=$(go list -f '{{.Version}}' -m gitea.dev/sdk)" >> "$GITHUB_OUTPUT"
- name: goreleaser - name: goreleaser
uses: goreleaser/goreleaser-action@v7 uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7
with: with:
distribution: goreleaser-pro distribution: goreleaser-pro
version: "~> v1" version: "~> v2"
args: release args: release
env: env:
SDK_VERSION: ${{ steps.sdk_version.outputs.version }} SDK_VERSION: ${{ steps.sdk_version.outputs.version }}
@@ -39,6 +51,10 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
S3_REGION: ${{ secrets.AWS_REGION }} S3_REGION: ${{ secrets.AWS_REGION }}
S3_BUCKET: ${{ secrets.AWS_BUCKET }} S3_BUCKET: ${{ secrets.AWS_BUCKET }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
GORELEASER_FORCE_TOKEN: 'gitea' GORELEASER_FORCE_TOKEN: 'gitea'
GPGSIGN_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }} GPGSIGN_PASSPHRASE: ${{ secrets.GPGSIGN_PASSPHRASE }}
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
@@ -50,18 +66,18 @@ jobs:
DOCKER_LATEST: nightly DOCKER_LATEST: nightly
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v7 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with: with:
fetch-depth: 0 # all history for all branches and tags fetch-depth: 0 # all history for all branches and tags
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v4 uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
- name: Set up Docker BuildX - name: Set up Docker BuildX
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v4 uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
@@ -71,7 +87,7 @@ jobs:
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Build and push - name: Build and push
uses: docker/build-push-action@v7 uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
env: env:
ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119 ACTIONS_RUNTIME_TOKEN: '' # See https://gitea.com/gitea/act_runner/issues/119
with: with:
+17 -6
View File
@@ -16,8 +16,8 @@ jobs:
name: Lint Build And Unit Coverage name: Lint Build And Unit Coverage
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with: with:
go-version-file: 'go.mod' go-version-file: 'go.mod'
- name: lint and build - name: lint and build
@@ -41,17 +41,28 @@ jobs:
GITEA_TEA_TEST_USERNAME: "test01" GITEA_TEA_TEST_USERNAME: "test01"
GITEA_TEA_TEST_PASSWORD: "test01" GITEA_TEA_TEST_PASSWORD: "test01"
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-go@v6 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with: with:
go-version-file: 'go.mod' go-version-file: 'go.mod'
- run: curl --noproxy "*" http://gitea:3000/api/v1/version # verify connection to instance - name: wait for the gitea instance to be ready
run: |
for i in $(seq 1 30); do
if curl --noproxy "*" -sf http://gitea:3000/api/v1/version; then
echo "gitea is ready after ${i} attempt(s)"
exit 0
fi
echo "waiting for gitea, attempt ${i}/30"
sleep 2
done
echo "::error::gitea did not become ready within 60s"
exit 1
- name: integration test - name: integration test
run: | run: |
make integration-test make integration-test
services: services:
gitea: gitea:
image: docker.gitea.com/gitea:1.26.2 image: docker.gitea.com/gitea:1.27.0
cmd: cmd:
- bash - bash
- -c - -c
+44 -3
View File
@@ -1,3 +1,5 @@
version: 2
before: before:
hooks: hooks:
- go mod tidy - go mod tidy
@@ -79,11 +81,50 @@ blobs:
provider: s3 provider: s3
bucket: "{{ .Env.S3_BUCKET }}" bucket: "{{ .Env.S3_BUCKET }}"
region: "{{ .Env.S3_REGION }}" region: "{{ .Env.S3_REGION }}"
folder: "tea/{{.Version}}" directory: "tea/{{.Version}}"
extra_files: extra_files:
- glob: ./**.xz - glob: ./**.xz
- glob: ./**.sha256 - glob: ./**.sha256
# Mirrors the S3 `blobs:` upload above into Cloudflare R2 during the
# parallel S3+R2 period (S3 will be removed once migration completes).
# A second `blobs:` entry is impossible here since the blob pipe
# authenticates from the global AWS_* env with no per-entry
# credentials; `publishers:` supports per-entry `env:` instead, so
# it's used to invoke scripts/upload-r2.sh once per artifact. Custom
# publishers inherit almost nothing from the environment, hence the
# explicit R2_* forwarding below.
#
# This publisher fires more than once per distinct key because
# goreleaser's release pipe already registers `release.extra_files`
# (./**.xz and ./**.xz.sha256, see the `release:` block below) as
# UploadableFile artifacts, and `internal/exec`'s filterArtifacts
# appends this block's own extra_files with no de-duplication. It
# can't be globbed away, since gobwas/glob (via goreleaser/fileglob)
# has no substring-exclusion matcher. It's harmless: PUT is
# idempotent, and the `./**.xz` glob below is kept deliberately so
# this publisher declares its own complete file set rather than
# implicitly depending on the `release:` block's globs.
#
# checksum: true mirrors goreleaser's generated checksums.txt;
# signature: true additionally mirrors checksums.txt.sig, which the
# `signs:` block below produces by GPG-signing that checksum file.
# Without signature: true, artifacts downloaded from the R2 mirror
# would have no signature file to verify against.
publishers:
- name: cloudflare-r2
checksum: true
signature: true
extra_files:
- glob: ./**.xz
- glob: ./**.sha256
cmd: sh scripts/upload-r2.sh {{ abs .ArtifactPath }} tea/{{ .Version }}/{{ .ArtifactName }}
env:
- R2_ENDPOINT={{ index .Env "R2_ENDPOINT" }}
- R2_BUCKET={{ index .Env "R2_BUCKET" }}
- R2_ACCESS_KEY_ID={{ index .Env "R2_ACCESS_KEY_ID" }}
- R2_SECRET_ACCESS_KEY={{ index .Env "R2_SECRET_ACCESS_KEY" }}
archives: archives:
- format: binary - format: binary
name_template: "{{ .Binary }}" name_template: "{{ .Binary }}"
@@ -104,10 +145,10 @@ signs:
args: ["--batch", "-u", "{{ .Env.GPG_FINGERPRINT }}", "--output", "${signature}", "--detach-sign", "${artifact}"] args: ["--batch", "-u", "{{ .Env.GPG_FINGERPRINT }}", "--output", "${signature}", "--detach-sign", "${artifact}"]
snapshot: snapshot:
name_template: "{{ .Branch }}-devel" version_template: "{{ .Branch }}-devel"
nightly: nightly:
name_template: "{{ .Branch }}" version_template: "{{ .Branch }}"
gitea_urls: gitea_urls:
api: https://gitea.com/api/v1 api: https://gitea.com/api/v1
+20 -4
View File
@@ -177,10 +177,18 @@ func GetIssuePRCreateFlags(requestCtx stdctx.Context, ctx *context.TeaContext) (
// IssuePREditFlags defines flags for editing properties of issues and PRs // IssuePREditFlags defines flags for editing properties of issues and PRs
var IssuePREditFlags = append([]cli.Flag{ var IssuePREditFlags = append([]cli.Flag{
&cli.StringFlag{
Name: "set-assignees",
Usage: "Clear all existing assignees and assign comma-separated list of usernames. Takes precedence over --add-assignees and --remove-assignees",
},
&cli.StringFlag{ &cli.StringFlag{
Name: "add-assignees", Name: "add-assignees",
Aliases: []string{"a"}, Aliases: []string{"a"},
Usage: "Comma-separated list of usernames to assign", Usage: "Comma-separated list of usernames to assign. Takes precedence over --remove-assignees",
},
&cli.StringFlag{
Name: "remove-assignees",
Usage: "Comma-separated list of usernames to remove",
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "add-labels", Name: "add-labels",
@@ -224,17 +232,25 @@ func GetIssuePREditFlags(ctx *context.TeaContext) (*task.EditIssueOption, error)
opts.Deadline = &t opts.Deadline = &t
} }
} }
if ctx.IsSet("set-assignees") {
val := ctx.String("set-assignees")
opts.SetAssignees = strings.Split(strings.ReplaceAll(val, " ", ""), ",")
}
if ctx.IsSet("add-assignees") { if ctx.IsSet("add-assignees") {
val := ctx.String("add-assignees") val := ctx.String("add-assignees")
opts.AddAssignees = strings.Split(val, ",") opts.AddAssignees = strings.Split(strings.ReplaceAll(val, " ", ""), ",")
}
if ctx.IsSet("remove-assignees") {
val := ctx.String("remove-assignees")
opts.RemoveAssignees = strings.Split(strings.ReplaceAll(val, " ", ""), ",")
} }
if ctx.IsSet("add-labels") { if ctx.IsSet("add-labels") {
val := ctx.String("add-labels") val := ctx.String("add-labels")
opts.AddLabels = strings.Split(val, ",") opts.AddLabels = strings.Split(strings.ReplaceAll(val, " ", ""), ",")
} }
if ctx.IsSet("remove-labels") { if ctx.IsSet("remove-labels") {
val := ctx.String("remove-labels") val := ctx.String("remove-labels")
opts.RemoveLabels = strings.Split(val, ",") opts.RemoveLabels = strings.Split(strings.ReplaceAll(val, " ", ""), ",")
} }
return &opts, nil return &opts, nil
} }
+2 -2
View File
@@ -63,12 +63,12 @@ func listNotifications(requestCtx stdctx.Context, cmd *cli.Command, status []git
var news []*gitea.NotificationThread var news []*gitea.NotificationThread
var err error var err error
ctx, err := context.InitCommand(cmd) all := cmd.Bool("mine")
ctx, err := context.InitCommandWithOptions(cmd, context.InitOptions{SkipLocalRepo: all})
if err != nil { if err != nil {
return err return err
} }
client := ctx.Login.Client() client := ctx.Login.Client()
all := ctx.Bool("mine")
// This enforces pagination (see https://github.com/go-gitea/gitea/issues/16733) // This enforces pagination (see https://github.com/go-gitea/gitea/issues/16733)
listOpts := flags.GetListOptions(cmd) listOpts := flags.GetListOptions(cmd)
+55
View File
@@ -0,0 +1,55 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package notifications
import (
stdctx "context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"testing"
"gitea.dev/tea/modules/config"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
)
func TestRunNotificationsListMineDoesNotProbeGitRepository(t *testing.T) {
gitPath := filepath.Join(t.TempDir(), "git")
gitScript := "#!/bin/sh\necho 'git should not be called' >&2\nexit 1\n"
if runtime.GOOS == "windows" {
gitPath += ".bat"
gitScript = "@echo git should not be called 1>&2\r\nexit /b 1\r\n"
}
require.NoError(t, os.WriteFile(gitPath, []byte(gitScript), 0o755))
t.Setenv("PATH", filepath.Dir(gitPath))
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/api/v1/notifications", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[]`))
}))
defer server.Close()
config.SetConfigForTesting(config.LocalConfig{
Logins: []config.Login{{
Name: "default",
URL: server.URL,
Token: "token",
User: "user",
Default: true,
}},
})
cmd := cli.Command{
Name: CmdNotificationsList.Name,
Flags: CmdNotificationsList.Flags,
}
require.NoError(t, cmd.Set("mine", "true"))
require.NoError(t, cmd.Set("output", "json"))
require.NoError(t, RunNotificationsList(stdctx.Background(), &cmd))
}
+4 -4
View File
@@ -24,7 +24,7 @@ var CmdNotificationsMarkRead = cli.Command{
ArgsUsage: "[all | <notification id>]", ArgsUsage: "[all | <notification id>]",
Flags: flags.NotificationFlags, Flags: flags.NotificationFlags,
Action: func(requestCtx stdctx.Context, cmd *cli.Command) error { Action: func(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd) ctx, err := context.InitCommandWithOptions(cmd, context.InitOptions{SkipLocalRepo: cmd.Bool("mine")})
if err != nil { if err != nil {
return err return err
} }
@@ -48,7 +48,7 @@ var CmdNotificationsMarkUnread = cli.Command{
ArgsUsage: "[all | <notification id>]", ArgsUsage: "[all | <notification id>]",
Flags: flags.NotificationFlags, Flags: flags.NotificationFlags,
Action: func(requestCtx stdctx.Context, cmd *cli.Command) error { Action: func(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd) ctx, err := context.InitCommandWithOptions(cmd, context.InitOptions{SkipLocalRepo: cmd.Bool("mine")})
if err != nil { if err != nil {
return err return err
} }
@@ -72,7 +72,7 @@ var CmdNotificationsMarkPinned = cli.Command{
ArgsUsage: "[all | <notification id>]", ArgsUsage: "[all | <notification id>]",
Flags: flags.NotificationFlags, Flags: flags.NotificationFlags,
Action: func(requestCtx stdctx.Context, cmd *cli.Command) error { Action: func(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd) ctx, err := context.InitCommandWithOptions(cmd, context.InitOptions{SkipLocalRepo: cmd.Bool("mine")})
if err != nil { if err != nil {
return err return err
} }
@@ -95,7 +95,7 @@ var CmdNotificationsUnpin = cli.Command{
ArgsUsage: "[all | <notification id>]", ArgsUsage: "[all | <notification id>]",
Flags: flags.NotificationFlags, Flags: flags.NotificationFlags,
Action: func(requestCtx stdctx.Context, cmd *cli.Command) error { Action: func(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd) ctx, err := context.InitCommandWithOptions(cmd, context.InitOptions{SkipLocalRepo: cmd.Bool("mine")})
if err != nil { if err != nil {
return err return err
} }
+10 -2
View File
@@ -239,7 +239,7 @@ Create an issue on repository
Edit one or more issues Edit one or more issues
**--add-assignees, -a**="": Comma-separated list of usernames to assign **--add-assignees, -a**="": Comma-separated list of usernames to assign. Takes precedence over --remove-assignees
**--add-labels, -L**="": Comma-separated list of labels to assign. Takes precedence over --remove-labels **--add-labels, -L**="": Comma-separated list of labels to assign. Takes precedence over --remove-labels
@@ -255,10 +255,14 @@ Edit one or more issues
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
**--remove-assignees**="": Comma-separated list of usernames to remove
**--remove-labels**="": Comma-separated list of labels to remove **--remove-labels**="": Comma-separated list of labels to remove
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional **--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
**--set-assignees**="": Clear all existing assignees and assign comma-separated list of usernames. Takes precedence over --add-assignees and --remove-assignees
**--title, -t**="": **--title, -t**="":
### reopen, open ### reopen, open
@@ -423,7 +427,7 @@ Change state of one or more pull requests to 'open'
Edit one or more pull requests Edit one or more pull requests
**--add-assignees, -a**="": Comma-separated list of usernames to assign **--add-assignees, -a**="": Comma-separated list of usernames to assign. Takes precedence over --remove-assignees
**--add-labels, -L**="": Comma-separated list of labels to assign. Takes precedence over --remove-labels **--add-labels, -L**="": Comma-separated list of labels to assign. Takes precedence over --remove-labels
@@ -445,12 +449,16 @@ Edit one or more pull requests
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
**--remove-assignees**="": Comma-separated list of usernames to remove
**--remove-labels**="": Comma-separated list of labels to remove **--remove-labels**="": Comma-separated list of labels to remove
**--remove-reviewers**="": Comma-separated list of usernames to remove from reviewers **--remove-reviewers**="": Comma-separated list of usernames to remove from reviewers
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional **--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
**--set-assignees**="": Clear all existing assignees and assign comma-separated list of usernames. Takes precedence over --add-assignees and --remove-assignees
**--title, -t**="": **--title, -t**="":
### review ### review
+7 -7
View File
@@ -12,17 +12,17 @@ require (
github.com/adrg/xdg v0.5.3 github.com/adrg/xdg v0.5.3
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/enescakir/emoji v1.0.0 github.com/enescakir/emoji v1.0.0
github.com/go-authgate/sdk-go v0.14.0 github.com/go-signet/sdk-go v1.1.0
github.com/muesli/termenv v0.16.0 github.com/muesli/termenv v0.16.0
github.com/olekukonko/tablewriter v1.1.4 github.com/olekukonko/tablewriter v1.1.4
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/urfave/cli-docs/v3 v3.1.0 github.com/urfave/cli-docs/v3 v3.1.0
github.com/urfave/cli/v3 v3.10.1 github.com/urfave/cli/v3 v3.10.1
golang.org/x/crypto v0.53.0 golang.org/x/crypto v0.54.0
golang.org/x/oauth2 v0.36.0 golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.47.0 golang.org/x/sys v0.47.0
golang.org/x/term v0.44.0 golang.org/x/term v0.45.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@@ -76,10 +76,10 @@ require (
github.com/yuin/goldmark-emoji v1.0.6 // indirect github.com/yuin/goldmark-emoji v1.0.6 // indirect
github.com/zalando/go-keyring v0.2.8 // indirect github.com/zalando/go-keyring v0.2.8 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.55.0 // indirect golang.org/x/net v0.56.0 // indirect
golang.org/x/sync v0.21.0 // indirect golang.org/x/sync v0.22.0 // indirect
golang.org/x/text v0.38.0 // indirect golang.org/x/text v0.40.0 // indirect
golang.org/x/tools v0.45.0 // indirect golang.org/x/tools v0.47.0 // indirect
) )
retract v1.3.3 // accidental release, tag deleted retract v1.3.3 // accidental release, tag deleted
+16 -16
View File
@@ -89,8 +89,8 @@ github.com/enescakir/emoji v1.0.0 h1:W+HsNql8swfCQFtioDGDHCHri8nudlK1n5p2rHCJoog
github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkKp+WKFD0= github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkKp+WKFD0=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/go-authgate/sdk-go v0.14.0 h1:s1i/UCX2Edf3A1pKDW6oXv+oACQfTroxiGY52eqKx+4= github.com/go-signet/sdk-go v1.1.0 h1:wHKg9P+goQ14A1Q0gtC6m3mCzRFWwL1peAGz/zhmAZQ=
github.com/go-authgate/sdk-go v0.14.0/go.mod h1:sa0ige5wtayj2WcnXlxa8wGuyi5z/c/chc0mXPJTl/Q= github.com/go-signet/sdk-go v1.1.0/go.mod h1:bmi7nDAu7o6MQnUE3K7ZNEKU4xqh3u/SMbPC5GanOR8=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
@@ -162,42 +162,42 @@ github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cma
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"time" "time"
"github.com/adrg/xdg" "github.com/adrg/xdg"
"github.com/go-authgate/sdk-go/credstore" "github.com/go-signet/sdk-go/credstore"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
+34 -18
View File
@@ -37,6 +37,12 @@ type TeaContext struct {
LocalRepo *git.TeaRepo // is set if flags specified a local repo via --repo, or if $PWD is a git repo LocalRepo *git.TeaRepo // is set if flags specified a local repo via --repo, or if $PWD is a git repo
} }
// InitOptions controls which optional sources InitCommand may inspect.
type InitOptions struct {
// SkipLocalRepo avoids probing the current directory for a git repository.
SkipLocalRepo bool
}
// GetRemoteRepoHTMLURL returns the web-ui url of the remote repo, // GetRemoteRepoHTMLURL returns the web-ui url of the remote repo,
// after ensuring a remote repo is present in the context. // after ensuring a remote repo is present in the context.
func (ctx *TeaContext) GetRemoteRepoHTMLURL() (string, error) { func (ctx *TeaContext) GetRemoteRepoHTMLURL() (string, error) {
@@ -61,6 +67,12 @@ func shouldPromptFallbackLogin(login *config.Login, canPrompt bool) bool {
// the remotes of the .git repo specified in repoFlag or $PWD, and using overrides from // the remotes of the .git repo specified in repoFlag or $PWD, and using overrides from
// command flags. If a local git repo can't be found, repo slug values are unset. // command flags. If a local git repo can't be found, repo slug values are unset.
func InitCommand(cmd *cli.Command) (*TeaContext, error) { func InitCommand(cmd *cli.Command) (*TeaContext, error) {
return InitCommandWithOptions(cmd, InitOptions{})
}
// InitCommandWithOptions resolves the application context like InitCommand, with
// optional controls for commands that do not need repository context.
func InitCommandWithOptions(cmd *cli.Command, opts InitOptions) (*TeaContext, error) {
// these flags are used as overrides to the context detection via local git repo // these flags are used as overrides to the context detection via local git repo
repoFlag := cmd.String("repo") repoFlag := cmd.String("repo")
loginFlag := cmd.String("login") loginFlag := cmd.String("login")
@@ -76,7 +88,7 @@ func InitCommand(cmd *cli.Command) (*TeaContext, error) {
) )
// check if repoFlag can be interpreted as path to local repo. // check if repoFlag can be interpreted as path to local repo.
if len(repoFlag) != 0 { if len(repoFlag) != 0 && !opts.SkipLocalRepo {
if repoFlagPathExists, err = utils.DirExists(repoFlag); err != nil { if repoFlagPathExists, err = utils.DirExists(repoFlag); err != nil {
return nil, err return nil, err
} }
@@ -85,6 +97,8 @@ func InitCommand(cmd *cli.Command) (*TeaContext, error) {
} else { } else {
c.RepoSlug = repoFlag c.RepoSlug = repoFlag
} }
} else if len(repoFlag) != 0 {
c.RepoSlug = repoFlag
} }
if len(remoteFlag) == 0 { if len(remoteFlag) == 0 {
@@ -101,24 +115,26 @@ func InitCommand(cmd *cli.Command) (*TeaContext, error) {
extraLogins = append(extraLogins, *envLogin) extraLogins = append(extraLogins, *envLogin)
} }
// try to read local git repo & extract context: if repoFlag specifies a valid path, read repo in that dir, if !opts.SkipLocalRepo {
// otherwise attempt PWD. if no repo is found, continue with default login // try to read local git repo & extract context: if repoFlag specifies a valid path, read repo in that dir,
if repoPath == "" { // otherwise attempt PWD. if no repo is found, continue with default login
if repoPath, err = os.Getwd(); err != nil { if repoPath == "" {
return nil, err if repoPath, err = os.Getwd(); err != nil {
return nil, err
}
} }
}
var localSlug string var localSlug string
if c.LocalRepo, c.Login, localSlug, err = contextFromLocalRepo(repoPath, remoteFlag, extraLogins); err != nil { if c.LocalRepo, c.Login, localSlug, err = contextFromLocalRepo(repoPath, remoteFlag, extraLogins); err != nil {
if err == errNotAGiteaRepo || err == git.ErrRepositoryNotExists { if err == errNotAGiteaRepo || err == git.ErrRepositoryNotExists {
// we can deal with that, commands needing the optional values use ctx.Ensure() // we can deal with that, commands needing the optional values use ctx.Ensure()
} else { } else {
return nil, err return nil, err
}
}
if c.RepoSlug == "" && localSlug != "" {
c.RepoSlug = localSlug
} }
}
if c.RepoSlug == "" && localSlug != "" {
c.RepoSlug = localSlug
} }
// If env vars are set, always use the env login (but repo slug was already // If env vars are set, always use the env login (but repo slug was already
@@ -150,7 +166,7 @@ and then run your command again`)
if shouldPromptFallbackLogin(c.Login, canPrompt) { if shouldPromptFallbackLogin(c.Login, canPrompt) {
fallback := false fallback := false
if err := huh.NewConfirm(). if err := huh.NewConfirm().
Title(fmt.Sprintf("NOTE: no gitea login detected, whether falling back to login '%s'?", c.Login.Name)). Title(fmt.Sprintf("NOTE: no login matched this repository. Fall back to login '%s'?", c.Login.Name)).
Value(&fallback). Value(&fallback).
WithTheme(theme.GetTheme()). WithTheme(theme.GetTheme()).
Run(); err != nil { Run(); err != nil {
@@ -160,7 +176,7 @@ and then run your command again`)
return nil, ErrCommandCanceled return nil, ErrCommandCanceled
} }
} else if !c.Login.Default { } else if !c.Login.Default {
fmt.Fprintf(os.Stderr, "NOTE: no gitea login detected, falling back to login '%s' in non-interactive mode.\n", c.Login.Name) fmt.Fprintf(os.Stderr, "NOTE: no login matched this repository, falling back to login '%s' in non-interactive mode.\n", c.Login.Name)
} }
} }
+9 -1
View File
@@ -73,7 +73,15 @@ func (r *cliRepository) Config() (*Config, error) {
for _, remoteName := range strings.Fields(remoteOut) { for _, remoteName := range strings.Fields(remoteOut) {
urlOut, err := r.git(nil, nil, "config", "--get-all", "remote."+remoteName+".url") urlOut, err := r.git(nil, nil, "config", "--get-all", "remote."+remoteName+".url")
if err != nil { if err != nil {
return nil, err // tolerate remotes without a url key (e.g. git-annex special remotes)
var gitErr *gitCommandError
if !errors.As(err, &gitErr) {
return nil, err
}
stderr := strings.TrimSpace(gitErr.stderr)
if stderr != "" && !strings.Contains(stderr, "No such section or key") {
return nil, err
}
} }
cfg.Remotes[remoteName] = &RemoteConfig{Name: remoteName, URLs: splitNonEmptyLines(urlOut)} cfg.Remotes[remoteName] = &RemoteConfig{Name: remoteName, URLs: splitNonEmptyLines(urlOut)}
} }
+20 -4
View File
@@ -8,6 +8,7 @@ import (
"slices" "slices"
"strings" "strings"
gitea "gitea.dev/sdk"
"gitea.dev/tea/modules/config" "gitea.dev/tea/modules/config"
"gitea.dev/tea/modules/context" "gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/task" "gitea.dev/tea/modules/task"
@@ -40,9 +41,10 @@ func EditIssue(requestCtx stdctx.Context, ctx context.TeaContext, index int64) (
Deadline: i.Deadline, Deadline: i.Deadline,
} }
i.Assignees = cleanAssignees(i.Assignees)
if len(i.Assignees) != 0 { if len(i.Assignees) != 0 {
for _, a := range i.Assignees { for _, a := range i.Assignees {
opts.AddAssignees = append(opts.AddAssignees, a.UserName) opts.SetAssignees = append(opts.SetAssignees, a.UserName)
} }
} }
@@ -109,7 +111,7 @@ func promptIssueEditProperties(requestCtx stdctx.Context, ctx *context.TeaContex
return nil return nil
} }
currAssignees := o.AddAssignees currAssignees := o.SetAssignees
newAssignees := selectables.Assignees newAssignees := selectables.Assignees
for _, c := range currAssignees { for _, c := range currAssignees {
@@ -119,10 +121,14 @@ func promptIssueEditProperties(requestCtx stdctx.Context, ctx *context.TeaContex
} }
// assignees // assignees
if o.AddAssignees, err = promptMultiSelect("Add Assignees:", newAssignees, "[other]"); err != nil { if currAssignees, err = promptMultiSelectWithPreselect("Set Assignees:", currAssignees, newAssignees, "[other]"); err != nil {
return err return err
} }
printTitleAndContent("Assignees:", strings.Join(o.AddAssignees, "\n")) if len(currAssignees) == 0 && len(o.SetAssignees) > 0 {
o.RemoveAssignees = o.SetAssignees
}
o.SetAssignees = currAssignees
printTitleAndContent("Assignees:", strings.Join(o.SetAssignees, "\n"))
// milestone // milestone
if len(selectables.MilestoneList) != 0 { if len(selectables.MilestoneList) != 0 {
@@ -175,3 +181,13 @@ func promptIssueEditProperties(requestCtx stdctx.Context, ctx *context.TeaContex
return nil return nil
} }
func cleanAssignees(list []*gitea.User) []*gitea.User {
out := make([]*gitea.User, 0, len(list))
for _, a := range list {
if strings.TrimSpace(a.UserName) != "" {
out = append(out, a)
}
}
return out
}
+17 -1
View File
@@ -92,10 +92,26 @@ func promptDatetime(prompt string) (val *time.Time, err error) {
// promptSelect creates a generic multiselect prompt, with processing of custom values. // promptSelect creates a generic multiselect prompt, with processing of custom values.
func promptMultiSelect(prompt string, options []string, customVal string) ([]string, error) { func promptMultiSelect(prompt string, options []string, customVal string) ([]string, error) {
opts := huh.NewOptions(makeSelectOpts(options, customVal, "")...)
return runMultiSelect(prompt, opts, customVal)
}
// promptMultiSelectWithPreselect creates a generic multiselect prompt with preselected values and processing of custom values.
func promptMultiSelectWithPreselect(prompt string, selected []string, options []string, customVal string) ([]string, error) {
opts := make([]huh.Option[string], 0, len(selected)+len(options)+1)
for _, name := range selected {
opts = append(opts, huh.NewOption(name, name).Selected(true))
}
opts = append(opts, huh.NewOptions(makeSelectOpts(options, customVal, "")...)...)
return runMultiSelect(prompt, opts, customVal)
}
func runMultiSelect(prompt string, opts []huh.Option[string], customVal string) ([]string, error) {
var selection []string var selection []string
if err := huh.NewMultiSelect[string](). if err := huh.NewMultiSelect[string]().
Title(prompt). Title(prompt).
Options(huh.NewOptions(makeSelectOpts(options, customVal, "")...)...). Options(opts...).
Value(&selection). Value(&selection).
WithTheme(theme.GetTheme()). WithTheme(theme.GetTheme()).
Run(); err != nil { Run(); err != nil {
+49
View File
@@ -0,0 +1,49 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package task
import (
stdctx "context"
"fmt"
"strings"
gitea "gitea.dev/sdk"
)
// ResolveAssigneeOpts resolves assignee names to IssueAssigneesOption. Returns nil if names is empty.
func ResolveAssigneeOpts(names []string) *gitea.IssueAssigneesOption {
names = cleanAssignees(names)
if len(names) == 0 {
return nil
}
return &gitea.IssueAssigneesOption{Assignees: names}
}
// ApplyAssigneeChanges adds and removes assignees on an issue or pull request.
func ApplyAssigneeChanges(requestCtx stdctx.Context, client *gitea.Client, owner, repo string, index int64, add, rm *gitea.IssueAssigneesOption) error {
if rm != nil {
_, _, err := client.Issues.DeleteIssueAssignees(requestCtx, owner, repo, index, *rm)
if err != nil {
return fmt.Errorf("could not remove assignees: %s", err)
}
}
if add != nil {
_, _, err := client.Issues.AddIssueAssignees(requestCtx, owner, repo, index, *add)
if err != nil {
return fmt.Errorf("could not add assignees: %s", err)
}
}
return nil
}
func cleanAssignees(list []string) []string {
out := make([]string, 0, len(list))
for _, a := range list {
if strings.TrimSpace(a) != "" {
out = append(out, a)
}
}
return out
}
+18 -10
View File
@@ -23,23 +23,26 @@ type EditIssueOption struct {
Deadline *time.Time Deadline *time.Time
AddLabels []string AddLabels []string
RemoveLabels []string RemoveLabels []string
SetAssignees []string
AddAssignees []string AddAssignees []string
RemoveAssignees []string
AddReviewers []string AddReviewers []string
RemoveReviewers []string RemoveReviewers []string
// RemoveAssignees []string // NOTE: with the current go-sdk, clearing assignees is not possible.
} }
// Normalizes the options into parameters that can be passed to the sdk. // Normalizes the options into parameters that can be passed to the sdk.
// the returned value will be nil, when no change to this part of the issue is requested. // the returned value will be nil, when no change to this part of the issue is requested.
func (o EditIssueOption) toSdkOptions(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea.Client) (*gitea.EditIssueOption, *gitea.IssueLabelsOption, *gitea.IssueLabelsOption, error) { func (o EditIssueOption) toSdkOptions(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea.Client) (*gitea.EditIssueOption, *gitea.IssueLabelsOption, *gitea.IssueLabelsOption, *gitea.IssueAssigneesOption, *gitea.IssueAssigneesOption, error) {
addLabelOpts, err := ResolveLabelOpts(requestCtx, client, ctx.Owner, ctx.Repo, o.AddLabels) addLabelOpts, err := ResolveLabelOpts(requestCtx, client, ctx.Owner, ctx.Repo, o.AddLabels)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, nil, nil, err
} }
rmLabelOpts, err := ResolveLabelOpts(requestCtx, client, ctx.Owner, ctx.Repo, o.RemoveLabels) rmLabelOpts, err := ResolveLabelOpts(requestCtx, client, ctx.Owner, ctx.Repo, o.RemoveLabels)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, nil, nil, err
} }
addAssigneeOpts := ResolveAssigneeOpts(o.AddAssignees)
rmAssigneeOpts := ResolveAssigneeOpts(o.RemoveAssignees)
issueOpts := gitea.EditIssueOption{} issueOpts := gitea.EditIssueOption{}
var issueOptsDirty bool var issueOptsDirty bool
@@ -58,7 +61,7 @@ func (o EditIssueOption) toSdkOptions(requestCtx stdctx.Context, ctx *context.Te
if o.Milestone != nil { if o.Milestone != nil {
id, err := ResolveMilestoneID(requestCtx, client, ctx.Owner, ctx.Repo, *o.Milestone) id, err := ResolveMilestoneID(requestCtx, client, ctx.Owner, ctx.Repo, *o.Milestone)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, nil, nil, err
} }
issueOpts.Milestone = gitea.OptionalInt64(id) issueOpts.Milestone = gitea.OptionalInt64(id)
issueOptsDirty = true issueOptsDirty = true
@@ -70,15 +73,16 @@ func (o EditIssueOption) toSdkOptions(requestCtx stdctx.Context, ctx *context.Te
issueOpts.RemoveDeadline = gitea.OptionalBool(true) issueOpts.RemoveDeadline = gitea.OptionalBool(true)
} }
} }
if len(o.AddAssignees) != 0 { o.SetAssignees = cleanAssignees(o.SetAssignees)
issueOpts.Assignees = o.AddAssignees if len(o.SetAssignees) != 0 {
issueOpts.Assignees = o.SetAssignees
issueOptsDirty = true issueOptsDirty = true
} }
if issueOptsDirty { if issueOptsDirty {
return &issueOpts, addLabelOpts, rmLabelOpts, nil return &issueOpts, addLabelOpts, rmLabelOpts, addAssigneeOpts, rmAssigneeOpts, nil
} }
return nil, addLabelOpts, rmLabelOpts, nil return nil, addLabelOpts, rmLabelOpts, addAssigneeOpts, rmAssigneeOpts, nil
} }
// EditIssue edits an issue and returns the updated issue. // EditIssue edits an issue and returns the updated issue.
@@ -87,7 +91,7 @@ func EditIssue(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea
client = ctx.Login.Client() client = ctx.Login.Client()
} }
issueOpts, addLabelOpts, rmLabelOpts, err := opts.toSdkOptions(requestCtx, ctx, client) issueOpts, addLabelOpts, rmLabelOpts, addAssigneeOpts, rmAssigneeOpts, err := opts.toSdkOptions(requestCtx, ctx, client)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -96,6 +100,10 @@ func EditIssue(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea
return nil, err return nil, err
} }
if err := ApplyAssigneeChanges(requestCtx, client, ctx.Owner, ctx.Repo, opts.Index, addAssigneeOpts, rmAssigneeOpts); err != nil {
return nil, err
}
var issue *gitea.Issue var issue *gitea.Issue
if issueOpts != nil { if issueOpts != nil {
issue, _, err = client.Issues.EditIssue(requestCtx, ctx.Owner, ctx.Repo, opts.Index, *issueOpts) issue, _, err = client.Issues.EditIssue(requestCtx, ctx.Owner, ctx.Repo, opts.Index, *issueOpts)
+9 -2
View File
@@ -25,6 +25,8 @@ func EditPull(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea.
if err != nil { if err != nil {
return nil, err return nil, err
} }
addAssigneeOpts := ResolveAssigneeOpts(opts.AddAssignees)
rmAssigneeOpts := ResolveAssigneeOpts(opts.RemoveAssignees)
prOpts := gitea.EditPullRequestOption{} prOpts := gitea.EditPullRequestOption{}
var prOptsDirty bool var prOptsDirty bool
@@ -51,8 +53,9 @@ func EditPull(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea.
prOpts.RemoveDeadline = gitea.OptionalBool(true) prOpts.RemoveDeadline = gitea.OptionalBool(true)
} }
} }
if len(opts.AddAssignees) != 0 { opts.SetAssignees = cleanAssignees(opts.SetAssignees)
prOpts.Assignees = opts.AddAssignees if len(opts.SetAssignees) != 0 {
prOpts.Assignees = opts.SetAssignees
prOptsDirty = true prOptsDirty = true
} }
@@ -60,6 +63,10 @@ func EditPull(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea.
return nil, err return nil, err
} }
if err := ApplyAssigneeChanges(requestCtx, client, ctx.Owner, ctx.Repo, opts.Index, addAssigneeOpts, rmAssigneeOpts); err != nil {
return nil, err
}
if err := ApplyReviewerChanges(requestCtx, client, ctx.Owner, ctx.Repo, opts.Index, opts.AddReviewers, opts.RemoveReviewers); err != nil { if err := ApplyReviewerChanges(requestCtx, client, ctx.Owner, ctx.Repo, opts.Index, opts.AddReviewers, opts.RemoveReviewers); err != nil {
return nil, err return nil, err
} }
+112
View File
@@ -0,0 +1,112 @@
#!/bin/sh
# Copyright 2026 The Gitea Authors. All rights reserved.
# SPDX-License-Identifier: MIT
#
# upload-r2.sh uploads a single local file to a single object key in a
# Cloudflare R2 bucket, using curl's built-in AWS SigV4 signer (R2 is
# S3-API compatible).
#
# This is the R2 half of the release process's parallel S3+R2 upload
# period: goreleaser's `blobs:` pipe still uploads every release
# artifact to AWS S3, and this script is invoked once per artifact
# (via a goreleaser `publishers:` entry) to mirror the same artifact
# into R2. Once the migration away from S3 is complete, the `blobs:`
# block and the AWS_* secrets can be dropped without touching this
# script.
#
# Usage:
# upload-r2.sh <local-file> <remote-key>
# upload-r2.sh --check-config
#
# The second form only validates that the required environment
# variables below are set (it does not touch the network or the
# filesystem beyond that), and is meant to be run as an early
# preflight step in CI: goreleaser custom publishers run as the very
# last step of the publish pipeline, so without a preflight check a
# missing R2_* secret would only be discovered after the Gitea release
# has already been created and every artifact already uploaded to S3.
#
# Required environment variables:
# R2_ENDPOINT Base URL of the R2 endpoint, e.g.
# https://<account>.r2.cloudflarestorage.com
# R2_BUCKET Destination bucket name.
# R2_ACCESS_KEY_ID R2 access key id.
# R2_SECRET_ACCESS_KEY R2 secret access key.
set -eu
# check_env validates that all required R2_* environment variables are
# set and non-empty, printing a single "missing required environment
# variable(s): ..." message and exiting non-zero otherwise. Used by
# both the normal upload mode and --check-config, so the validation
# logic only exists in one place.
check_env() {
missing=""
if [ -z "${R2_ENDPOINT:-}" ]; then
missing="$missing R2_ENDPOINT"
fi
if [ -z "${R2_BUCKET:-}" ]; then
missing="$missing R2_BUCKET"
fi
if [ -z "${R2_ACCESS_KEY_ID:-}" ]; then
missing="$missing R2_ACCESS_KEY_ID"
fi
if [ -z "${R2_SECRET_ACCESS_KEY:-}" ]; then
missing="$missing R2_SECRET_ACCESS_KEY"
fi
if [ -n "$missing" ]; then
echo "upload-r2.sh: missing required environment variable(s):$missing" >&2
exit 1
fi
}
if [ "$#" -eq 1 ] && [ "$1" = "--check-config" ]; then
check_env
echo "upload-r2.sh: R2 configuration OK"
exit 0
fi
if [ "$#" -ne 2 ]; then
echo "usage: upload-r2.sh <local-file> <remote-key>" >&2
echo " upload-r2.sh --check-config" >&2
exit 1
fi
local_file="$1"
remote_key="$2"
if [ ! -f "$local_file" ]; then
echo "upload-r2.sh: local file not found: $local_file" >&2
exit 1
fi
check_env
# Strip a single trailing slash from the endpoint, if present, so that
# building the path-style URL below never produces a double slash.
endpoint="${R2_ENDPOINT%/}"
url="$endpoint/$R2_BUCKET/$remote_key"
# Credentials are passed to curl through a config file read from
# stdin rather than as a command-line argument, so they never show up
# in `ps` output.
#
# --fail-with-body (instead of plain --fail) still exits non-zero on
# HTTP errors, but also prints R2's XML error body, which is where the
# actual error code lives (SignatureDoesNotMatch, NoSuchBucket,
# AccessDenied, ...); with plain --fail that body is discarded and the
# failure is silent. --retry 3 (without --retry-all-errors) still
# retries the transient cases (5xx, 408, 429, connection failures);
# --retry-all-errors would additionally retry permanent 4xx responses
# three times with backoff, which only delays an inevitable failure.
printf 'user = "%s:%s"\n' "$R2_ACCESS_KEY_ID" "$R2_SECRET_ACCESS_KEY" | curl \
--config - \
--fail-with-body \
--silent \
--show-error \
--retry 3 \
--aws-sigv4 "aws:amz:auto:s3" \
--upload-file "$local_file" \
"$url"
+2
View File
@@ -29,6 +29,8 @@ func TestRepoFromPath_Worktree(t *testing.T) {
assert.NoError(t, cmd.Run()) assert.NoError(t, cmd.Run())
cmd = exec.Command("git", "-C", mainRepoPath, "config", "user.name", "Test User") cmd = exec.Command("git", "-C", mainRepoPath, "config", "user.name", "Test User")
assert.NoError(t, cmd.Run()) assert.NoError(t, cmd.Run())
cmd = exec.Command("git", "-C", mainRepoPath, "config", "commit.gpgsign", "false")
assert.NoError(t, cmd.Run())
cmd = exec.Command("git", "-C", mainRepoPath, "remote", "add", "origin", "https://gitea.com/owner/repo.git") cmd = exec.Command("git", "-C", mainRepoPath, "remote", "add", "origin", "https://gitea.com/owner/repo.git")
assert.NoError(t, cmd.Run()) assert.NoError(t, cmd.Run())
+131
View File
@@ -0,0 +1,131 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"context"
"fmt"
"testing"
"time"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd"
"github.com/stretchr/testify/require"
)
func TestEditIssue_ModifiesAssignees(t *testing.T) {
// This test verifies that EditIssue correctly modifies assignees of an issue via set, add, and remove.
// It sets up a test repository and organization with known users, then calls EditIssue and checks the results.
login := createIntegrationLogin(t)
client := login.Client()
orgName := fmt.Sprintf("issue-org-%d", time.Now().UnixNano()%1_000_000)
orgRepoName := fmt.Sprintf("issue-repo-%d", time.Now().UnixNano()%1_000_000)
ctx := context.Background()
// Clean up any existing test data that might interfere with the test.
_, _ = client.Repositories.DeleteRepo(ctx, orgName, orgRepoName)
_, _ = client.Organizations.DeleteOrg(ctx, orgName)
_, _ = client.Admin.DeleteUser(ctx, "user1")
_, _ = client.Admin.DeleteUser(ctx, "user2")
_, _, err := client.Admin.CreateOrg(ctx, integrationUsername, gitea.CreateOrgOption{Name: orgName})
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.Organizations.DeleteOrg(ctx, orgName); delErr != nil {
t.Logf("failed to delete integration test org %q: %v", orgName, delErr)
}
})
orgRepo, _, err := client.Repositories.CreateOrgRepo(ctx, orgName, gitea.CreateRepoOption{Name: orgRepoName})
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.Repositories.DeleteRepo(ctx, orgName, orgRepoName); delErr != nil {
t.Logf("failed to delete integration test repo %q: %v", orgRepoName, delErr)
}
})
user1, _, err := client.Admin.CreateUser(ctx, gitea.CreateUserOption{Username: "user1", Password: "user1!1234", Email: "user1@test.com"})
require.NoError(t, err)
user2, _, err := client.Admin.CreateUser(ctx, gitea.CreateUserOption{Username: "user2", Password: "user2!1234", Email: "user2@test.com"})
require.NoError(t, err)
t.Cleanup(func() {
_, _ = client.Admin.DeleteUser(ctx, "user1")
_, _ = client.Admin.DeleteUser(ctx, "user2")
})
permission := gitea.AccessModeOwner
team, _, err := client.Organizations.CreateTeam(ctx, orgName, gitea.CreateTeamOption{Name: "writers", Permission: permission})
require.NoError(t, err)
_, err = client.Organizations.AddTeamMember(ctx, team.ID, "user1")
require.NoError(t, err)
_, err = client.Organizations.AddTeamMember(ctx, team.ID, "user2")
require.NoError(t, err)
_, err = client.Organizations.AddTeamRepository(ctx, team.ID, orgName, orgRepoName)
require.NoError(t, err)
assigneeValid, _, err := client.Repositories.CheckRepoIssueAssignee(ctx, orgName, orgRepoName, user1.UserName)
require.NoError(t, err)
require.True(t, assigneeValid)
assigneeValid, _, err = client.Repositories.CheckRepoIssueAssignee(ctx, orgName, orgRepoName, user2.UserName)
require.NoError(t, err)
require.True(t, assigneeValid)
orgIssue, _, err := client.Issues.CreateIssue(ctx, orgName, orgRepoName, gitea.CreateIssueOption{Title: "issue_integration_test", Assignees: []string{integrationUsername}, Closed: false})
require.NoError(t, err)
require.Equal(t, integrationUsername, orgIssue.Assignees[0].UserName)
curUser, _, err := client.Users.GetMyUserInfo(ctx)
require.NoError(t, err)
getNames := func(issue *gitea.Issue) []string {
names := make([]string, len(issue.Assignees))
for i, u := range issue.Assignees {
names[i] = u.UserName
}
return names
}
checkAssignees := func(expected []string) {
updatedIssue, _, err := client.Issues.GetIssue(ctx, orgName, orgRepoName, orgIssue.Index)
require.NoError(t, err)
require.ElementsMatch(t, getNames(updatedIssue), expected)
}
app := cmd.App()
// test set overwrites add and remove
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--set-assignees", "user1", "--add-assignees", "user2", "--remove-assignees", integrationUsername, "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{user1.UserName})
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--set-assignees", "user2," + integrationUsername, "--add-assignees", "user1", "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{user2.UserName, curUser.UserName})
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--set-assignees", "user1", "--remove-assignees", integrationUsername, "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{user1.UserName})
// test remove one assignee
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--remove-assignees", "user1", "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{})
// test add multiple assignees and overwrites remove
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--add-assignees", "user1," + integrationUsername, "--remove-assignees", integrationUsername, "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{curUser.UserName, user1.UserName})
// test add one assignee
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--add-assignees", "user2", "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{curUser.UserName, user1.UserName, user2.UserName})
// test remove multiple assignees
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--remove-assignees", "user1,user2," + integrationUsername, "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{})
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--set-assignees", "user_not_exists"})
require.Error(t, err)
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--add-assignees", "user_not_exists"})
require.Error(t, err)
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--remove-assignees", "user_not_exists"})
require.Error(t, err)
}
+3 -3
View File
@@ -80,7 +80,7 @@ func TestResolveLabelNames_ReturnsRepoAndOrgLabels(t *testing.T) {
runGit("commit", "--allow-empty", "-m", "Initial commit") runGit("commit", "--allow-empty", "-m", "Initial commit")
runGit("push", "-u", "origin", "HEAD:branch-with-labels") runGit("push", "-u", "origin", "HEAD:branch-with-labels")
waitForBranches(t, orgRepo.FullName) waitForBranches(t, orgRepo.FullName, "branch-with-labels")
_ = runTeaCommand( _ = runTeaCommand(
t, "pr", "create", "--repo", orgRepo.FullName, t, "pr", "create", "--repo", orgRepo.FullName,
"--login", login.Name, "--base", "main", "--head", "branch-with-labels", "--login", login.Name, "--base", "main", "--head", "branch-with-labels",
@@ -94,7 +94,7 @@ func TestResolveLabelNames_ReturnsRepoAndOrgLabels(t *testing.T) {
require.ElementsMatch(t, labels, []*gitea.Label{orgLabel, repoLabel}) require.ElementsMatch(t, labels, []*gitea.Label{orgLabel, repoLabel})
} }
func waitForBranches(t *testing.T, repoFullName string) { func waitForBranches(t *testing.T, repoFullName string, branchName string) {
t.Helper() t.Helper()
url := fmt.Sprintf("%s/api/v1/repos/%s/branches", os.Getenv("GITEA_TEA_TEST_URL"), repoFullName) url := fmt.Sprintf("%s/api/v1/repos/%s/branches", os.Getenv("GITEA_TEA_TEST_URL"), repoFullName)
@@ -112,7 +112,7 @@ func waitForBranches(t *testing.T, repoFullName string) {
for _, b := range branches { for _, b := range branches {
have[b.Name] = true have[b.Name] = true
} }
if have["main"] && have["branch-with-labels"] { if have["main"] && have[branchName] {
return return
} }
} }
+156
View File
@@ -0,0 +1,156 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"context"
"fmt"
"os/exec"
"strings"
"testing"
"time"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd"
"github.com/stretchr/testify/require"
)
func TestEditPull_ModifiesAssignees(t *testing.T) {
// This test verifies that EditPull correctly modifies assignees of an pull request via set, add, and remove.
// It sets up a test repository and organization with known users, then calls EditPull and checks the results.
login := createIntegrationLogin(t)
client := login.Client()
orgName := fmt.Sprintf("pull-org-%d", time.Now().UnixNano()%1_000_000)
orgRepoName := fmt.Sprintf("pull-repo-%d", time.Now().UnixNano()%1_000_000)
ctx := context.Background()
// Clean up any existing test data that might interfere with the test.
_, _ = client.Repositories.DeleteRepo(ctx, orgName, orgRepoName)
_, _ = client.Organizations.DeleteOrg(ctx, orgName)
_, _ = client.Admin.DeleteUser(ctx, "user1")
_, _ = client.Admin.DeleteUser(ctx, "user2")
_, _, err := client.Admin.CreateOrg(ctx, integrationUsername, gitea.CreateOrgOption{Name: orgName})
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.Organizations.DeleteOrg(ctx, orgName); delErr != nil {
t.Logf("failed to delete integration test org %q: %v", orgName, delErr)
}
})
orgRepo, _, err := client.Repositories.CreateOrgRepo(ctx, orgName, gitea.CreateRepoOption{Name: orgRepoName})
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.Repositories.DeleteRepo(ctx, orgName, orgRepoName); delErr != nil {
t.Logf("failed to delete integration test repo %q: %v", orgRepoName, delErr)
}
})
user1, _, err := client.Admin.CreateUser(ctx, gitea.CreateUserOption{Username: "user1", Password: "user1!1234", Email: "user1@test.com"})
require.NoError(t, err)
user2, _, err := client.Admin.CreateUser(ctx, gitea.CreateUserOption{Username: "user2", Password: "user2!1234", Email: "user2@test.com"})
require.NoError(t, err)
t.Cleanup(func() {
_, _ = client.Admin.DeleteUser(ctx, "user1")
_, _ = client.Admin.DeleteUser(ctx, "user2")
})
permission := gitea.AccessModeOwner
team, _, err := client.Organizations.CreateTeam(ctx, orgName, gitea.CreateTeamOption{Name: "writers", Permission: permission})
require.NoError(t, err)
_, err = client.Organizations.AddTeamMember(ctx, team.ID, "user1")
require.NoError(t, err)
_, err = client.Organizations.AddTeamMember(ctx, team.ID, "user2")
require.NoError(t, err)
_, err = client.Organizations.AddTeamRepository(ctx, team.ID, orgName, orgRepoName)
require.NoError(t, err)
assigneeValid, _, err := client.Repositories.CheckRepoIssueAssignee(ctx, orgName, orgRepoName, user1.UserName)
require.NoError(t, err)
require.True(t, assigneeValid)
assigneeValid, _, err = client.Repositories.CheckRepoIssueAssignee(ctx, orgName, orgRepoName, user2.UserName)
require.NoError(t, err)
require.True(t, assigneeValid)
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")
httpsURL := fmt.Sprintf("%s/%s.git", login.URL, orgRepo.FullName)
httpsURL = strings.Replace(httpsURL, "://", fmt.Sprintf("://%s:%s@", login.Name, login.Token), 1)
runGit("remote", "add", "origin", httpsURL)
runGit("checkout", "-b", "main")
runGit("commit", "--allow-empty", "-m", "Initial commit")
runGit("push", "-u", "origin", "HEAD:main")
runGit("checkout", "-b", "branch-with-assignees")
runGit("commit", "--allow-empty", "-m", "Initial commit")
runGit("push", "-u", "origin", "HEAD:branch-with-assignees")
waitForBranches(t, orgRepo.FullName, "branch-with-assignees")
getNames := func(pr *gitea.PullRequest) []string {
names := make([]string, len(pr.Assignees))
for i, u := range pr.Assignees {
names[i] = u.UserName
}
return names
}
checkAssignees := func(expected []string) {
updatedPr, _, err := client.PullRequests.GetPullRequest(ctx, orgName, orgRepoName, 1)
require.NoError(t, err)
require.ElementsMatch(t, getNames(updatedPr), expected)
}
app := cmd.App()
err = app.Run(ctx, []string{"tea", "pr", "create", "--repo", orgRepo.FullName, "--base", "main", "--head", "branch-with-assignees", "--a", integrationUsername})
require.NoError(t, err)
checkAssignees([]string{integrationUsername})
curUser, _, err := client.Users.GetMyUserInfo(ctx)
require.NoError(t, err)
// test set overwrites add and remove
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--set-assignees", "user1", "--add-assignees", "user2", "--remove-assignees", integrationUsername, "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{user1.UserName})
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--set-assignees", "user2," + integrationUsername, "--add-assignees", "user1", "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{user2.UserName, curUser.UserName})
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--set-assignees", "user1", "--remove-assignees", integrationUsername, "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{user1.UserName})
// test remove one assignee
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--remove-assignees", "user1", "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{})
// test add multiple assignees and overwrites remove
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--add-assignees", "user1," + integrationUsername, "--remove-assignees", integrationUsername, "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{curUser.UserName, user1.UserName})
// test add one assignee
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--add-assignees", "user2", "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{curUser.UserName, user1.UserName, user2.UserName})
// test remove multiple assignees
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--remove-assignees", "user1,user2," + integrationUsername, "--repo", orgRepo.FullName})
require.NoError(t, err)
checkAssignees([]string{})
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--set-assignees", "user_not_exists"})
require.Error(t, err)
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--add-assignees", "user_not_exists"})
require.Error(t, err)
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--remove-assignees", "user_not_exists"})
require.Error(t, err)
}