github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/pkg/project/v02/metadata.go (about)

     1  package v02
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/buildpacks/lifecycle/platform/files"
    10  	"github.com/go-git/go-git/v5"
    11  	"github.com/go-git/go-git/v5/plumbing"
    12  )
    13  
    14  type TagInfo struct {
    15  	Name    string
    16  	Message string
    17  	Type    string
    18  	TagHash string
    19  	TagTime time.Time
    20  }
    21  
    22  func GitMetadata(appPath string) *files.ProjectSource {
    23  	repo, err := git.PlainOpen(appPath)
    24  	if err != nil {
    25  		return nil
    26  	}
    27  	headRef, err := repo.Head()
    28  	if err != nil {
    29  		return nil
    30  	}
    31  	commitTagMap := generateTagsMap(repo)
    32  
    33  	describe := parseGitDescribe(repo, headRef, commitTagMap)
    34  	refs := parseGitRefs(repo, headRef, commitTagMap)
    35  	remote := parseGitRemote(repo)
    36  
    37  	projectSource := &files.ProjectSource{
    38  		Type: "git",
    39  		Version: map[string]interface{}{
    40  			"commit":   headRef.Hash().String(),
    41  			"describe": describe,
    42  		},
    43  		Metadata: map[string]interface{}{
    44  			"refs": refs,
    45  			"url":  remote,
    46  		},
    47  	}
    48  	return projectSource
    49  }
    50  
    51  func generateTagsMap(repo *git.Repository) map[string][]TagInfo {
    52  	commitTagMap := make(map[string][]TagInfo)
    53  	tags, err := repo.Tags()
    54  	if err != nil {
    55  		return commitTagMap
    56  	}
    57  
    58  	tags.ForEach(func(ref *plumbing.Reference) error {
    59  		tagObj, err := repo.TagObject(ref.Hash())
    60  		switch err {
    61  		case nil:
    62  			commitTagMap[tagObj.Target.String()] = append(
    63  				commitTagMap[tagObj.Target.String()],
    64  				TagInfo{Name: tagObj.Name, Message: tagObj.Message, Type: "annotated", TagHash: ref.Hash().String(), TagTime: tagObj.Tagger.When},
    65  			)
    66  		case plumbing.ErrObjectNotFound:
    67  			commitTagMap[ref.Hash().String()] = append(
    68  				commitTagMap[ref.Hash().String()],
    69  				TagInfo{Name: getRefName(ref.Name().String()), Message: "", Type: "unannotated", TagHash: ref.Hash().String(), TagTime: time.Now()},
    70  			)
    71  		default:
    72  			return err
    73  		}
    74  		return nil
    75  	})
    76  
    77  	for _, tagRefs := range commitTagMap {
    78  		sort.Slice(tagRefs, func(i, j int) bool {
    79  			if tagRefs[i].Type == "annotated" && tagRefs[j].Type == "annotated" {
    80  				return tagRefs[i].TagTime.After(tagRefs[j].TagTime)
    81  			}
    82  			if tagRefs[i].Type == "unannotated" && tagRefs[j].Type == "unannotated" {
    83  				return tagRefs[i].Name < tagRefs[j].Name
    84  			}
    85  			if tagRefs[i].Type == "annotated" && tagRefs[j].Type == "unannotated" {
    86  				return true
    87  			}
    88  			return false
    89  		})
    90  	}
    91  	return commitTagMap
    92  }
    93  
    94  func generateBranchMap(repo *git.Repository) map[string][]string {
    95  	commitBranchMap := make(map[string][]string)
    96  	branches, err := repo.Branches()
    97  	if err != nil {
    98  		return commitBranchMap
    99  	}
   100  	branches.ForEach(func(ref *plumbing.Reference) error {
   101  		commitBranchMap[ref.Hash().String()] = append(commitBranchMap[ref.Hash().String()], getRefName(ref.Name().String()))
   102  		return nil
   103  	})
   104  	return commitBranchMap
   105  }
   106  
   107  // `git describe --tags --always`
   108  func parseGitDescribe(repo *git.Repository, headRef *plumbing.Reference, commitTagMap map[string][]TagInfo) string {
   109  	logOpts := &git.LogOptions{
   110  		From:  headRef.Hash(),
   111  		Order: git.LogOrderCommitterTime,
   112  	}
   113  	commits, err := repo.Log(logOpts)
   114  	if err != nil {
   115  		return ""
   116  	}
   117  
   118  	latestTag := headRef.Hash().String()
   119  	commitsFromHEAD := 0
   120  	commitBranchMap := generateBranchMap(repo)
   121  	branchAtHEAD := getRefName(headRef.String())
   122  	currentBranch := branchAtHEAD
   123  	for {
   124  		commitInfo, err := commits.Next()
   125  		if err != nil {
   126  			break
   127  		}
   128  
   129  		if branchesAtCommit, exists := commitBranchMap[commitInfo.Hash.String()]; exists {
   130  			currentBranch = branchesAtCommit[0]
   131  		}
   132  		if refs, exists := commitTagMap[commitInfo.Hash.String()]; exists {
   133  			if branchAtHEAD != currentBranch && commitsFromHEAD != 0 {
   134  				// https://git-scm.com/docs/git-describe#_examples
   135  				latestTag = fmt.Sprintf("%s-%d-g%s", refs[0].Name, commitsFromHEAD, headRef.Hash().String())
   136  			} else {
   137  				latestTag = refs[0].Name
   138  			}
   139  			break
   140  		}
   141  		commitsFromHEAD += 1
   142  	}
   143  	return latestTag
   144  }
   145  
   146  func parseGitRefs(repo *git.Repository, headRef *plumbing.Reference, commitTagMap map[string][]TagInfo) []string {
   147  	var parsedRefs []string
   148  	parsedRefs = append(parsedRefs, getRefName(headRef.Name().String()))
   149  	if refs, exists := commitTagMap[headRef.Hash().String()]; exists {
   150  		for _, ref := range refs {
   151  			parsedRefs = append(parsedRefs, ref.Name)
   152  		}
   153  	}
   154  	return parsedRefs
   155  }
   156  
   157  func parseGitRemote(repo *git.Repository) string {
   158  	remotes, err := repo.Remotes()
   159  	if err != nil || len(remotes) == 0 {
   160  		return ""
   161  	}
   162  
   163  	for _, remote := range remotes {
   164  		if remote.Config().Name == "origin" {
   165  			return remote.Config().URLs[0]
   166  		}
   167  	}
   168  	return remotes[0].Config().URLs[0]
   169  }
   170  
   171  // Parse ref name from refs/tags/<ref_name>
   172  func getRefName(ref string) string {
   173  	if refSplit := strings.SplitN(ref, "/", 3); len(refSplit) == 3 {
   174  		return refSplit[2]
   175  	}
   176  	return ""
   177  }