Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions cmd/remote/remote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package remote
import (
"testing"

"github.com/calypr/git-drs/client/indexd"
"github.com/calypr/git-drs/config"
"github.com/calypr/git-drs/internal/testutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestRemoteListArgs(t *testing.T) {
Expand Down Expand Up @@ -44,3 +47,52 @@ func TestRemoteSetArgs(t *testing.T) {
err = SetCmd.Args(SetCmd, []string{"origin", "extra"})
assert.Error(t, err)
}

func TestRemoteRemoveArgs(t *testing.T) {
err := RemoveCmd.Args(RemoveCmd, []string{"origin"})
assert.NoError(t, err)

err = RemoveCmd.Args(RemoveCmd, []string{})
assert.Error(t, err)

err = RemoveCmd.Args(RemoveCmd, []string{"origin", "extra"})
assert.Error(t, err)
}

func TestRemoteRemoveAliases(t *testing.T) {
assert.Contains(t, RemoveCmd.Aliases, "rm")
}

func TestRemoteRemoveRun(t *testing.T) {
tmpDir := testutils.SetupTestGitRepo(t)
testutils.CreateTestConfig(t, tmpDir, &config.Config{
DefaultRemote: config.Remote("origin"),
Remotes: map[config.Remote]config.RemoteSelect{
"origin": {
Gen3: &indexd.Gen3Remote{Endpoint: "https://one.example", ProjectID: "proj-a", Bucket: "bucket-a"},
},
"staging": {
Gen3: &indexd.Gen3Remote{Endpoint: "https://two.example", ProjectID: "proj-b", Bucket: "bucket-b"},
},
},
})

err := RemoveCmd.RunE(RemoveCmd, []string{"origin"})
require.NoError(t, err)

cfg, err := config.LoadConfig()
require.NoError(t, err)

_, hasOrigin := cfg.Remotes[config.Remote("origin")]
assert.False(t, hasOrigin)
assert.Equal(t, config.Remote("staging"), cfg.DefaultRemote)
}

func TestRemoteRemoveRunNotFound(t *testing.T) {
tmpDir := testutils.SetupTestGitRepo(t)
testutils.CreateDefaultTestConfig(t, tmpDir)

err := RemoveCmd.RunE(RemoveCmd, []string{"missing"})
require.Error(t, err)
assert.Contains(t, err.Error(), "remote 'missing' not found")
}
47 changes: 47 additions & 0 deletions cmd/remote/remove.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package remote

import (
"fmt"
"sort"

"github.com/calypr/git-drs/config"
"github.com/spf13/cobra"
)

var RemoveCmd = &cobra.Command{
Use: "remove <remote-name>",
Aliases: []string{"rm"},
Short: "Remove a configured DRS remote",
Long: "Remove a configured DRS remote and update the default remote if needed",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
cmd.SilenceUsage = false
return fmt.Errorf("error: requires exactly 1 argument (remote name), received %d\n\nUsage: %s\n\nRun 'git drs remote list' to see available remotes or 'git drs remote rm --help' for more details", len(args), cmd.UseLine())
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
remoteName := args[0]

cfg, err := config.LoadConfig()
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}

remote := config.Remote(remoteName)
if _, ok := cfg.Remotes[remote]; !ok {
availableRemotes := make([]string, 0, len(cfg.Remotes))
for name := range cfg.Remotes {
availableRemotes = append(availableRemotes, string(name))
}
sort.Strings(availableRemotes)
return fmt.Errorf("remote '%s' not found.\nAvailable remotes: %v", remoteName, availableRemotes)
}

if err := config.RemoveRemote(remote); err != nil {
return fmt.Errorf("failed to remove remote: %w", err)
}

return nil
},
}
1 change: 1 addition & 0 deletions cmd/remote/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ func init() {
Cmd.AddCommand(add.Cmd)
Cmd.AddCommand(ListCmd)
Cmd.AddCommand(SetCmd)
Cmd.AddCommand(RemoveCmd)
}
70 changes: 70 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"log"
"log/slog"
"path/filepath"
"sort"
"strings"

"github.com/calypr/data-client/g3client"
Expand Down Expand Up @@ -321,6 +322,75 @@ func GetProjectId(remote Remote) (string, error) {
return rmt.GetProjectId(), nil
}

// RemoveRemote removes a configured remote and updates default-remote when required
func RemoveRemote(name Remote) error {
repo, err := getRepo()
if err != nil {
return err
}

conf, err := repo.Config()
if err != nil {
return err
}

section := conf.Raw.Section(newConfigSection)
legacySection := conf.Raw.Section(legacyConfigSection)
root := section.Subsection(newConfigSubsectionRoot)

remoteSubsectionName := fmt.Sprintf("%s.%s%s", newConfigSubsectionRoot, remoteSubsectionPrefix, name)
legacyRemoteSubsectionName := fmt.Sprintf("%s%s", remoteSubsectionPrefix, name)

hasNamespaced := section.HasSubsection(remoteSubsectionName)
hasLegacy := legacySection.HasSubsection(legacyRemoteSubsectionName)
if !hasNamespaced && !hasLegacy {
return fmt.Errorf("remote '%s' not found", name)
}

if hasNamespaced {
section.RemoveSubsection(remoteSubsectionName)
}
if hasLegacy {
legacySection.RemoveSubsection(legacyRemoteSubsectionName)
}

defaultRemote := root.Option("default-remote")
if defaultRemote == "" {
defaultRemote = legacySection.Option("default-remote")
}

if defaultRemote == string(name) {
remainingSet := make(map[string]struct{})
for _, subsection := range section.Subsections {
if !strings.HasPrefix(subsection.Name, newConfigSubsectionRoot+"."+remoteSubsectionPrefix) {
continue
}
rest := strings.TrimPrefix(subsection.Name, newConfigSubsectionRoot+".")
remainingSet[strings.TrimPrefix(rest, remoteSubsectionPrefix)] = struct{}{}
}
for _, subsection := range legacySection.Subsections {
if !strings.HasPrefix(subsection.Name, remoteSubsectionPrefix) {
continue
}
remainingSet[strings.TrimPrefix(subsection.Name, remoteSubsectionPrefix)] = struct{}{}
}

remaining := make([]string, 0, len(remainingSet))
for remoteName := range remainingSet {
remaining = append(remaining, remoteName)
}
sort.Strings(remaining)

root.RemoveOption("default-remote")
legacySection.RemoveOption("default-remote")
if len(remaining) > 0 {
root.SetOption("default-remote", remaining[0])
}
}

return repo.Storer.SetConfig(conf)
}

// SaveConfig writes the configuration using go-git
func SaveConfig(cfg *Config) error {
repo, err := getRepo()
Expand Down
92 changes: 92 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,95 @@ func TestLoadConfig_NamespacedKeysTakePrecedence(t *testing.T) {
t.Fatalf("expected namespaced gen3 remote loaded, got %#v", newRemote)
}
}

func TestRemoveRemote(t *testing.T) {
setupTestRepo(t)

_, err := UpdateRemote(Remote("origin"), RemoteSelect{Gen3: &indexd.Gen3Remote{Endpoint: "https://origin.example", ProjectID: "origin-proj", Bucket: "origin-bucket"}})
if err != nil {
t.Fatalf("UpdateRemote origin error: %v", err)
}
_, err = UpdateRemote(Remote("staging"), RemoteSelect{Gen3: &indexd.Gen3Remote{Endpoint: "https://staging.example", ProjectID: "staging-proj", Bucket: "staging-bucket"}})
if err != nil {
t.Fatalf("UpdateRemote staging error: %v", err)
}

if err := RemoveRemote(Remote("origin")); err != nil {
t.Fatalf("RemoveRemote error: %v", err)
}

cfg, err := LoadConfig()
if err != nil {
t.Fatalf("LoadConfig error: %v", err)
}

if _, ok := cfg.Remotes[Remote("origin")]; ok {
t.Fatalf("expected origin to be removed")
}
if cfg.DefaultRemote != Remote("staging") {
t.Fatalf("expected default remote to switch to staging, got %s", cfg.DefaultRemote)
}
}

func TestRemoveRemote_LastRemoteClearsDefault(t *testing.T) {
setupTestRepo(t)

_, err := UpdateRemote(Remote("origin"), RemoteSelect{Gen3: &indexd.Gen3Remote{Endpoint: "https://origin.example", ProjectID: "origin-proj", Bucket: "origin-bucket"}})
if err != nil {
t.Fatalf("UpdateRemote origin error: %v", err)
}

if err := RemoveRemote(Remote("origin")); err != nil {
t.Fatalf("RemoveRemote error: %v", err)
}

cfg, err := LoadConfig()
if err != nil {
t.Fatalf("LoadConfig error: %v", err)
}
if cfg.DefaultRemote != "" {
t.Fatalf("expected default remote to be cleared, got %s", cfg.DefaultRemote)
}
if len(cfg.Remotes) != 0 {
t.Fatalf("expected no remotes, got %d", len(cfg.Remotes))
}
}

func TestRemoveRemote_LegacyRemote(t *testing.T) {
tmpDir := setupTestRepo(t)

commands := [][]string{
{"config", "drs.default-remote", "legacy"},
{"config", "drs.remote.legacy.type", "gen3"},
{"config", "drs.remote.legacy.endpoint", "https://legacy.example"},
{"config", "drs.remote.legacy.project", "legacy-proj"},
{"config", "drs.remote.legacy.bucket", "legacy-bucket"},
{"config", "lfs.customtransfer.drs.remote.new.type", "gen3"},
{"config", "lfs.customtransfer.drs.remote.new.endpoint", "https://new.example"},
{"config", "lfs.customtransfer.drs.remote.new.project", "new-proj"},
{"config", "lfs.customtransfer.drs.remote.new.bucket", "new-bucket"},
}
for _, args := range commands {
cmd := exec.Command("git", args...)
cmd.Dir = tmpDir
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git %v failed: %v: %s", args, err, string(out))
}
}

if err := RemoveRemote(Remote("legacy")); err != nil {
t.Fatalf("RemoveRemote legacy error: %v", err)
}

cfg, err := LoadConfig()
if err != nil {
t.Fatalf("LoadConfig error: %v", err)
}

if _, ok := cfg.Remotes[Remote("legacy")]; ok {
t.Fatalf("expected legacy remote to be removed")
}
if cfg.DefaultRemote != Remote("new") {
t.Fatalf("expected default remote to be reassigned to new, got %s", cfg.DefaultRemote)
}
}
Loading
Loading