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 }