Compare commits

...

8 Commits

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

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

---

### Release Notes

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

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

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

### Graphemes, Schmraphemes

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

Enjoy!

#### Changelog

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

***

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

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

</details>

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

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

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

#### What's Changed

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

#### New Contributors

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

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

</details>

---

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

---------

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

## What

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

## Root cause

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

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

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

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

## Changes

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

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

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

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

## Why fix this in tea

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

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

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

### What

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

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

### Behaviour / precedence

Unchanged for existing usage. Body resolution is now:

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

### Testing

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

---------

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

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

Co-authored-by: dinsmoor <204368+dinsmoor@noreply.gitea.com>
Co-committed-by: dinsmoor <204368+dinsmoor@noreply.gitea.com>
2026-06-26 19:59:05 +00:00
techknowlogick 6a57af24ad fix(config): write to keychain before config (#1044)
Reviewed-on: https://gitea.com/gitea/tea/pulls/1044
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
2026-06-26 19:53:59 +00:00
29 changed files with 1171 additions and 117 deletions
+11 -12
View File
@@ -7,7 +7,6 @@ import (
stdctx "context" stdctx "context"
"errors" "errors"
"fmt" "fmt"
"io"
"strings" "strings"
gitea "gitea.dev/sdk" gitea "gitea.dev/sdk"
@@ -32,7 +31,13 @@ var CmdCommentsAdd = cli.Command{
Description: "Add a comment to an issue or pull request.", Description: "Add a comment to an issue or pull request.",
ArgsUsage: "<issue / pr index> [<comment body>]", ArgsUsage: "<issue / pr index> [<comment body>]",
Action: RunCommentsAdd, Action: RunCommentsAdd,
Flags: flags.AllDefaultFlags, Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "description",
Aliases: []string{"d"},
Usage: "comment body (alternative to the positional argument)",
},
}, flags.AllDefaultFlags...),
} }
// RunCommentsAdd creates a new comment. // RunCommentsAdd creates a new comment.
@@ -54,18 +59,12 @@ func RunCommentsAdd(requestCtx stdctx.Context, cmd *cli.Command) error {
return err return err
} }
body := strings.Join(ctx.Args().Tail(), " ") stdinPiped := interact.IsStdinPiped()
// Only consume stdin if no positional body was given. interact.IsStdinPiped() body, err := resolveBody(strings.Join(ctx.Args().Tail(), " "), ctx.String("description"), stdinPiped, ctx.Reader)
// is true for any non-TTY stdin (CI, subshells, agent harnesses) — not just if err != nil {
// piped data — so reading unconditionally would block forever in those
// contexts when the body is supplied via args.
if len(body) == 0 && interact.IsStdinPiped() {
if bodyStdin, err := io.ReadAll(ctx.Reader); err != nil {
return err return err
} else if len(bodyStdin) != 0 {
body = string(bodyStdin)
} }
} else if len(body) == 0 { if len(body) == 0 && !stdinPiped {
if err := huh.NewForm( if err := huh.NewForm(
huh.NewGroup( huh.NewGroup(
huh.NewText(). huh.NewText().
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package comments
import "io"
// resolveBody picks the comment body from the non-interactive sources, in
// precedence order:
//
// 1. the positional argument (kept first for back-compat with the historical
// 'tea comment <idx> "<body>"' shorthand),
// 2. the -d/--description flag (mirrors the body flag on 'issue create',
// 'issue edit' and 'pr create'),
// 3. piped stdin.
//
// stdin is only read when stdinPiped is true (a non-TTY stdin, e.g. CI,
// subshells or agent harnesses) and no body was supplied otherwise, so the
// command never blocks reading an interactive terminal when a body is already
// given. An empty result means the caller should fall back to the editor (when
// interactive) or error out.
func resolveBody(positional, description string, stdinPiped bool, stdin io.Reader) (string, error) {
if len(positional) != 0 {
return positional, nil
}
if len(description) != 0 {
return description, nil
}
if stdinPiped {
stdinBytes, err := io.ReadAll(stdin)
if err != nil {
return "", err
}
return string(stdinBytes), nil
}
return "", nil
}
+101
View File
@@ -0,0 +1,101 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package comments
import (
"io"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestResolveBody(t *testing.T) {
testCases := []struct {
name string
positional string
description string
stdinPiped bool
stdin string
expected string
}{
{
name: "positional only",
positional: "from positional",
expected: "from positional",
},
{
name: "description flag only",
description: "from -d",
expected: "from -d",
},
{
name: "positional wins over description for back-compat",
positional: "from positional",
description: "from -d",
expected: "from positional",
},
{
name: "description wins over piped stdin",
description: "from -d",
stdinPiped: true,
stdin: "from stdin",
expected: "from -d",
},
{
name: "piped stdin used when nothing else given",
stdinPiped: true,
stdin: "from stdin",
expected: "from stdin",
},
{
name: "stdin ignored when not piped (interactive terminal)",
stdinPiped: false,
stdin: "should never be read",
expected: "",
},
{
name: "empty when no source provided",
stdinPiped: false,
expected: "",
},
{
name: "piped but empty stdin yields empty body",
stdinPiped: true,
stdin: "",
expected: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
body, err := resolveBody(tc.positional, tc.description, tc.stdinPiped, strings.NewReader(tc.stdin))
require.NoError(t, err)
assert.Equal(t, tc.expected, body)
})
}
}
// TestResolveBodyDoesNotReadStdinWhenBodyGiven guards the original bug: when a
// body is supplied positionally (or via -d), stdin must not be consumed, so the
// command can never block on a non-TTY stdin under CI / agent harnesses.
func TestResolveBodyDoesNotReadStdinWhenBodyGiven(t *testing.T) {
reader := &trackingReader{}
body, err := resolveBody("positional body", "", true, reader)
require.NoError(t, err)
assert.Equal(t, "positional body", body)
assert.False(t, reader.read, "stdin must not be read when a body is supplied")
}
// trackingReader records whether Read was ever called.
type trackingReader struct {
read bool
}
func (r *trackingReader) Read(p []byte) (int, error) {
r.read = true
return 0, io.EOF
}
+12 -9
View File
@@ -7,7 +7,6 @@ import (
stdctx "context" stdctx "context"
"errors" "errors"
"fmt" "fmt"
"io"
"strings" "strings"
gitea "gitea.dev/sdk" gitea "gitea.dev/sdk"
@@ -31,10 +30,16 @@ var CmdCommentsEdit = cli.Command{
Usage: "Edit the body of an existing comment", Usage: "Edit the body of an existing comment",
Description: `Edit the body of an existing comment by its comment ID. Use 'tea comments list <issue>' to find IDs. Description: `Edit the body of an existing comment by its comment ID. Use 'tea comments list <issue>' to find IDs.
The new body can be supplied as a positional argument, piped on stdin, or (if neither is given and stdin is a terminal) entered in your $EDITOR.`, The new body can be supplied as a positional argument, via -d/--description, piped on stdin, or (if none is given and stdin is a terminal) entered in your $EDITOR.`,
ArgsUsage: "<comment id> [<new body>]", ArgsUsage: "<comment id> [<new body>]",
Action: RunCommentsEdit, Action: RunCommentsEdit,
Flags: flags.AllDefaultFlags, Flags: append([]cli.Flag{
&cli.StringFlag{
Name: "description",
Aliases: []string{"d"},
Usage: "new comment body (alternative to the positional argument)",
},
}, flags.AllDefaultFlags...),
} }
// RunCommentsEdit updates the body of an existing comment. // RunCommentsEdit updates the body of an existing comment.
@@ -56,14 +61,12 @@ func RunCommentsEdit(requestCtx stdctx.Context, cmd *cli.Command) error {
return fmt.Errorf("invalid comment id %q: %s", ctx.Args().First(), err) return fmt.Errorf("invalid comment id %q: %s", ctx.Args().First(), err)
} }
body := strings.Join(ctx.Args().Tail(), " ") stdinPiped := interact.IsStdinPiped()
if len(body) == 0 && interact.IsStdinPiped() { body, err := resolveBody(strings.Join(ctx.Args().Tail(), " "), ctx.String("description"), stdinPiped, ctx.Reader)
if bodyStdin, err := io.ReadAll(ctx.Reader); err != nil { if err != nil {
return err return err
} else if len(bodyStdin) != 0 {
body = string(bodyStdin)
} }
} else if len(body) == 0 { if len(body) == 0 && !stdinPiped {
// Fetch current body to pre-populate the editor. // Fetch current body to pre-populate the editor.
client := ctx.Login.Client() client := ctx.Login.Client()
current, _, fetchErr := client.Issues.GetIssueComment(requestCtx, ctx.Owner, ctx.Repo, id) current, _, fetchErr := client.Issues.GetIssueComment(requestCtx, ctx.Owner, ctx.Repo, id)
+115
View File
@@ -0,0 +1,115 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issues
import (
stdctx "context"
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
"gitea.dev/tea/modules/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
"golang.org/x/crypto/ssh"
)
func TestRunIssuesListWithSSHPubkeyLoginDoesNotDeadlock(t *testing.T) {
t.Parallel()
sshKeyPath, fingerprint := writeTestSSHKey(t)
var versionRequests atomic.Int32
var issueRequests atomic.Int32
var signedVersionRequests atomic.Int32
var signedIssueRequests atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/version":
versionRequests.Add(1)
if r.Header.Get("Signature") != "" {
signedVersionRequests.Add(1)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"version":"1.26.4"}`))
case "/api/v1/repos/gitea/tea/issues":
issueRequests.Add(1)
if r.Header.Get("Signature") != "" {
signedIssueRequests.Add(1)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[]`))
default:
t.Errorf("unexpected path %s", r.URL.Path)
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
config.SetConfigForTesting(config.LocalConfig{
Logins: []config.Login{{
Name: "ssh-login",
URL: server.URL,
SSHKey: sshKeyPath,
SSHKeyFingerprint: fingerprint,
VersionCheck: true,
Default: true,
}},
})
cmd := cli.Command{
Name: CmdIssuesList.Name,
Flags: CmdIssuesList.Flags,
}
require.NoError(t, cmd.Set("login", "ssh-login"))
require.NoError(t, cmd.Set("repo", "gitea/tea"))
require.NoError(t, cmd.Set("output", "json"))
done := make(chan error, 1)
go func() {
done <- RunIssuesList(stdctx.Background(), &cmd)
}()
select {
case err := <-done:
require.NoError(t, err)
case <-time.After(2 * time.Second):
t.Fatal("RunIssuesList deadlocked while bootstrapping the server version for HTTPSign authentication")
}
assert.EqualValues(t, 1, versionRequests.Load())
assert.EqualValues(t, 0, signedVersionRequests.Load())
assert.EqualValues(t, 1, issueRequests.Load())
assert.EqualValues(t, 1, signedIssueRequests.Load())
}
func writeTestSSHKey(t *testing.T) (string, string) {
t.Helper()
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
pkcs8, err := x509.MarshalPKCS8PrivateKey(privateKey)
require.NoError(t, err)
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8})
sshKeyPath := filepath.Join(t.TempDir(), "id_ed25519")
require.NoError(t, os.WriteFile(sshKeyPath, pemBytes, 0o600))
signer, err := ssh.NewSignerFromKey(privateKey)
require.NoError(t, err)
return sshKeyPath, ssh.FingerprintSHA256(signer.PublicKey())
}
+1
View File
@@ -78,6 +78,7 @@ var CmdPulls = cli.Command{
&pulls.CmdPullsApprove, &pulls.CmdPullsApprove,
&pulls.CmdPullsReject, &pulls.CmdPullsReject,
&pulls.CmdPullsMerge, &pulls.CmdPullsMerge,
&pulls.CmdPullsReply,
&pulls.CmdPullsReviewComments, &pulls.CmdPullsReviewComments,
&pulls.CmdPullsResolve, &pulls.CmdPullsResolve,
&pulls.CmdPullsUnresolve, &pulls.CmdPullsUnresolve,
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
stdctx "context"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"github.com/urfave/cli/v3"
)
// CmdPullsReply replies to a review comment on a pull request.
var CmdPullsReply = cli.Command{
Name: "reply",
Usage: "Reply to a pull request review comment",
Description: "Reply to a pull request review comment",
ArgsUsage: "<pull index> <comment id> [<reply>]",
Action: func(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
return runPullReviewReply(requestCtx, ctx)
},
Flags: flags.AllDefaultFlags,
}
+70
View File
@@ -0,0 +1,70 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
"context"
"testing"
"gitea.dev/tea/modules/config"
"github.com/stretchr/testify/assert"
)
func TestReply(t *testing.T) {
config.SetConfigForTesting(config.LocalConfig{
Logins: []config.Login{{
Name: "testLogin",
URL: "https://gitea.example.com",
Token: "test-token",
User: "testUser",
Default: true,
}},
})
t.Cleanup(func() {
config.SetConfigForTesting(config.LocalConfig{})
})
tests := []struct {
name string
args []string
wantErr bool
errContains string
}{
{
name: "no arguments",
args: []string{},
wantErr: true,
errContains: "pull request index and comment ID are required",
},
{
name: "missing comment id",
args: []string{"1"},
wantErr: true,
errContains: "pull request index and comment ID are required",
},
{
name: "pull index and comment id",
args: []string{"1", "2"},
wantErr: true,
errContains: "no reply content provided",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := CmdPullsReply
args := append([]string{"reply"}, tt.args...)
args = append(args, "--login", "testLogin", "--repo", "user/repo")
err := cmd.Run(context.Background(), args)
if tt.wantErr {
assert.Error(t, err)
if tt.errContains != "" {
assert.Contains(t, err.Error(), tt.errContains)
}
return
}
})
}
}
+66
View File
@@ -5,14 +5,21 @@ package pulls
import ( import (
stdctx "context" stdctx "context"
"errors"
"fmt" "fmt"
"io"
"strings" "strings"
gitea "gitea.dev/sdk" gitea "gitea.dev/sdk"
"gitea.dev/tea/modules/config"
"gitea.dev/tea/modules/context" "gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/interact"
"gitea.dev/tea/modules/task" "gitea.dev/tea/modules/task"
"gitea.dev/tea/modules/theme"
"gitea.dev/tea/modules/utils" "gitea.dev/tea/modules/utils"
"charm.land/huh/v2"
) )
// runPullReview handles the common logic for approving/rejecting pull requests // runPullReview handles the common logic for approving/rejecting pull requests
@@ -60,3 +67,62 @@ func runResolveComment(requestCtx stdctx.Context, ctx *context.TeaContext, actio
return action(requestCtx, ctx, commentID) return action(requestCtx, ctx, commentID)
} }
// runPullReviewReply handles replying to a specific review comment on a pull request.
func runPullReviewReply(requestCtx stdctx.Context, ctx *context.TeaContext) error {
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if ctx.Args().Len() < 2 {
return fmt.Errorf("pull request index and comment ID are required")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
commentID, err := utils.ArgToIndex(ctx.Args().Get(1))
if err != nil {
return err
}
body, err := getCommentBody(ctx, ctx.Args().Slice()[2:], "Reply(markdown):", "reply")
if err != nil {
return err
}
return task.ReplyToPullReviewComment(requestCtx, ctx, idx, commentID, body)
}
func getCommentBody(ctx *context.TeaContext, extraArgs []string, promptTitle, noun string) (string, error) {
body := strings.Join(extraArgs, " ")
if interact.IsStdinPiped() {
bodyStdin, err := io.ReadAll(ctx.Reader)
if err != nil {
return "", err
}
if len(bodyStdin) != 0 {
body = strings.Join([]string{body, string(bodyStdin)}, "\n\n")
}
} else if len(body) == 0 {
if err := huh.NewForm(
huh.NewGroup(
huh.NewText().
Title(promptTitle).
ExternalEditor(config.GetPreferences().Editor).
EditorExtension("md").
Value(&body),
),
).WithTheme(theme.GetTheme()).Run(); err != nil {
return "", err
}
}
if len(strings.TrimSpace(body)) == 0 {
return "", errors.New("no " + noun + " content provided")
}
return body, nil
}
+18
View File
@@ -507,6 +507,18 @@ Merge a pull request
**--title, -t**="": Merge commit title **--title, -t**="": Merge commit title
### reply
Reply to a pull request review comment
**--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
### review-comments, rc ### review-comments, rc
List review comments on a pull request List review comments on a pull request
@@ -1915,6 +1927,8 @@ Update a webhook
Manage comments on issues and pull requests Manage comments on issues and pull requests
**--description, -d**="": comment body (alternative to the positional argument)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
@@ -1927,6 +1941,8 @@ Manage comments on issues and pull requests
Add a comment to an issue or pull request Add a comment to an issue or pull request
**--description, -d**="": comment body (alternative to the positional argument)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
@@ -1955,6 +1971,8 @@ List comments on an issue or pull request
Edit the body of an existing comment Edit the body of an existing comment
**--description, -d**="": new comment body (alternative to the positional argument)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
+10 -14
View File
@@ -5,10 +5,10 @@ go 1.26
require ( require (
charm.land/glamour/v2 v2.0.1 charm.land/glamour/v2 v2.0.1
charm.land/huh/v2 v2.0.3 charm.land/huh/v2 v2.0.3
charm.land/lipgloss/v2 v2.0.4 charm.land/lipgloss/v2 v2.0.5
code.gitea.io/gitea-vet v0.2.3 code.gitea.io/gitea-vet v0.2.3
gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c
gitea.dev/sdk v1.1.0 gitea.dev/sdk v1.2.0
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
@@ -18,11 +18,11 @@ require (
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.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/oauth2 v0.36.0
golang.org/x/sys v0.46.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
) )
@@ -56,10 +56,8 @@ require (
github.com/fatih/color v1.19.0 // indirect github.com/fatih/color v1.19.0 // indirect
github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-json v0.10.6 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/go-version v1.9.0 // indirect github.com/hashicorp/go-version v1.9.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-isatty v0.0.22 // indirect
@@ -72,18 +70,16 @@ require (
github.com/olekukonko/ll v0.1.8 // indirect github.com/olekukonko/ll v0.1.8 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.8.2 // indirect github.com/yuin/goldmark v1.8.2 // indirect
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
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
) )
retract v1.3.3 // accidental release, tag deleted retract v1.3.3 // accidental release, tag deleted
+25 -38
View File
@@ -6,14 +6,14 @@ charm.land/glamour/v2 v2.0.1 h1:xl+r00A4aJWU0z8fgwKd9fQQ4rsphqGUzuEiXZP5n+c=
charm.land/glamour/v2 v2.0.1/go.mod h1:jo9z8XqVKPeEFMVdvCRLGk++RyJ3CdUwgNr7EvXLw3k= charm.land/glamour/v2 v2.0.1/go.mod h1:jo9z8XqVKPeEFMVdvCRLGk++RyJ3CdUwgNr7EvXLw3k=
charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU= charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc= charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
charm.land/lipgloss/v2 v2.0.4 h1:lcPeVtcp23SNra7lHy8iYE4UC2aIipVQ47sbGyyxR5Q= charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY=
charm.land/lipgloss/v2 v2.0.4/go.mod h1:0653x8epbZSzdDfO/XPS1a/uYPOBeSsCssOpJOqDzik= charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc=
code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI= code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI=
code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c h1:8fTkq2UaVkLHZCF+iB4wTxINmVAToe2geZGayk9LMbA= gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c h1:8fTkq2UaVkLHZCF+iB4wTxINmVAToe2geZGayk9LMbA=
gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c/go.mod h1:Fc8iyPm4NINRWujeIk2bTfcbGc4ZYY29/oMAAGcr4qI= gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c/go.mod h1:Fc8iyPm4NINRWujeIk2bTfcbGc4ZYY29/oMAAGcr4qI=
gitea.dev/sdk v1.1.0 h1:wLlz03WkLEiXa2bQpO1JQBTlYf7tQI2neYtZK1kU+TE= gitea.dev/sdk v1.2.0 h1:avRtJl/nKCGispgSalo9czoZM9Rto1awnE0caNAoXGo=
gitea.dev/sdk v1.1.0/go.mod h1:Zfl+EZXdsGGCLkryDfsmvYrQo6GKMl4U3BJA8Beu+cs= gitea.dev/sdk v1.2.0/go.mod h1:rfh5oNdIK24cbCREwIn1tqWKQW+IICXFGWJyebuOAOE=
github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU= github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU=
github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps= github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
@@ -72,7 +72,6 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
@@ -96,21 +95,14 @@ 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=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
@@ -136,15 +128,11 @@ github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
@@ -160,8 +148,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw= github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw=
github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to= github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to=
github.com/urfave/cli/v3 v3.10.0 h1:0aU8yOObVDMkM13Cj4G+zb4P0PdeJMec65f81Ak1ioM= github.com/urfave/cli/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY=
github.com/urfave/cli/v3 v3.10.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/urfave/cli/v3 v3.10.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -174,48 +162,47 @@ 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.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.46.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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+1 -3
View File
@@ -31,9 +31,7 @@ func NewClient(login *config.Login) *Client {
} }
httpClient := &http.Client{ httpClient := &http.Client{
Transport: httputil.WrapTransport(&http.Transport{ Transport: httputil.WrapTransport(&tls.Config{InsecureSkipVerify: login.Insecure}),
TLSClientConfig: &tls.Config{InsecureSkipVerify: login.Insecure},
}),
} }
return &Client{ return &Client{
+3 -10
View File
@@ -201,9 +201,7 @@ func performBrowserOAuthFlow(ctx context.Context, opts OAuthOptions) (serverURL
// createHTTPClient creates an HTTP client with optional insecure setting // createHTTPClient creates an HTTP client with optional insecure setting
func createHTTPClient(insecure bool) *http.Client { func createHTTPClient(insecure bool) *http.Client {
return &http.Client{ return &http.Client{
Transport: httputil.WrapTransport(&http.Transport{ Transport: httputil.WrapTransport(&tls.Config{InsecureSkipVerify: insecure}),
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
}),
} }
} }
@@ -412,16 +410,11 @@ func createLoginFromToken(ctx context.Context, name, serverURL string, token *oa
} }
login.SSHHost = parsedURL.Host login.SSHHost = parsedURL.Host
// Add login to config // Save tokens and add login to config
if err := config.AddLogin(&login); err != nil { if err := config.AddOAuthLogin(&login, token.AccessToken, token.RefreshToken, token.Expiry); err != nil {
return err return err
} }
// Save tokens to credstore
if err := config.SaveOAuthToken(login.Name, token.AccessToken, token.RefreshToken, token.Expiry); err != nil {
return fmt.Errorf("failed to save token to secure store: %s", err)
}
fmt.Printf("Login as %s on %s successful. Added this login as %s\n", login.User, login.URL, login.Name) fmt.Printf("Login as %s on %s successful. Added this login as %s\n", login.User, login.URL, login.Name)
return nil return nil
} }
+14 -7
View File
@@ -16,6 +16,18 @@ import (
var ( var (
tokenStore *credstore.SecureStore[credstore.Token] tokenStore *credstore.SecureStore[credstore.Token]
tokenStoreOnce sync.Once tokenStoreOnce sync.Once
saveOAuthTokenToStore = func(loginName, accessToken, refreshToken string, expiresAt time.Time) error {
return getTokenStore().Save(loginName, credstore.Token{
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresAt: expiresAt,
ClientID: loginName,
})
}
deleteOAuthTokenFromStore = func(loginName string) error {
return getTokenStore().Delete(loginName)
}
) )
func getTokenStore() *credstore.SecureStore[credstore.Token] { func getTokenStore() *credstore.SecureStore[credstore.Token] {
@@ -37,17 +49,12 @@ func LoadOAuthToken(loginName string) (*credstore.Token, error) {
// SaveOAuthToken saves OAuth tokens to the secure store. // SaveOAuthToken saves OAuth tokens to the secure store.
func SaveOAuthToken(loginName, accessToken, refreshToken string, expiresAt time.Time) error { func SaveOAuthToken(loginName, accessToken, refreshToken string, expiresAt time.Time) error {
return getTokenStore().Save(loginName, credstore.Token{ return saveOAuthTokenToStore(loginName, accessToken, refreshToken, expiresAt)
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresAt: expiresAt,
ClientID: loginName,
})
} }
// DeleteOAuthToken removes tokens from the secure store. // DeleteOAuthToken removes tokens from the secure store.
func DeleteOAuthToken(loginName string) error { func DeleteOAuthToken(loginName string) error {
return getTokenStore().Delete(loginName) return deleteOAuthTokenFromStore(loginName)
} }
// SaveOAuthTokenFromOAuth2 saves an oauth2.Token to credstore, falling back to // SaveOAuthTokenFromOAuth2 saves an oauth2.Token to credstore, falling back to
+41 -7
View File
@@ -269,6 +269,34 @@ func AddLogin(login *Login) error {
}) })
} }
// AddOAuthLogin saves the OAuth token and login profile as one operation.
// The profile is only written after secure token storage succeeds.
func AddOAuthLogin(login *Login, accessToken, refreshToken string, expiresAt time.Time) error {
return withConfigLock(func() error {
// Check for duplicate login names before touching credential storage.
for _, existing := range config.Logins {
if strings.EqualFold(existing.Name, login.Name) {
return fmt.Errorf("login name '%s' already exists", login.Name)
}
}
if err := SaveOAuthToken(login.Name, accessToken, refreshToken, expiresAt); err != nil {
return fmt.Errorf("failed to save token to secure store: %w", err)
}
config.Logins = append(config.Logins, *login)
if err := saveConfigUnsafe(); err != nil {
config.Logins = config.Logins[:len(config.Logins)-1]
if deleteErr := DeleteOAuthToken(login.Name); deleteErr != nil {
return errors.Join(err, fmt.Errorf("failed to clean up OAuth token after config save failure: %w", deleteErr))
}
return err
}
return nil
})
}
// SaveLoginTokens updates the token fields for an existing login. // SaveLoginTokens updates the token fields for an existing login.
// This is used after browser-based re-authentication to save new tokens. // This is used after browser-based re-authentication to save new tokens.
func SaveLoginTokens(login *Login) error { func SaveLoginTokens(login *Login) error {
@@ -390,9 +418,7 @@ func doOAuthRefresh(ctx context.Context, l *Login) (*oauth2.Token, error) {
} }
httpClient := &http.Client{ httpClient := &http.Client{
Transport: httputil.WrapTransport(&http.Transport{ Transport: httputil.WrapTransport(&tls.Config{InsecureSkipVerify: l.Insecure}),
TLSClientConfig: &tls.Config{InsecureSkipVerify: l.Insecure},
}),
} }
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
@@ -420,15 +446,19 @@ func (l *Login) Client(options ...gitea.ClientOption) *gitea.Client {
os.Exit(1) os.Exit(1)
} }
httpClient := &http.Client{} // Configure transport-level timeouts so a stalled or unresponsive server
// fails fast instead of hanging forever. These bound connection setup and
// time-to-first-response-byte only, so slow-but-progressing transfers (e.g.
// large attachment uploads) are unaffected.
httpClient := &http.Client{
Transport: httputil.WrapTransport(nil),
}
if l.Insecure { if l.Insecure {
cookieJar, _ := cookiejar.New(nil) // New with nil options never returns an error cookieJar, _ := cookiejar.New(nil) // New with nil options never returns an error
httpClient = &http.Client{ httpClient = &http.Client{
Jar: cookieJar, Jar: cookieJar,
Transport: &http.Transport{ Transport: httputil.WrapTransport(&tls.Config{InsecureSkipVerify: true}),
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
} }
} }
@@ -437,6 +467,10 @@ func (l *Login) Client(options ...gitea.ClientOption) *gitea.Client {
options = append([]gitea.ClientOption{gitea.SetGiteaVersion("")}, options...) options = append([]gitea.ClientOption{gitea.SetGiteaVersion("")}, options...)
} }
// SetUserAgent is intentionally redundant with the User-Agent the WrapTransport
// transport already sets: this is the SDK's own guarantee, so the UA survives
// even if the client is ever given a transport that didn't come from WrapTransport.
// Both resolve to httputil.UserAgent(), so the duplicate Header.Set is a no-op.
options = append(options, gitea.SetToken(l.GetAccessToken()), gitea.SetHTTPClient(httpClient), gitea.SetUserAgent(httputil.UserAgent())) options = append(options, gitea.SetToken(l.GetAccessToken()), gitea.SetHTTPClient(httpClient), gitea.SetUserAgent(httputil.UserAgent()))
if debug.IsDebug() { if debug.IsDebug() {
options = append(options, gitea.SetDebugMode()) options = append(options, gitea.SetDebugMode())
+110
View File
@@ -0,0 +1,110 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package config
import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
gitea "gitea.dev/sdk"
"golang.org/x/crypto/ssh"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLoginClientWithSSHPubkeyDoesNotDeadlockOnFirstRequest(t *testing.T) {
t.Parallel()
sshKeyPath, fingerprint := writeTestSSHKey(t)
var versionRequests atomic.Int32
var issueRequests atomic.Int32
var signedVersionRequests atomic.Int32
var signedIssueRequests atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/version":
versionRequests.Add(1)
if r.Header.Get("Signature") != "" {
signedVersionRequests.Add(1)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"version":"1.26.4"}`))
case "/api/v1/repos/gitea/tea/issues":
issueRequests.Add(1)
if r.Header.Get("Signature") != "" {
signedIssueRequests.Add(1)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[]`))
default:
t.Errorf("unexpected path %s", r.URL.Path)
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
login := &Login{
Name: "ssh-login",
URL: server.URL,
SSHKey: sshKeyPath,
SSHKeyFingerprint: fingerprint,
VersionCheck: true,
}
type result struct {
issues []*gitea.Issue
err error
}
done := make(chan result, 1)
go func() {
issues, _, err := login.Client().Issues.ListRepoIssues(context.Background(), "gitea", "tea", gitea.ListIssueOption{})
done <- result{issues: issues, err: err}
}()
select {
case res := <-done:
require.NoError(t, res.err)
assert.Empty(t, res.issues)
case <-time.After(2 * time.Second):
t.Fatal("ListRepoIssues deadlocked while bootstrapping the server version for SSH-signed requests")
}
assert.EqualValues(t, 1, versionRequests.Load())
assert.EqualValues(t, 0, signedVersionRequests.Load())
assert.EqualValues(t, 1, issueRequests.Load())
assert.EqualValues(t, 1, signedIssueRequests.Load())
}
func writeTestSSHKey(t *testing.T) (string, string) {
t.Helper()
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
pkcs8, err := x509.MarshalPKCS8PrivateKey(privateKey)
require.NoError(t, err)
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8})
sshKeyPath := filepath.Join(t.TempDir(), "id_ed25519")
require.NoError(t, os.WriteFile(sshKeyPath, pemBytes, 0o600))
signer, err := ssh.NewSignerFromKey(privateKey)
require.NoError(t, err)
return sshKeyPath, ssh.FingerprintSHA256(signer.PublicKey())
}
+14 -1
View File
@@ -164,7 +164,20 @@ func (r *cliRepository) CreateTrackingBranch(localBranchName, remoteBranchName,
} }
func (r *cliRepository) Checkout(ref ReferenceName) error { func (r *cliRepository) Checkout(ref ReferenceName) error {
_, err := r.git(nil, nil, "checkout", ref.String()) args := []string{"checkout"}
switch {
case ref.IsBranch():
// `git checkout refs/heads/<branch>` detaches HEAD, while the short branch
// name switches to the local branch as intended.
args = append(args, ref.Short())
case ref.IsRemote():
// Be explicit about detached HEAD when checking out a remote-tracking ref.
args = append(args, "--detach", ref.String())
default:
args = append(args, ref.String())
}
_, err := r.git(nil, nil, args...)
return err return err
} }
+14 -6
View File
@@ -4,6 +4,7 @@
package httputil package httputil
import ( import (
"crypto/tls"
"fmt" "fmt"
"net/http" "net/http"
"runtime" "runtime"
@@ -20,12 +21,14 @@ func UserAgent() string {
return ua return ua
} }
// WrapTransport wraps an http.RoundTripper to add the User-Agent header. // WrapTransport returns tea's standard HTTP transport: an *http.Transport
func WrapTransport(base http.RoundTripper) http.RoundTripper { // preset with tea's connection / response-header timeouts (see timeoutTransport)
if base == nil { // and decorated to add the User-Agent header on every request. The supplied
base = http.DefaultTransport // tlsConfig is attached as-is (nil is fine); callers use it for insecure /
} // skip-verify logins. This is the single entry point for building a tea HTTP
return &userAgentTransport{base: base} // client transport, so the timeouts can't be accidentally omitted.
func WrapTransport(tlsConfig *tls.Config) http.RoundTripper {
return &userAgentTransport{base: timeoutTransport(tlsConfig)}
} }
type userAgentTransport struct { type userAgentTransport struct {
@@ -33,6 +36,11 @@ type userAgentTransport struct {
} }
func (t *userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) { func (t *userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Set the UA at the transport so every client built from WrapTransport
// identifies itself, including the non-SDK clients (oauth2 flow, token
// refresh) that never pass through the SDK's own SetUserAgent. For SDK
// clients this overlaps gitea.SetUserAgent; both use httputil.UserAgent(),
// so the duplicate Header.Set is a no-op.
req.Header.Set("User-Agent", UserAgent()) req.Header.Set("User-Agent", UserAgent())
return t.base.RoundTrip(req) return t.base.RoundTrip(req)
} }
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package httputil
import (
"crypto/tls"
"net"
"net/http"
"time"
)
// Timeout values applied to every Gitea API request. These are deliberately
// connection-establishment and time-to-first-response-byte timeouts, NOT an
// overall request deadline: a large release-attachment upload can legitimately
// run for minutes, and as long as bytes keep flowing none of these fire. They
// only trip when a server accepts the connection but never (or far too slowly)
// starts responding — the "hangs forever" case from a stalled or unresponsive
// server (issue #1018).
const (
// DialTimeout bounds establishing the TCP connection.
DialTimeout = 10 * time.Second
// TLSHandshakeTimeout bounds completing the TLS handshake.
TLSHandshakeTimeout = 10 * time.Second
// ResponseHeaderTimeout bounds the wait, after the request is written, for
// the server to begin sending response headers. This is the only timeout
// that protects against a server which accepts the connection but then goes
// silent — the originally reported #1018 symptom; DialTimeout/
// TLSHandshakeTimeout do not, because the connection already succeeded.
//
// The value must clear Gitea's legitimate synchronous pre-response work.
// Profiling a self-hosted Gitea 1.24.6 (on hardware slower than gitea.com)
// showed creating a pull request that triggers conflict detection across
// ~1500 changed files takes ~10s before the first byte (3 runs: 10.06 /
// 10.08 / 10.24s); clean-diff PR creation was ~1s and large attachment
// uploads ~9ms. 120s is ~12x that measured worst case, leaving generous
// headroom for larger repos and busier servers while still failing in two
// minutes instead of hanging forever.
ResponseHeaderTimeout = 120 * time.Second
)
// timeoutTransport returns an *http.Transport configured with tea's standard
// timeouts. The supplied tlsConfig is attached as-is (callers use it for
// insecure / skip-verify logins). It is a clone of http.DefaultTransport so
// connection pooling, proxy support and HTTP/2 keep working. Callers obtain it
// through WrapTransport, which also adds the User-Agent header.
func timeoutTransport(tlsConfig *tls.Config) *http.Transport {
t := http.DefaultTransport.(*http.Transport).Clone()
t.DialContext = (&net.Dialer{
Timeout: DialTimeout,
KeepAlive: 30 * time.Second,
}).DialContext
t.TLSHandshakeTimeout = TLSHandshakeTimeout
t.ResponseHeaderTimeout = ResponseHeaderTimeout
t.TLSClientConfig = tlsConfig
return t
}
+91
View File
@@ -0,0 +1,91 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package httputil
import (
"net"
"net/http"
"testing"
"time"
)
// TestWrapTransportTimeouts verifies the transport returned by WrapTransport
// carries tea's standard timeout values, so a stalled server can't make tea
// hang forever (issue #1018).
func TestWrapTransportTimeouts(t *testing.T) {
rt := WrapTransport(nil)
uat, ok := rt.(*userAgentTransport)
if !ok {
t.Fatalf("WrapTransport returned %T, want *userAgentTransport", rt)
}
tr, ok := uat.base.(*http.Transport)
if !ok {
t.Fatalf("underlying base is %T, want *http.Transport", uat.base)
}
if tr.TLSHandshakeTimeout != TLSHandshakeTimeout {
t.Errorf("TLSHandshakeTimeout = %v, want %v", tr.TLSHandshakeTimeout, TLSHandshakeTimeout)
}
if tr.ResponseHeaderTimeout != ResponseHeaderTimeout {
t.Errorf("ResponseHeaderTimeout = %v, want %v", tr.ResponseHeaderTimeout, ResponseHeaderTimeout)
}
if tr.DialContext == nil {
t.Error("DialContext is nil, want a dialer with DialTimeout")
}
}
// newStallListener returns a listener that accepts connections, reads the
// request, then goes silent without ever sending response headers — the
// "server accepts the connection but never responds" case ResponseHeaderTimeout
// guards against. The returned closer stops the listener.
func newStallListener(t *testing.T) (addr string, closer func()) {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
done := make(chan struct{})
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go func(c net.Conn) {
buf := make([]byte, 4096)
_, _ = c.Read(buf) // drain the request, then never respond
<-done // hold the connection open until the test ends
c.Close()
}(conn)
}
}()
return ln.Addr().String(), func() {
close(done)
ln.Close()
}
}
// TestResponseHeaderTimeoutFires proves a request to a server that accepts the
// connection and request but never sends response headers aborts via
// ResponseHeaderTimeout rather than hanging. It builds the transport the same
// way WrapTransport does, with a short ResponseHeaderTimeout so the test is fast.
func TestResponseHeaderTimeoutFires(t *testing.T) {
addr, closer := newStallListener(t)
defer closer()
tr := timeoutTransport(nil)
tr.ResponseHeaderTimeout = 2 * time.Second
client := &http.Client{Transport: &userAgentTransport{base: tr}}
start := time.Now()
_, err := client.Get("http://" + addr + "/")
elapsed := time.Since(start)
if err == nil {
t.Fatal("expected a timeout error from stalled server, got nil")
}
if elapsed > 10*time.Second {
t.Errorf("request took %v; ResponseHeaderTimeout did not fire", elapsed)
}
t.Logf("request failed as expected after %v: %v", elapsed, err)
}
+1 -2
View File
@@ -5,7 +5,6 @@ package interact
import ( import (
"fmt" "fmt"
"os"
"gitea.dev/tea/modules/theme" "gitea.dev/tea/modules/theme"
@@ -14,7 +13,7 @@ import (
// printTitleAndContent prints a title and content with the gitea theme // printTitleAndContent prints a title and content with the gitea theme
func printTitleAndContent(title, content string) { func printTitleAndContent(title, content string) {
hasDarkBG := lipgloss.HasDarkBackground(os.Stdin, os.Stdout) hasDarkBG := theme.HasDarkBackground()
style := lipgloss.NewStyle(). style := lipgloss.NewStyle().
Foreground(theme.GetTheme().Theme(hasDarkBG).Blurred.Title.GetForeground()).Bold(true). Foreground(theme.GetTheme().Theme(hasDarkBG).Blurred.Title.GetForeground()).Bold(true).
Padding(0, 1) Padding(0, 1)
+1 -1
View File
@@ -77,9 +77,9 @@ func doPRFetch(
localRemote *local_git.Remote, localRemote *local_git.Remote,
callback func(string) (string, error), callback func(string) (string, error),
) (string, error) { ) (string, error) {
_ = callback
localRemoteName := localRemote.Config().Name localRemoteName := localRemote.Config().Name
localBranchName := pr.Head.Ref localBranchName := pr.Head.Ref
// get auth & fetch remote via its configured protocol
url, err := localRepo.TeaRemoteURL(localRemoteName) url, err := localRepo.TeaRemoteURL(localRemoteName)
if err != nil { if err != nil {
return "", err return "", err
+15
View File
@@ -56,6 +56,21 @@ func ResolvePullReviewComment(requestCtx stdctx.Context, ctx *context.TeaContext
return nil return nil
} }
// ReplyToPullReviewComment replies to a review comment on a pull request.
func ReplyToPullReviewComment(requestCtx stdctx.Context, ctx *context.TeaContext, idx, commentID int64, body string) error {
c := ctx.Login.Client()
comment, _, err := c.PullRequests.CreatePullReviewCommentReply(requestCtx, ctx.Owner, ctx.Repo, idx, commentID, gitea.CreatePullReviewCommentReplyOptions{
Body: body,
})
if err != nil {
return err
}
fmt.Println(comment.HTMLURL)
return nil
}
// UnresolvePullReviewComment unresolves a review comment // UnresolvePullReviewComment unresolves a review comment
func UnresolvePullReviewComment(requestCtx stdctx.Context, ctx *context.TeaContext, commentID int64) error { func UnresolvePullReviewComment(requestCtx stdctx.Context, ctx *context.TeaContext, commentID int64) error {
c := ctx.Login.Client() c := ctx.Login.Client()
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package theme
import (
"os"
"charm.land/lipgloss/v2"
"golang.org/x/term"
)
// defaultDarkBackground is the background to assume when we cannot detect one. It
// matches the default lipgloss falls back to.
const defaultDarkBackground = true
// HasDarkBackground reports whether the terminal has a dark background.
//
// It only asks the terminal when stdin and stdout are both terminals. Detection
// works by writing an escape sequence to the output and waiting for the terminal
// to answer on the input, and nothing answers when stdio is redirected, so asking
// means waiting on a reply that never comes. On Windows that wait is unbounded:
// lipgloss opens the console directly rather than giving up, which is why tea used
// to hang at start-up under a service or a CI runner.
func HasDarkBackground() bool {
if !term.IsTerminal(int(os.Stdin.Fd())) || !term.IsTerminal(int(os.Stdout.Fd())) {
return defaultDarkBackground
}
return lipgloss.HasDarkBackground(os.Stdin, os.Stdout)
}
+2 -2
View File
@@ -6,7 +6,6 @@ package theme
import ( import (
"charm.land/huh/v2" "charm.land/huh/v2"
"charm.land/lipgloss/v2" "charm.land/lipgloss/v2"
"charm.land/lipgloss/v2/compat"
) )
// TeaTheme implements the huh.Theme interface with tea-cli styling. // TeaTheme implements the huh.Theme interface with tea-cli styling.
@@ -16,7 +15,8 @@ type TeaTheme struct{}
func (t TeaTheme) Theme(isDark bool) *huh.Styles { func (t TeaTheme) Theme(isDark bool) *huh.Styles {
theme := huh.ThemeCharm(isDark) theme := huh.ThemeCharm(isDark)
title := compat.AdaptiveColor{Light: lipgloss.Color("#02BA84"), Dark: lipgloss.Color("#02BF87")} lightDark := lipgloss.LightDark(isDark)
title := lightDark(lipgloss.Color("#02BA84"), lipgloss.Color("#02BF87"))
theme.Focused.Title = theme.Focused.Title.Foreground(title).Bold(true) theme.Focused.Title = theme.Focused.Title.Foreground(title).Bold(true)
theme.Blurred = theme.Focused theme.Blurred = theme.Focused
return theme return theme
+64
View File
@@ -0,0 +1,64 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package theme
import (
"os/exec"
"slices"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// compatPkg detects the terminal background from package-level vars, so importing
// it anywhere makes tea query the terminal before main() runs. On Windows that
// query can block forever when stdio is redirected, which hung every tea command,
// including tea --version.
//
// lipgloss.LightDark covers what we need without the package-level detection, so
// nothing in tea should depend on compat again.
const compatPkg = "charm.land/lipgloss/v2/compat"
func TestBinaryDoesNotImportLipglossCompat(t *testing.T) {
if _, err := exec.LookPath("go"); err != nil {
t.Skip("go is not on PATH")
}
out, err := exec.Command("go", "list", "-deps", "gitea.dev/tea").Output()
require.NoError(t, err, "go list -deps")
imported := slices.Contains(strings.Fields(string(out)), compatPkg)
assert.False(t, imported,
"%s is back in tea's import graph. It detects the terminal background from "+
"package-level vars, so tea queries the terminal before main() runs, and on "+
"Windows that hangs at start-up when stdio is redirected.", compatPkg)
}
// Under go test neither stdin nor stdout is a terminal, so HasDarkBackground must
// take the default and return, rather than querying and waiting for an answer.
func TestHasDarkBackgroundDoesNotBlockWithoutTTY(t *testing.T) {
done := make(chan bool, 1)
go func() {
done <- HasDarkBackground()
}()
select {
case got := <-done:
assert.Equal(t, defaultDarkBackground, got)
case <-time.After(5 * time.Second):
t.Fatal("HasDarkBackground blocked when stdio is not a terminal")
}
}
// The title color has to come from the isDark we are handed. It used to come from
// a process-wide value that compat detected at init, which ignored this argument.
func TestThemeHonorsIsDark(t *testing.T) {
dark := GetTheme().Theme(true).Focused.Title.GetForeground()
light := GetTheme().Theme(false).Focused.Title.GetForeground()
assert.NotEqual(t, dark, light, "Theme ignored isDark when picking the title color")
}
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
teagit "gitea.dev/tea/modules/git"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTeaCheckoutRemoteReferenceKeepsWorktreeClean(t *testing.T) {
clonePath := setupGitCheckoutTestRepo(t)
t.Chdir(clonePath)
repo, err := teagit.RepoFromPath(clonePath)
require.NoError(t, err)
err = repo.TeaCheckout(teagit.NewRemoteReferenceName("origin", "feature/test-branch"))
require.NoError(t, err)
assert.Empty(t, gitOutput(t, clonePath, "status", "--porcelain"))
assert.Equal(t, "HEAD", gitOutput(t, clonePath, "rev-parse", "--abbrev-ref", "HEAD"))
}
func TestTeaCreateBranchTracksRemoteBranch(t *testing.T) {
clonePath := setupGitCheckoutTestRepo(t)
t.Chdir(clonePath)
repo, err := teagit.RepoFromPath(clonePath)
require.NoError(t, err)
err = repo.TeaCreateBranch("pulls/123", "feature/test-branch", "origin")
require.NoError(t, err)
err = repo.TeaCheckout(teagit.NewBranchReferenceName("pulls/123"))
require.NoError(t, err)
assert.Empty(t, gitOutput(t, clonePath, "status", "--porcelain"))
assert.Equal(t, "origin", gitOutput(t, clonePath, "config", "--get", "branch.pulls/123.remote"))
assert.Equal(t, "refs/heads/feature/test-branch", gitOutput(t, clonePath, "config", "--get", "branch.pulls/123.merge"))
assert.Equal(t, "pulls/123", gitOutput(t, clonePath, "rev-parse", "--abbrev-ref", "HEAD"))
}
func setupGitCheckoutTestRepo(t *testing.T) string {
t.Helper()
tmpDir := t.TempDir()
remotePath := filepath.Join(tmpDir, "remote.git")
seedPath := filepath.Join(tmpDir, "seed")
clonePath := filepath.Join(tmpDir, "clone")
runGit(t, tmpDir, "init", "--bare", remotePath)
runGit(t, tmpDir, "init", seedPath)
runGit(t, seedPath, "config", "user.email", "test@example.com")
runGit(t, seedPath, "config", "user.name", "Test User")
require.NoError(t, os.WriteFile(filepath.Join(seedPath, "README.md"), []byte("# Test Repo\n"), 0o644))
runGit(t, seedPath, "add", "README.md")
runGit(t, seedPath, "commit", "-m", "Initial commit")
runGit(t, seedPath, "branch", "-M", "main")
runGit(t, seedPath, "remote", "add", "origin", remotePath)
runGit(t, seedPath, "push", "-u", "origin", "main")
runGit(t, seedPath, "checkout", "-b", "feature/test-branch")
require.NoError(t, os.WriteFile(filepath.Join(seedPath, "feature.txt"), []byte("feature\n"), 0o644))
runGit(t, seedPath, "add", "feature.txt")
runGit(t, seedPath, "commit", "-m", "Add feature")
runGit(t, seedPath, "push", "-u", "origin", "feature/test-branch")
runGit(t, tmpDir, "clone", remotePath, clonePath)
return clonePath
}
func runGit(t *testing.T, dir string, args ...string) {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Dir = dir
output, err := cmd.CombinedOutput()
require.NoErrorf(t, err, "git %s failed: %s", strings.Join(args, " "), strings.TrimSpace(string(output)))
}
func gitOutput(t *testing.T, dir string, args ...string) string {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Dir = dir
output, err := cmd.CombinedOutput()
require.NoErrorf(t, err, "git %s failed: %s", strings.Join(args, " "), strings.TrimSpace(string(output)))
return strings.TrimSpace(string(output))
}
+114
View File
@@ -0,0 +1,114 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"context"
"encoding/base64"
"fmt"
"strconv"
"strings"
"testing"
"time"
"gitea.dev/tea/cmd/pulls"
gitea "gitea.dev/sdk"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
)
func TestPullsReply(t *testing.T) {
login := createIntegrationLogin(t)
client := login.Client()
timestamp := time.Now().UnixNano()
repoName := fmt.Sprintf("tea-pr-reply-%d", timestamp)
featureBranch := fmt.Sprintf("reply-test-%d", timestamp)
replyBody := fmt.Sprintf("Thanks for the review %d", timestamp)
repo, _, err := client.Repositories.CreateRepo(t.Context(), gitea.CreateRepoOption{
Name: repoName,
AutoInit: true,
DefaultBranch: "main",
})
require.NoError(t, err)
t.Cleanup(func() {
if _, delErr := client.Repositories.DeleteRepo(t.Context(), login.User, repoName); delErr != nil {
t.Logf("failed to delete integration test repo %q: %v", repoName, delErr)
}
})
baseBranch := repo.DefaultBranch
if baseBranch == "" {
baseBranch = "main"
}
_, _, err = client.Repositories.CreateFile(t.Context(), login.User, repoName, "review.txt", gitea.CreateFileOptions{
FileOptions: gitea.FileOptions{
Message: "add review target",
BranchName: baseBranch,
NewBranchName: featureBranch,
},
Content: base64.StdEncoding.EncodeToString([]byte("line for review\n")),
})
require.NoError(t, err)
pr, _, err := client.PullRequests.CreatePullRequest(t.Context(), login.User, repoName, gitea.CreatePullRequestOption{
Base: baseBranch,
Head: featureBranch,
Title: "Integration test for pr reply",
Body: "Adds a file so we can reply to a review comment.",
})
require.NoError(t, err)
review, _, err := client.PullRequests.CreatePullReview(t.Context(), login.User, repoName, pr.Index, gitea.CreatePullReviewOptions{
State: gitea.ReviewStateComment,
Body: "Please take another look.",
Comments: []gitea.CreatePullReviewComment{{
Path: "review.txt",
Body: "Could you clarify this line?",
NewLineNum: 1,
}},
})
require.NoError(t, err)
comments, _, err := client.PullRequests.ListPullReviewComments(t.Context(), login.User, repoName, pr.Index, review.ID)
require.NoError(t, err)
require.Len(t, comments, 1)
pullsCmd := &cli.Command{
Name: "pulls",
Commands: []*cli.Command{&pulls.CmdPullsReply},
}
err = pullsCmd.Run(context.Background(), []string{
"pulls",
"reply",
strconv.FormatInt(pr.Index, 10),
strconv.FormatInt(comments[0].ID, 10),
replyBody,
"--login",
login.Name,
"--repo",
repo.FullName,
})
if err != nil && strings.Contains(err.Error(), "unknown API error: 405") {
t.Skip("pull review comment replies are not supported by this integration Gitea instance")
}
require.NoError(t, err)
require.Eventually(t, func() bool {
reviewComments, _, listErr := client.PullRequests.ListPullReviewComments(t.Context(), login.User, repoName, pr.Index, review.ID)
if listErr != nil {
t.Logf("failed to list review comments: %v", listErr)
return false
}
for _, reviewComment := range reviewComments {
if reviewComment.Body == replyBody && reviewComment.ReviewID == review.ID {
return true
}
}
return false
}, 10*time.Second, 500*time.Millisecond)
}