aboutsummaryrefslogtreecommitdiffstats
path: root/plumbing
diff options
context:
space:
mode:
authorAyman Bagabas <ayman.bagabas@gmail.com>2023-10-29 16:35:09 -0400
committerAyman Bagabas <ayman.bagabas@gmail.com>2023-11-03 16:59:11 -0400
commitce0b76e7674d683db547103bc773305129a0ded4 (patch)
tree3b609eb2f88bee83076250d1d69fe80b65cf439e /plumbing
parent22585738b4b13797f241a04b9cb6b48b31056aac (diff)
downloadgo-git-ce0b76e7674d683db547103bc773305129a0ded4.tar.gz
git: implement upload-server-info. Fixes #731
This adds UpdateServerInfo along with a new go-git command to generate info files to help git dumb http serve refs and their objects. This also updates the docs to reflect this. Docs: https://git-scm.com/docs/git-update-server-info Fixes: https://github.com/go-git/go-git/issues/731
Diffstat (limited to 'plumbing')
-rw-r--r--plumbing/serverinfo/serverinfo.go94
-rw-r--r--plumbing/serverinfo/serverinfo_test.go185
2 files changed, 279 insertions, 0 deletions
diff --git a/plumbing/serverinfo/serverinfo.go b/plumbing/serverinfo/serverinfo.go
new file mode 100644
index 0000000..d7ea7ef
--- /dev/null
+++ b/plumbing/serverinfo/serverinfo.go
@@ -0,0 +1,94 @@
+package serverinfo
+
+import (
+ "fmt"
+
+ "github.com/go-git/go-billy/v5"
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/internal/reference"
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/object"
+ "github.com/go-git/go-git/v5/plumbing/storer"
+ "github.com/go-git/go-git/v5/storage"
+)
+
+// UpdateServerInfo updates the server info files in the repository.
+//
+// It generates a list of available refs for the repository.
+// Used by git http transport (dumb), for more information refer to:
+// https://git-scm.com/book/id/v2/Git-Internals-Transfer-Protocols#_the_dumb_protocol
+func UpdateServerInfo(s storage.Storer, fs billy.Filesystem) error {
+ pos, ok := s.(storer.PackedObjectStorer)
+ if !ok {
+ return git.ErrPackedObjectsNotSupported
+ }
+
+ infoRefs, err := fs.Create("info/refs")
+ if err != nil {
+ return err
+ }
+
+ defer infoRefs.Close()
+
+ refsIter, err := s.IterReferences()
+ if err != nil {
+ return err
+ }
+
+ defer refsIter.Close()
+
+ var refs []*plumbing.Reference
+ if err := refsIter.ForEach(func(ref *plumbing.Reference) error {
+ refs = append(refs, ref)
+ return nil
+ }); err != nil {
+ return err
+ }
+
+ reference.Sort(refs)
+ for _, ref := range refs {
+ name := ref.Name()
+ hash := ref.Hash()
+ switch ref.Type() {
+ case plumbing.SymbolicReference:
+ if name == plumbing.HEAD {
+ continue
+ }
+ ref, err := s.Reference(ref.Target())
+ if err != nil {
+ return err
+ }
+
+ hash = ref.Hash()
+ fallthrough
+ case plumbing.HashReference:
+ fmt.Fprintf(infoRefs, "%s\t%s\n", hash, name)
+ if name.IsTag() {
+ tag, err := object.GetTag(s, hash)
+ if err == nil {
+ fmt.Fprintf(infoRefs, "%s\t%s^{}\n", tag.Target, name)
+ }
+ }
+ }
+ }
+
+ infoPacks, err := fs.Create("objects/info/packs")
+ if err != nil {
+ return err
+ }
+
+ defer infoPacks.Close()
+
+ packs, err := pos.ObjectPacks()
+ if err != nil {
+ return err
+ }
+
+ for _, p := range packs {
+ fmt.Fprintf(infoPacks, "P pack-%s.pack\n", p)
+ }
+
+ fmt.Fprintln(infoPacks)
+
+ return nil
+}
diff --git a/plumbing/serverinfo/serverinfo_test.go b/plumbing/serverinfo/serverinfo_test.go
new file mode 100644
index 0000000..0a52ea2
--- /dev/null
+++ b/plumbing/serverinfo/serverinfo_test.go
@@ -0,0 +1,185 @@
+package serverinfo
+
+import (
+ "io"
+ "strings"
+ "testing"
+
+ "github.com/go-git/go-billy/v5"
+ "github.com/go-git/go-billy/v5/memfs"
+ fixtures "github.com/go-git/go-git-fixtures/v4"
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/object"
+ "github.com/go-git/go-git/v5/plumbing/storer"
+ "github.com/go-git/go-git/v5/storage"
+ "github.com/go-git/go-git/v5/storage/memory"
+ . "gopkg.in/check.v1"
+)
+
+type ServerInfoSuite struct{}
+
+var _ = Suite(&ServerInfoSuite{})
+
+func Test(t *testing.T) { TestingT(t) }
+
+func (s *ServerInfoSuite) TestUpdateServerInfoInit(c *C) {
+ fs := memfs.New()
+ st := memory.NewStorage()
+ r, err := git.Init(st, fs)
+ c.Assert(err, IsNil)
+ c.Assert(r, NotNil)
+
+ err = UpdateServerInfo(st, fs)
+ c.Assert(err, IsNil)
+}
+
+func assertInfoRefs(c *C, st storage.Storer, fs billy.Filesystem) {
+ refsFile, err := fs.Open("info/refs")
+ c.Assert(err, IsNil)
+
+ defer refsFile.Close()
+ bts, err := io.ReadAll(refsFile)
+ c.Assert(err, IsNil)
+
+ localRefs := make(map[plumbing.ReferenceName]plumbing.Hash)
+ for _, line := range strings.Split(string(bts), "\n") {
+ if line == "" {
+ continue
+ }
+ parts := strings.Split(line, "\t")
+ c.Assert(parts, HasLen, 2)
+ hash := plumbing.NewHash(parts[0])
+ name := plumbing.ReferenceName(parts[1])
+ localRefs[name] = hash
+ }
+
+ refs, err := st.IterReferences()
+ c.Assert(err, IsNil)
+
+ err = refs.ForEach(func(ref *plumbing.Reference) error {
+ name := ref.Name()
+ hash := ref.Hash()
+ switch ref.Type() {
+ case plumbing.SymbolicReference:
+ if name == plumbing.HEAD {
+ return nil
+ }
+ ref, err := st.Reference(ref.Target())
+ c.Assert(err, IsNil)
+ hash = ref.Hash()
+ fallthrough
+ case plumbing.HashReference:
+ h, ok := localRefs[name]
+ c.Assert(ok, Equals, true)
+ c.Assert(h, Equals, hash)
+ if name.IsTag() {
+ tag, err := object.GetTag(st, hash)
+ if err == nil {
+ t, ok := localRefs[name+"^{}"]
+ c.Assert(ok, Equals, true)
+ c.Assert(t, Equals, tag.Target)
+ }
+ }
+ }
+ return nil
+ })
+
+ c.Assert(err, IsNil)
+}
+
+func assertObjectPacks(c *C, st storage.Storer, fs billy.Filesystem) {
+ infoPacks, err := fs.Open("objects/info/packs")
+ c.Assert(err, IsNil)
+
+ defer infoPacks.Close()
+ bts, err := io.ReadAll(infoPacks)
+ c.Assert(err, IsNil)
+
+ pos, ok := st.(storer.PackedObjectStorer)
+ c.Assert(ok, Equals, true)
+ localPacks := make(map[string]struct{})
+ packs, err := pos.ObjectPacks()
+ c.Assert(err, IsNil)
+
+ for _, line := range strings.Split(string(bts), "\n") {
+ if line == "" {
+ continue
+ }
+ parts := strings.Split(line, " ")
+ c.Assert(parts, HasLen, 2)
+ pack := strings.TrimPrefix(parts[1], "pack-")
+ pack = strings.TrimSuffix(pack, ".pack")
+ localPacks[pack] = struct{}{}
+ }
+
+ for _, p := range packs {
+ _, ok := localPacks[p.String()]
+ c.Assert(ok, Equals, true)
+ }
+}
+
+func (s *ServerInfoSuite) TestUpdateServerInfoTags(c *C) {
+ fs := memfs.New()
+ st := memory.NewStorage()
+ r, err := git.Clone(st, fs, &git.CloneOptions{
+ URL: fixtures.ByURL("https://github.com/git-fixtures/tags.git").One().URL,
+ })
+ c.Assert(err, IsNil)
+ c.Assert(r, NotNil)
+
+ err = UpdateServerInfo(st, fs)
+ c.Assert(err, IsNil)
+
+ assertInfoRefs(c, st, fs)
+ assertObjectPacks(c, st, fs)
+}
+
+func (s *ServerInfoSuite) TestUpdateServerInfoBasic(c *C) {
+ fs := memfs.New()
+ st := memory.NewStorage()
+ r, err := git.Clone(st, fs, &git.CloneOptions{
+ URL: fixtures.Basic().One().URL,
+ })
+ c.Assert(err, IsNil)
+ c.Assert(r, NotNil)
+
+ err = UpdateServerInfo(st, fs)
+ c.Assert(err, IsNil)
+
+ assertInfoRefs(c, st, fs)
+ assertObjectPacks(c, st, fs)
+}
+
+func (s *ServerInfoSuite) TestUpdateServerInfoBasicChange(c *C) {
+ fs := memfs.New()
+ st := memory.NewStorage()
+ r, err := git.Clone(st, fs, &git.CloneOptions{
+ URL: fixtures.Basic().One().URL,
+ })
+ c.Assert(err, IsNil)
+ c.Assert(r, NotNil)
+
+ err = UpdateServerInfo(st, fs)
+ c.Assert(err, IsNil)
+
+ assertInfoRefs(c, st, fs)
+ assertObjectPacks(c, st, fs)
+
+ head, err := r.Head()
+ c.Assert(err, IsNil)
+
+ ref := plumbing.NewHashReference("refs/heads/my-branch", head.Hash())
+ err = r.Storer.SetReference(ref)
+ c.Assert(err, IsNil)
+
+ _, err = r.CreateTag("test-tag", head.Hash(), &git.CreateTagOptions{
+ Message: "test-tag",
+ })
+ c.Assert(err, IsNil)
+
+ err = UpdateServerInfo(st, fs)
+
+ assertInfoRefs(c, st, fs)
+ assertObjectPacks(c, st, fs)
+}