package main import ( "context" "fmt" "net/url" "os" "path" "path/filepath" "strings" "sync" "github.com/x-motemen/ghq/logger" ) var seen sync.Map func getRepoLock(localRepoRoot string) bool { _, loaded := seen.LoadOrStore(localRepoRoot, struct{}{}) return !loaded } type getInfo struct { localRepository *LocalRepository } type getter struct { update, shallow, silent, ssh, recursive, bare bool vcs, branch, partial string } func (g *getter) get(ctx context.Context, argURL string) (getInfo, error) { u, err := newURL(argURL, g.ssh, false) if err != nil { return getInfo{}, fmt.Errorf("could not parse URL %q: %w", argURL, err) } branch := g.branch if pos := strings.LastIndexByte(u.Path, '@'); pos >= 0 { u.Path, branch = u.Path[:pos], u.Path[pos+1:] } remote, err := NewRemoteRepository(u) if err != nil { return getInfo{}, err } return g.getRemoteRepository(ctx, remote, branch) } // getRemoteRepository clones or updates a remote repository remote. // If doUpdate is true, updates the locally cloned repository. Otherwise does nothing. // If isShallow is true, does shallow cloning. (no effect if already cloned or the VCS is Mercurial and git-svn) func (g *getter) getRemoteRepository(ctx context.Context, remote RemoteRepository, branch string) (getInfo, error) { remoteURL := remote.URL() local, err := LocalRepositoryFromURL(remoteURL, g.bare) if err != nil { return getInfo{}, err } info := getInfo{ localRepository: local, } var ( fpath = local.FullPath newPath = false ) _, err = os.Stat(fpath) if err != nil { if os.IsNotExist(err) { newPath = true err = nil } if err != nil { return getInfo{}, err } } switch { case newPath: if remoteURL.Scheme == "codecommit" { logger.Log("clone", fmt.Sprintf("%s -> %s", remoteURL.Opaque, fpath)) } else { logger.Log("clone", fmt.Sprintf("%s -> %s", remoteURL, fpath)) } var ( localRepoRoot = fpath repoURL = remoteURL ) vcs, ok := vcsRegistry[g.vcs] if !ok { vcs, repoURL, err = remote.VCS() if err != nil { return getInfo{}, err } } if l := detectLocalRepoRoot(remoteURL.Path, repoURL.Path); l != "" { localRepoRoot = filepath.Join(local.RootPath, remoteURL.Hostname(), l) } if g.bare { localRepoRoot = localRepoRoot + ".git" } if remoteURL.Scheme == "codecommit" { repoURL, _ = url.Parse(remoteURL.Opaque) } if getRepoLock(localRepoRoot) { return info, vcs.Clone(&vcsGetOption{ url: repoURL, dir: localRepoRoot, shallow: g.shallow, silent: g.silent, branch: branch, recursive: g.recursive, bare: g.bare, partial: g.partial, }) } return info, nil case g.update: logger.Log("update", fpath) vcs, localRepoRoot := local.VCS() if vcs == nil { return getInfo{}, fmt.Errorf("failed to detect VCS for %q", fpath) } repoURL := remoteURL if remoteURL.Scheme == "codecommit" { repoURL, _ = url.Parse(remoteURL.Opaque) } if getRepoLock(localRepoRoot) { return info, vcs.Update(&vcsGetOption{ url: repoURL, dir: localRepoRoot, silent: g.silent, recursive: g.recursive, bare: g.bare, }) } return info, nil } logger.Log("exists", fpath) return info, nil } func detectLocalRepoRoot(remotePath, repoPath string) string { remotePath = strings.TrimSuffix(strings.TrimSuffix(remotePath, "/"), ".git") repoPath = strings.TrimSuffix(strings.TrimSuffix(repoPath, "/"), ".git") pathParts := strings.Split(repoPath, "/") pathParts = pathParts[1:] for i := 0; i < len(pathParts); i++ { subPath := "/" + path.Join(pathParts[i:]...) if before, _, ok := strings.Cut(remotePath, subPath); ok { return before + subPath } } return "" }