github.com/goreleaser/goreleaser@v1.25.1/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/charmbracelet/x/exp/ordered"
    14  	"github.com/goreleaser/goreleaser/internal/git"
    15  	"github.com/goreleaser/goreleaser/internal/pipe"
    16  	"github.com/goreleaser/goreleaser/internal/skips"
    17  	"github.com/goreleaser/goreleaser/internal/tmpl"
    18  	"github.com/goreleaser/goreleaser/pkg/context"
    19  )
    20  
    21  // Pipe that sets up git state.
    22  type Pipe struct{}
    23  
    24  func (Pipe) String() string {
    25  	return "getting and validating git state"
    26  }
    27  
    28  // this pipe does not implement Defaulter because it runs before the defaults
    29  // pipe, and we need to set some defaults of our own first.
    30  func setDefaults(ctx *context.Context) {
    31  	if ctx.Config.Git.TagSort == "" {
    32  		ctx.Config.Git.TagSort = "-version:refname"
    33  	}
    34  }
    35  
    36  // Run the pipe.
    37  func (Pipe) Run(ctx *context.Context) error {
    38  	if _, err := exec.LookPath("git"); err != nil {
    39  		return ErrNoGit
    40  	}
    41  	setDefaults(ctx)
    42  	info, err := getInfo(ctx)
    43  	if err != nil {
    44  		return err
    45  	}
    46  	ctx.Git = info
    47  	log.WithField("commit", info.Commit).
    48  		WithField("branch", info.Branch).
    49  		WithField("current_tag", info.CurrentTag).
    50  		WithField("previous_tag", ordered.First(info.PreviousTag, "<unknown>")).
    51  		WithField("dirty", info.Dirty).
    52  		Info("git state")
    53  	ctx.Version = strings.TrimPrefix(ctx.Git.CurrentTag, "v")
    54  	return validate(ctx)
    55  }
    56  
    57  // nolint: gochecknoglobals
    58  var fakeInfo = context.GitInfo{
    59  	Branch:      "none",
    60  	CurrentTag:  "v0.0.0",
    61  	Commit:      "none",
    62  	ShortCommit: "none",
    63  	FullCommit:  "none",
    64  	Summary:     "none",
    65  }
    66  
    67  func getInfo(ctx *context.Context) (context.GitInfo, error) {
    68  	if !git.IsRepo(ctx) && ctx.Snapshot {
    69  		log.Warn("accepting to run without a git repository because this is a snapshot")
    70  		return fakeInfo, nil
    71  	}
    72  	if !git.IsRepo(ctx) {
    73  		return context.GitInfo{}, ErrNotRepository
    74  	}
    75  	info, err := getGitInfo(ctx)
    76  	if err != nil && ctx.Snapshot {
    77  		log.WithError(err).Warn("ignoring errors because this is a snapshot")
    78  		if info.Commit == "" {
    79  			info = fakeInfo
    80  		}
    81  		return info, nil
    82  	}
    83  	return info, err
    84  }
    85  
    86  func getGitInfo(ctx *context.Context) (context.GitInfo, error) {
    87  	branch, err := getBranch(ctx)
    88  	if err != nil {
    89  		return context.GitInfo{}, fmt.Errorf("couldn't get current branch: %w", err)
    90  	}
    91  	short, err := getShortCommit(ctx)
    92  	if err != nil {
    93  		return context.GitInfo{}, fmt.Errorf("couldn't get current commit: %w", err)
    94  	}
    95  	full, err := getFullCommit(ctx)
    96  	if err != nil {
    97  		return context.GitInfo{}, fmt.Errorf("couldn't get current commit: %w", err)
    98  	}
    99  	first, err := getFirstCommit(ctx)
   100  	if err != nil {
   101  		return context.GitInfo{}, fmt.Errorf("couldn't get first commit: %w", err)
   102  	}
   103  	date, err := getCommitDate(ctx)
   104  	if err != nil {
   105  		return context.GitInfo{}, fmt.Errorf("couldn't get commit date: %w", err)
   106  	}
   107  	summary, err := getSummary(ctx)
   108  	if err != nil {
   109  		return context.GitInfo{}, fmt.Errorf("couldn't get summary: %w", err)
   110  	}
   111  	gitURL, err := getURL(ctx)
   112  	if err != nil {
   113  		return context.GitInfo{}, fmt.Errorf("couldn't get remote URL: %w", err)
   114  	}
   115  
   116  	if strings.HasPrefix(gitURL, "https://") {
   117  		u, err := url.Parse(gitURL)
   118  		if err != nil {
   119  			return context.GitInfo{}, fmt.Errorf("couldn't parse remote URL: %w", err)
   120  		}
   121  		u.User = nil
   122  		gitURL = u.String()
   123  	}
   124  
   125  	var excluding []string
   126  	tpl := tmpl.New(ctx)
   127  	for _, exclude := range ctx.Config.Git.IgnoreTags {
   128  		tag, err := tpl.Apply(exclude)
   129  		if err != nil {
   130  			return context.GitInfo{}, err
   131  		}
   132  		excluding = append(excluding, tag)
   133  	}
   134  
   135  	tag, err := getTag(ctx, excluding)
   136  	if err != nil {
   137  		return context.GitInfo{
   138  			Branch:      branch,
   139  			Commit:      full,
   140  			FullCommit:  full,
   141  			ShortCommit: short,
   142  			FirstCommit: first,
   143  			CommitDate:  date,
   144  			URL:         gitURL,
   145  			CurrentTag:  "v0.0.0",
   146  			Summary:     summary,
   147  		}, ErrNoTag
   148  	}
   149  
   150  	subject, err := getTagWithFormat(ctx, tag, "contents:subject")
   151  	if err != nil {
   152  		return context.GitInfo{}, fmt.Errorf("couldn't get tag subject: %w", err)
   153  	}
   154  
   155  	contents, err := getTagWithFormat(ctx, tag, "contents")
   156  	if err != nil {
   157  		return context.GitInfo{}, fmt.Errorf("couldn't get tag contents: %w", err)
   158  	}
   159  
   160  	body, err := getTagWithFormat(ctx, tag, "contents:body")
   161  	if err != nil {
   162  		return context.GitInfo{}, fmt.Errorf("couldn't get tag content body: %w", err)
   163  	}
   164  
   165  	previous, err := getPreviousTag(ctx, tag, excluding)
   166  	if err != nil {
   167  		// shouldn't error, will only affect templates and changelog
   168  		log.Warnf("couldn't find any tags before %q", tag)
   169  	}
   170  
   171  	return context.GitInfo{
   172  		Branch:      branch,
   173  		CurrentTag:  tag,
   174  		PreviousTag: previous,
   175  		Commit:      full,
   176  		FullCommit:  full,
   177  		ShortCommit: short,
   178  		FirstCommit: first,
   179  		CommitDate:  date,
   180  		URL:         gitURL,
   181  		Summary:     summary,
   182  		TagSubject:  subject,
   183  		TagContents: contents,
   184  		TagBody:     body,
   185  		Dirty:       CheckDirty(ctx) != nil,
   186  	}, nil
   187  }
   188  
   189  func validate(ctx *context.Context) error {
   190  	if ctx.Snapshot {
   191  		return pipe.ErrSnapshotEnabled
   192  	}
   193  	if skips.Any(ctx, skips.Validate) {
   194  		return pipe.ErrSkipValidateEnabled
   195  	}
   196  	if _, err := os.Stat(".git/shallow"); err == nil {
   197  		log.Warn("running against a shallow clone - check your CI documentation at https://goreleaser.com/ci")
   198  	}
   199  	if err := CheckDirty(ctx); err != nil {
   200  		return err
   201  	}
   202  	_, err := git.Clean(git.Run(ctx, "describe", "--exact-match", "--tags", "--match", ctx.Git.CurrentTag))
   203  	if err != nil {
   204  		return ErrWrongRef{
   205  			commit: ctx.Git.Commit,
   206  			tag:    ctx.Git.CurrentTag,
   207  		}
   208  	}
   209  	return nil
   210  }
   211  
   212  // CheckDirty returns an error if the current git repository is dirty.
   213  func CheckDirty(ctx *context.Context) error {
   214  	out, err := git.Run(ctx, "status", "--porcelain")
   215  	if strings.TrimSpace(out) != "" || err != nil {
   216  		return ErrDirty{status: out}
   217  	}
   218  	return nil
   219  }
   220  
   221  func getBranch(ctx *context.Context) (string, error) {
   222  	return git.Clean(git.Run(ctx, "rev-parse", "--abbrev-ref", "HEAD", "--quiet"))
   223  }
   224  
   225  func getCommitDate(ctx *context.Context) (time.Time, error) {
   226  	ct, err := git.Clean(git.Run(ctx, "show", "--format='%ct'", "HEAD", "--quiet"))
   227  	if err != nil {
   228  		return time.Time{}, err
   229  	}
   230  	if ct == "" {
   231  		return time.Time{}, nil
   232  	}
   233  	i, err := strconv.ParseInt(ct, 10, 64)
   234  	if err != nil {
   235  		return time.Time{}, err
   236  	}
   237  	t := time.Unix(i, 0).UTC()
   238  	return t, nil
   239  }
   240  
   241  func getShortCommit(ctx *context.Context) (string, error) {
   242  	return git.Clean(git.Run(ctx, "show", "--format=%h", "HEAD", "--quiet"))
   243  }
   244  
   245  func getFullCommit(ctx *context.Context) (string, error) {
   246  	return git.Clean(git.Run(ctx, "show", "--format=%H", "HEAD", "--quiet"))
   247  }
   248  
   249  func getFirstCommit(ctx *context.Context) (string, error) {
   250  	return git.Clean(git.Run(ctx, "rev-list", "--max-parents=0", "HEAD"))
   251  }
   252  
   253  func getSummary(ctx *context.Context) (string, error) {
   254  	return git.Clean(git.Run(ctx, "describe", "--always", "--dirty", "--tags"))
   255  }
   256  
   257  func getTagWithFormat(ctx *context.Context, tag, format string) (string, error) {
   258  	out, err := git.Run(ctx, "tag", "-l", "--format='%("+format+")'", tag)
   259  	return strings.TrimSpace(strings.TrimSuffix(strings.ReplaceAll(out, "'", ""), "\n\n")), err
   260  }
   261  
   262  func getTag(ctx *context.Context, excluding []string) (string, error) {
   263  	for _, fn := range []func() ([]string, error){
   264  		getFromEnv("GORELEASER_CURRENT_TAG"),
   265  		func() ([]string, error) {
   266  			return gitTagsPointingAt(ctx, "HEAD")
   267  		},
   268  		func() ([]string, error) {
   269  			// this will get the last tag, even if it wasn't made against the
   270  			// last commit...
   271  			return git.CleanAllLines(gitDescribe(ctx, "HEAD", excluding))
   272  		},
   273  	} {
   274  		tags, err := fn()
   275  		if err != nil {
   276  			return "", err
   277  		}
   278  		if tag := filterOut(tags, excluding); tag != "" {
   279  			return tag, err
   280  		}
   281  	}
   282  
   283  	return "", nil
   284  }
   285  
   286  func getPreviousTag(ctx *context.Context, current string, excluding []string) (string, error) {
   287  	for _, fn := range []func() ([]string, error){
   288  		getFromEnv("GORELEASER_PREVIOUS_TAG"),
   289  		func() ([]string, error) {
   290  			sha, err := previousTagSha(ctx, current, excluding)
   291  			if err != nil {
   292  				return nil, err
   293  			}
   294  			return gitTagsPointingAt(ctx, sha)
   295  		},
   296  	} {
   297  		tags, err := fn()
   298  		if err != nil {
   299  			return "", err
   300  		}
   301  		if tag := filterOut(tags, excluding); tag != "" {
   302  			return tag, nil
   303  		}
   304  	}
   305  
   306  	return "", nil
   307  }
   308  
   309  func gitTagsPointingAt(ctx *context.Context, ref string) ([]string, error) {
   310  	args := []string{}
   311  	if ctx.Config.Git.PrereleaseSuffix != "" {
   312  		args = append(
   313  			args,
   314  			"-c",
   315  			"versionsort.suffix="+ctx.Config.Git.PrereleaseSuffix,
   316  		)
   317  	}
   318  	args = append(
   319  		args,
   320  		"tag",
   321  		"--points-at",
   322  		ref,
   323  		"--sort",
   324  		ctx.Config.Git.TagSort,
   325  	)
   326  	return git.CleanAllLines(git.Run(ctx, args...))
   327  }
   328  
   329  func gitDescribe(ctx *context.Context, ref string, excluding []string) (string, error) {
   330  	args := []string{
   331  		"describe",
   332  		"--tags",
   333  		"--abbrev=0",
   334  		ref,
   335  	}
   336  	for _, exclude := range excluding {
   337  		args = append(args, "--exclude="+exclude)
   338  	}
   339  	return git.Clean(git.Run(ctx, args...))
   340  }
   341  
   342  func previousTagSha(ctx *context.Context, current string, excluding []string) (string, error) {
   343  	tag, err := gitDescribe(ctx, fmt.Sprintf("tags/%s^", current), excluding)
   344  	if err != nil {
   345  		return "", err
   346  	}
   347  	return git.Clean(git.Run(ctx, "rev-list", "-n1", tag))
   348  }
   349  
   350  func getURL(ctx *context.Context) (string, error) {
   351  	return git.Clean(git.Run(ctx, "ls-remote", "--get-url"))
   352  }
   353  
   354  func getFromEnv(s string) func() ([]string, error) {
   355  	return func() ([]string, error) {
   356  		if tag := os.Getenv(s); tag != "" {
   357  			return []string{tag}, nil
   358  		}
   359  		return nil, nil
   360  	}
   361  }
   362  
   363  func filterOut(tags []string, exclude []string) string {
   364  	if len(exclude) == 0 && len(tags) > 0 {
   365  		return tags[0]
   366  	}
   367  	for _, tag := range tags {
   368  		for _, exl := range exclude {
   369  			if exl != tag {
   370  				return tag
   371  			}
   372  		}
   373  	}
   374  	return ""
   375  }