Skip to content
Open
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
8 changes: 8 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,19 @@ ghq.<url>.root::
you can specify a repository-specific root directory instead of the common ghq root directory. +
The URL is matched against '<url>' using 'git config --get-urlmatch'.

ghq<url>.hostFolderName::
By default, ghq uses the hostname from the repository URL as the directory name
(e.g., "github.com" for GitHub repositories). With this option, you can specify
a custom folder name to use instead of the hostname for all repositories. +
For example, setting this to "gh" will cause GitHub repositories to be stored
under "gh/" instead of "github.com/".


=== Example configuration (.gitconfig):

....
[ghq "https://git.example.com/repos/"]
hostFolderName = example
vcs = git
root = ~/myproj
....
Expand Down
14 changes: 14 additions & 0 deletions cmd_get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,20 @@ func TestCommandGet(t *testing.T) {
t.Errorf("got: %s, expect: %s", filepath.ToSlash(cloneArgs.local), filepath.ToSlash(localDir))
}
},
}, {
name: "ghq<url>.hostFolderName",
scenario: func(t *testing.T, tmpRoot string, cloneArgs *_cloneArgs, updateArgs *_updateArgs) {
t.Cleanup(gitconfig.WithConfig(t, `
[ghq "https://github.com"]
hostFolderName = gh
`))
app.Run([]string{"", "get", "motemen/ghq-test-repo"})

localDir := filepath.Join(tmpRoot, "gh", "motemen", "ghq-test-repo")
if filepath.ToSlash(cloneArgs.local) != filepath.ToSlash(localDir) {
t.Errorf("got: %s, expect: %s", filepath.ToSlash(cloneArgs.local), filepath.ToSlash(localDir))
}
},
}, {
name: "bare",
scenario: func(t *testing.T, tmpRoot string, cloneArgs *_cloneArgs, updateArgs *_updateArgs) {
Expand Down
8 changes: 6 additions & 2 deletions getter.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,12 @@ func (g *getter) getRemoteRepository(remote RemoteRepository, branch string) (ge
return getInfo{}, err
}
}
hostFolderName, err := getHostFolderName(remoteURL)
if err != nil {
return getInfo{}, err
}
if l := detectLocalRepoRoot(remoteURL.Path, repoURL.Path); l != "" {
localRepoRoot = filepath.Join(local.RootPath, remoteURL.Hostname(), l)
localRepoRoot = filepath.Join(local.RootPath, hostFolderName, l)
}

if g.bare {
Expand Down Expand Up @@ -154,4 +158,4 @@ func detectLocalRepoRoot(remotePath, repoPath string) string {
}
}
return ""
}
}
30 changes: 29 additions & 1 deletion local_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,12 @@ func LocalRepositoryFromFullPath(fullPath string, backend *VCSBackend) (*LocalRe

// LocalRepositoryFromURL resolve LocalRepository from URL
func LocalRepositoryFromURL(remoteURL *url.URL, bare bool) (*LocalRepository, error) {
hostFolderName, err := getHostFolderName(remoteURL)
if err != nil {
return nil, err
}
pathParts := append(
[]string{remoteURL.Hostname()}, strings.Split(remoteURL.Path, "/")...,
[]string{hostFolderName}, strings.Split(remoteURL.Path, "/")...,
)
relPath := strings.TrimSuffix(filepath.Join(pathParts...), ".git")
pathParts[len(pathParts)-1] = strings.TrimSuffix(pathParts[len(pathParts)-1], ".git")
Expand Down Expand Up @@ -141,6 +145,30 @@ func getRoot(u string) (string, error) {
return prim, nil
}

// getHostFolderName returns the configured host folder name for the given URL,
// or the hostname if no specific configuration is found
// getHostFolderName returns the configured host folder name for the given URL,
// or the hostname if no specific configuration is found
func getHostFolderName(remoteURL *url.URL) (string, error) {
// Try to get ghq.hostFolderName config
hostFolderName, err := gitconfig.Do("--path", "--get-urlmatch", "ghq.hostFolderName", remoteURL.String())
if err != nil {
if gitconfig.IsNotFound(err) {
// No config found, use hostname
return remoteURL.Hostname(), nil
}
return "", err
}

// If config exists and is not empty, use it
if strings.TrimSpace(hostFolderName) != "" {
return strings.TrimSpace(hostFolderName), nil
}

// If config exists but is empty, use hostname
return remoteURL.Hostname(), nil
}

// Subpaths returns lists of tail parts of relative path from the root directory (shortest first)
// for example, {"ghq", "motemen/ghq", "github.com/motemen/ghq"} for $root/github.com/motemen/ghq.
func (repo *LocalRepository) Subpaths() []string {
Expand Down
55 changes: 55 additions & 0 deletions local_repository_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"fmt"
"os"
"path/filepath"
"reflect"
Expand Down Expand Up @@ -73,6 +74,60 @@ func TestLocalRepositoryFromFullPath(t *testing.T) {
}
}

func TestGetHostFolderName(t *testing.T) {
testCases := []struct {
name string
url string
configValue string
configExists bool
expect string
}{{
name: "no config, uses hostname",
url: "https://github.com/motemen/ghq.git",
configExists: false,
expect: "github.com",
}, {
name: "config with custom value",
url: "https://github.com/motemen/ghq.git",
configValue: "gh",
configExists: true,
expect: "gh",
}, {
name: "config with empty value, uses hostname",
url: "https://github.com/motemen/ghq.git",
configValue: "",
configExists: true,
expect: "github.com",
}, {
name: "config with whitespace value, uses hostname",
url: "https://github.com/motemen/ghq.git",
configValue: " ",
configExists: true,
expect: "github.com",
}}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.configExists {
cleanup := gitconfig.WithConfig(t, fmt.Sprintf(`
[ghq "https://github.com"]
hostFolderName = %s
`, tc.configValue))
defer cleanup()
}

url := mustParseURL(tc.url)
got, err := getHostFolderName(url)
if err != nil {
t.Errorf("error should be nil but: %s", err)
}
if got != tc.expect {
t.Errorf("got: %s, expect: %s", got, tc.expect)
}
})
}
}

func TestNewLocalRepository(t *testing.T) {
defer func(orig []string) { _localRepositoryRoots = orig }(_localRepositoryRoots)
tmproot := newTempDir(t)
Expand Down