From 993eb37b5768a763b5fbc25d5b05efe807c3a38e Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 26 Jul 2026 01:37:33 +0000 Subject: [PATCH] Fix notifications --mine outside git repositories (#1056) Fixes #1055. ## Root cause `tea notifications --mine` still initialized full repository context before checking the global notification scope, so it probed the current working directory with git and could select or fail on repository-derived context even though repository data is not needed. ## Changes - Add an InitCommand option to skip local git repository discovery when a command does not need repository context. - Use that option for notification list and mark-as operations when `--mine` is set. - Add a regression test that makes `git` fail if invoked and verifies `notifications --mine` still uses the global notifications API. ## Tests - `go test ./cmd/notifications ./modules/context` Reviewed-on: https://gitea.com/gitea/tea/pulls/1056 --- cmd/notifications/list.go | 4 +-- cmd/notifications/list_test.go | 55 ++++++++++++++++++++++++++++++++++ cmd/notifications/mark_as.go | 8 ++--- modules/context/context.go | 48 +++++++++++++++++++---------- 4 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 cmd/notifications/list_test.go diff --git a/cmd/notifications/list.go b/cmd/notifications/list.go index 9daf021f..6ebea505 100644 --- a/cmd/notifications/list.go +++ b/cmd/notifications/list.go @@ -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) diff --git a/cmd/notifications/list_test.go b/cmd/notifications/list_test.go new file mode 100644 index 00000000..197e6fba --- /dev/null +++ b/cmd/notifications/list_test.go @@ -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)) +} diff --git a/cmd/notifications/mark_as.go b/cmd/notifications/mark_as.go index 7d436bbb..82cab66e 100644 --- a/cmd/notifications/mark_as.go +++ b/cmd/notifications/mark_as.go @@ -24,7 +24,7 @@ var CmdNotificationsMarkRead = cli.Command{ ArgsUsage: "[all | ]", 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 | ]", 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 | ]", 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 | ]", 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 } diff --git a/modules/context/context.go b/modules/context/context.go index eedbcf3e..d3f49474 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -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