github.com/triarius/goreleaser@v1.12.5/internal/pipe/git/git.go (about)

     1  package git
     2  
     3  import (
     4  	"fmt"
     5  	"net/url"
     6  	"os"
     7  	"os/exec"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/caarlos0/log"
    13  	"github.com/triarius/goreleaser/internal/git"
    14  	"github.com/triarius/goreleaser/internal/pipe"
    15  	"github.com/triarius/goreleaser/pkg/context"
    16  )
    17  
    18  // Pipe that sets up git state.
    19  type Pipe struct{}
    20  
    21  func (Pipe) String() string {
    22  	return "getting and validating git state"
    23  }
    24  
    25  // Run the pipe.
    26  func (Pipe) Run(ctx *context.Context) error {
    27  	if _, err := exec.LookPath("git"); err != nil {
    28  		return ErrNoGit
    29  	}
    30  	info, err := getInfo(ctx)
    31  	if err != nil {
    32  		return err
    33  	}
    34  	ctx.Git = info
    35  	log.WithField("commit", info.Commit).WithField("latest tag", info.CurrentTag).Info("building...")
    36  	ctx.Version = strings.TrimPrefix(ctx.Git.CurrentTag, "v")
    37  	return validate(ctx)
    38  }
    39  
    40  // nolint: gochecknoglobals
    41  var fakeInfo = context.GitInfo{
    42  	Branch:      "none",
    43  	CurrentTag:  "v0.0.0",
    44  	Commit:      "none",
    45  	ShortCommit: "none",
    46  	FullCommit:  "none",
    47  	Summary:     "none",
    48  }
    49  
    50  func getInfo(ctx *context.Context) (context.GitInfo, error) {
    51  	if !git.IsRepo(ctx) && ctx.Snapshot {
    52  		log.Warn("accepting to run without a git repo because this is a snapshot")
    53  		return fakeInfo, nil
    54  	}
    55  	if !git.IsRepo(ctx) {
    56  		return context.GitInfo{}, ErrNotRepository
    57  	}
    58  	info, err := getGitInfo(ctx)
    59  	if err != nil && ctx.Snapshot {
    60  		log.WithError(err).Warn("ignoring errors because this is a snapshot")
    61  		if info.Commit == "" {
    62  			info = fakeInfo
    63  		}
    64  		return info, nil
    65  	}
    66  	return info, err
    67  }
    68  
    69  func getGitInfo(ctx *context.Context) (context.GitInfo, error) {
    70  	branch, err := getBranch(ctx)
    71  	if err != nil {
    72  		return context.GitInfo{}, fmt.Errorf("couldn't get current branch: %w", err)
    73  	}
    74  	short, err := getShortCommit(ctx)
    75  	if err != nil {
    76  		return context.GitInfo{}, fmt.Errorf("couldn't get current commit: %w", err)
    77  	}
    78  	full, err := getFullCommit(ctx)
    79  	if err != nil {
    80  		return context.GitInfo{}, fmt.Errorf("couldn't get current commit: %w", err)
    81  	}
    82  	date, err := getCommitDate(ctx)
    83  	if err != nil {
    84  		return context.GitInfo{}, fmt.Errorf("couldn't get commit date: %w", err)
    85  	}
    86  	summary, err := getSummary(ctx)
    87  	if err != nil {
    88  		return context.GitInfo{}, fmt.Errorf("couldn't get summary: %w", err)
    89  	}
    90  	gitURL, err := getURL(ctx)
    91  	if err != nil {
    92  		return context.GitInfo{}, fmt.Errorf("couldn't get remote URL: %w", err)
    93  	}
    94  
    95  	if strings.HasPrefix(gitURL, "https://") {
    96  		u, err := url.Parse(gitURL)
    97  		if err != nil {
    98  			return context.GitInfo{}, fmt.Errorf("couldn't parse remote URL: %w", err)
    99  		}
   100  		u.User = nil
   101  		gitURL = u.String()
   102  	}
   103  
   104  	tag, err := getTag(ctx)
   105  	if err != nil {
   106  		return context.GitInfo{
   107  			Branch:      branch,
   108  			Commit:      full,
   109  			FullCommit:  full,
   110  			ShortCommit: short,
   111  			CommitDate:  date,
   112  			URL:         gitURL,
   113  			CurrentTag:  "v0.0.0",
   114  			Summary:     summary,
   115  		}, ErrNoTag
   116  	}
   117  
   118  	subject, err := getTagWithFormat(ctx, tag, "contents:subject")
   119  	if err != nil {
   120  		return context.GitInfo{}, fmt.Errorf("couldn't get tag subject: %w", err)
   121  	}
   122  
   123  	contents, err := getTagWithFormat(ctx, tag, "contents")
   124  	if err != nil {
   125  		return context.GitInfo{}, fmt.Errorf("couldn't get tag contents: %w", err)
   126  	}
   127  
   128  	body, err := getTagWithFormat(ctx, tag, "contents:body")
   129  	if err != nil {
   130  		return context.GitInfo{}, fmt.Errorf("couldn't get tag content body: %w", err)
   131  	}
   132  
   133  	previous, err := getPreviousTag(ctx, tag)
   134  	if err != nil {
   135  		// shouldn't error, will only affect templates
   136  		log.Warnf("couldn't find any tags before %q", tag)
   137  	}
   138  
   139  	return context.GitInfo{
   140  		Branch:      branch,
   141  		CurrentTag:  tag,
   142  		PreviousTag: previous,
   143  		Commit:      full,
   144  		FullCommit:  full,
   145  		ShortCommit: short,
   146  		CommitDate:  date,
   147  		URL:         gitURL,
   148  		Summary:     summary,
   149  		TagSubject:  subject,
   150  		TagContents: contents,
   151  		TagBody:     body,
   152  	}, nil
   153  }
   154  
   155  func validate(ctx *context.Context) error {
   156  	if ctx.Snapshot {
   157  		return pipe.ErrSnapshotEnabled
   158  	}
   159  	if ctx.SkipValidate {
   160  		return pipe.ErrSkipValidateEnabled
   161  	}
   162  	if _, err := os.Stat(".git/shallow"); err == nil {
   163  		log.Warn("running against a shallow clone - check your CI documentation at https://goreleaser.com/ci")
   164  	}
   165  	if err := CheckDirty(ctx); err != nil {
   166  		return err
   167  	}
   168  	_, err := git.Clean(git.Run(ctx, "describe", "--exact-match", "--tags", "--match", ctx.Git.CurrentTag))
   169  	if err != nil {
   170  		return ErrWrongRef{
   171  			commit: ctx.Git.Commit,
   172  			tag:    ctx.Git.CurrentTag,
   173  		}
   174  	}
   175  	return nil
   176  }
   177  
   178  // CheckDirty returns an error if the current git repository is dirty.
   179  func CheckDirty(ctx *context.Context) error {
   180  	out, err := git.Run(ctx, "status", "--porcelain")
   181  	if strings.TrimSpace(out) != "" || err != nil {
   182  		return ErrDirty{status: out}
   183  	}
   184  	return nil
   185  }
   186  
   187  func getBranch(ctx *context.Context) (string, error) {
   188  	return git.Clean(git.Run(ctx, "rev-parse", "--abbrev-ref", "HEAD", "--quiet"))
   189  }
   190  
   191  func getCommitDate(ctx *context.Context) (time.Time, error) {
   192  	ct, err := git.Clean(git.Run(ctx, "show", "--format='%ct'", "HEAD", "--quiet"))
   193  	if err != nil {
   194  		return time.Time{}, err
   195  	}
   196  	if ct == "" {
   197  		return time.Time{}, nil
   198  	}
   199  	i, err := strconv.ParseInt(ct, 10, 64)
   200  	if err != nil {
   201  		return time.Time{}, err
   202  	}
   203  	t := time.Unix(i, 0).UTC()
   204  	return t, nil
   205  }
   206  
   207  func getShortCommit(ctx *context.Context) (string, error) {
   208  	return git.Clean(git.Run(ctx, "show", "--format=%h", "HEAD", "--quiet"))
   209  }
   210  
   211  func getFullCommit(ctx *context.Context) (string, error) {
   212  	return git.Clean(git.Run(ctx, "show", "--format=%H", "HEAD", "--quiet"))
   213  }
   214  
   215  func getSummary(ctx *context.Context) (string, error) {
   216  	return git.Clean(git.Run(ctx, "describe", "--always", "--dirty", "--tags"))
   217  }
   218  
   219  func getTagWithFormat(ctx *context.Context, tag, format string) (string, error) {
   220  	out, err := git.Run(ctx, "tag", "-l", "--format='%("+format+")'", tag)
   221  	return strings.TrimSpace(strings.TrimSuffix(strings.ReplaceAll(out, "'", ""), "\n\n")), err
   222  }
   223  
   224  func getTag(ctx *context.Context) (string, error) {
   225  	for _, fn := range []func() ([]string, error){
   226  		getFromEnv("GORELEASER_CURRENT_TAG"),
   227  		func() ([]string, error) {
   228  			return gitTagsPointingAt(ctx, "HEAD")
   229  		},
   230  		func() ([]string, error) {
   231  			// this will get the last tag, even if it wasn't made against the
   232  			// last commit...
   233  			return git.CleanAllLines(gitDescribe(ctx, "HEAD"))
   234  		},
   235  	} {
   236  		tags, err := fn()
   237  		if len(tags) > 0 {
   238  			return tags[0], err
   239  		}
   240  		if err != nil {
   241  			return "", err
   242  		}
   243  	}
   244  
   245  	return "", nil
   246  }
   247  
   248  func getPreviousTag(ctx *context.Context, current string) (string, error) {
   249  	for _, fn := range []func() ([]string, error){
   250  		getFromEnv("GORELEASER_PREVIOUS_TAG"),
   251  		func() ([]string, error) {
   252  			sha, err := previousTagSha(ctx, current)
   253  			if err != nil {
   254  				return nil, err
   255  			}
   256  			return gitTagsPointingAt(ctx, sha)
   257  		},
   258  	} {
   259  		tags, err := fn()
   260  		if len(tags) > 0 {
   261  			return tags[0], err
   262  		}
   263  		if err != nil {
   264  			return "", err
   265  		}
   266  	}
   267  
   268  	return "", nil
   269  }
   270  
   271  func gitTagsPointingAt(ctx *context.Context, ref string) ([]string, error) {
   272  	return git.CleanAllLines(git.Run(
   273  		ctx,
   274  		"tag",
   275  		"--points-at",
   276  		ref,
   277  		"--sort",
   278  		"-version:refname",
   279  	))
   280  }
   281  
   282  func gitDescribe(ctx *context.Context, ref string) (string, error) {
   283  	return git.Clean(git.Run(
   284  		ctx,
   285  		"describe",
   286  		"--tags",
   287  		"--abbrev=0",
   288  		ref,
   289  	))
   290  }
   291  
   292  func previousTagSha(ctx *context.Context, current string) (string, error) {
   293  	tag, err := gitDescribe(ctx, fmt.Sprintf("tags/%s^", current))
   294  	if err != nil {
   295  		return "", err
   296  	}
   297  	return git.Clean(git.Run(ctx, "rev-list", "-n1", tag))
   298  }
   299  
   300  func getURL(ctx *context.Context) (string, error) {
   301  	return git.Clean(git.Run(ctx, "ls-remote", "--get-url"))
   302  }
   303  
   304  func getFromEnv(s string) func() ([]string, error) {
   305  	return func() ([]string, error) {
   306  		if tag := os.Getenv(s); tag != "" {
   307  			return []string{tag}, nil
   308  		}
   309  		return nil, nil
   310  	}
   311  }