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 }