github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/tag/git_commit.go (about) 1 /* 2 Copyright 2019 The Skaffold Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package tag 18 19 import ( 20 "bytes" 21 "context" 22 "fmt" 23 "os/exec" 24 "path/filepath" 25 "regexp" 26 "strings" 27 28 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log" 29 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" 30 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" 31 ) 32 33 // GitCommit tags an image by the git commit it was built at. 34 type GitCommit struct { 35 prefix string 36 runGitFn func(context.Context, string) (string, error) 37 ignoreChanges bool 38 } 39 40 var variants = map[string]func(context.Context, string) (string, error){ 41 "": gitTags, 42 "tags": gitTags, 43 "commitsha": gitCommitsha, 44 "abbrevcommitsha": gitAbbrevcommitsha, 45 "treesha": gitTreesha, 46 "abbrevtreesha": gitAbbrevtreesha, 47 "branches": gitBranches, 48 } 49 50 // NewGitCommit creates a new git commit tagger. It fails if the tagger variant is invalid. 51 func NewGitCommit(prefix, variant string, ignoreChanges bool) (*GitCommit, error) { 52 runGitFn, found := variants[strings.ToLower(variant)] 53 if !found { 54 return nil, fmt.Errorf("%q is not a valid git tagger variant", variant) 55 } 56 57 return &GitCommit{ 58 prefix: prefix, 59 runGitFn: runGitFn, 60 ignoreChanges: ignoreChanges, 61 }, nil 62 } 63 64 // GenerateTag generates a tag from the git commit. 65 func (t *GitCommit) GenerateTag(ctx context.Context, image latest.Artifact) (string, error) { 66 ref, err := t.runGitFn(ctx, image.Workspace) 67 if err != nil { 68 return "", fmt.Errorf("unable to find git commit: %w", err) 69 } 70 71 ref = sanitizeTag(ref) 72 73 if !t.ignoreChanges { 74 changes, err := runGit(ctx, image.Workspace, "status", ".", "--porcelain") 75 if err != nil { 76 return "", fmt.Errorf("getting git status: %w", err) 77 } 78 79 if len(changes) > 0 { 80 return fmt.Sprintf("%s%s-dirty", t.prefix, ref), nil 81 } 82 } 83 84 return t.prefix + ref, nil 85 } 86 87 // sanitizeTag takes a git tag and converts it to a docker tag by removing 88 // all the characters that are not allowed by docker. 89 func sanitizeTag(tag string) string { 90 // Replace unsupported characters with `_` 91 sanitized := regexp.MustCompile(`[^a-zA-Z0-9-._]`).ReplaceAllString(tag, `_`) 92 93 // Remove leading `-`s and `.`s 94 prefixSuffix := regexp.MustCompile(`([-.]*)(.*)`).FindStringSubmatch(sanitized) 95 sanitized = strings.Repeat("_", len(prefixSuffix[1])) + prefixSuffix[2] 96 97 // Truncate to 128 characters 98 if len(sanitized) > 128 { 99 return sanitized[0:128] 100 } 101 102 if tag != sanitized { 103 log.Entry(context.TODO()).Warnf("Using %q instead of %q as an image tag", sanitized, tag) 104 } 105 106 return sanitized 107 } 108 109 func gitTags(ctx context.Context, workingDir string) (string, error) { 110 return runGit(ctx, workingDir, "describe", "--tags", "--always") 111 } 112 113 func gitCommitsha(ctx context.Context, workingDir string) (string, error) { 114 return runGit(ctx, workingDir, "rev-list", "-1", "HEAD") 115 } 116 117 func gitAbbrevcommitsha(ctx context.Context, workingDir string) (string, error) { 118 return runGit(ctx, workingDir, "rev-list", "-1", "HEAD", "--abbrev-commit") 119 } 120 121 func gitTreesha(ctx context.Context, workingDir string) (string, error) { 122 gitPath, err := getGitPathToWorkdir(ctx, workingDir) 123 if err != nil { 124 return "", err 125 } 126 127 return runGit(ctx, workingDir, "rev-parse", "HEAD:"+gitPath+"/") 128 } 129 130 func gitAbbrevtreesha(ctx context.Context, workingDir string) (string, error) { 131 gitPath, err := getGitPathToWorkdir(ctx, workingDir) 132 if err != nil { 133 return "", err 134 } 135 136 return runGit(ctx, workingDir, "rev-parse", "--short", "HEAD:"+gitPath+"/") 137 } 138 139 func gitBranches(ctx context.Context, workingDir string) (string, error) { 140 gitBranch, err := runGit(ctx, workingDir, "branch", "--show-current") 141 if err != nil { 142 return gitAbbrevcommitsha(ctx, workingDir) 143 } 144 145 return gitBranch, nil 146 } 147 148 func getGitPathToWorkdir(ctx context.Context, workingDir string) (string, error) { 149 absWorkingDir, err := filepath.Abs(workingDir) 150 if err != nil { 151 return "", err 152 } 153 154 // git reports the gitdir with resolved symlinks, so we need to do this too in order for filepath.Rel to work 155 absWorkingDir, err = filepath.EvalSymlinks(absWorkingDir) 156 if err != nil { 157 return "", err 158 } 159 160 gitRoot, err := runGit(ctx, workingDir, "rev-parse", "--show-toplevel") 161 if err != nil { 162 return "", err 163 } 164 165 return filepath.Rel(gitRoot, absWorkingDir) 166 } 167 168 func runGit(ctx context.Context, workingDir string, arg ...string) (string, error) { 169 cmd := exec.Command("git", arg...) 170 cmd.Dir = workingDir 171 172 out, err := util.RunCmdOut(ctx, cmd) 173 if err != nil { 174 return "", err 175 } 176 177 return string(bytes.TrimSpace(out)), nil 178 }