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

Add "squash merge" support to merge API #8464

Merged
merged 6 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
9 changes: 8 additions & 1 deletion api/swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,14 @@ components:
type: boolean
default: false
description: Allow merge when the branches have the same content

squash_merge:
type: boolean
default: true
description: |
If set, set only the destination branch as a parent, which "squashes" the merge to
appear as a single commit on the destination branch. The source commit is no longer
a part of the merge commit; consider adding it to the 'metadata' or 'message'
fields.
BranchCreation:
type: object
required:
Expand Down
9 changes: 9 additions & 0 deletions clients/java/api/openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions clients/java/docs/Merge.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 30 additions & 2 deletions clients/java/src/main/java/io/lakefs/clients/sdk/model/Merge.java

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions clients/python/docs/Merge.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions clients/python/lakefs_sdk/models/merge.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion clients/python/test/test_merge.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions clients/rust/docs/Merge.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions clients/rust/src/models/merge.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion docs/assets/js/swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,14 @@ components:
type: boolean
default: false
description: Allow merge when the branches have the same content

squash_merge:
type: boolean
default: true
description: |
If set, set only the destination branch as a parent, which "squashes" the merge to
appear as a single commit on the destination branch. The source commit is no longer
a part of the merge commit; consider adding it to the 'metadata' or 'message'
fields.
BranchCreation:
type: object
required:
Expand Down
1 change: 1 addition & 0 deletions pkg/api/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -4809,6 +4809,7 @@ func (c *Controller) MergeIntoBranch(w http.ResponseWriter, r *http.Request, bod
swag.StringValue(body.Strategy),
graveler.WithForce(swag.BoolValue(body.Force)),
graveler.WithAllowEmpty(swag.BoolValue(body.AllowEmpty)),
graveler.WithSquashMerge(swag.BoolValue(body.SquashMerge)),
)

if errors.Is(err, graveler.ErrConflictFound) {
Expand Down
71 changes: 71 additions & 0 deletions pkg/api/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3797,6 +3797,77 @@ func TestController_MergeIntoExplicitBranch(t *testing.T) {
}
}

func namer(number int) func(name string) string {
return func(name string) string {
return fmt.Sprint(name, number)
}
}

func TestController_MergeSquashing(t *testing.T) {
const numCommits = 3
clt, deps := setupClientWithAdmin(t)
ctx := context.Background()

// setup env
repo := testUniqueRepoName()
_, err := deps.catalog.CreateRepository(ctx, repo, onBlock(deps, repo), "main", false)
testutil.Must(t, err)
base, err := deps.catalog.CreateBranch(ctx, repo, "branch", "main")
testutil.Must(t, err)
baseCommit := base.Reference

for commitNumber := 1; commitNumber <= numCommits; commitNumber++ {
n := namer(commitNumber)
err = deps.catalog.CreateEntry(ctx, repo, "branch", catalog.DBEntry{Path: n("foo/bar"), PhysicalAddress: n("bar-addr"), CreationDate: time.Now(), Size: 1, Checksum: n("checksum")})
testutil.Must(t, err)
_, err = deps.catalog.Commit(ctx, repo, "branch", "some message", DefaultUserID, nil, nil, nil, false)
testutil.Must(t, err)
}

cases := []struct {
Name string
Squash bool
ExpectedNumCommits int
}{{
Name: "regular",
Squash: false,
// Commits: 1 "created repository", numCommits on branch, 1 merge.
ExpectedNumCommits: numCommits + 2,
}, {
Name: "squash",
Squash: true,
// Commits: 1 "created repository", 1 merge.
ExpectedNumCommits: 2,
}}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
destinationBranch := "main-" + tc.Name

_, err := deps.catalog.CreateBranch(ctx, repo, destinationBranch, "main")
testutil.Must(t, err)

mergeResp, err := clt.MergeIntoBranchWithResponse(ctx, repo, "branch", destinationBranch, apigen.MergeIntoBranchJSONRequestBody{SquashMerge: &tc.Squash})
testutil.MustDo(t, "perform merge into branch", err)
if !apiutil.IsStatusCodeOK(mergeResp.StatusCode()) {
t.Fatal("merge request failed", mergeResp.Status())
}

commits, hasMore, err := deps.catalog.ListCommits(ctx, repo, destinationBranch, catalog.LogParams{Amount: numCommits + 5, StopAt: baseCommit})
testutil.MustDo(t, "log from merged commit", err)
if hasMore {
t.Errorf("Got pagination after %d results when no pagination expected", len(commits))
}

if len(commits) != tc.ExpectedNumCommits {
for i, commit := range commits {
t.Log(i, " ", commit)
}
t.Errorf("Got %d commits when expecting %d", len(commits), tc.ExpectedNumCommits)
}
})
}
}

func TestController_MergeDirtyBranch(t *testing.T) {
clt, deps := setupClientWithAdmin(t)
ctx := context.Background()
Expand Down
15 changes: 14 additions & 1 deletion pkg/graveler/graveler.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ type SetOptions struct {
AllowEmpty bool
// Hidden Will create the branch with the hidden property
Hidden bool
// SquashMerge causes merge commits to be "squashed", losing parent
// information about the merged-from branch.
SquashMerge bool
}

type SetOptionsFunc func(opts *SetOptions)
Expand Down Expand Up @@ -232,6 +235,12 @@ func WithHidden(v bool) SetOptionsFunc {
}
}

func WithSquashMerge(v bool) SetOptionsFunc {
return func(opts *SetOptions) {
opts.SquashMerge = v
}
}

// ListOptions controls list request defaults
type ListOptions struct {
// Shows entities marked as hidden
Expand Down Expand Up @@ -2926,7 +2935,11 @@ func (g *Graveler) Merge(ctx context.Context, repository *RepositoryRecord, dest
commit.Committer = commitParams.Committer
commit.Message = commitParams.Message
commit.MetaRangeID = metaRangeID
commit.Parents = []CommitID{toCommit.CommitID, fromCommit.CommitID}
if options.SquashMerge {
commit.Parents = []CommitID{toCommit.CommitID}
} else {
commit.Parents = []CommitID{toCommit.CommitID, fromCommit.CommitID}
}
if toCommit.Generation > fromCommit.Generation {
commit.Generation = toCommit.Generation + 1
} else {
Expand Down
Loading