aboutsummaryrefslogtreecommitdiffstats
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
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
-rw-r--r--COMPATIBILITY.md8
-rw-r--r--cli/go-git/main.go1
-rw-r--r--cli/go-git/update_server_info.go34
-rw-r--r--internal/reference/sort.go14
-rw-r--r--plumbing/serverinfo/serverinfo.go94
-rw-r--r--plumbing/serverinfo/serverinfo_test.go185
6 files changed, 332 insertions, 4 deletions
diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index bbffea5..c1f280d 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -109,10 +109,10 @@ compatibility status with go-git.
## Server admin
-| Feature | Sub-feature | Status | Notes | Examples |
-| -------------------- | ----------- | ------ | ----- | -------- |
-| `daemon` | | ❌ | | |
-| `update-server-info` | | ❌ | | |
+| Feature | Sub-feature | Status | Notes | Examples |
+| -------------------- | ----------- | ------ | ----- | ----------------------------------------- |
+| `daemon` | | ❌ | | |
+| `update-server-info` | | ✅ | | [cli](./cli/go-git/update_server_info.go) |
## Advanced
diff --git a/cli/go-git/main.go b/cli/go-git/main.go
index 97b8c3e..0a5ad2c 100644
--- a/cli/go-git/main.go
+++ b/cli/go-git/main.go
@@ -22,6 +22,7 @@ func main() {
}
parser := flags.NewNamedParser(bin, flags.Default)
+ parser.AddCommand("update-server-info", "", "", &CmdUpdateServerInfo{})
parser.AddCommand("receive-pack", "", "", &CmdReceivePack{})
parser.AddCommand("upload-pack", "", "", &CmdUploadPack{})
parser.AddCommand("version", "Show the version information.", "", &CmdVersion{})
diff --git a/cli/go-git/update_server_info.go b/cli/go-git/update_server_info.go
new file mode 100644
index 0000000..a7f3e3e
--- /dev/null
+++ b/cli/go-git/update_server_info.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing/serverinfo"
+ "github.com/go-git/go-git/v5/storage/filesystem"
+)
+
+// CmdUpdateServerInfo command updates the server info files in the repository.
+// This is used by git http transport (dumb) to generate a list of available
+// refs for the repository. See:
+// https://git-scm.com/docs/git-update-server-info
+type CmdUpdateServerInfo struct {
+ cmd
+}
+
+// Usage returns the usage of the command.
+func (CmdUpdateServerInfo) Usage() string {
+ return fmt.Sprintf("within a git repository run: %s", os.Args[0])
+}
+
+// Execute runs the command.
+func (c *CmdUpdateServerInfo) Execute(args []string) error {
+ r, err := git.PlainOpen(".")
+ if err != nil {
+ return err
+ }
+
+ fs := r.Storer.(*filesystem.Storage).Filesystem()
+ return serverinfo.UpdateServerInfo(r.Storer, fs)
+}
diff --git a/internal/reference/sort.go b/internal/reference/sort.go
new file mode 100644
index 0000000..726edbd
--- /dev/null
+++ b/internal/reference/sort.go
@@ -0,0 +1,14 @@
+package reference
+
+import (
+ "sort"
+
+ "github.com/go-git/go-git/v5/plumbing"
+)
+
+// Sort sorts the references by name to ensure a consistent order.
+func Sort(refs []*plumbing.Reference) {
+ sort.Slice(refs, func(i, j int) bool {
+ return refs[i].Name() < refs[j].Name()
+ })
+}
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)
+}