mirror of
https://github.com/apricote/releaser-pleaser.git
synced 2026-01-13 21:21:03 +00:00
Previously all commits were authored and committed by
releaser-pleaser <>
This looked weird when looking at the commit. We now check with the
Forge API for details on the currently authenticated user, and use that
name and email as the commit author. The commit committer stays the same
for now.
In GitHub, the default `$GITHUB_TOKEN` does not allow access to the
required endpoint, so for github the user `github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>` is hardcoded
when the request fails.
248 lines
5.4 KiB
Go
248 lines
5.4 KiB
Go
package git
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/go-git/go-git/v5"
|
|
"github.com/go-git/go-git/v5/config"
|
|
"github.com/go-git/go-git/v5/plumbing"
|
|
"github.com/go-git/go-git/v5/plumbing/object"
|
|
"github.com/go-git/go-git/v5/plumbing/transport"
|
|
|
|
"github.com/apricote/releaser-pleaser/internal/updater"
|
|
)
|
|
|
|
const (
|
|
remoteName = "origin"
|
|
newFilePermissions = 0o644
|
|
)
|
|
|
|
type Commit struct {
|
|
Hash string
|
|
Message string
|
|
|
|
PullRequest *PullRequest
|
|
}
|
|
|
|
type PullRequest struct {
|
|
ID int
|
|
Title string
|
|
Description string
|
|
}
|
|
|
|
type Tag struct {
|
|
Hash string
|
|
Name string
|
|
}
|
|
|
|
type Releases struct {
|
|
Latest *Tag
|
|
Stable *Tag
|
|
}
|
|
|
|
type Author struct {
|
|
Name string
|
|
Email string
|
|
}
|
|
|
|
func (a Author) signature(when time.Time) *object.Signature {
|
|
return &object.Signature{
|
|
Name: a.Name,
|
|
Email: a.Email,
|
|
When: when,
|
|
}
|
|
}
|
|
|
|
func (a Author) String() string {
|
|
return fmt.Sprintf("%s <%s>", a.Name, a.Email)
|
|
}
|
|
|
|
var (
|
|
committer = Author{Name: "releaser-pleaser", Email: ""}
|
|
)
|
|
|
|
func CloneRepo(ctx context.Context, logger *slog.Logger, cloneURL, branch string, auth transport.AuthMethod) (*Repository, error) {
|
|
dir, err := os.MkdirTemp("", "releaser-pleaser.*")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create temporary directory for repo clone: %w", err)
|
|
}
|
|
|
|
repo, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{
|
|
URL: cloneURL,
|
|
RemoteName: remoteName,
|
|
ReferenceName: plumbing.NewBranchReferenceName(branch),
|
|
SingleBranch: false,
|
|
Auth: auth,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to clone repository: %w", err)
|
|
}
|
|
|
|
return &Repository{r: repo, logger: logger, auth: auth}, nil
|
|
}
|
|
|
|
type Repository struct {
|
|
r *git.Repository
|
|
logger *slog.Logger
|
|
auth transport.AuthMethod
|
|
}
|
|
|
|
func (r *Repository) DeleteBranch(ctx context.Context, branch string) error {
|
|
if b, _ := r.r.Branch(branch); b != nil {
|
|
r.logger.DebugContext(ctx, "deleting local branch", "branch.name", branch)
|
|
if err := r.r.DeleteBranch(branch); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) Checkout(_ context.Context, branch string) error {
|
|
worktree, err := r.r.Worktree()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = worktree.Checkout(&git.CheckoutOptions{
|
|
Branch: plumbing.NewBranchReferenceName(branch),
|
|
Create: true,
|
|
}); err != nil {
|
|
return fmt.Errorf("failed to check out branch: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) UpdateFile(_ context.Context, path string, create bool, updaters []updater.Updater) error {
|
|
worktree, err := r.r.Worktree()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fileFlags := os.O_RDWR
|
|
if create {
|
|
fileFlags |= os.O_CREATE
|
|
}
|
|
|
|
file, err := worktree.Filesystem.OpenFile(path, fileFlags, newFilePermissions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close() //nolint:errcheck
|
|
|
|
content, err := io.ReadAll(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
updatedContent := string(content)
|
|
|
|
for _, update := range updaters {
|
|
updatedContent, err = update(updatedContent)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to run updater on file %s", path)
|
|
}
|
|
}
|
|
|
|
err = file.Truncate(0)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to replace file content: %w", err)
|
|
}
|
|
_, err = file.Seek(0, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to replace file content: %w", err)
|
|
}
|
|
_, err = file.Write([]byte(updatedContent))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to replace file content: %w", err)
|
|
}
|
|
|
|
_, err = worktree.Add(path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add updated file to git worktree: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *Repository) Commit(_ context.Context, message string, author Author) (Commit, error) {
|
|
worktree, err := r.r.Worktree()
|
|
if err != nil {
|
|
return Commit{}, err
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
releaseCommitHash, err := worktree.Commit(message, &git.CommitOptions{
|
|
Author: author.signature(now),
|
|
Committer: committer.signature(now),
|
|
})
|
|
if err != nil {
|
|
return Commit{}, fmt.Errorf("failed to commit changes: %w", err)
|
|
}
|
|
|
|
return Commit{
|
|
Hash: releaseCommitHash.String(),
|
|
Message: message,
|
|
}, nil
|
|
}
|
|
|
|
func (r *Repository) HasChangesWithRemote(ctx context.Context, branch string) (bool, error) {
|
|
remoteRef, err := r.r.Reference(plumbing.NewRemoteReferenceName(remoteName, branch), false)
|
|
if err != nil {
|
|
if err.Error() == "reference not found" {
|
|
// No remote branch means that there are changes
|
|
return true, nil
|
|
}
|
|
|
|
return false, err
|
|
}
|
|
|
|
remoteCommit, err := r.r.CommitObject(remoteRef.Hash())
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
localRef, err := r.r.Reference(plumbing.NewBranchReferenceName(branch), false)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
localCommit, err := r.r.CommitObject(localRef.Hash())
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
diff, err := localCommit.PatchContext(ctx, remoteCommit)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
hasChanges := len(diff.FilePatches()) > 0
|
|
|
|
return hasChanges, nil
|
|
}
|
|
|
|
func (r *Repository) ForcePush(ctx context.Context, branch string) error {
|
|
pushRefSpec := config.RefSpec(fmt.Sprintf(
|
|
"+%s:%s",
|
|
plumbing.NewBranchReferenceName(branch),
|
|
// This needs to be the local branch name, not the remotes/origin ref
|
|
// See https://stackoverflow.com/a/75727620
|
|
plumbing.NewBranchReferenceName(branch),
|
|
))
|
|
|
|
r.logger.DebugContext(ctx, "pushing branch", "branch.name", branch, "refspec", pushRefSpec.String())
|
|
return r.r.PushContext(ctx, &git.PushOptions{
|
|
RemoteName: remoteName,
|
|
RefSpecs: []config.RefSpec{pushRefSpec},
|
|
Force: true,
|
|
Auth: r.auth,
|
|
})
|
|
}
|