github.com/motemen/ghq@v1.0.3/local_repository.go (about)

     1  package main
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/url"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  	"sync"
    11  
    12  	"github.com/Songmu/gitconfig"
    13  	"github.com/motemen/ghq/logger"
    14  	"github.com/saracen/walker"
    15  )
    16  
    17  const envGhqRoot = "GHQ_ROOT"
    18  
    19  // LocalRepository represents local repository
    20  type LocalRepository struct {
    21  	FullPath  string
    22  	RelPath   string
    23  	RootPath  string
    24  	PathParts []string
    25  
    26  	repoPath   string
    27  	vcsBackend *VCSBackend
    28  }
    29  
    30  // RepoPath returns local repository path
    31  func (repo *LocalRepository) RepoPath() string {
    32  	if repo.repoPath != "" {
    33  		return repo.repoPath
    34  	}
    35  	return repo.FullPath
    36  }
    37  
    38  // LocalRepositoryFromFullPath resolve LocalRepository from file path
    39  func LocalRepositoryFromFullPath(fullPath string, backend *VCSBackend) (*LocalRepository, error) {
    40  	var relPath string
    41  
    42  	roots, err := localRepositoryRoots(true)
    43  	if err != nil {
    44  		return nil, err
    45  	}
    46  	var root string
    47  	for _, root = range roots {
    48  		if !strings.HasPrefix(fullPath, root) {
    49  			continue
    50  		}
    51  
    52  		var err error
    53  		relPath, err = filepath.Rel(root, fullPath)
    54  		if err == nil {
    55  			break
    56  		}
    57  	}
    58  
    59  	if relPath == "" {
    60  		return nil, fmt.Errorf("no local repository found for: %s", fullPath)
    61  	}
    62  
    63  	pathParts := strings.Split(relPath, string(filepath.Separator))
    64  
    65  	return &LocalRepository{
    66  		FullPath:   fullPath,
    67  		RelPath:    filepath.ToSlash(relPath),
    68  		RootPath:   root,
    69  		PathParts:  pathParts,
    70  		vcsBackend: backend,
    71  	}, nil
    72  }
    73  
    74  // LocalRepositoryFromURL resolve LocalRepository from URL
    75  func LocalRepositoryFromURL(remoteURL *url.URL) (*LocalRepository, error) {
    76  	pathParts := append(
    77  		[]string{remoteURL.Hostname()}, strings.Split(remoteURL.Path, "/")...,
    78  	)
    79  	relPath := strings.TrimSuffix(filepath.Join(pathParts...), ".git")
    80  	pathParts[len(pathParts)-1] = strings.TrimSuffix(pathParts[len(pathParts)-1], ".git")
    81  
    82  	var (
    83  		localRepository *LocalRepository
    84  		mu              sync.Mutex
    85  	)
    86  	// Find existing local repository first
    87  	if err := walkAllLocalRepositories(func(repo *LocalRepository) {
    88  		if repo.RelPath == relPath {
    89  			mu.Lock()
    90  			localRepository = repo
    91  			mu.Unlock()
    92  		}
    93  	}); err != nil {
    94  		return nil, err
    95  	}
    96  
    97  	if localRepository != nil {
    98  		return localRepository, nil
    99  	}
   100  
   101  	prim, err := getRoot(remoteURL.String())
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	// No local repository found, returning new one
   107  	return &LocalRepository{
   108  		FullPath:  filepath.Join(prim, relPath),
   109  		RelPath:   relPath,
   110  		RootPath:  prim,
   111  		PathParts: pathParts,
   112  	}, nil
   113  }
   114  
   115  func getRoot(u string) (string, error) {
   116  	prim := os.Getenv(envGhqRoot)
   117  	if prim != "" {
   118  		return prim, nil
   119  	}
   120  	prim, err := gitconfig.Do("--path", "--get-urlmatch", "ghq.root", u)
   121  	if err != nil && !gitconfig.IsNotFound(err) {
   122  		return "", err
   123  	}
   124  	if prim == "" {
   125  		prim, err = primaryLocalRepositoryRoot()
   126  		if err != nil {
   127  			return "", err
   128  		}
   129  	}
   130  	return prim, nil
   131  }
   132  
   133  // Subpaths returns lists of tail parts of relative path from the root directory (shortest first)
   134  // for example, {"ghq", "motemen/ghq", "github.com/motemen/ghq"} for $root/github.com/motemen/ghq.
   135  func (repo *LocalRepository) Subpaths() []string {
   136  	tails := make([]string, len(repo.PathParts))
   137  
   138  	for i := range repo.PathParts {
   139  		tails[i] = strings.Join(repo.PathParts[len(repo.PathParts)-(i+1):], "/")
   140  	}
   141  
   142  	return tails
   143  }
   144  
   145  // NonHostPath returns non host path
   146  func (repo *LocalRepository) NonHostPath() string {
   147  	return strings.Join(repo.PathParts[1:], "/")
   148  }
   149  
   150  // list as bellow
   151  // - "$GHQ_ROOT/github.com/motemen/ghq/cmdutil" // repo.FullPath
   152  // - "$GHQ_ROOT/github.com/motemen/ghq"
   153  // - "$GHQ_ROOT/github.com/motemen
   154  func (repo *LocalRepository) repoRootCandidates() []string {
   155  	hostRoot := filepath.Join(repo.RootPath, repo.PathParts[0])
   156  	nonHostParts := repo.PathParts[1:]
   157  	candidates := make([]string, len(nonHostParts))
   158  	for i := 0; i < len(nonHostParts); i++ {
   159  		candidates[i] = filepath.Join(append(
   160  			[]string{hostRoot}, nonHostParts[0:len(nonHostParts)-i]...)...)
   161  	}
   162  	return candidates
   163  }
   164  
   165  // IsUnderPrimaryRoot or not
   166  func (repo *LocalRepository) IsUnderPrimaryRoot() bool {
   167  	prim, err := primaryLocalRepositoryRoot()
   168  	if err != nil {
   169  		return false
   170  	}
   171  	return strings.HasPrefix(repo.FullPath, prim)
   172  }
   173  
   174  // Matches checks if any subpath of the local repository equals the query.
   175  func (repo *LocalRepository) Matches(pathQuery string) bool {
   176  	for _, p := range repo.Subpaths() {
   177  		if p == pathQuery {
   178  			return true
   179  		}
   180  	}
   181  
   182  	return false
   183  }
   184  
   185  // VCS returns VCSBackend of the repository
   186  func (repo *LocalRepository) VCS() (*VCSBackend, string) {
   187  	if repo.vcsBackend == nil {
   188  		for _, dir := range repo.repoRootCandidates() {
   189  			backend := findVCSBackend(dir, "")
   190  			if backend != nil {
   191  				repo.vcsBackend = backend
   192  				repo.repoPath = dir
   193  				break
   194  			}
   195  		}
   196  	}
   197  	return repo.vcsBackend, repo.RepoPath()
   198  }
   199  
   200  var vcsContentsMap = map[string]*VCSBackend{
   201  	".git":           GitBackend,
   202  	".hg":            MercurialBackend,
   203  	".svn":           SubversionBackend,
   204  	"_darcs":         DarcsBackend,
   205  	".bzr":           BazaarBackend,
   206  	".fslckout":      FossilBackend, // file
   207  	"_FOSSIL_":       FossilBackend, // file
   208  	"CVS/Repository": cvsDummyBackend,
   209  }
   210  
   211  var vcsContents = [...]string{
   212  	".git",
   213  	".hg",
   214  	".svn",
   215  	"_darcs",
   216  	".bzr",
   217  	".fslckout",
   218  	"._FOSSIL_",
   219  	"CVS/Repository",
   220  }
   221  
   222  func findVCSBackend(fpath, vcs string) *VCSBackend {
   223  	// When vcs is not empty, search only specified contents of vcs
   224  	if vcs != "" {
   225  		vcsBackend, ok := vcsRegistry[vcs]
   226  		if !ok {
   227  			return nil
   228  		}
   229  		for _, d := range vcsBackend.Contents {
   230  			if _, err := os.Stat(filepath.Join(fpath, d)); err == nil {
   231  				return vcsBackend
   232  			}
   233  		}
   234  		return nil
   235  	}
   236  	for _, d := range vcsContents {
   237  		if _, err := os.Stat(filepath.Join(fpath, d)); err == nil {
   238  			return vcsContentsMap[d]
   239  		}
   240  	}
   241  	return nil
   242  }
   243  
   244  func walkAllLocalRepositories(callback func(*LocalRepository)) error {
   245  	return walkLocalRepositories("", callback)
   246  }
   247  
   248  func walkLocalRepositories(vcs string, callback func(*LocalRepository)) error {
   249  	roots, err := localRepositoryRoots(true)
   250  	if err != nil {
   251  		return err
   252  	}
   253  
   254  	walkFn := func(fpath string, fi os.FileInfo) error {
   255  		isSymlink := false
   256  		if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
   257  			isSymlink = true
   258  			realpath, err := filepath.EvalSymlinks(fpath)
   259  			if err != nil {
   260  				return nil
   261  			}
   262  			fi, err = os.Stat(realpath)
   263  			if err != nil {
   264  				return nil
   265  			}
   266  		}
   267  		if !fi.IsDir() {
   268  			return nil
   269  		}
   270  		vcsBackend := findVCSBackend(fpath, vcs)
   271  		if vcsBackend == nil {
   272  			return nil
   273  		}
   274  
   275  		repo, err := LocalRepositoryFromFullPath(fpath, vcsBackend)
   276  		if err != nil || repo == nil {
   277  			return nil
   278  		}
   279  		callback(repo)
   280  
   281  		if isSymlink {
   282  			return nil
   283  		}
   284  		return filepath.SkipDir
   285  	}
   286  
   287  	errCb := walker.WithErrorCallback(func(pathname string, err error) error {
   288  		if os.IsPermission(errors.Unwrap(err)) {
   289  			logger.Log("warning", fmt.Sprintf("%s: Permission denied", pathname))
   290  			return nil
   291  		}
   292  		return err
   293  	})
   294  
   295  	for _, root := range roots {
   296  		fi, err := os.Stat(root)
   297  		if err != nil {
   298  			if os.IsNotExist(err) {
   299  				continue
   300  			}
   301  		}
   302  		if fi.Mode()&0444 == 0 {
   303  			logger.Log("warning", fmt.Sprintf("%s: Permission denied", root))
   304  			continue
   305  		}
   306  		if err := walker.Walk(root, walkFn, errCb); err != nil {
   307  			return err
   308  		}
   309  	}
   310  	return nil
   311  }
   312  
   313  var (
   314  	_home    string
   315  	_homeErr error
   316  	homeOnce = &sync.Once{}
   317  )
   318  
   319  func getHome() (string, error) {
   320  	homeOnce.Do(func() {
   321  		_home, _homeErr = os.UserHomeDir()
   322  	})
   323  	return _home, _homeErr
   324  }
   325  
   326  var (
   327  	_localRepositoryRoots []string
   328  	_localRepoErr         error
   329  	localRepoOnce         = &sync.Once{}
   330  )
   331  
   332  // localRepositoryRoots returns locally cloned repositories' root directories.
   333  // The root dirs are determined as following:
   334  //
   335  //   - If GHQ_ROOT environment variable is nonempty, use it as the only root dir.
   336  //   - Otherwise, use the result of `git config --get-all ghq.root` as the dirs.
   337  //   - Otherwise, fallback to the default root, `~/.ghq`.
   338  //   - When GHQ_ROOT is empty, specific root dirs are added from the result of
   339  //     `git config --path --get-regexp '^ghq\..+\.root$`
   340  func localRepositoryRoots(all bool) ([]string, error) {
   341  	localRepoOnce.Do(func() {
   342  		var roots []string
   343  		envRoot := os.Getenv(envGhqRoot)
   344  		if envRoot != "" {
   345  			roots = filepath.SplitList(envRoot)
   346  		} else {
   347  			var err error
   348  			roots, err = gitconfig.PathAll("ghq.root")
   349  			if err != nil && !gitconfig.IsNotFound(err) {
   350  				_localRepoErr = err
   351  				return
   352  			}
   353  			// reverse slice
   354  			for i := len(roots)/2 - 1; i >= 0; i-- {
   355  				opp := len(roots) - 1 - i
   356  				roots[i], roots[opp] =
   357  					roots[opp], roots[i]
   358  			}
   359  		}
   360  
   361  		if len(roots) == 0 {
   362  			homeDir, err := getHome()
   363  			if err != nil {
   364  				_localRepoErr = err
   365  				return
   366  			}
   367  			roots = []string{filepath.Join(homeDir, "ghq")}
   368  		}
   369  
   370  		if all && envRoot == "" {
   371  			roots, err := urlMatchLocalRepositoryRoots()
   372  			if err != nil {
   373  				_localRepoErr = err
   374  				return
   375  			}
   376  			roots = append(roots, roots...)
   377  		}
   378  
   379  		seen := make(map[string]bool, len(roots))
   380  		for _, v := range roots {
   381  			path := filepath.Clean(v)
   382  			if _, err := os.Stat(path); err == nil {
   383  				if path, err = filepath.EvalSymlinks(path); err != nil {
   384  					_localRepoErr = err
   385  					return
   386  				}
   387  			}
   388  			if !filepath.IsAbs(path) {
   389  				var err error
   390  				if path, err = filepath.Abs(path); err != nil {
   391  					_localRepoErr = err
   392  					return
   393  				}
   394  			}
   395  			if seen[path] {
   396  				continue
   397  			}
   398  			seen[path] = true
   399  			_localRepositoryRoots = append(_localRepositoryRoots, path)
   400  		}
   401  	})
   402  	return _localRepositoryRoots, _localRepoErr
   403  }
   404  
   405  func urlMatchLocalRepositoryRoots() ([]string, error) {
   406  	out, err := gitconfig.Do("--path", "--get-regexp", `^ghq\..+\.root$`)
   407  	if err != nil {
   408  		if gitconfig.IsNotFound(err) {
   409  			return nil, nil
   410  		}
   411  		return nil, err
   412  	}
   413  	items := strings.Split(out, "\x00")
   414  	ret := make([]string, len(items))
   415  	for i, kvStr := range items {
   416  		kv := strings.SplitN(kvStr, "\n", 2)
   417  		ret[i] = kv[1]
   418  	}
   419  	return ret, nil
   420  }
   421  
   422  func primaryLocalRepositoryRoot() (string, error) {
   423  	roots, err := localRepositoryRoots(false)
   424  	if err != nil {
   425  		return "", err
   426  	}
   427  	return roots[0], nil
   428  }