Skip to content

Commit

Permalink
feat: avoid pushing release branch only for rebasing
Browse files Browse the repository at this point in the history
  • Loading branch information
apricote committed Nov 20, 2024
1 parent 7c9b18f commit 3938c7a
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 9 deletions.
57 changes: 48 additions & 9 deletions internal/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,19 @@ func (r *Repository) Commit(_ context.Context, message string) (Commit, error) {
}, nil
}

func (r *Repository) HasChangesWithRemote(ctx context.Context, branch string) (bool, error) {
remoteRef, err := r.r.Reference(plumbing.NewRemoteReferenceName(remoteName, branch), false)
// HasChangesWithRemote checks if the following two diffs are equal:
//
// - **Local**: remote/main..branch
// - **Remote**: (git merge-base remote/main remote/branch)..remote/branch
//
// This is done to avoid pushing when the only change would be a rebase of remote/branch onto the current remote/main.
func (r *Repository) HasChangesWithRemote(ctx context.Context, mainBranch, prBranch string) (bool, error) {
commitOnRemoteMain, err := r.commitFromRef(plumbing.NewRemoteReferenceName(remoteName, mainBranch))
if err != nil {
return false, err
}

commitOnRemotePRBranch, err := r.commitFromRef(plumbing.NewRemoteReferenceName(remoteName, prBranch))
if err != nil {
if err.Error() == "reference not found" {
// No remote branch means that there are changes
Expand All @@ -181,29 +192,57 @@ func (r *Repository) HasChangesWithRemote(ctx context.Context, branch string) (b
return false, err
}

remoteCommit, err := r.r.CommitObject(remoteRef.Hash())
currentRemotePRMergeBase, err := r.mergeBase(commitOnRemoteMain, commitOnRemotePRBranch)
if err != nil {
return false, err
}
if currentRemotePRMergeBase == nil {
// If there is no merge base something weird has happened with the
// remote main branch, and we should definitely push updates.
return false, nil
}

localRef, err := r.r.Reference(plumbing.NewBranchReferenceName(branch), false)
remoteDiff, err := currentRemotePRMergeBase.PatchContext(ctx, commitOnRemotePRBranch)
if err != nil {
return false, err
}

localCommit, err := r.r.CommitObject(localRef.Hash())
commitOnLocalPRBranch, err := r.commitFromRef(plumbing.NewBranchReferenceName(prBranch))

localDiff, err := commitOnRemoteMain.PatchContext(ctx, commitOnLocalPRBranch)
if err != nil {
return false, err
}

diff, err := localCommit.PatchContext(ctx, remoteCommit)
return remoteDiff.String() == localDiff.String(), nil
}

func (r *Repository) commitFromRef(refName plumbing.ReferenceName) (*object.Commit, error) {
ref, err := r.r.Reference(refName, false)
if err != nil {
return false, err
return nil, err
}

hasChanges := len(diff.FilePatches()) > 0
commit, err := r.r.CommitObject(ref.Hash())
if err != nil {
return nil, err
}

return commit, nil
}

func (r *Repository) mergeBase(a, b *object.Commit) (*object.Commit, error) {
mergeBases, err := a.MergeBase(b)
if err != nil {
return nil, err
}

if len(mergeBases) == 0 {
return nil, nil
}

return hasChanges, nil
// :shrug: We dont really care which commit we pick, at worst we do an unnecessary push.
return mergeBases[0], nil
}

func (r *Repository) ForcePush(ctx context.Context, branch string) error {
Expand Down
131 changes: 131 additions & 0 deletions internal/git/git_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package git

import (
"context"
"testing"

"github.com/go-git/go-git/v5/plumbing"
"github.com/stretchr/testify/assert"
)

const testMainBranch = "main"
const testPRBranch = "releaser-pleaser"

func TestRepository_HasChangesWithRemote(t *testing.T) {
tests := []struct {
name string
repo TestRepo
want bool
wantErr assert.ErrorAssertionFunc
}{
{
name: "no remote pr branch",
repo: WithTestRepo(
WithCommit(
"chore: release v1.0.0",
OnBranch(plumbing.NewBranchReferenceName(testMainBranch)),
AsNewBranch(plumbing.NewRemoteReferenceName(remoteName, testMainBranch)),
WithFile("VERSION", "v1.0.0"),
),
WithCommit(
"chore: release v1.1.0",
OnBranch(plumbing.NewRemoteReferenceName(remoteName, testMainBranch)),
AsNewBranch(plumbing.NewBranchReferenceName(testPRBranch)),
WithFile("VERSION", "v1.1.0"),
),
),
want: true,
wantErr: assert.NoError,
},
{
name: "remote pr branch matches local",
repo: WithTestRepo(
WithCommit(
"chore: release v1.0.0",
OnBranch(plumbing.NewBranchReferenceName(testMainBranch)),
AsNewBranch(plumbing.NewRemoteReferenceName(remoteName, testMainBranch)),
WithFile("VERSION", "v1.0.0"),
),
WithCommit(
"chore: release v1.1.0",
OnBranch(plumbing.NewRemoteReferenceName(remoteName, testMainBranch)),
AsNewBranch(plumbing.NewBranchReferenceName(testPRBranch)),
WithFile("VERSION", "v1.1.0"),
),
WithCommit(
"chore: release v1.1.0",
OnBranch(plumbing.NewRemoteReferenceName(remoteName, testMainBranch)),
AsNewBranch(plumbing.NewRemoteReferenceName(remoteName, testPRBranch)),
WithFile("VERSION", "v1.1.0"),
),
),
want: false,
wantErr: assert.NoError,
},
{
name: "remote pr only needs rebase",
repo: WithTestRepo(
WithCommit(
"chore: release v1.0.0",
OnBranch(plumbing.NewBranchReferenceName(testMainBranch)),
AsNewBranch(plumbing.NewRemoteReferenceName(remoteName, testMainBranch)),
WithFile("VERSION", "v1.0.0"),
),
WithCommit(
"chore: release v1.1.0",
OnBranch(plumbing.NewRemoteReferenceName(remoteName, testMainBranch)),
AsNewBranch(plumbing.NewRemoteReferenceName(remoteName, testPRBranch)),
WithFile("VERSION", "v1.1.0"),
),
WithCommit(
"feat: new feature on remote",
OnBranch(plumbing.NewRemoteReferenceName(remoteName, testMainBranch)),
WithFile("feature", "yes"),
),
WithCommit(
"chore: release v1.1.0",
OnBranch(plumbing.NewRemoteReferenceName(remoteName, testMainBranch)),
AsNewBranch(plumbing.NewBranchReferenceName(testPRBranch)),
WithFile("VERSION", "v1.1.0"),
),
),
want: false,
wantErr: assert.NoError,
},
{
name: "needs update",
repo: WithTestRepo(
WithCommit(
"chore: release v1.0.0",
OnBranch(plumbing.NewBranchReferenceName(testMainBranch)),
AsNewBranch(plumbing.NewRemoteReferenceName(remoteName, testMainBranch)),
WithFile("VERSION", "v1.0.0"),
),
WithCommit(
"chore: release v1.1.0",
OnBranch(plumbing.NewRemoteReferenceName(remoteName, testMainBranch)),
AsNewBranch(plumbing.NewRemoteReferenceName(remoteName, testPRBranch)),
WithFile("VERSION", "v1.1.0"),
),
WithCommit(
"chore: release v1.2.0",
OnBranch(plumbing.NewRemoteReferenceName(remoteName, testMainBranch)),
AsNewBranch(plumbing.NewBranchReferenceName(testPRBranch)),
WithFile("VERSION", "v1.2.0"),
),
),
want: false,
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := tt.repo(t)
got, err := repo.HasChangesWithRemote(context.Background(), testMainBranch, testPRBranch)
if !tt.wantErr(t, err) {
return
}
assert.Equal(t, got, tt.want)
})
}
}

0 comments on commit 3938c7a

Please sign in to comment.