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  }