Compare commits
13
Commits
7f0213940d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12c0603966 | ||
|
|
df6e626533 | ||
|
|
3ef1e5626f | ||
|
|
e0bdf2f57b | ||
|
|
f34697c5ed | ||
|
|
a613a344de | ||
|
|
6435b12202 | ||
|
|
61b8536e4a | ||
|
|
73b6bf3e23 | ||
|
|
993eb37b57 | ||
|
|
cd93d8561b | ||
|
|
d664c01e18 | ||
|
|
2d6dcd062f |
@@ -1,80 +0,0 @@
|
||||
name: goreleaser
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: git fetch --force --tags
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: import gpg
|
||||
id: import_gpg
|
||||
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@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 }}
|
||||
GORELEASER_FORCE_TOKEN: 'gitea'
|
||||
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: 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:latest
|
||||
@@ -1,85 +0,0 @@
|
||||
name: goreleaser
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: git fetch --force --tags
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- name: import gpg
|
||||
id: import_gpg
|
||||
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@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 }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
S3_REGION: ${{ secrets.AWS_REGION }}
|
||||
S3_BUCKET: ${{ secrets.AWS_BUCKET }}
|
||||
GORELEASER_FORCE_TOKEN: 'gitea'
|
||||
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 }}
|
||||
@@ -0,0 +1,65 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
releases: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: build
|
||||
run: |
|
||||
set -e
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
SDK="$(go list -f '{{.Version}}' -m gitea.dev/sdk)"
|
||||
mkdir -p dist
|
||||
for target in "linux amd64" "linux arm64" "darwin amd64" "darwin arm64" "windows amd64"; do
|
||||
GOOS="${target% *}"
|
||||
GOARCH="${target#* }"
|
||||
EXT=""
|
||||
if [ "$GOOS" = "windows" ]; then EXT=".exe"; fi
|
||||
GOOS="$GOOS" GOARCH="$GOARCH" CGO_ENABLED=0 go build -trimpath \
|
||||
-ldflags "-X 'gitea.dev/tea/modules/version.Version=${VERSION}' -X 'gitea.dev/tea/modules/version.SDK=${SDK}' -s -w" \
|
||||
-o "dist/tea-${GOOS}-${GOARCH}${EXT}" .
|
||||
done
|
||||
cd dist
|
||||
for f in tea-*; do
|
||||
sha256sum "$f" > "$f.sha256"
|
||||
done
|
||||
- name: create release + upload assets
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -e
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||
RELEASE_ID="$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${API}/releases/tags/${TAG}" \
|
||||
| python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])' || true)"
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
RESPONSE="$(curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/releases" \
|
||||
-d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\"}")"
|
||||
RELEASE_ID="$(printf '%s' "$RESPONSE" | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')"
|
||||
fi
|
||||
for f in dist/tea-*; do
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-F "attachment=@${f}" \
|
||||
"${API}/releases/${RELEASE_ID}/assets" >/dev/null
|
||||
done
|
||||
@@ -16,8 +16,8 @@ jobs:
|
||||
name: Lint Build And Unit Coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- name: lint and build
|
||||
@@ -41,17 +41,28 @@ jobs:
|
||||
GITEA_TEA_TEST_USERNAME: "test01"
|
||||
GITEA_TEA_TEST_PASSWORD: "test01"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||
with:
|
||||
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
|
||||
run: |
|
||||
make integration-test
|
||||
services:
|
||||
gitea:
|
||||
image: docker.gitea.com/gitea:1.26.2
|
||||
image: docker.gitea.com/gitea:1.27.0
|
||||
cmd:
|
||||
- bash
|
||||
- -c
|
||||
|
||||
+44
-3
@@ -1,3 +1,5 @@
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
@@ -79,11 +81,50 @@ blobs:
|
||||
provider: s3
|
||||
bucket: "{{ .Env.S3_BUCKET }}"
|
||||
region: "{{ .Env.S3_REGION }}"
|
||||
folder: "tea/{{.Version}}"
|
||||
directory: "tea/{{.Version}}"
|
||||
extra_files:
|
||||
- glob: ./**.xz
|
||||
- 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:
|
||||
- format: binary
|
||||
name_template: "{{ .Binary }}"
|
||||
@@ -104,10 +145,10 @@ signs:
|
||||
args: ["--batch", "-u", "{{ .Env.GPG_FINGERPRINT }}", "--output", "${signature}", "--detach-sign", "${artifact}"]
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ .Branch }}-devel"
|
||||
version_template: "{{ .Branch }}-devel"
|
||||
|
||||
nightly:
|
||||
name_template: "{{ .Branch }}"
|
||||
version_template: "{{ .Branch }}"
|
||||
|
||||
gitea_urls:
|
||||
api: https://gitea.com/api/v1
|
||||
|
||||
+20
-4
@@ -177,10 +177,18 @@ func GetIssuePRCreateFlags(requestCtx stdctx.Context, ctx *context.TeaContext) (
|
||||
|
||||
// IssuePREditFlags defines flags for editing properties of issues and PRs
|
||||
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{
|
||||
Name: "add-assignees",
|
||||
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{
|
||||
Name: "add-labels",
|
||||
@@ -224,17 +232,25 @@ func GetIssuePREditFlags(ctx *context.TeaContext) (*task.EditIssueOption, error)
|
||||
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") {
|
||||
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") {
|
||||
val := ctx.String("add-labels")
|
||||
opts.AddLabels = strings.Split(val, ",")
|
||||
opts.AddLabels = strings.Split(strings.ReplaceAll(val, " ", ""), ",")
|
||||
}
|
||||
if ctx.IsSet("remove-labels") {
|
||||
val := ctx.String("remove-labels")
|
||||
opts.RemoveLabels = strings.Split(val, ",")
|
||||
opts.RemoveLabels = strings.Split(strings.ReplaceAll(val, " ", ""), ",")
|
||||
}
|
||||
return &opts, nil
|
||||
}
|
||||
|
||||
@@ -63,12 +63,12 @@ func listNotifications(requestCtx stdctx.Context, cmd *cli.Command, status []git
|
||||
var news []*gitea.NotificationThread
|
||||
var err error
|
||||
|
||||
ctx, err := context.InitCommand(cmd)
|
||||
all := cmd.Bool("mine")
|
||||
ctx, err := context.InitCommandWithOptions(cmd, context.InitOptions{SkipLocalRepo: all})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := ctx.Login.Client()
|
||||
all := ctx.Bool("mine")
|
||||
|
||||
// This enforces pagination (see https://github.com/go-gitea/gitea/issues/16733)
|
||||
listOpts := flags.GetListOptions(cmd)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -24,7 +24,7 @@ var CmdNotificationsMarkRead = cli.Command{
|
||||
ArgsUsage: "[all | <notification id>]",
|
||||
Flags: flags.NotificationFlags,
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -48,7 +48,7 @@ var CmdNotificationsMarkUnread = cli.Command{
|
||||
ArgsUsage: "[all | <notification id>]",
|
||||
Flags: flags.NotificationFlags,
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -72,7 +72,7 @@ var CmdNotificationsMarkPinned = cli.Command{
|
||||
ArgsUsage: "[all | <notification id>]",
|
||||
Flags: flags.NotificationFlags,
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -95,7 +95,7 @@ var CmdNotificationsUnpin = cli.Command{
|
||||
ArgsUsage: "[all | <notification id>]",
|
||||
Flags: flags.NotificationFlags,
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
+10
-2
@@ -239,7 +239,7 @@ Create an issue on repository
|
||||
|
||||
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
|
||||
|
||||
@@ -255,10 +255,14 @@ Edit one or more issues
|
||||
|
||||
**--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
|
||||
|
||||
**--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**="":
|
||||
|
||||
### reopen, open
|
||||
@@ -423,7 +427,7 @@ Change state of one or more pull requests to 'open'
|
||||
|
||||
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
|
||||
|
||||
@@ -445,12 +449,16 @@ Edit one or more pull requests
|
||||
|
||||
**--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-reviewers**="": Comma-separated list of usernames to remove from reviewers
|
||||
|
||||
**--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**="":
|
||||
|
||||
### review
|
||||
|
||||
@@ -12,17 +12,17 @@ require (
|
||||
github.com/adrg/xdg v0.5.3
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
||||
github.com/enescakir/emoji v1.0.0
|
||||
github.com/go-authgate/sdk-go v0.14.0
|
||||
github.com/go-signet/sdk-go v1.1.0
|
||||
github.com/muesli/termenv v0.16.0
|
||||
github.com/olekukonko/tablewriter v1.1.4
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/urfave/cli-docs/v3 v3.1.0
|
||||
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/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
|
||||
)
|
||||
|
||||
@@ -76,10 +76,10 @@ require (
|
||||
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
||||
github.com/zalando/go-keyring v0.2.8 // indirect
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
golang.org/x/tools v0.45.0 // indirect
|
||||
golang.org/x/net v0.56.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
golang.org/x/tools v0.47.0 // indirect
|
||||
)
|
||||
|
||||
retract v1.3.3 // accidental release, tag deleted
|
||||
|
||||
@@ -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/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/go-authgate/sdk-go v0.14.0 h1:s1i/UCX2Edf3A1pKDW6oXv+oACQfTroxiGY52eqKx+4=
|
||||
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 h1:wHKg9P+goQ14A1Q0gtC6m3mCzRFWwL1peAGz/zhmAZQ=
|
||||
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/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
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-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.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
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/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
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.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
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-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
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/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
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-20190412213103-97732733099d/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/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
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.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
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-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.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/go-authgate/sdk-go/credstore"
|
||||
"github.com/go-signet/sdk-go/credstore"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
|
||||
+34
-18
@@ -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
|
||||
}
|
||||
|
||||
// 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,
|
||||
// after ensuring a remote repo is present in the context.
|
||||
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
|
||||
// command flags. If a local git repo can't be found, repo slug values are unset.
|
||||
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
|
||||
repoFlag := cmd.String("repo")
|
||||
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.
|
||||
if len(repoFlag) != 0 {
|
||||
if len(repoFlag) != 0 && !opts.SkipLocalRepo {
|
||||
if repoFlagPathExists, err = utils.DirExists(repoFlag); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -85,6 +97,8 @@ func InitCommand(cmd *cli.Command) (*TeaContext, error) {
|
||||
} else {
|
||||
c.RepoSlug = repoFlag
|
||||
}
|
||||
} else if len(repoFlag) != 0 {
|
||||
c.RepoSlug = repoFlag
|
||||
}
|
||||
|
||||
if len(remoteFlag) == 0 {
|
||||
@@ -101,24 +115,26 @@ func InitCommand(cmd *cli.Command) (*TeaContext, error) {
|
||||
extraLogins = append(extraLogins, *envLogin)
|
||||
}
|
||||
|
||||
// try to read local git repo & extract context: if repoFlag specifies a valid path, read repo in that dir,
|
||||
// otherwise attempt PWD. if no repo is found, continue with default login
|
||||
if repoPath == "" {
|
||||
if repoPath, err = os.Getwd(); err != nil {
|
||||
return nil, err
|
||||
if !opts.SkipLocalRepo {
|
||||
// try to read local git repo & extract context: if repoFlag specifies a valid path, read repo in that dir,
|
||||
// otherwise attempt PWD. if no repo is found, continue with default login
|
||||
if repoPath == "" {
|
||||
if repoPath, err = os.Getwd(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var localSlug string
|
||||
if c.LocalRepo, c.Login, localSlug, err = contextFromLocalRepo(repoPath, remoteFlag, extraLogins); err != nil {
|
||||
if err == errNotAGiteaRepo || err == git.ErrRepositoryNotExists {
|
||||
// we can deal with that, commands needing the optional values use ctx.Ensure()
|
||||
} else {
|
||||
return nil, err
|
||||
var localSlug string
|
||||
if c.LocalRepo, c.Login, localSlug, err = contextFromLocalRepo(repoPath, remoteFlag, extraLogins); err != nil {
|
||||
if err == errNotAGiteaRepo || err == git.ErrRepositoryNotExists {
|
||||
// we can deal with that, commands needing the optional values use ctx.Ensure()
|
||||
} else {
|
||||
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
|
||||
@@ -150,7 +166,7 @@ and then run your command again`)
|
||||
if shouldPromptFallbackLogin(c.Login, canPrompt) {
|
||||
fallback := false
|
||||
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).
|
||||
WithTheme(theme.GetTheme()).
|
||||
Run(); err != nil {
|
||||
@@ -160,7 +176,7 @@ and then run your command again`)
|
||||
return nil, ErrCommandCanceled
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,15 @@ func (r *cliRepository) Config() (*Config, error) {
|
||||
for _, remoteName := range strings.Fields(remoteOut) {
|
||||
urlOut, err := r.git(nil, nil, "config", "--get-all", "remote."+remoteName+".url")
|
||||
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)}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
gitea "gitea.dev/sdk"
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/context"
|
||||
"gitea.dev/tea/modules/task"
|
||||
@@ -40,9 +41,10 @@ func EditIssue(requestCtx stdctx.Context, ctx context.TeaContext, index int64) (
|
||||
Deadline: i.Deadline,
|
||||
}
|
||||
|
||||
i.Assignees = cleanAssignees(i.Assignees)
|
||||
if len(i.Assignees) != 0 {
|
||||
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
|
||||
}
|
||||
|
||||
currAssignees := o.AddAssignees
|
||||
currAssignees := o.SetAssignees
|
||||
newAssignees := selectables.Assignees
|
||||
|
||||
for _, c := range currAssignees {
|
||||
@@ -119,10 +121,14 @@ func promptIssueEditProperties(requestCtx stdctx.Context, ctx *context.TeaContex
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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
|
||||
if len(selectables.MilestoneList) != 0 {
|
||||
@@ -175,3 +181,13 @@ func promptIssueEditProperties(requestCtx stdctx.Context, ctx *context.TeaContex
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -92,10 +92,26 @@ func promptDatetime(prompt string) (val *time.Time, err error) {
|
||||
|
||||
// promptSelect creates a generic multiselect prompt, with processing of custom values.
|
||||
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
|
||||
if err := huh.NewMultiSelect[string]().
|
||||
Title(prompt).
|
||||
Options(huh.NewOptions(makeSelectOpts(options, customVal, "")...)...).
|
||||
Options(opts...).
|
||||
Value(&selection).
|
||||
WithTheme(theme.GetTheme()).
|
||||
Run(); err != nil {
|
||||
|
||||
@@ -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
@@ -23,23 +23,26 @@ type EditIssueOption struct {
|
||||
Deadline *time.Time
|
||||
AddLabels []string
|
||||
RemoveLabels []string
|
||||
SetAssignees []string
|
||||
AddAssignees []string
|
||||
RemoveAssignees []string
|
||||
AddReviewers []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.
|
||||
// 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)
|
||||
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)
|
||||
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{}
|
||||
var issueOptsDirty bool
|
||||
@@ -58,7 +61,7 @@ func (o EditIssueOption) toSdkOptions(requestCtx stdctx.Context, ctx *context.Te
|
||||
if o.Milestone != nil {
|
||||
id, err := ResolveMilestoneID(requestCtx, client, ctx.Owner, ctx.Repo, *o.Milestone)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
issueOpts.Milestone = gitea.OptionalInt64(id)
|
||||
issueOptsDirty = true
|
||||
@@ -70,15 +73,16 @@ func (o EditIssueOption) toSdkOptions(requestCtx stdctx.Context, ctx *context.Te
|
||||
issueOpts.RemoveDeadline = gitea.OptionalBool(true)
|
||||
}
|
||||
}
|
||||
if len(o.AddAssignees) != 0 {
|
||||
issueOpts.Assignees = o.AddAssignees
|
||||
o.SetAssignees = cleanAssignees(o.SetAssignees)
|
||||
if len(o.SetAssignees) != 0 {
|
||||
issueOpts.Assignees = o.SetAssignees
|
||||
issueOptsDirty = true
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -87,7 +91,7 @@ func EditIssue(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -96,6 +100,10 @@ func EditIssue(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea
|
||||
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
|
||||
if issueOpts != nil {
|
||||
issue, _, err = client.Issues.EditIssue(requestCtx, ctx.Owner, ctx.Repo, opts.Index, *issueOpts)
|
||||
|
||||
@@ -25,6 +25,8 @@ func EditPull(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addAssigneeOpts := ResolveAssigneeOpts(opts.AddAssignees)
|
||||
rmAssigneeOpts := ResolveAssigneeOpts(opts.RemoveAssignees)
|
||||
|
||||
prOpts := gitea.EditPullRequestOption{}
|
||||
var prOptsDirty bool
|
||||
@@ -51,8 +53,9 @@ func EditPull(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea.
|
||||
prOpts.RemoveDeadline = gitea.OptionalBool(true)
|
||||
}
|
||||
}
|
||||
if len(opts.AddAssignees) != 0 {
|
||||
prOpts.Assignees = opts.AddAssignees
|
||||
opts.SetAssignees = cleanAssignees(opts.SetAssignees)
|
||||
if len(opts.SetAssignees) != 0 {
|
||||
prOpts.Assignees = opts.SetAssignees
|
||||
prOptsDirty = true
|
||||
}
|
||||
|
||||
@@ -60,6 +63,10 @@ func EditPull(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea.
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Executable
+112
@@ -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"
|
||||
@@ -29,6 +29,8 @@ func TestRepoFromPath_Worktree(t *testing.T) {
|
||||
assert.NoError(t, cmd.Run())
|
||||
cmd = exec.Command("git", "-C", mainRepoPath, "config", "user.name", "Test User")
|
||||
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")
|
||||
assert.NoError(t, cmd.Run())
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -80,7 +80,7 @@ func TestResolveLabelNames_ReturnsRepoAndOrgLabels(t *testing.T) {
|
||||
runGit("commit", "--allow-empty", "-m", "Initial commit")
|
||||
runGit("push", "-u", "origin", "HEAD:branch-with-labels")
|
||||
|
||||
waitForBranches(t, orgRepo.FullName)
|
||||
waitForBranches(t, orgRepo.FullName, "branch-with-labels")
|
||||
_ = runTeaCommand(
|
||||
t, "pr", "create", "--repo", orgRepo.FullName,
|
||||
"--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})
|
||||
}
|
||||
|
||||
func waitForBranches(t *testing.T, repoFullName string) {
|
||||
func waitForBranches(t *testing.T, repoFullName string, branchName string) {
|
||||
t.Helper()
|
||||
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 {
|
||||
have[b.Name] = true
|
||||
}
|
||||
if have["main"] && have["branch-with-labels"] {
|
||||
if have["main"] && have[branchName] {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user