Skip to content
Open
4 changes: 2 additions & 2 deletions cmd/src/batch_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ func executeBatchSpec(ctx context.Context, opts executeBatchSpecOpts) (err error
if err != nil {
return execUI.CreatingBatchSpecError(lr.MaxUnlicensedChangesets, err)
}
previewURL := cfg.Endpoint + url
previewURL := cfg.endpointURL.JoinPath(url).String()
execUI.CreatingBatchSpecSuccess(previewURL)

hasWorkspaceFiles := false
Expand Down Expand Up @@ -567,7 +567,7 @@ func executeBatchSpec(ctx context.Context, opts executeBatchSpecOpts) (err error
if err != nil {
return err
}
execUI.ApplyingBatchSpecSuccess(cfg.Endpoint + batch.URL)
execUI.ApplyingBatchSpecSuccess(cfg.endpointURL.JoinPath(batch.URL).String())

return nil
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/src/batch_remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ Examples:

executionURL := fmt.Sprintf(
"%s/%s/batch-changes/%s/executions/%s",
strings.TrimSuffix(cfg.Endpoint, "/"),
cfg.endpointURL,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switch to using JoinPath?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe? It is a pretty complex concatenation. Here are a few options for comparison:
Current:

		executionURL := fmt.Sprintf(
			"%s/%s/batch-changes/%s/executions/%s",
			cfg.endpointURL,
			strings.TrimPrefix(namespace.URL, "/"),
			batchChangeName,
			batchSpecID,
		)

Proposal A:

		executionURL := cfg.endpointURL.JoinPath(
			fmt.Sprintf(
				"/%s/batch-changes/%s/executions/%s", namespace.URL,
				batchChangeName,
				batchSpecID,
			),
		).String()

Proposal B:

		executionURL := endpointURL.
			JoinPath(namespace.URL).
			JoinPath("batch-changes").
			JoinPath(batchChangeName).
			JoinPath("executions").
			JoinPath(batchSpecID).
			String()

I think proposal B is the easiest to read, but also it's nearly the same as the original fmt.Sprintf.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to use proposal A unless someone has a strong opinion otherwise.

strings.TrimPrefix(namespace.URL, "/"),
batchChangeName,
batchSpecID,
Expand Down
2 changes: 1 addition & 1 deletion cmd/src/batch_repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ Examples:
Max: max,
RepoCount: len(repos),
Repos: repos,
SourcegraphEndpoint: cfg.Endpoint,
SourcegraphEndpoint: cfg.endpointURL.String(),
}); err != nil {
return err
}
Expand Down
28 changes: 10 additions & 18 deletions cmd/src/code_intel_upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"flag"
"fmt"
"io"
"net/url"
"os"
"strings"
"time"
Expand Down Expand Up @@ -87,10 +86,7 @@ func handleCodeIntelUpload(args []string) error {
return handleUploadError(uploadOptions.SourcegraphInstanceOptions.AccessToken, err)
}

uploadURL, err := makeCodeIntelUploadURL(uploadID)
if err != nil {
return err
}
uploadURL := makeCodeIntelUploadURL(uploadID)

if codeintelUploadFlags.json {
serialized, err := json.Marshal(map[string]any{
Expand Down Expand Up @@ -132,7 +128,7 @@ func codeintelUploadOptions(out *output.Output) upload.UploadOptions {
associatedIndexID = &codeintelUploadFlags.associatedIndexID
}

cfg.AdditionalHeaders["Content-Type"] = "application/x-protobuf+scip"
cfg.additionalHeaders["Content-Type"] = "application/x-protobuf+scip"

logger := upload.NewRequestLogger(
os.Stdout,
Expand All @@ -153,9 +149,9 @@ func codeintelUploadOptions(out *output.Output) upload.UploadOptions {
AssociatedIndexID: associatedIndexID,
},
SourcegraphInstanceOptions: upload.SourcegraphInstanceOptions{
SourcegraphURL: cfg.Endpoint,
AccessToken: cfg.AccessToken,
AdditionalHeaders: cfg.AdditionalHeaders,
SourcegraphURL: cfg.endpointURL.String(),
AccessToken: cfg.accessToken,
AdditionalHeaders: cfg.additionalHeaders,
MaxRetries: 5,
RetryInterval: time.Second,
Path: codeintelUploadFlags.uploadRoute,
Expand Down Expand Up @@ -191,16 +187,12 @@ func printInferredArguments(out *output.Output) {

// makeCodeIntelUploadURL constructs a URL to the upload with the given internal identifier.
// The base of the URL is constructed from the configured Sourcegraph instance.
func makeCodeIntelUploadURL(uploadID int) (string, error) {
url, err := url.Parse(cfg.Endpoint)
if err != nil {
return "", err
}

func makeCodeIntelUploadURL(uploadID int) string {
// Careful: copy by dereference makes a shallow copy, so User is not duplicated.
u := *cfg.endpointURL
graphqlID := base64.URLEncoding.EncodeToString(fmt.Appendf(nil, `SCIPUpload:%d`, uploadID))
url.Path = codeintelUploadFlags.repo + "/-/code-intelligence/uploads/" + graphqlID
url.User = nil
return url.String(), nil
u.Path = codeintelUploadFlags.repo + "/-/code-intelligence/uploads/" + graphqlID
return u.String()
}

type errorWithHint struct {
Expand Down
2 changes: 1 addition & 1 deletion cmd/src/debug_compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Examples:
return errors.Wrap(err, "failed to get containers for subcommand with err")
}
// Safety check user knows what they are targeting with this debug command
log.Printf("This command will archive docker-cli data for %d containers\n SRC_ENDPOINT: %v\n Output filename: %v", len(containers), cfg.Endpoint, base)
log.Printf("This command will archive docker-cli data for %d containers\n SRC_ENDPOINT: %v\n Output filename: %v", len(containers), cfg.endpointURL, base)
if verified, _ := verify("Do you want to start writing to an archive?"); !verified {
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/src/debug_kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Examples:
return errors.Wrapf(err, "failed to get current-context")
}
// Safety check user knows what they've targeted with this command
log.Printf("Archiving kubectl data for %d pods\n SRC_ENDPOINT: %v\n Context: %s Namespace: %v\n Output filename: %v", len(pods.Items), cfg.Endpoint, kubectx, namespace, base)
log.Printf("Archiving kubectl data for %d pods\n SRC_ENDPOINT: %v\n Context: %s Namespace: %v\n Output filename: %v", len(pods.Items), cfg.endpointURL, kubectx, namespace, base)
if verified, _ := verify("Do you want to start writing to an archive?"); !verified {
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/src/debug_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Examples:
defer zw.Close()

// Safety check user knows what they are targeting with this debug command
log.Printf("This command will archive docker-cli data for container: %s\n SRC_ENDPOINT: %s\n Output filename: %s", container, cfg.Endpoint, base)
log.Printf("This command will archive docker-cli data for container: %s\n SRC_ENDPOINT: %s\n Output filename: %s", container, cfg.endpointURL, base)
if verified, _ := verify("Do you want to start writing to an archive?"); !verified {
return nil
}
Expand Down
34 changes: 15 additions & 19 deletions cmd/src/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,23 @@ Examples:
if err := flagSet.Parse(args); err != nil {
return err
}
endpoint := cfg.Endpoint

if flagSet.NArg() >= 1 {
endpoint = flagSet.Arg(0)
}
if endpoint == "" {
return cmderrors.Usage("expected exactly one argument: the Sourcegraph URL, or SRC_ENDPOINT to be set")
arg := flagSet.Arg(0)
parsed, err := parseEndpoint(arg)
if err != nil {
return cmderrors.Usage(fmt.Sprintf("invalid endpoint URL: %s", arg))
}
if parsed.String() != cfg.endpointURL.String() {
return cmderrors.Usage(fmt.Sprintf("The configured endpoint is %s, not %s", cfg.endpointURL, parsed))
}
Comment on lines +64 to +66
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existence of this check calls into question the command-line parameter - SRC_ENDPOINT (or the value from the config file) is the source[1] of truth, forcing the user to put the same value on the command line.
Adding to the confusion is that if there is nothing in the config file or in SRC_ENDPOINT, cfg.endpointURL defaults to https://sourcegraph.com, which is probably wrong pretty much all of the time, and still invalidates anything different on the command line.
I kept the error condition and message (although I did change the semantics by changing from an exit error to a usage error) because I didn't want to rip that rug out from under anyone at this point.
Thoughts?

[1] no pun intended, but I do like it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's reasonable to keep backwards maintainability in this PR, but follow up with one to make the breaking changes. That makes it easier to isolate and revert if necessary.

Copy link
Member

@keegancsmith keegancsmith Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you are changing the behaviour here. The old behaviour ignored SRC_ENDPOINT for login if the argument was set. The old error message was misleading

Edit: I see in the loginCmd function they checked if cfg.Endpoint is the same. I imagine that was a mistake. I agree with you about how this should work. But for now lets keep the same behaviour.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For illustration, here's how it works currently (before these changes):

▶ SRC_ENDPOINT=https://sourcegraph.coursegraph.com src login https://sourcegraph.com

❌ Problem: The configured endpoint is https://sourcegraph.coursegraph.com, not https://sourcegraph.com.

🛠  To fix: Create an access token by going to https://sourcegraph.com/user/settings/tokens, then set the following environment variables in your terminal:

   export SRC_ENDPOINT=https://sourcegraph.com
   export SRC_ACCESS_TOKEN=(your access token)

   To verify that it's working, run the login command again.

And here's how it works after these changes:

▶ SRC_ENDPOINT=https://sourcegraph.coursegraph.com ./src login https://sourcegraph.com
error: The configured endpoint is https://sourcegraph.coursegraph.com, not https://sourcegraph.com

'src login' helps you authenticate 'src' to access a Sourcegraph instance with your user credentials.

Usage:

    src login SOURCEGRAPH_URL

Examples:
...

}

client := cfg.apiClient(apiFlags, io.Discard)

return loginCmd(context.Background(), loginParams{
cfg: cfg,
client: client,
endpoint: endpoint,
out: os.Stdout,
useOAuth: *useOAuth,
apiFlags: apiFlags,
Expand All @@ -85,7 +88,6 @@ Examples:
type loginParams struct {
cfg *config
client api.Client
endpoint string
out io.Writer
useOAuth bool
apiFlags *api.Flags
Expand All @@ -99,16 +101,15 @@ type loginFlowKind int
const (
loginFlowOAuth loginFlowKind = iota
loginFlowMissingAuth
loginFlowEndpointConflict
loginFlowValidate
)

var loadStoredOAuthToken = oauth.LoadToken

func loginCmd(ctx context.Context, p loginParams) error {
if p.cfg.ConfigFilePath != "" {
if p.cfg.configFilePath != "" {
fmt.Fprintln(p.out)
fmt.Fprintf(p.out, "⚠️ Warning: Configuring src with a JSON file is deprecated. Please migrate to using the env vars SRC_ENDPOINT, SRC_ACCESS_TOKEN, and SRC_PROXY instead, and then remove %s. See https://github.com/sourcegraph/src-cli#readme for more information.\n", p.cfg.ConfigFilePath)
fmt.Fprintf(p.out, "⚠️ Warning: Configuring src with a JSON file is deprecated. Please migrate to using the env vars SRC_ENDPOINT, SRC_ACCESS_TOKEN, and SRC_PROXY instead, and then remove %s. See https://github.com/sourcegraph/src-cli#readme for more information.\n", p.cfg.configFilePath)
}

_, flow := selectLoginFlow(ctx, p)
Expand All @@ -117,28 +118,23 @@ func loginCmd(ctx context.Context, p loginParams) error {

// selectLoginFlow decides what login flow to run based on flags and config.
func selectLoginFlow(ctx context.Context, p loginParams) (loginFlowKind, loginFlow) {
endpointArg := cleanEndpoint(p.endpoint)

if p.useOAuth {
return loginFlowOAuth, runOAuthLogin
}
if !hasEffectiveAuth(ctx, p.cfg, endpointArg) {
if !hasEffectiveAuth(ctx, p.cfg) {
return loginFlowMissingAuth, runMissingAuthLogin
}
if endpointArg != p.cfg.Endpoint {
return loginFlowEndpointConflict, runEndpointConflictLogin
}
return loginFlowValidate, runValidatedLogin
}

// hasEffectiveAuth determines whether we have auth credentials to continue. It first checks for a resolved Access Token in
// config, then it checks for a stored OAuth token.
func hasEffectiveAuth(ctx context.Context, cfg *config, resolvedEndpoint string) bool {
if cfg.AccessToken != "" {
func hasEffectiveAuth(ctx context.Context, cfg *config) bool {
if cfg.accessToken != "" {
return true
}

if _, err := loadStoredOAuthToken(ctx, resolvedEndpoint); err == nil {
if _, err := loadStoredOAuthToken(ctx, cfg.endpointURL.String()); err == nil {
return true
}

Expand Down
16 changes: 8 additions & 8 deletions cmd/src/login_oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ import (
)

func runOAuthLogin(ctx context.Context, p loginParams) error {
endpointArg := cleanEndpoint(p.endpoint)
client, err := oauthLoginClient(ctx, p, endpointArg)
endpoint := p.cfg.endpointURL.String()
client, err := oauthLoginClient(ctx, p, endpoint)
if err != nil {
printLoginProblem(p.out, fmt.Sprintf("OAuth Device flow authentication failed: %s", err))
fmt.Fprintln(p.out, loginAccessTokenMessage(endpointArg))
fmt.Fprintln(p.out, loginAccessTokenMessage(endpoint))
return cmderrors.ExitCode1
}

if err := validateCurrentUser(ctx, client, p.out, endpointArg); err != nil {
if err := validateCurrentUser(ctx, client, p.out, endpoint); err != nil {
return err
}

Expand All @@ -44,12 +44,12 @@ func oauthLoginClient(ctx context.Context, p loginParams, endpoint string) (api.
}

return api.NewClient(api.ClientOpts{
Endpoint: p.cfg.Endpoint,
AdditionalHeaders: p.cfg.AdditionalHeaders,
EndpointURL: p.cfg.endpointURL,
AdditionalHeaders: p.cfg.additionalHeaders,
Flags: p.apiFlags,
Out: p.out,
ProxyURL: p.cfg.ProxyURL,
ProxyPath: p.cfg.ProxyPath,
ProxyURL: p.cfg.proxyURL,
ProxyPath: p.cfg.proxyPath,
OAuthToken: token,
}), nil
}
Expand Down
Loading
Loading