1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-01-16 03:52:12 +02:00
goreleaser/internal/pipe/git/git.go
Carlos Alexandro Becker ec2db4a727
feat!: rename module to /v2 (#4894)
<!--

Hi, thanks for contributing!

Please make sure you read our CONTRIBUTING guide.

Also, add tests and the respective documentation changes as well.

-->


<!-- If applied, this commit will... -->

...

<!-- Why is this change being made? -->

...

<!-- # Provide links to any relevant tickets, URLs or other resources
-->

...

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
2024-05-26 15:02:57 -03:00

376 lines
9.4 KiB
Go

package git
import (
"fmt"
"net/url"
"os"
"os/exec"
"strconv"
"strings"
"time"
"github.com/caarlos0/log"
"github.com/charmbracelet/x/exp/ordered"
"github.com/goreleaser/goreleaser/v2/internal/git"
"github.com/goreleaser/goreleaser/v2/internal/pipe"
"github.com/goreleaser/goreleaser/v2/internal/skips"
"github.com/goreleaser/goreleaser/v2/internal/tmpl"
"github.com/goreleaser/goreleaser/v2/pkg/context"
)
// Pipe that sets up git state.
type Pipe struct{}
func (Pipe) String() string {
return "getting and validating git state"
}
// this pipe does not implement Defaulter because it runs before the defaults
// pipe, and we need to set some defaults of our own first.
func setDefaults(ctx *context.Context) {
if ctx.Config.Git.TagSort == "" {
ctx.Config.Git.TagSort = "-version:refname"
}
}
// Run the pipe.
func (Pipe) Run(ctx *context.Context) error {
if _, err := exec.LookPath("git"); err != nil {
return ErrNoGit
}
setDefaults(ctx)
info, err := getInfo(ctx)
if err != nil {
return err
}
ctx.Git = info
log.WithField("commit", info.Commit).
WithField("branch", info.Branch).
WithField("current_tag", info.CurrentTag).
WithField("previous_tag", ordered.First(info.PreviousTag, "<unknown>")).
WithField("dirty", info.Dirty).
Info("git state")
ctx.Version = strings.TrimPrefix(ctx.Git.CurrentTag, "v")
return validate(ctx)
}
//nolint:gochecknoglobals
var fakeInfo = context.GitInfo{
Branch: "none",
CurrentTag: "v0.0.0",
Commit: "none",
ShortCommit: "none",
FullCommit: "none",
Summary: "none",
}
func getInfo(ctx *context.Context) (context.GitInfo, error) {
if !git.IsRepo(ctx) && ctx.Snapshot {
log.Warn("accepting to run without a git repository because this is a snapshot")
return fakeInfo, nil
}
if !git.IsRepo(ctx) {
return context.GitInfo{}, ErrNotRepository
}
info, err := getGitInfo(ctx)
if err != nil && ctx.Snapshot {
log.WithError(err).Warn("ignoring errors because this is a snapshot")
if info.Commit == "" {
info = fakeInfo
}
return info, nil
}
return info, err
}
func getGitInfo(ctx *context.Context) (context.GitInfo, error) {
branch, err := getBranch(ctx)
if err != nil {
return context.GitInfo{}, fmt.Errorf("couldn't get current branch: %w", err)
}
short, err := getShortCommit(ctx)
if err != nil {
return context.GitInfo{}, fmt.Errorf("couldn't get current commit: %w", err)
}
full, err := getFullCommit(ctx)
if err != nil {
return context.GitInfo{}, fmt.Errorf("couldn't get current commit: %w", err)
}
first, err := getFirstCommit(ctx)
if err != nil {
return context.GitInfo{}, fmt.Errorf("couldn't get first commit: %w", err)
}
date, err := getCommitDate(ctx)
if err != nil {
return context.GitInfo{}, fmt.Errorf("couldn't get commit date: %w", err)
}
summary, err := getSummary(ctx)
if err != nil {
return context.GitInfo{}, fmt.Errorf("couldn't get summary: %w", err)
}
gitURL, err := getURL(ctx)
if err != nil {
return context.GitInfo{}, fmt.Errorf("couldn't get remote URL: %w", err)
}
if strings.HasPrefix(gitURL, "https://") {
u, err := url.Parse(gitURL)
if err != nil {
return context.GitInfo{}, fmt.Errorf("couldn't parse remote URL: %w", err)
}
u.User = nil
gitURL = u.String()
}
var excluding []string
tpl := tmpl.New(ctx)
for _, exclude := range ctx.Config.Git.IgnoreTags {
tag, err := tpl.Apply(exclude)
if err != nil {
return context.GitInfo{}, err
}
excluding = append(excluding, tag)
}
tag, err := getTag(ctx, excluding)
if err != nil {
return context.GitInfo{
Branch: branch,
Commit: full,
FullCommit: full,
ShortCommit: short,
FirstCommit: first,
CommitDate: date,
URL: gitURL,
CurrentTag: "v0.0.0",
Summary: summary,
}, ErrNoTag
}
subject, err := getTagWithFormat(ctx, tag, "contents:subject")
if err != nil {
return context.GitInfo{}, fmt.Errorf("couldn't get tag subject: %w", err)
}
contents, err := getTagWithFormat(ctx, tag, "contents")
if err != nil {
return context.GitInfo{}, fmt.Errorf("couldn't get tag contents: %w", err)
}
body, err := getTagWithFormat(ctx, tag, "contents:body")
if err != nil {
return context.GitInfo{}, fmt.Errorf("couldn't get tag content body: %w", err)
}
previous, err := getPreviousTag(ctx, tag, excluding)
if err != nil {
// shouldn't error, will only affect templates and changelog
log.Warnf("couldn't find any tags before %q", tag)
}
return context.GitInfo{
Branch: branch,
CurrentTag: tag,
PreviousTag: previous,
Commit: full,
FullCommit: full,
ShortCommit: short,
FirstCommit: first,
CommitDate: date,
URL: gitURL,
Summary: summary,
TagSubject: subject,
TagContents: contents,
TagBody: body,
Dirty: CheckDirty(ctx) != nil,
}, nil
}
func validate(ctx *context.Context) error {
if ctx.Snapshot {
return pipe.ErrSnapshotEnabled
}
if skips.Any(ctx, skips.Validate) {
return pipe.ErrSkipValidateEnabled
}
if _, err := os.Stat(".git/shallow"); err == nil {
log.Warn("running against a shallow clone - check your CI documentation at https://goreleaser.com/ci")
}
if err := CheckDirty(ctx); err != nil {
return err
}
_, err := git.Clean(git.Run(ctx, "describe", "--exact-match", "--tags", "--match", ctx.Git.CurrentTag))
if err != nil {
return ErrWrongRef{
commit: ctx.Git.Commit,
tag: ctx.Git.CurrentTag,
}
}
return nil
}
// CheckDirty returns an error if the current git repository is dirty.
func CheckDirty(ctx *context.Context) error {
out, err := git.Run(ctx, "status", "--porcelain")
if strings.TrimSpace(out) != "" || err != nil {
return ErrDirty{status: out}
}
return nil
}
func getBranch(ctx *context.Context) (string, error) {
return git.Clean(git.Run(ctx, "rev-parse", "--abbrev-ref", "HEAD", "--quiet"))
}
func getCommitDate(ctx *context.Context) (time.Time, error) {
ct, err := git.Clean(git.Run(ctx, "show", "--format='%ct'", "HEAD", "--quiet"))
if err != nil {
return time.Time{}, err
}
if ct == "" {
return time.Time{}, nil
}
i, err := strconv.ParseInt(ct, 10, 64)
if err != nil {
return time.Time{}, err
}
t := time.Unix(i, 0).UTC()
return t, nil
}
func getShortCommit(ctx *context.Context) (string, error) {
return git.Clean(git.Run(ctx, "show", "--format=%h", "HEAD", "--quiet"))
}
func getFullCommit(ctx *context.Context) (string, error) {
return git.Clean(git.Run(ctx, "show", "--format=%H", "HEAD", "--quiet"))
}
func getFirstCommit(ctx *context.Context) (string, error) {
return git.Clean(git.Run(ctx, "rev-list", "--max-parents=0", "HEAD"))
}
func getSummary(ctx *context.Context) (string, error) {
return git.Clean(git.Run(ctx, "describe", "--always", "--dirty", "--tags"))
}
func getTagWithFormat(ctx *context.Context, tag, format string) (string, error) {
out, err := git.Run(ctx, "tag", "-l", "--format='%("+format+")'", tag)
return strings.TrimSpace(strings.TrimSuffix(strings.ReplaceAll(out, "'", ""), "\n\n")), err
}
func getTag(ctx *context.Context, excluding []string) (string, error) {
for _, fn := range []func() ([]string, error){
getFromEnv("GORELEASER_CURRENT_TAG"),
func() ([]string, error) {
return gitTagsPointingAt(ctx, "HEAD")
},
func() ([]string, error) {
// this will get the last tag, even if it wasn't made against the
// last commit...
return git.CleanAllLines(gitDescribe(ctx, "HEAD", excluding))
},
} {
tags, err := fn()
if err != nil {
return "", err
}
if tag := filterOut(tags, excluding); tag != "" {
return tag, err
}
}
return "", nil
}
func getPreviousTag(ctx *context.Context, current string, excluding []string) (string, error) {
for _, fn := range []func() ([]string, error){
getFromEnv("GORELEASER_PREVIOUS_TAG"),
func() ([]string, error) {
sha, err := previousTagSha(ctx, current, excluding)
if err != nil {
return nil, err
}
return gitTagsPointingAt(ctx, sha)
},
} {
tags, err := fn()
if err != nil {
return "", err
}
if tag := filterOut(tags, excluding); tag != "" {
return tag, nil
}
}
return "", nil
}
func gitTagsPointingAt(ctx *context.Context, ref string) ([]string, error) {
args := []string{}
if ctx.Config.Git.PrereleaseSuffix != "" {
args = append(
args,
"-c",
"versionsort.suffix="+ctx.Config.Git.PrereleaseSuffix,
)
}
args = append(
args,
"tag",
"--points-at",
ref,
"--sort",
ctx.Config.Git.TagSort,
)
return git.CleanAllLines(git.Run(ctx, args...))
}
func gitDescribe(ctx *context.Context, ref string, excluding []string) (string, error) {
args := []string{
"describe",
"--tags",
"--abbrev=0",
ref,
}
for _, exclude := range excluding {
args = append(args, "--exclude="+exclude)
}
return git.Clean(git.Run(ctx, args...))
}
func previousTagSha(ctx *context.Context, current string, excluding []string) (string, error) {
tag, err := gitDescribe(ctx, fmt.Sprintf("tags/%s^", current), excluding)
if err != nil {
return "", err
}
return git.Clean(git.Run(ctx, "rev-list", "-n1", tag))
}
func getURL(ctx *context.Context) (string, error) {
return git.Clean(git.Run(ctx, "ls-remote", "--get-url"))
}
func getFromEnv(s string) func() ([]string, error) {
return func() ([]string, error) {
if tag := os.Getenv(s); tag != "" {
return []string{tag}, nil
}
return nil, nil
}
}
func filterOut(tags []string, exclude []string) string {
if len(exclude) == 0 && len(tags) > 0 {
return tags[0]
}
for _, tag := range tags {
for _, exl := range exclude {
if exl != tag {
return tag
}
}
}
return ""
}