Skip to content

Commit

Permalink
internal/gitrepo: add NotesForCommit function
Browse files Browse the repository at this point in the history
  • Loading branch information
zombiezen committed Dec 13, 2023
1 parent af3ab0b commit 6def10f
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 4 deletions.
19 changes: 16 additions & 3 deletions internal/gitrepo/gitrepo.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ type Catter interface {
Cat(ctx context.Context, dst io.Writer, tp object.Type, id githash.SHA1) error
}

// Cat copies the content of the given object from the cache into dst.
// Cat copies the content of the given object from the repository into dst.
// If the type of the object requested does not match the requested type
// and it can be trivially dereferenced to the requested type
// (e.g. a commit is found during a request for a tree),
Expand All @@ -58,7 +58,7 @@ type Catter interface {
// it's assumed that reading from buf will read the bytes
// previously written to buf.
//
// If cat implements [TypeCatter], it is used instead of reading individual objects.
// If cat implements [Catter], it is used instead of reading individual objects.
// buf will not be used in this case.
func Cat(ctx context.Context, repo Repository, dst io.Writer, wantType object.Type, id githash.SHA1) error {
if typeCat, ok := repo.(Catter); ok {
Expand Down Expand Up @@ -119,7 +119,6 @@ func Cat(ctx context.Context, repo Repository, dst io.Writer, wantType object.Ty
return fmt.Errorf("cat %v %v: %v is a %v", wantType, id, nextID, got.Type)
}
}

}

// Map is an in-memory implementation of [Repository].
Expand All @@ -144,6 +143,15 @@ func (m Map) Stat(ctx context.Context, id githash.SHA1) (object.Prefix, error) {
return obj.Prefix(), nil
}

// Cat copies the content of the given object from the map into dst.
// If the type of the object requested does not match the requested type
// and it can be trivially dereferenced to the requested type
// (e.g. a commit is found during a request for a tree),
// then the referenced object is written to dst.
func (m Map) Cat(ctx context.Context, dst io.Writer, tp object.Type, id githash.SHA1) error {
return Cat(ctx, onlyRepository{m}, dst, tp, id)
}

func (m Map) get(ctx context.Context, id githash.SHA1) (Object, error) {
obj, ok := m[id]
if !ok {
Expand Down Expand Up @@ -213,3 +221,8 @@ func (obj Object) SHA1() githash.SHA1 {
h.Sum(id[:0])
return id
}

// onlyRepository is a small wrapper type that hides any non-Repository methods.
type onlyRepository struct {
Repository
}
5 changes: 4 additions & 1 deletion internal/gitrepo/gitrepo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ import (
"github.com/google/go-cmp/cmp"
)

var _ Repository = Map(nil)
var _ interface {
Repository
Catter
} = Map(nil)

func TestMap(t *testing.T) {
ctx := context.Background()
Expand Down
74 changes: 74 additions & 0 deletions internal/gitrepo/notes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2023 The gg Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package gitrepo

import (
"bytes"
"context"
"fmt"
"io"
"strings"

"gg-scm.io/pkg/git/githash"
"gg-scm.io/pkg/git/object"
)

// NotesForCommit reads the notes for a particular commit and root reference.
// If there are no notes for the given ID,
// nothing will be written to dst and NotesForCommit will return nil.
func NotesForCommit(ctx context.Context, cat Catter, dst io.Writer, notesRef githash.SHA1, id githash.SHA1) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("read notes for %v: %v", id, err)
}
}()

buf := new(bytes.Buffer)
var prefix strings.Builder
rest := id.String()
for curr := notesRef; ; {
if err := cat.Cat(ctx, buf, object.TypeTree, curr); err != nil {
return err
}
currTree, err := object.ParseTree(buf.Bytes())
if err != nil {
return err
}
buf.Reset()
if ent := currTree.Search(rest); ent != nil {
// Notes file found.
if !ent.Mode.IsRegular() {
return fmt.Errorf("%s%s: not a regular file", prefix.String(), rest)
}
// TODO(maybe): Prevent tags to blobs?
return cat.Cat(ctx, dst, object.TypeBlob, ent.ObjectID)
}

dir := rest[:2]
rest = rest[2:]
ent := currTree.Search(dir)
if ent == nil {
return nil
}
if ent.Mode != object.ModeDir {
return fmt.Errorf("%s%s: not a directory", prefix.String(), dir)
}
prefix.WriteString(dir)
prefix.WriteString("/")
curr = ent.ObjectID
}
}
121 changes: 121 additions & 0 deletions internal/gitrepo/notes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright 2023 The gg Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package gitrepo

import (
"bytes"
"context"
"testing"

"gg-scm.io/pkg/git/githash"
"gg-scm.io/pkg/git/object"
)

func TestNotesForCommit(t *testing.T) {
commit1, err := githash.ParseSHA1("85be80f2ff7482f53f6581be8683036d7a59c0e7")
if err != nil {
t.Fatal(err)
}
commit2, err := githash.ParseSHA1("af3ab0b8ddca5513f86812d820802202514ed986")
if err != nil {
t.Fatal(err)
}
commit3, err := githash.ParseSHA1("3134253d1adfa21be31624f6d48243e18651db68")
if err != nil {
t.Fatal(err)
}

repo := make(Map)
commit2Notes := []byte("Commit #2\n")
commit2NotesBlob := repo.Add(Object{
Type: object.TypeBlob,
Data: commit2Notes,
})
subtreeData, err := (object.Tree{
{
Name: "3ab0b8ddca5513f86812d820802202514ed986",
Mode: object.ModePlain,
ObjectID: commit2NotesBlob,
},
}).MarshalBinary()
if err != nil {
t.Fatal(err)
}
subtreeID := repo.Add(Object{
Type: object.TypeTree,
Data: subtreeData,
})

commit1Notes := []byte("Hello, World!\n")
commit1NotesBlob := repo.Add(Object{
Type: object.TypeBlob,
Data: commit1Notes,
})
rootData, err := (object.Tree{
{
Name: "85be80f2ff7482f53f6581be8683036d7a59c0e7",
Mode: object.ModePlain,
ObjectID: commit1NotesBlob,
},
{
Name: "af",
Mode: object.ModeDir,
ObjectID: subtreeID,
},
}).MarshalBinary()
if err != nil {
t.Fatal(err)
}
root := repo.Add(Object{
Type: object.TypeTree,
Data: rootData,
})

tests := []struct {
name string
commit githash.SHA1
want []byte
}{
{
name: "Root",
commit: commit1,
want: commit1Notes,
},
{
name: "Subdir",
commit: commit2,
want: commit2Notes,
},
{
name: "NotFound",
commit: commit3,
want: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := context.Background()
got := new(bytes.Buffer)
if err := NotesForCommit(ctx, repo, got, root, test.commit); err != nil {
t.Error(err)
}
if !bytes.Equal(got.Bytes(), test.want) {
t.Errorf("notes = %q; want %q", got, test.want)
}
})
}
}

0 comments on commit 6def10f

Please sign in to comment.