mirror of
https://github.com/apricote/releaser-pleaser.git
synced 2026-01-13 13:21:00 +00:00
227 lines
5 KiB
Go
227 lines
5 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"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
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, updaters []updater.Updater) error {
|
|
worktree, err := r.r.Worktree()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
file, err := worktree.Filesystem.OpenFile(path, os.O_RDWR, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
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) (Commit, error) {
|
|
worktree, err := r.r.Worktree()
|
|
if err != nil {
|
|
return Commit{}, err
|
|
}
|
|
|
|
releaseCommitHash, err := worktree.Commit(message, &git.CommitOptions{
|
|
Author: signature(),
|
|
Committer: signature(),
|
|
})
|
|
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,
|
|
})
|
|
}
|
|
|
|
func signature() *object.Signature {
|
|
return &object.Signature{
|
|
Name: "releaser-pleaser",
|
|
Email: "",
|
|
When: time.Now(),
|
|
}
|
|
}
|