Skip to content

Commit 231f9e5

Browse files
authored
Add support for custom SSH hostname in clone URLs (#612)
1 parent 4067b48 commit 231f9e5

File tree

10 files changed

+107
-6
lines changed

10 files changed

+107
-6
lines changed

cmd/clone.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,10 @@ func cloneFunc(cmd *cobra.Command, argz []string) {
314314
os.Setenv("GHORG_OUTPUT_DIR", d)
315315
}
316316

317+
if cmd.Flags().Changed("ssh-hostname") {
318+
os.Setenv("GHORG_SSH_HOSTNAME", cmd.Flag("ssh-hostname").Value.String())
319+
}
320+
317321
if len(argz) < 1 {
318322
if os.Getenv("GHORG_SCM_TYPE") == "github" && os.Getenv("GHORG_CLONE_TYPE") == "user" {
319323
argz = append(argz, "")

cmd/root.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ var (
5353
cronTimerMinutes string
5454
recloneServerPort string
5555
cloneDelaySeconds string
56+
sshHostname string
5657
includeSubmodules bool
5758
skipArchived bool
5859
skipForks bool
@@ -230,6 +231,8 @@ func getOrSetDefaults(envVar string) {
230231
os.Setenv(envVar, "1")
231232
case "GHORG_GITHUB_TOKEN_FROM_GITHUB_APP":
232233
os.Setenv(envVar, "false")
234+
case "GHORG_SSH_HOSTNAME":
235+
os.Setenv(envVar, "")
233236
}
234237
} else {
235238
s := viper.GetString(envVar)
@@ -350,6 +353,7 @@ func InitConfig() {
350353
getOrSetDefaults("GHORG_GITEA_TOKEN")
351354
getOrSetDefaults("GHORG_SOURCEHUT_TOKEN")
352355
getOrSetDefaults("GHORG_INSECURE_GITEA_CLIENT")
356+
getOrSetDefaults("GHORG_SSH_HOSTNAME")
353357
getOrSetDefaults("GHORG_GITHUB_APP_PEM_PATH")
354358
getOrSetDefaults("GHORG_GITHUB_APP_INSTALLATION_ID")
355359
getOrSetDefaults("GHORG_GITHUB_APP_ID")
@@ -427,8 +431,10 @@ func init() {
427431
cloneCmd.Flags().StringVarP(&githubFilterLanguage, "github-filter-language", "", "", "GHORG_GITHUB_FILTER_LANGUAGE - Filter repos by a language. Can be a comma separated value with no spaces.")
428432
cloneCmd.Flags().StringVarP(&githubUserOption, "github-user-option", "", "", "GHORG_GITHUB_USER_OPTION - Only available when also using GHORG_CLONE_TYPE: user e.g. --clone-type=user can be one of: all, owner, member (default: owner)")
429433
cloneCmd.Flags().StringVarP(&githubAppID, "github-app-id", "", "", "GHORG_GITHUB_APP_ID - GitHub App ID, for authenticating with GitHub App")
434+
cloneCmd.Flags().StringVar(&sshHostname, "ssh-hostname", "", "GHORG_SSH_HOSTNAME - Hostname to use for SSH clone URLs (e.g., my-github-alias for git@my-github-alias:org/repo.git)")
430435

431436
reCloneCmd.Flags().StringVarP(&ghorgReClonePath, "reclone-path", "", "", "GHORG_RECLONE_PATH - If you want to set a path other than $HOME/.config/ghorg/reclone.yaml for your reclone configuration")
437+
reCloneCmd.Flags().StringVar(&sshHostname, "ssh-hostname", "", "GHORG_SSH_HOSTNAME - Hostname to use for SSH clone URLs (e.g., my-github-alias for git@my-github-alias:org/repo.git)")
432438
reCloneCmd.Flags().BoolVar(&ghorgReCloneQuiet, "quiet", false, "GHORG_RECLONE_QUIET - Quiet logging output")
433439
reCloneCmd.Flags().BoolVar(&ghorgReCloneList, "list", false, "Prints reclone commands and optional descriptions to stdout then will exit 0. Does not obsfucate tokens, and is only available as a commandline argument")
434440
reCloneCmd.Flags().BoolVar(&ghorgReCloneEnvConfigOnly, "env-config-only", false, "GHORG_RECLONE_ENV_CONFIG_ONLY - Only use environment variables to set the configuration for all reclones.")

sample-conf.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ GHORG_SCM_TYPE: github
1313
# flag (--protocol) eg: --protocol=https
1414
GHORG_CLONE_PROTOCOL: https
1515

16+
# Override SSH hostname in clone URLs. Useful when you have multiple SSH host
17+
# aliases in your ~/.ssh/config for the same SCM provider.
18+
# For example, setting this to "my-github-alias" will change clone URLs from:
19+
# git@github.com:org/repo.git -> git@my-github-alias:org/repo.git
20+
# Only applies when using SSH clone protocol (--protocol=ssh)
21+
# flag (--ssh-hostname) eg: --ssh-hostname=my-github-alias
22+
GHORG_SSH_HOSTNAME:
23+
1624
# This is where your ghorg directory will be created, use absolute pathing, shell expansions will not work.
1725
# The ghorg directory is the home for all ghorg clones
1826
# See https://github.com/gabrie30/ghorg#changing-clone-directories for example

scm/bitbucket.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ func (c Bitbucket) filter(resp []bitbucket.Repository) (repoData []Repo, err err
361361

362362
if os.Getenv("GHORG_CLONE_PROTOCOL") == "ssh" && linkType == "ssh" {
363363
r.URL = link.(string)
364-
r.CloneURL = link.(string)
364+
r.CloneURL = ReplaceSSHHostname(link.(string), os.Getenv("GHORG_SSH_HOSTNAME"))
365365
cloneData = append(cloneData, r)
366366
} else if os.Getenv("GHORG_CLONE_PROTOCOL") == "https" && linkType == "https" {
367367
r.URL = link.(string)

scm/filter.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package scm
22

33
import (
44
"os"
5+
"regexp"
56
"strings"
67
)
78

@@ -23,3 +24,16 @@ func hasMatchingTopic(rpTopics []string) bool {
2324
// If no user defined topics are specified, accept any topics
2425
return true
2526
}
27+
28+
// ReplaceSSHHostname replaces the hostname in an SSH clone URL with a custom hostname.
29+
// This allows users to leverage SSH configs with multiple host aliases.
30+
// For example: git@gitlab.com:org/repo.git -> git@my-gitlab-alias:org/repo.git
31+
func ReplaceSSHHostname(sshURL string, newHostname string) string {
32+
if newHostname == "" {
33+
return sshURL
34+
}
35+
36+
// Match SSH URLs in the format git@hostname:path or git@hostname/path
37+
re := regexp.MustCompile(`^git@([^:/]+)([:\/])`)
38+
return re.ReplaceAllString(sshURL, "git@"+newHostname+"$2")
39+
}

scm/filter_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,68 @@ func TestMatchingTopicsWithMultipleEnvTopics(t *testing.T) {
110110

111111
os.Setenv("GHORG_TOPICS", "")
112112
}
113+
114+
func TestReplaceSSHHostname(t *testing.T) {
115+
t.Run("When newHostname is empty, return original URL unchanged", func(tt *testing.T) {
116+
originalURL := "git@github.com:org/repo.git"
117+
want := originalURL
118+
got := ReplaceSSHHostname(originalURL, "")
119+
if want != got {
120+
tt.Errorf("Expected %v, got: %v", want, got)
121+
}
122+
})
123+
124+
t.Run("When replacing GitHub SSH hostname with colon separator", func(tt *testing.T) {
125+
originalURL := "git@github.com:org/repo.git"
126+
want := "git@my-github-alias:org/repo.git"
127+
got := ReplaceSSHHostname(originalURL, "my-github-alias")
128+
if want != got {
129+
tt.Errorf("Expected %v, got: %v", want, got)
130+
}
131+
})
132+
133+
t.Run("When replacing GitLab SSH hostname", func(tt *testing.T) {
134+
originalURL := "git@gitlab.com:group/subgroup/repo.git"
135+
want := "git@custom.gitlab.host:group/subgroup/repo.git"
136+
got := ReplaceSSHHostname(originalURL, "custom.gitlab.host")
137+
if want != got {
138+
tt.Errorf("Expected %v, got: %v", want, got)
139+
}
140+
})
141+
142+
t.Run("When replacing Bitbucket SSH hostname", func(tt *testing.T) {
143+
originalURL := "git@bitbucket.org:workspace/repo.git"
144+
want := "git@bitbucket-alias:workspace/repo.git"
145+
got := ReplaceSSHHostname(originalURL, "bitbucket-alias")
146+
if want != got {
147+
tt.Errorf("Expected %v, got: %v", want, got)
148+
}
149+
})
150+
151+
t.Run("When replacing self-hosted GitLab SSH hostname", func(tt *testing.T) {
152+
originalURL := "git@gitlab.example.com:org/repo.git"
153+
want := "git@my-gitlab:org/repo.git"
154+
got := ReplaceSSHHostname(originalURL, "my-gitlab")
155+
if want != got {
156+
tt.Errorf("Expected %v, got: %v", want, got)
157+
}
158+
})
159+
160+
t.Run("When URL has slash separator instead of colon", func(tt *testing.T) {
161+
originalURL := "git@git.sr.ht/~user/repo"
162+
want := "git@sourcehut-alias/~user/repo"
163+
got := ReplaceSSHHostname(originalURL, "sourcehut-alias")
164+
if want != got {
165+
tt.Errorf("Expected %v, got: %v", want, got)
166+
}
167+
})
168+
169+
t.Run("When hostname has subdomain", func(tt *testing.T) {
170+
originalURL := "git@git.company.example.com:org/repo.git"
171+
want := "git@my-gitlab:org/repo.git"
172+
got := ReplaceSSHHostname(originalURL, "my-gitlab")
173+
if want != got {
174+
tt.Errorf("Expected %v, got: %v", want, got)
175+
}
176+
})
177+
}

scm/gitea.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ func (c Gitea) filter(rps []*gitea.Repository) (repoData []Repo, err error) {
200200
r.URL = cloneURL
201201
repoData = append(repoData, r)
202202
} else {
203-
r.CloneURL = rp.SSHURL
203+
r.CloneURL = ReplaceSSHHostname(rp.SSHURL, os.Getenv("GHORG_SSH_HOSTNAME"))
204204
r.URL = rp.SSHURL
205205
repoData = append(repoData, r)
206206
}

scm/github.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ func (c Github) filter(allRepos []*github.Repository) []Repo {
247247
r.URL = *ghRepo.CloneURL
248248
repoData = append(repoData, r)
249249
} else {
250-
r.CloneURL = *ghRepo.SSHURL
250+
r.CloneURL = ReplaceSSHHostname(*ghRepo.SSHURL, os.Getenv("GHORG_SSH_HOSTNAME"))
251251
r.URL = *ghRepo.SSHURL
252252
repoData = append(repoData, r)
253253
}

scm/gitlab.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ func (c Gitlab) createRepoSnippetCloneURL(cloneTargetURL string, snippetID strin
170170
// git@gitlab.example.com/snippets/1.git
171171
cloneURL = strings.Replace(cloneURL, "/", ":", 1)
172172
// git@gitlab.example.com:snippets/1.git
173-
return cloneURL
173+
// Apply custom SSH hostname if configured
174+
return ReplaceSSHHostname(cloneURL, os.Getenv("GHORG_SSH_HOSTNAME"))
174175
}
175176

176177
// hosted example
@@ -192,7 +193,8 @@ func (c Gitlab) createRootLevelSnippetCloneURL(snippetWebURL string) string {
192193
// git@gitlab.example.com/snippets/1.git
193194
cloneURL = strings.Replace(cloneURL, "/", ":", 1)
194195
// git@gitlab.example.com:snippets/1.git
195-
return cloneURL
196+
// Apply custom SSH hostname if configured
197+
return ReplaceSSHHostname(cloneURL, os.Getenv("GHORG_SSH_HOSTNAME"))
196198
}
197199

198200
func (c Gitlab) getRepoSnippets(r Repo) []*gitlab.Snippet {
@@ -557,7 +559,7 @@ func (c Gitlab) filter(group string, ps []*gitlab.Project) []Repo {
557559
r.URL = p.HTTPURLToRepo
558560
repoData = append(repoData, r)
559561
} else {
560-
r.CloneURL = p.SSHURLToRepo
562+
r.CloneURL = ReplaceSSHHostname(p.SSHURLToRepo, os.Getenv("GHORG_SSH_HOSTNAME"))
561563
r.URL = p.SSHURLToRepo
562564
repoData = append(repoData, r)
563565
}

scm/sourcehut.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,8 @@ func (c Sourcehut) filter(rps []repository, apiUsername string, localUsername st
266266
}
267267
// Use repoPathWithTilde for clone URL (git needs the ~ prefix)
268268
r.CloneURL = fmt.Sprintf("%s:%s", gitBase, repoPathWithTilde)
269+
// Apply custom SSH hostname if configured
270+
r.CloneURL = ReplaceSSHHostname(r.CloneURL, os.Getenv("GHORG_SSH_HOSTNAME"))
269271
}
270272

271273
r.URL = r.CloneURL

0 commit comments

Comments
 (0)