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  }