github.com/bilus/oya@v0.0.3-0.20190301162104-da4acbd394c6/pkg/repo/github.go (about) 1 package repo 2 3 import ( 4 "fmt" 5 "io" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "strings" 10 11 "github.com/bilus/oya/pkg/oyafile" 12 "github.com/bilus/oya/pkg/pack" 13 "github.com/bilus/oya/pkg/semver" 14 "github.com/bilus/oya/pkg/types" 15 log "github.com/sirupsen/logrus" 16 "gopkg.in/src-d/go-billy.v4/memfs" 17 git "gopkg.in/src-d/go-git.v4" 18 "gopkg.in/src-d/go-git.v4/plumbing" 19 "gopkg.in/src-d/go-git.v4/plumbing/object" 20 "gopkg.in/src-d/go-git.v4/plumbing/transport" 21 "gopkg.in/src-d/go-git.v4/storage/memory" 22 ) 23 24 // GithubRepo represents all versions of an Oya pack stored in a git repository on Github.com. 25 type GithubRepo struct { 26 repoUris []string 27 basePath string 28 packPath string 29 importPath types.ImportPath 30 } 31 32 // AvailableVersions returns a sorted list of remotely available pack versions. 33 func (l *GithubRepo) AvailableVersions() ([]semver.Version, error) { 34 versions := make([]semver.Version, 0) 35 36 r, err := l.clone() 37 if err != nil { 38 return nil, err 39 } 40 tags, err := r.Tags() 41 if err != nil { 42 return nil, err 43 } 44 err = tags.ForEach( 45 func(t *plumbing.Reference) error { 46 n := t.Name() 47 if n.IsTag() { 48 version, ok := l.parseRef(n.Short()) 49 if ok { 50 versions = append(versions, version) 51 } 52 } 53 return nil 54 }, 55 ) 56 if err != nil { 57 return nil, err 58 } 59 semver.Sort(versions) 60 return versions, nil 61 } 62 63 func (l *GithubRepo) clone() (*git.Repository, error) { 64 var lastErr error 65 for _, uri := range l.repoUris { 66 fs := memfs.New() 67 storer := memory.NewStorage() 68 repo, err := git.Clone(storer, fs, &git.CloneOptions{ 69 URL: uri, 70 }) 71 if err == nil { 72 return repo, nil 73 } 74 lastErr = err 75 76 } 77 return nil, lastErr 78 } 79 80 // LatestVersion returns the latest available pack version based on tags in the remote Github repo. 81 func (l *GithubRepo) LatestVersion() (pack.Pack, error) { 82 versions, err := l.AvailableVersions() 83 if err != nil { 84 return pack.Pack{}, err 85 } 86 if len(versions) == 0 { 87 return pack.Pack{}, ErrNoTaggedVersions{ImportPath: l.importPath} 88 } 89 latestVersion := versions[len(versions)-1] 90 return l.Version(latestVersion) 91 } 92 93 // Version returns the specified version of the pack. 94 // NOTE: It doesn't check if it's available remotely. This may change. 95 // It is used when loading Oyafiles so we probably shouldn't do it or use a different function there. 96 func (l *GithubRepo) Version(version semver.Version) (pack.Pack, error) { 97 // BUG(bilus): Check if version exists? 98 return pack.New(l, version) 99 } 100 101 // ImportPath returns the pack's import path, e.g. github.com/tooploox/oya-packs/docker. 102 func (l *GithubRepo) ImportPath() types.ImportPath { 103 return l.importPath 104 } 105 106 // InstallPath returns the local path for the specific pack version. 107 func (l *GithubRepo) InstallPath(version semver.Version, installDir string) string { 108 path := filepath.Join(installDir, l.basePath, l.packPath) 109 return fmt.Sprintf("%v@%v", path, version.String()) 110 } 111 112 func (l *GithubRepo) checkout(version semver.Version) (*object.Commit, error) { 113 r, err := l.clone() 114 if err != nil { 115 return nil, ErrCheckout{ImportPath: l.importPath, ImportVersion: version, GitMsg: err.Error()} 116 } 117 tree, err := r.Worktree() 118 if err != nil { 119 return nil, ErrCheckout{ImportPath: l.importPath, ImportVersion: version, GitMsg: err.Error()} 120 } 121 err = tree.Checkout(&git.CheckoutOptions{ 122 Branch: plumbing.NewTagReferenceName(l.makeRef(version)), 123 }) 124 if err != nil { 125 return nil, ErrCheckout{ImportPath: l.importPath, ImportVersion: version, GitMsg: err.Error()} 126 } 127 ref, err := r.Head() 128 if err != nil { 129 return nil, ErrCheckout{ImportPath: l.importPath, ImportVersion: version, GitMsg: err.Error()} 130 } 131 return r.CommitObject(ref.Hash()) 132 } 133 134 // Install downloads & copies the specified version of the path to the output directory, 135 // preserving its import path. 136 // For example, for /home/bilus/.oya output directory and import path github.com/bilus/foo, 137 // the pack will be extracted to /home/bilus/.oya/github.com/bilus/foo. 138 func (l *GithubRepo) Install(version semver.Version, installDir string) error { 139 commit, err := l.checkout(version) 140 if err != nil { 141 return err 142 } 143 144 fIter, err := commit.Files() 145 if err != nil { 146 return err 147 } 148 149 sourceBasePath := l.packPath 150 targetPath := l.InstallPath(version, installDir) 151 log.Printf("Installing pack %v version %v into %q (git tag: %v)", l.ImportPath(), version, targetPath, l.makeRef(version)) 152 153 return fIter.ForEach(func(f *object.File) error { 154 if outside, err := l.isOutsidePack(f.Name); outside || err != nil { 155 return err // May be nil if outside true. 156 } 157 relPath, err := filepath.Rel(sourceBasePath, f.Name) 158 if err != nil { 159 return err 160 } 161 targetPath := filepath.Join(targetPath, relPath) 162 return copyFile(f, targetPath) 163 }) 164 } 165 166 func (l *GithubRepo) IsInstalled(version semver.Version, installDir string) (bool, error) { 167 fullPath := l.InstallPath(version, installDir) 168 _, err := os.Stat(fullPath) 169 if err != nil { 170 if os.IsNotExist(err) { 171 return false, nil 172 } 173 return false, err 174 } 175 return true, nil 176 } 177 178 func (l *GithubRepo) Reqs(version semver.Version) ([]pack.Pack, error) { 179 // BUG(bilus): This is a slow way to get requirements for a pack. 180 // It involves installing it out to a local directory. 181 // But it's also the simplest one. We can optimize by using HTTP 182 // access to pull in Oyafile and then parse the Require: section here. 183 // It means duplicating the logic including the assumption that the requires 184 // will always be stored in Oyafile, rather than a separate file along the lines 185 // of go.mod. 186 187 tempDir, err := ioutil.TempDir("", "oya") 188 defer os.RemoveAll(tempDir) 189 190 err = l.Install(version, tempDir) 191 if err != nil { 192 return nil, err 193 } 194 195 fullPath := l.InstallPath(version, tempDir) 196 o, found, err := oyafile.LoadFromDir(fullPath, fullPath) 197 if err != nil { 198 return nil, err 199 } 200 if !found { 201 return nil, ErrNoRootOyafile{l.importPath, version} 202 } 203 204 // BUG(bilus): This doesn't take Oyafile#Replacements into account. 205 // This probably doesn't matter because it's likely meaningless for 206 // packs accessed remotely but we may want to revisit it. 207 208 packs := make([]pack.Pack, len(o.Requires)) 209 for i, require := range o.Requires { 210 repo, err := Open(require.ImportPath) 211 if err != nil { 212 return nil, err 213 } 214 pack, err := repo.Version(require.Version) 215 if err != nil { 216 return nil, err 217 } 218 packs[i] = pack 219 } 220 221 return packs, nil 222 } 223 224 func copyFile(f *object.File, targetPath string) error { 225 err := os.MkdirAll(filepath.Dir(targetPath), os.ModePerm) 226 if err != nil { 227 return err 228 } 229 reader, err := f.Reader() 230 if err != nil { 231 return err 232 } 233 // BUG(bilus): Copy permissions. 234 writer, err := os.OpenFile(targetPath, os.O_RDWR|os.O_CREATE, 0666) 235 if err != nil { 236 return err 237 } 238 _, err = io.Copy(writer, reader) 239 if err != nil { 240 return err 241 } 242 err = writer.Sync() 243 if err != nil { 244 return err 245 } 246 mode, err := f.Mode.ToOSFileMode() 247 if err != nil { 248 return err 249 } 250 err = os.Chmod(targetPath, mode) 251 if err != nil { 252 return err 253 } 254 return err 255 } 256 257 func parseImportPath(importPath types.ImportPath) (uris []string, basePath string, packPath string, err error) { 258 parts := strings.Split(string(importPath), "/") 259 if len(parts) < 3 { 260 return nil, "", "", ErrNotGithub{ImportPath: importPath} 261 } 262 basePath = strings.Join(parts[0:3], "/") 263 packPath = strings.Join(parts[3:], "/") 264 // Prefer https but fall back on ssh if cannot clone via https 265 // as would be the case for private repositories. 266 uris = []string{ 267 fmt.Sprintf("https://%v.git", basePath), 268 fmt.Sprintf("git@%s:%s/%s.git", parts[0], parts[1], parts[2]), 269 } 270 return 271 } 272 273 func (l *GithubRepo) parseRef(tag string) (semver.Version, bool) { 274 if len(l.packPath) > 0 && strings.HasPrefix(tag, l.packPath) { 275 tag = tag[len(l.packPath)+1:] // e.g. "pack1/v1.0.0" => v1.0.0 276 } 277 version, err := semver.Parse(tag) 278 return version, err == nil 279 } 280 281 func (l *GithubRepo) makeRef(version semver.Version) string { 282 if len(l.packPath) > 0 { 283 return fmt.Sprintf("%v/%v", l.packPath, version.String()) 284 285 } else { 286 return fmt.Sprintf("%v", version.String()) 287 } 288 } 289 290 func (l *GithubRepo) isOutsidePack(relPath string) (bool, error) { 291 r, err := filepath.Rel(l.packPath, relPath) 292 if err != nil { 293 return false, err 294 } 295 return strings.Contains(r, ".."), nil 296 } 297 298 func toErrClone(url string, err error) error { 299 if err == transport.ErrAuthenticationRequired { 300 return ErrClone{RepoUrl: url, GitMsg: "Repository not found or private"} 301 } 302 return ErrClone{RepoUrl: url, GitMsg: err.Error()} 303 }