Skip to content

Commit

Permalink
git: add new RefMutation constructors
Browse files Browse the repository at this point in the history
  • Loading branch information
zombiezen committed Feb 6, 2021
1 parent fe4db91 commit 64cb5ec
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 1 deletion.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 52 additions & 1 deletion revision.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand All @@ -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 ""
Expand All @@ -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())
Expand Down
101 changes: 101 additions & 0 deletions revision_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down

0 comments on commit 64cb5ec

Please sign in to comment.