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
25 changes: 23 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,26 @@ type DefaultSection struct {
}

type DomainSection struct {
Type string // github, gitlab, gitea, forgejo
Token string // only from user config, never .forge
Type string // github, gitlab, gitea, forgejo
Token string // only from user config, never .forge
SSHHost string // alternate host for git-over-ssh; the section name remains the API host
}

// DomainForSSHHost returns the API domain (the section name) whose ssh_host
// matches the given host, or "" if none. Self-hosted GitLab in particular can
// serve git-over-ssh on a different host than the web/API, so a remote URL like
// git@ssh.gitlab.test:owner/repo needs mapping back to gitlab.test before we
// build an API client.
func (c *Config) DomainForSSHHost(sshHost string) string {
if c == nil {
return ""
}
for name, ds := range c.Domains {
if ds.SSHHost == sshHost {
return name
}
}
return ""
}

var (
Expand Down Expand Up @@ -108,6 +126,9 @@ func loadFile(cfg *Config, path string, allowTokens bool) error {
if v, ok := kv["type"]; ok {
ds.Type = v
}
if v, ok := kv["ssh_host"]; ok {
ds.SSHHost = v
}
if allowTokens {
if v, ok := kv["token"]; ok {
ds.Token = v
Expand Down
83 changes: 83 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,89 @@ key = value with spaces
}
}

func TestParseINISSHHost(t *testing.T) {
input := `[gitlab.test]
type = gitlab
ssh_host = ssh.gitlab.test
`

cfg := &Config{Domains: make(map[string]DomainSection)}
sections, err := parseINI(strings.NewReader(input))
if err != nil {
t.Fatal(err)
}
for name, kv := range sections {
ds := cfg.Domains[name]
if v, ok := kv["type"]; ok {
ds.Type = v
}
if v, ok := kv["ssh_host"]; ok {
ds.SSHHost = v
}
cfg.Domains[name] = ds
}

got := cfg.Domains["gitlab.test"].SSHHost
if got != "ssh.gitlab.test" {
t.Errorf("expected SSHHost=ssh.gitlab.test, got %q", got)
}
}

func TestDomainForSSHHost(t *testing.T) {
cfg := &Config{
Domains: map[string]DomainSection{
"gitlab.test": {Type: "gitlab", SSHHost: "ssh.gitlab.test"},
"github.com": {Type: "github"},
"gitea.test": {Type: "gitea", SSHHost: "git.gitea.test"},
},
}

tests := []struct {
sshHost string
want string
}{
{"ssh.gitlab.test", "gitlab.test"},
{"git.gitea.test", "gitea.test"},
{"github.com", ""}, // no ssh_host configured, no mapping
{"unknown.host", ""}, // not in config at all
{"gitlab.test", ""}, // section name, not ssh_host
}

for _, tt := range tests {
got := cfg.DomainForSSHHost(tt.sshHost)
if got != tt.want {
t.Errorf("DomainForSSHHost(%q) = %q, want %q", tt.sshHost, got, tt.want)
}
}
}

func TestDomainForSSHHostNilConfig(t *testing.T) {
var cfg *Config
got := cfg.DomainForSSHHost("ssh.gitlab.test")
if got != "" {
t.Errorf("nil config should return empty, got %q", got)
}
}

func TestLoadFileReadsSSHHost(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config")
_ = os.WriteFile(path, []byte(`[gitlab.test]
type = gitlab
ssh_host = ssh.gitlab.test
`), 0600)

cfg := &Config{Domains: make(map[string]DomainSection)}
if err := loadFile(cfg, path, true); err != nil {
t.Fatal(err)
}

got := cfg.Domains["gitlab.test"].SSHHost
if got != "ssh.gitlab.test" {
t.Errorf("loadFile should populate SSHHost, got %q", got)
}
}

func TestLoadMergesUserAndProject(t *testing.T) {
ResetCache()
defer ResetCache()
Expand Down
18 changes: 17 additions & 1 deletion internal/resolve/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,23 @@ func resolveRemote() (domain, owner, repo string, err error) {
if err != nil {
return "", "", "", fmt.Errorf("parsing remote %q URL: %w", remoteName, err)
}
return domain, owner, repo, nil
return mapSSHHost(domain), owner, repo, nil
}

// mapSSHHost translates a git-over-ssh hostname to the corresponding API
// hostname when the config declares them as different. Self-hosted GitLab
// can serve ssh on ssh.gitlab.test while the API lives at gitlab.test;
// without this mapping the parsed remote domain would point at the wrong host.
// Returns the input unchanged when no mapping is configured.
func mapSSHHost(domain string) string {
cfg, err := config.Load()
if err != nil {
return domain
}
if api := cfg.DomainForSSHHost(domain); api != "" {
return api
}
return domain
}

func gitRemoteURL(name string) (string, error) {
Expand Down
48 changes: 48 additions & 0 deletions internal/resolve/resolve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,58 @@ package resolve
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"

"github.com/git-pkgs/forge/internal/config"
)

func TestMapSSHHost(t *testing.T) {
config.ResetCache()
defer config.ResetCache()

dir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", dir)
cfgDir := filepath.Join(dir, "forge")
_ = os.MkdirAll(cfgDir, 0700)
_ = os.WriteFile(filepath.Join(cfgDir, "config"), []byte(`[gitlab.test]
type = gitlab
ssh_host = ssh.gitlab.test
`), 0600)

tests := []struct {
in string
want string
}{
// remote URL host matches a configured ssh_host: map to the API host
{"ssh.gitlab.test", "gitlab.test"},
// no mapping: pass through unchanged
{"github.com", "github.com"},
{"gitlab.test", "gitlab.test"},
}

for _, tt := range tests {
got := mapSSHHost(tt.in)
if got != tt.want {
t.Errorf("mapSSHHost(%q) = %q, want %q", tt.in, got, tt.want)
}
}
}

func TestMapSSHHostNoConfig(t *testing.T) {
config.ResetCache()
defer config.ResetCache()

t.Setenv("XDG_CONFIG_HOME", t.TempDir())

// With no config file, the domain passes through unchanged.
got := mapSSHHost("ssh.gitlab.test")
if got != "ssh.gitlab.test" {
t.Errorf("with no config, expected passthrough, got %q", got)
}
}

func TestTokenForDomain(t *testing.T) {
// With no env vars set, should return empty
got := TokenForDomain("example.com")
Expand Down