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 }