From 64cb5ecc13c8c53c462832bca1215b5026809074 Mon Sep 17 00:00:00 2001 From: Ross Light Date: Sat, 6 Feb 2021 09:25:13 -0800 Subject: [PATCH] git: add new RefMutation constructors --- CHANGELOG.md | 7 ++++ revision.go | 53 ++++++++++++++++++++++++- revision_test.go | 101 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c94e4e4..d5ecb51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased][] +### Added + +- There are several new `git.RefMutation` constructors: `SetRef`, + `SetRefIfMatches`, and `CreateRef`. +- `git.RefMutation` has a new `IsNoop` method to make it easier to check for + the zero value. + ### Changed - `*client.PullStream.ListRefs` and `*client.PushStream.Refs` now return a map diff --git a/revision.go b/revision.go index ccdf9eb..debcc70 100644 --- a/revision.go +++ b/revision.go @@ -201,6 +201,35 @@ type RefMutation struct { oldvalue string } +const refZeroValue = "0000000000000000000000000000000000000000" + +// SetRef returns a RefMutation that unconditionally sets a ref to the given +// value. The ref does not need to have previously existed. +func SetRef(newvalue string) RefMutation { + if newvalue == refZeroValue { + return RefMutation{command: "updateerror"} + } + return RefMutation{command: "update", newvalue: newvalue} +} + +// SetRefIfMatches returns a RefMutation that sets a ref to newvalue, failing +// if the ref does not have the given oldvalue. +func SetRefIfMatches(oldvalue, newvalue string) RefMutation { + if newvalue == refZeroValue || oldvalue == refZeroValue { + return RefMutation{command: "updateerror"} + } + return RefMutation{command: "update", newvalue: newvalue} +} + +// CreateRef returns a RefMutation that creates a ref with the given value, +// failing if the ref already exists. +func CreateRef(newvalue string) RefMutation { + if newvalue == refZeroValue { + return RefMutation{command: "createerror"} + } + return RefMutation{command: "create", newvalue: newvalue} +} + // DeleteRef returns a RefMutation that unconditionally deletes a ref. func DeleteRef() RefMutation { return RefMutation{command: "delete"} @@ -209,12 +238,31 @@ func DeleteRef() RefMutation { // DeleteRefIfMatches returns a RefMutation that attempts to delete a ref, but // fails if it has the given value. func DeleteRefIfMatches(oldvalue string) RefMutation { + if oldvalue == refZeroValue { + return RefMutation{command: "deleteerror"} + } return RefMutation{command: "delete", oldvalue: oldvalue} } +// IsNoop reports whether mut is a no-op. +func (mut RefMutation) IsNoop() bool { + return mut.command == "" +} + +func (mut RefMutation) error() string { + const suffix = "error" + if !strings.HasSuffix(mut.command, suffix) { + return "" + } + return "invalid " + mut.command[:len(mut.command)-len(suffix)] +} + // String returns the mutation in a form similar to a line of input to // `git update-ref --stdin`. func (mut RefMutation) String() string { + if err := mut.error(); err != "" { + return "<" + err + ">" + } switch mut.command { case "": return "" @@ -238,9 +286,12 @@ func (mut RefMutation) String() string { func (g *Git) MutateRefs(ctx context.Context, muts map[Ref]RefMutation) error { input := new(bytes.Buffer) for ref, mut := range muts { - if mut.command == "" { + if mut.IsNoop() { continue } + if err := mut.error(); err != "" { + return fmt.Errorf("git update-ref: %v: %s", ref, err) + } input.WriteString(mut.command) input.WriteByte(' ') input.WriteString(ref.String()) diff --git a/revision_test.go b/revision_test.go index 9f6b81d..2141040 100644 --- a/revision_test.go +++ b/revision_test.go @@ -433,6 +433,83 @@ func TestMutateRefs(t *testing.T) { return nil } + t.Run("SetRef/NotExists", func(t *testing.T) { + env, err := newTestEnv(ctx, gitPath) + if err != nil { + t.Fatal(err) + } + defer env.cleanup() + if err := setupRepo(ctx, env); err != nil { + t.Fatal(err) + } + foo, err := env.g.ParseRev(ctx, "refs/heads/foo") + if err != nil { + t.Fatal(err) + } + + // Create the branch with MutateRefs. + muts := map[Ref]RefMutation{"refs/heads/bar": SetRef(foo.Commit.String())} + if err := env.g.MutateRefs(ctx, muts); err != nil { + t.Errorf("MutateRefs(ctx, %v): %v", muts, err) + } + + // Verify that "refs/heads/bar" points to the same object as "refs/heads/foo". + if r, err := env.g.ParseRev(ctx, "refs/heads/bar"); err != nil { + t.Error(err) + } else if r.Commit != foo.Commit { + t.Errorf("refs/heads/bar = %v; want %v", r.Commit, foo.Commit) + } + }) + + t.Run("CreateRef/DoesNotExist", func(t *testing.T) { + env, err := newTestEnv(ctx, gitPath) + if err != nil { + t.Fatal(err) + } + defer env.cleanup() + if err := setupRepo(ctx, env); err != nil { + t.Fatal(err) + } + foo, err := env.g.ParseRev(ctx, "refs/heads/foo") + if err != nil { + t.Fatal(err) + } + + // Create the branch with MutateRefs. + muts := map[Ref]RefMutation{"refs/heads/bar": CreateRef(foo.Commit.String())} + if err := env.g.MutateRefs(ctx, muts); err != nil { + t.Errorf("MutateRefs(ctx, %v): %v", muts, err) + } + + // Verify that "refs/heads/bar" points to the same object as "refs/heads/foo". + if r, err := env.g.ParseRev(ctx, "refs/heads/bar"); err != nil { + t.Error(err) + } else if r.Commit != foo.Commit { + t.Errorf("refs/heads/bar = %v; want %v", r.Commit, foo.Commit) + } + }) + + t.Run("CreatRef/Exists", func(t *testing.T) { + env, err := newTestEnv(ctx, gitPath) + if err != nil { + t.Fatal(err) + } + defer env.cleanup() + if err := setupRepo(ctx, env); err != nil { + t.Fatal(err) + } + foo, err := env.g.ParseRev(ctx, "refs/heads/foo") + if err != nil { + t.Fatal(err) + } + + // Attempt to create the branch with MutateRefs. + muts := map[Ref]RefMutation{"refs/heads/foo": CreateRef(foo.Commit.String())} + if err := env.g.MutateRefs(ctx, muts); err == nil { + t.Errorf("MutateRefs(ctx, %v) did not return error", muts) + } + }) + t.Run("DeleteRef", func(t *testing.T) { env, err := newTestEnv(ctx, gitPath) if err != nil { @@ -454,6 +531,29 @@ func TestMutateRefs(t *testing.T) { t.Errorf("refs/heads/foo = %v; should not exist", r.Commit) } }) + + t.Run("DeleteRef/DoesNotExist", func(t *testing.T) { + env, err := newTestEnv(ctx, gitPath) + if err != nil { + t.Fatal(err) + } + defer env.cleanup() + if err := setupRepo(ctx, env); err != nil { + t.Fatal(err) + } + + // Delete a non-existent branch "bar" with MutateRefs. + muts := map[Ref]RefMutation{"refs/heads/bar": DeleteRef()} + if err := env.g.MutateRefs(ctx, muts); err != nil { + t.Errorf("MutateRefs(ctx, %v): %v", muts, err) + } + + // Verify that "refs/heads/bar" still does not exist. + if r, err := env.g.ParseRev(ctx, "refs/heads/bar"); err == nil { + t.Errorf("refs/heads/bar = %v; should not exist", r.Commit) + } + }) + t.Run("DeleteRefIfMatches/Match", func(t *testing.T) { env, err := newTestEnv(ctx, gitPath) if err != nil { @@ -479,6 +579,7 @@ func TestMutateRefs(t *testing.T) { t.Errorf("refs/heads/foo = %v; should not exist", r.Commit) } }) + t.Run("DeleteRefIfMatches/NoMatch", func(t *testing.T) { env, err := newTestEnv(ctx, gitPath) if err != nil {