Skip to content

Commit fff62e7

Browse files
committed
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. Docs: https://git-scm.com/docs/git-update-server-info Fixes: #731
1 parent 2258573 commit fff62e7

File tree

5 files changed

+321
-0
lines changed

5 files changed

+321
-0
lines changed

cli/go-git/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ func main() {
2222
}
2323

2424
parser := flags.NewNamedParser(bin, flags.Default)
25+
parser.AddCommand("update-server-info", "", "", &CmdUpdateServerInfo{})
2526
parser.AddCommand("receive-pack", "", "", &CmdReceivePack{})
2627
parser.AddCommand("upload-pack", "", "", &CmdUploadPack{})
2728
parser.AddCommand("version", "Show the version information.", "", &CmdVersion{})

cli/go-git/update_server_info.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/go-git/go-git/v5"
8+
"github.com/go-git/go-git/v5/plumbing/serverinfo"
9+
"github.com/go-git/go-git/v5/storage/filesystem"
10+
)
11+
12+
type CmdUpdateServerInfo struct {
13+
cmd
14+
}
15+
16+
func (CmdUpdateServerInfo) Usage() string {
17+
return fmt.Sprintf("within a git repository run: %s", os.Args[0])
18+
}
19+
20+
func (c *CmdUpdateServerInfo) Execute(args []string) error {
21+
r, err := git.PlainOpen(".")
22+
if err != nil {
23+
return err
24+
}
25+
26+
fs := r.Storer.(*filesystem.Storage).Filesystem()
27+
return serverinfo.UpdateServerInfo(r.Storer, fs)
28+
}

internal/reference/sort.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package reference
2+
3+
import (
4+
"sort"
5+
6+
"github.com/go-git/go-git/v5/plumbing"
7+
)
8+
9+
// Sort sorts the references by name to ensure a consistent order.
10+
func Sort(refs []*plumbing.Reference) {
11+
sort.Slice(refs, func(i, j int) bool {
12+
return refs[i].Name() < refs[j].Name()
13+
})
14+
}

plumbing/serverinfo/serverinfo.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package serverinfo
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/go-git/go-billy/v5"
7+
"github.com/go-git/go-git/v5"
8+
"github.com/go-git/go-git/v5/internal/reference"
9+
"github.com/go-git/go-git/v5/plumbing"
10+
"github.com/go-git/go-git/v5/plumbing/object"
11+
"github.com/go-git/go-git/v5/plumbing/storer"
12+
"github.com/go-git/go-git/v5/storage"
13+
)
14+
15+
// UpdateServerInfo updates the server info files in the repository.
16+
//
17+
// It generates a list of available refs for the repository.
18+
// Used by git http transport (dumb).
19+
func UpdateServerInfo(s storage.Storer, fs billy.Filesystem) error {
20+
pos, ok := s.(storer.PackedObjectStorer)
21+
if !ok {
22+
return git.ErrPackedObjectsNotSupported
23+
}
24+
25+
infoRefs, err := fs.Create("info/refs")
26+
if err != nil {
27+
return err
28+
}
29+
30+
defer infoRefs.Close()
31+
32+
refsIter, err := s.IterReferences()
33+
if err != nil {
34+
return err
35+
}
36+
37+
defer refsIter.Close()
38+
39+
var refs []*plumbing.Reference
40+
if err := refsIter.ForEach(func(ref *plumbing.Reference) error {
41+
refs = append(refs, ref)
42+
return nil
43+
}); err != nil {
44+
return err
45+
}
46+
47+
reference.Sort(refs)
48+
for _, ref := range refs {
49+
name := ref.Name()
50+
hash := ref.Hash()
51+
switch ref.Type() {
52+
case plumbing.SymbolicReference:
53+
if name == plumbing.HEAD {
54+
continue
55+
}
56+
ref, err := s.Reference(ref.Target())
57+
if err != nil {
58+
return err
59+
}
60+
61+
hash = ref.Hash()
62+
fallthrough
63+
case plumbing.HashReference:
64+
fmt.Fprintf(infoRefs, "%s\t%s\n", hash, name)
65+
if name.IsTag() {
66+
tag, err := object.GetTag(s, hash)
67+
if err == nil {
68+
fmt.Fprintf(infoRefs, "%s\t%s^{}\n", tag.Target, name)
69+
}
70+
}
71+
}
72+
}
73+
74+
infoPacks, err := fs.Create("objects/info/packs")
75+
if err != nil {
76+
return err
77+
}
78+
79+
defer infoPacks.Close()
80+
81+
packs, err := pos.ObjectPacks()
82+
if err != nil {
83+
return err
84+
}
85+
86+
for _, p := range packs {
87+
fmt.Fprintf(infoPacks, "P pack-%s.pack\n", p)
88+
}
89+
90+
fmt.Fprintln(infoPacks)
91+
92+
return nil
93+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package serverinfo
2+
3+
import (
4+
"io"
5+
"strings"
6+
"testing"
7+
8+
"github.com/go-git/go-billy/v5"
9+
"github.com/go-git/go-billy/v5/memfs"
10+
fixtures "github.com/go-git/go-git-fixtures/v4"
11+
"github.com/go-git/go-git/v5"
12+
"github.com/go-git/go-git/v5/plumbing"
13+
"github.com/go-git/go-git/v5/plumbing/object"
14+
"github.com/go-git/go-git/v5/plumbing/storer"
15+
"github.com/go-git/go-git/v5/storage"
16+
"github.com/go-git/go-git/v5/storage/memory"
17+
. "gopkg.in/check.v1"
18+
)
19+
20+
type ServerInfoSuite struct{}
21+
22+
var _ = Suite(&ServerInfoSuite{})
23+
24+
func Test(t *testing.T) { TestingT(t) }
25+
26+
func (s *ServerInfoSuite) TestUpdateServerInfoInit(c *C) {
27+
fs := memfs.New()
28+
st := memory.NewStorage()
29+
r, err := git.Init(st, fs)
30+
c.Assert(err, IsNil)
31+
c.Assert(r, NotNil)
32+
33+
err = UpdateServerInfo(st, fs)
34+
c.Assert(err, IsNil)
35+
}
36+
37+
func assertInfoRefs(c *C, st storage.Storer, fs billy.Filesystem) {
38+
refsFile, err := fs.Open("info/refs")
39+
c.Assert(err, IsNil)
40+
41+
defer refsFile.Close()
42+
bts, err := io.ReadAll(refsFile)
43+
c.Assert(err, IsNil)
44+
45+
localRefs := make(map[plumbing.ReferenceName]plumbing.Hash)
46+
for _, line := range strings.Split(string(bts), "\n") {
47+
if line == "" {
48+
continue
49+
}
50+
parts := strings.Split(line, "\t")
51+
c.Assert(parts, HasLen, 2)
52+
hash := plumbing.NewHash(parts[0])
53+
name := plumbing.ReferenceName(parts[1])
54+
localRefs[name] = hash
55+
}
56+
57+
refs, err := st.IterReferences()
58+
c.Assert(err, IsNil)
59+
60+
err = refs.ForEach(func(ref *plumbing.Reference) error {
61+
name := ref.Name()
62+
hash := ref.Hash()
63+
switch ref.Type() {
64+
case plumbing.SymbolicReference:
65+
if name == plumbing.HEAD {
66+
return nil
67+
}
68+
ref, err := st.Reference(ref.Target())
69+
c.Assert(err, IsNil)
70+
hash = ref.Hash()
71+
fallthrough
72+
case plumbing.HashReference:
73+
h, ok := localRefs[name]
74+
c.Assert(ok, Equals, true)
75+
c.Assert(h, Equals, hash)
76+
if name.IsTag() {
77+
tag, err := object.GetTag(st, hash)
78+
if err == nil {
79+
t, ok := localRefs[name+"^{}"]
80+
c.Assert(ok, Equals, true)
81+
c.Assert(t, Equals, tag.Target)
82+
}
83+
}
84+
}
85+
return nil
86+
})
87+
88+
c.Assert(err, IsNil)
89+
}
90+
91+
func assertObjectPacks(c *C, st storage.Storer, fs billy.Filesystem) {
92+
infoPacks, err := fs.Open("objects/info/packs")
93+
c.Assert(err, IsNil)
94+
95+
defer infoPacks.Close()
96+
bts, err := io.ReadAll(infoPacks)
97+
c.Assert(err, IsNil)
98+
99+
pos, ok := st.(storer.PackedObjectStorer)
100+
c.Assert(ok, Equals, true)
101+
localPacks := make(map[string]struct{})
102+
packs, err := pos.ObjectPacks()
103+
c.Assert(err, IsNil)
104+
105+
for _, line := range strings.Split(string(bts), "\n") {
106+
if line == "" {
107+
continue
108+
}
109+
parts := strings.Split(line, " ")
110+
c.Assert(parts, HasLen, 2)
111+
pack := strings.TrimPrefix(parts[1], "pack-")
112+
pack = strings.TrimSuffix(pack, ".pack")
113+
localPacks[pack] = struct{}{}
114+
}
115+
116+
for _, p := range packs {
117+
_, ok := localPacks[p.String()]
118+
c.Assert(ok, Equals, true)
119+
}
120+
}
121+
122+
func (s *ServerInfoSuite) TestUpdateServerInfoTags(c *C) {
123+
fs := memfs.New()
124+
st := memory.NewStorage()
125+
r, err := git.Clone(st, fs, &git.CloneOptions{
126+
URL: fixtures.ByURL("https://github.com/git-fixtures/tags.git").One().URL,
127+
})
128+
c.Assert(err, IsNil)
129+
c.Assert(r, NotNil)
130+
131+
err = UpdateServerInfo(st, fs)
132+
c.Assert(err, IsNil)
133+
134+
assertInfoRefs(c, st, fs)
135+
assertObjectPacks(c, st, fs)
136+
}
137+
138+
func (s *ServerInfoSuite) TestUpdateServerInfoBasic(c *C) {
139+
fs := memfs.New()
140+
st := memory.NewStorage()
141+
r, err := git.Clone(st, fs, &git.CloneOptions{
142+
URL: fixtures.Basic().One().URL,
143+
})
144+
c.Assert(err, IsNil)
145+
c.Assert(r, NotNil)
146+
147+
err = UpdateServerInfo(st, fs)
148+
c.Assert(err, IsNil)
149+
150+
assertInfoRefs(c, st, fs)
151+
assertObjectPacks(c, st, fs)
152+
}
153+
154+
func (s *ServerInfoSuite) TestUpdateServerInfoBasicChange(c *C) {
155+
fs := memfs.New()
156+
st := memory.NewStorage()
157+
r, err := git.Clone(st, fs, &git.CloneOptions{
158+
URL: fixtures.Basic().One().URL,
159+
})
160+
c.Assert(err, IsNil)
161+
c.Assert(r, NotNil)
162+
163+
err = UpdateServerInfo(st, fs)
164+
c.Assert(err, IsNil)
165+
166+
assertInfoRefs(c, st, fs)
167+
assertObjectPacks(c, st, fs)
168+
169+
head, err := r.Head()
170+
c.Assert(err, IsNil)
171+
172+
ref := plumbing.NewHashReference("refs/heads/my-branch", head.Hash())
173+
err = r.Storer.SetReference(ref)
174+
c.Assert(err, IsNil)
175+
176+
_, err = r.CreateTag("test-tag", head.Hash(), &git.CreateTagOptions{
177+
Message: "test-tag",
178+
})
179+
c.Assert(err, IsNil)
180+
181+
err = UpdateServerInfo(st, fs)
182+
183+
assertInfoRefs(c, st, fs)
184+
assertObjectPacks(c, st, fs)
185+
}

0 commit comments

Comments
 (0)