package main import ( "fmt" "net/url" "regexp" "strings" "github.com/Songmu/gitconfig" "github.com/x-motemen/ghq/cmdutil" "github.com/x-motemen/ghq/logger" ) // A RemoteRepository represents a remote repository. type RemoteRepository interface { // URL returns the repository URL. URL() *url.URL // IsValid checks if the URL is valid. IsValid() bool // VCS returns the VCS backend that hosts the repository. VCS() (*VCSBackend, *url.URL, error) } // A GitHubRepository represents a GitHub repository. Implements RemoteRepository. type GitHubRepository struct { url *url.URL } // URL returns URL of the repository func (repo *GitHubRepository) URL() *url.URL { return repo.url } // IsValid determine if the repository is valid or not func (repo *GitHubRepository) IsValid() bool { if strings.HasPrefix(repo.url.Path, "/blog/") { logger.Log("github", `the user or organization named "blog" is invalid on github, "https://github.com/blog" is redirected to "https://github.blog".`) return false } pathComponents := strings.Split(strings.Trim(repo.url.Path, "/"), "/") return len(pathComponents) >= 2 } // VCS returns VCSBackend of the repository func (repo *GitHubRepository) VCS() (*VCSBackend, *url.URL, error) { u := *repo.url pathComponents := strings.Split(strings.Trim(strings.TrimSuffix(u.Path, ".git"), "/"), "/") path := "/" + strings.Join(pathComponents[0:2], "/") if strings.HasSuffix(u.String(), ".git") { path += ".git" } u.Path = path return GitBackend, &u, nil } // A GitHubGistRepository represents a GitHub Gist repository. type GitHubGistRepository struct { url *url.URL } // URL returns URL of the GistRepository func (repo *GitHubGistRepository) URL() *url.URL { return repo.url } // IsValid determine if the gist repository is valid or not func (repo *GitHubGistRepository) IsValid() bool { return true } // VCS returns VCSBackend of the gist func (repo *GitHubGistRepository) VCS() (*VCSBackend, *url.URL, error) { return GitBackend, repo.URL(), nil } // DarksHubRepository represents DarcsHub Repository type DarksHubRepository struct { url *url.URL } // URL returns URL of darks repository func (repo *DarksHubRepository) URL() *url.URL { return repo.url } // IsValid determine if the DarcsHub repository is valid or not func (repo *DarksHubRepository) IsValid() bool { return strings.Count(repo.url.Path, "/") == 2 } // VCS returns VCSBackend of the DarcsHub repository func (repo *DarksHubRepository) VCS() (*VCSBackend, *url.URL, error) { return DarcsBackend, repo.URL(), nil } // NestPijulRepository represents the Nest repository type NestPijulRepository struct { url *url.URL } // URL returns URL of the Nest repository func (repo *NestPijulRepository) URL() *url.URL { return repo.url } // IsValid determine if the Nest repository is valid or not func (repo *NestPijulRepository) IsValid() bool { return strings.Count(repo.url.Path, "/") == 2 } // VCS returns VCSBackend of the Nest repository func (repo *NestPijulRepository) VCS() (*VCSBackend, *url.URL, error) { return PijulBackend, repo.URL(), nil } // A CodeCommitRepository represents a CodeCommit repository. Implements RemoteRepository. type CodeCommitRepository struct { url *url.URL } // URL returns URL of the repository func (repo *CodeCommitRepository) URL() *url.URL { return repo.url } // IsValid determine if the repository is valid or not func (repo *CodeCommitRepository) IsValid() bool { return true } // VCS returns VCSBackend of the repository func (repo *CodeCommitRepository) VCS() (*VCSBackend, *url.URL, error) { u := *repo.url return GitBackend, &u, nil } type ChiselRepository struct { url *url.URL } // URL returns URL of the repository func (repo *ChiselRepository) URL() *url.URL { return repo.url } // IsValid determine if the repository is valid or not func (repo *ChiselRepository) IsValid() bool { return true } // VCS returns VCSBackend of the repository func (repo *ChiselRepository) VCS() (*VCSBackend, *url.URL, error) { return FossilBackend, repo.URL(), nil } // OtherRepository represents other repository type OtherRepository struct { url *url.URL } // URL returns URL of the repository func (repo *OtherRepository) URL() *url.URL { return repo.url } // IsValid determine if the repository is valid or not func (repo *OtherRepository) IsValid() bool { return true } var ( vcsSchemeReg = regexp.MustCompile(`^(git|svn|bzr|codecommit)(?:\+|$)`) scheme2vcs = map[string]*VCSBackend{ "git": GitBackend, "codecommit": GitBackend, "svn": SubversionBackend, "bzr": BazaarBackend, } ) // VCS detects VCSBackend of the OtherRepository func (repo *OtherRepository) VCS() (*VCSBackend, *url.URL, error) { // Respect 'ghq.url.https://ghe.example.com/.vcs' config variable // (in gitconfig:) // [ghq "https://ghe.example.com/"] // vcs = github vcs, err := gitconfig.Do("--path", "--get-urlmatch", "ghq.vcs", repo.URL().String()) if err != nil && !gitconfig.IsNotFound(err) { logger.Log("error", err.Error()) } if backend, ok := vcsRegistry[vcs]; ok { return backend, repo.URL(), nil } if m := vcsSchemeReg.FindStringSubmatch(repo.url.Scheme); len(m) > 1 { return scheme2vcs[m[1]], repo.URL(), nil } mayBeSvn := strings.HasPrefix(repo.url.Host, "svn.") if mayBeSvn && cmdutil.RunSilently("svn", "info", repo.url.String()) == nil { return SubversionBackend, repo.URL(), nil } // Detect VCS backend if repo.url.Scheme == "ssh" && repo.url.User.Username() == "git" { return GitBackend, repo.URL(), nil } switch repo.url.Host { case "fossil-scm.org", "sqlite.org": return FossilBackend, repo.URL(), nil } if cmdutil.RunSilently("git", "ls-remote", repo.url.String()) == nil { return GitBackend, repo.URL(), nil } vcs, repoURL, err := detectGoImport(repo.url) if err == nil { // vcs == "mod" (modproxy) not supported yet return vcsRegistry[vcs], repoURL, nil } if cmdutil.RunSilently("hg", "identify", repo.url.String()) == nil { return MercurialBackend, repo.URL(), nil } if !mayBeSvn && cmdutil.RunSilently("svn", "info", repo.url.String()) == nil { return SubversionBackend, repo.URL(), nil } return nil, nil, fmt.Errorf("unsupported VCS, url=%s: %w", repo.URL(), err) } // NewRemoteRepository returns new RemoteRepository object from URL func NewRemoteRepository(u *url.URL) (RemoteRepository, error) { repo := func() RemoteRepository { if u.Scheme == "codecommit" { return &CodeCommitRepository{u} } switch u.Host { case "github.com": return &GitHubRepository{u} case "gist.github.com": return &GitHubGistRepository{u} case "hub.darcs.net": return &DarksHubRepository{u} case "nest.pijul.com": return &NestPijulRepository{u} case "chiselapp.com": return &ChiselRepository{u} default: return &OtherRepository{u} } }() if !repo.IsValid() { return nil, fmt.Errorf("not a valid repository: %s", u) } return repo, nil }