Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: avoid pushing release branch only for rebasing #114

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.23.0

require (
github.com/blang/semver/v4 v4.0.0
github.com/go-git/go-billy/v5 v5.5.0
github.com/go-git/go-git/v5 v5.12.0
github.com/google/go-github/v66 v66.0.0
github.com/leodido/go-conventionalcommits v0.12.0
Expand All @@ -23,7 +24,6 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-querystring v1.1.0 // indirect
Expand Down
58 changes: 50 additions & 8 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,60 @@ 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))
if err != nil {
return false, err
}

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

hasChanges := len(diff.FilePatches()) > 0
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 nil, err
}

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, tt.want, got)
})
}
}
Loading
Loading