github.com/sirkon/goproxy@v1.4.8/plugin/gitlab/module.go (about) 1 package gitlab 2 3 import ( 4 "archive/zip" 5 "bytes" 6 "context" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "os" 11 12 "github.com/rs/zerolog" 13 "github.com/sirkon/gitlab" 14 "github.com/sirkon/gitlab/gitlabdata" 15 16 "github.com/sirkon/goproxy/internal/errors" 17 18 "github.com/sirkon/goproxy" 19 "github.com/sirkon/goproxy/fsrepack" 20 "github.com/sirkon/goproxy/gomod" 21 "github.com/sirkon/goproxy/semver" 22 ) 23 24 type gitlabModule struct { 25 client gitlab.Client 26 fullPath string 27 path string 28 pathUnversioned string 29 major int 30 } 31 32 func (s *gitlabModule) ModulePath() string { 33 return s.path 34 } 35 36 func (s *gitlabModule) Versions(ctx context.Context, prefix string) ([]string, error) { 37 tags, err := s.getVersions(ctx, prefix, s.pathUnversioned) 38 if err == nil { 39 return tags, err 40 } 41 42 if s.pathUnversioned == s.path { 43 return nil, err 44 } 45 zerolog.Ctx(ctx).Warn().Err(err).Msgf("failed to get with unversioned path `%s` (original %s), someone is loving cars a bit too much :)", s.pathUnversioned, s.path) 46 return s.getVersions(ctx, prefix, s.path) 47 } 48 49 func (s *gitlabModule) getVersions(ctx context.Context, prefix string, path string) ([]string, error) { 50 tags, err := s.client.Tags(ctx, path, "") 51 if err != nil { 52 return nil, errors.Wrapf(err, "gitlab getting tags from gitlab repository") 53 } 54 55 var resp []string 56 for _, tag := range tags { 57 if semver.IsValid(tag.Name) { 58 resp = append(resp, tag.Name) 59 } 60 } 61 if len(resp) > 0 { 62 return resp, nil 63 } 64 info, err := s.getStat(ctx, "master") 65 if err != nil { 66 zerolog.Ctx(ctx).Warn().Err(err).Msg("getting revision info for master") 67 return nil, errors.Newf("gitlab no tags found in the current repo") 68 } 69 return []string{info.Version}, nil 70 } 71 72 func (s *gitlabModule) Stat(ctx context.Context, rev string) (*goproxy.RevInfo, error) { 73 res, err := s.getStat(ctx, rev) 74 if err != nil { 75 return nil, err 76 } 77 78 if major := semver.Major(res.Version); major >= 2 && s.major < major { 79 return nil, errors.Newf("gitlab branch relates to higher major version v%d than what was expected from module path (v%d)", major, s.major) 80 } 81 return res, nil 82 } 83 84 func (s *gitlabModule) getStat(ctx context.Context, rev string) (res *goproxy.RevInfo, err error) { 85 if semver.IsValid(rev) { 86 return s.statVersion(ctx, rev) 87 } 88 89 // revision looks like a branch or non-semver tag, need to build pseudo-version 90 return s.statWithPseudoVersion(ctx, rev) 91 } 92 93 // statVersion processing for semver revision 94 func (s *gitlabModule) statVersion(ctx context.Context, rev string) (*goproxy.RevInfo, error) { 95 // check if this rev does look like pseudo-version – will try statWithPseudoVersion in this case with short SHA 96 pseudo := semver.Pseudo(rev) 97 if len(pseudo) > 0 { 98 res, err := s.statWithPseudoVersion(ctx, pseudo) 99 if err == nil { 100 // should use base version from the commit itself 101 if semver.Compare(rev, res.Version) > 0 { 102 res.Version = rev 103 } 104 return res, nil 105 } 106 } 107 108 tags, err := s.client.Tags(ctx, s.pathUnversioned, rev) 109 if err != nil { 110 tags, err = s.client.Tags(ctx, s.path, rev) 111 if err != nil { 112 return nil, errors.Wrapf(err, "gitlab getting tags from gitlab repository") 113 } 114 } 115 116 // Looking for exact revision match 117 for _, tag := range tags { 118 if tag.Name == rev { 119 return &goproxy.RevInfo{ 120 Version: tag.Name, 121 Time: tag.Commit.CreatedAt, 122 Name: tag.Commit.ID, 123 Short: tag.Commit.ShortID, 124 }, nil 125 } 126 } 127 128 return nil, errors.Newf("gitlab state: unknown revision %s for %s", rev, s.path) 129 } 130 131 func (s *gitlabModule) statWithPseudoVersion(ctx context.Context, rev string) (*goproxy.RevInfo, error) { 132 commits, err := s.client.Commits(ctx, s.pathUnversioned, rev) 133 if err != nil { 134 commits, err = s.client.Commits(ctx, s.path, rev) 135 if err != nil { 136 return nil, errors.Wrapf(err, "getting commits for `%s`", rev) 137 } 138 } 139 if len(commits) == 0 { 140 return nil, errors.Newf("no commits found for revision %s", rev) 141 } 142 143 commitMap := make(map[string]*gitlabdata.Commit, len(commits)) 144 for _, commit := range commits { 145 commitMap[commit.ID] = commit 146 } 147 148 // looking for the most recent semver tag 149 tags, err := s.client.Tags(ctx, s.pathUnversioned, "") // all tags 150 if err != nil { 151 tags, err = s.client.Tags(ctx, s.path, "") 152 if err != nil { 153 return nil, errors.Wrapf(err, "getting tags") 154 } 155 } 156 maxVer := "v0.0.0" 157 for _, tag := range tags { 158 if _, ok := commitMap[tag.Commit.ID]; !ok { 159 continue 160 } 161 if !semver.IsValid(tag.Name) { 162 continue 163 } 164 maxVer = semver.Max(maxVer, tag.Name) 165 } 166 167 var base string 168 if semver.Major(maxVer) < s.major { 169 base = fmt.Sprintf("v%d.0.0-", s.major) 170 } else { 171 major, minor, patch := semver.MajorMinorPatch(maxVer) 172 base = fmt.Sprintf("v%d.%d.%d-0.", major, minor, patch+1) 173 } 174 175 // Should set appropriate version 176 commit := commits[0] 177 178 moment := commit.CreatedAt 179 var ( 180 year = moment[:4] 181 month = moment[5:7] 182 day = moment[8:10] 183 hour = moment[11:13] 184 minute = moment[14:16] 185 second = moment[17:19] 186 ) 187 pseudoVersion := fmt.Sprintf("%s%s%s%s%s%s%s-%s", 188 base, 189 year, month, day, hour, minute, second, 190 commit.ShortID, 191 ) 192 return &goproxy.RevInfo{ 193 Version: pseudoVersion, 194 Time: moment, 195 }, nil 196 } 197 198 func (s *gitlabModule) GoMod(ctx context.Context, version string) (data []byte, err error) { 199 goMod, err := s.getGoMod(ctx, version) 200 if err != nil { 201 if os.IsNotExist(err) { 202 return []byte("module " + s.fullPath), nil 203 } 204 return nil, errors.Wrap(err, "gitlab getting go.mod") 205 } 206 207 res, err := gomod.Parse("go.mod", goMod) 208 if err != nil { 209 return nil, errors.Wrapf(err, "gitlab parsing repository go.mod") 210 } 211 212 if res.Name != s.fullPath { 213 return nil, errors.Newf("gitlab module path is not equal to go.mod module path: %s ≠ %s", res.Name, s.fullPath) 214 } 215 216 return goMod, nil 217 } 218 219 func (s *gitlabModule) getGoMod(ctx context.Context, version string) ([]byte, error) { 220 // try with pseudo version first 221 if sha := semver.Pseudo(version); len(sha) > 0 { 222 res, err := s.client.File(ctx, s.pathUnversioned, "go.mod", sha) 223 if err == nil { 224 return res, nil 225 } 226 res, err = s.client.File(ctx, s.path, "go.mod", sha) 227 if err == nil { 228 return res, nil 229 } 230 } 231 res, err := s.client.File(ctx, s.pathUnversioned, "go.mod", version) 232 if err == nil { 233 return res, nil 234 } 235 return s.client.File(ctx, s.path, "go.mod", version) 236 } 237 238 type bufferCloser struct { 239 bytes.Buffer 240 } 241 242 // Close makes bufferCloser io.ReadCloser 243 func (*bufferCloser) Close() error { return nil } 244 245 func (s *gitlabModule) Zip(ctx context.Context, version string) (io.ReadCloser, error) { 246 if sha := semver.Pseudo(version); len(sha) > 0 { 247 res, err := s.getZip(ctx, sha, version) 248 if err == nil { 249 return res, nil 250 } 251 } 252 return s.getZip(ctx, version, version) 253 } 254 255 func (s *gitlabModule) getZip(ctx context.Context, revision, version string) (io.ReadCloser, error) { 256 modInfo, err := s.client.ProjectInfo(ctx, s.pathUnversioned) 257 if err != nil { 258 modInfo, err = s.client.ProjectInfo(ctx, s.path) 259 if err != nil { 260 return nil, errors.Wrapf(err, "gitlab getting project %s info", s.path) 261 } 262 } 263 264 archive, err := s.client.Archive(ctx, modInfo.ID, revision) 265 if err != nil { 266 return nil, errors.Wrap(err, "getting zipped archive data") 267 } 268 269 repacker, err := fsrepack.Gitlab(s.fullPath, version) 270 if err != nil { 271 return nil, errors.Wrap(err, "gitlab initiating repacker for gitlab archive source") 272 } 273 274 // now need to repack archive content from <pkg-name>-<hash> → <full pkg name, such as gitlab.com/user/module>, e.g. 275 // 276 // > module-f5d5d62240829ba7f38614add00c4aba587cffb1: 277 // > go.mod 278 // > pkg.go 279 // 280 // from gitlab.com/user/module, where f5d5d62240829ba7f38614add00c4aba587cffb1 is a hash of the revision tagged 281 // v0.0.1 will be repacked into 282 // 283 // > gitlab.com: 284 // > user.name: 285 // > module@v0.1.2: 286 // > go.mod 287 // > pkg.go 288 zipped, err := ioutil.ReadAll(archive) 289 if err != nil { 290 return nil, errors.Wrap(err, "gitlab reading gitlab source archive") 291 } 292 293 zipReader, err := zip.NewReader(bytes.NewReader(zipped), int64(len(zipped))) 294 if err != nil { 295 return nil, errors.Wrap(err, "gitlab extracting zipped source data") 296 } 297 298 rawDest := &bufferCloser{} 299 result := rawDest 300 dest := zip.NewWriter(rawDest) 301 defer func() { 302 if err := dest.Close(); err != nil { 303 logger := zerolog.Ctx(ctx) 304 logger.Error().Err(err).Msgf("closing an output archive") 305 } 306 }() 307 308 if err := dest.SetComment(zipReader.Comment); err != nil { 309 return nil, errors.Wrap(err, "setting comment to output archive") 310 } 311 312 for _, file := range zipReader.File { 313 tmp, err := repacker.Relativer(file.Name) 314 if err != nil { 315 return nil, errors.Wrap(err, "gitlab relative file name computation") 316 } 317 fileName := repacker.Destinator(tmp) 318 319 isDir := file.FileInfo().IsDir() 320 321 fh := file.FileHeader 322 fh.Name = fileName 323 324 fileWriter, err := dest.CreateHeader(&fh) 325 if err != nil { 326 return nil, errors.Wrapf(err, "copying attributes for %s", fileName) 327 } 328 329 if isDir { 330 continue 331 } 332 333 fileData, err := file.Open() 334 if err != nil { 335 return nil, errors.Wrapf(err, "opening file for %s", fileName) 336 } 337 338 if _, err := io.Copy(fileWriter, fileData); err != nil { 339 if cErr := fileData.Close(); cErr != nil { 340 logger := zerolog.Ctx(ctx) 341 logger.Error().Err(cErr).Msgf("closing a file for %s", fileName) 342 } 343 return nil, errors.Wrapf(err, "copying content for %s", fileName) 344 } 345 346 if err := fileData.Close(); err != nil { 347 return nil, errors.Wrapf(err, "closing zip file for %s", fileName) 348 } 349 } 350 351 return result, nil 352 }