Skip to content

Commit 735aba1

Browse files
committed
update: add incremental bundle on top of previous bundles
Be sure to parse the existing bundle headers in order to get the previous refs, then use those as prerequisites for the new incremental bundle. It's not enough to only use the refs from the previous layer, since perhaps not all refs were updated or maybe a ref was force-pushed.
1 parent ae096ff commit 735aba1

File tree

5 files changed

+258
-7
lines changed

5 files changed

+258
-7
lines changed

cmd/git-bundle-server/init.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,13 @@ func (Init) run(args []string) error {
3535
bundle := bundles.CreateInitialBundle(repo)
3636
fmt.Printf("Constructing base bundle file at %s\n", bundle.Filename)
3737

38-
gitErr = git.GitCommand("-C", repo.RepoDir, "bundle", "create", bundle.Filename, "--all")
38+
written, gitErr := git.CreateBundle(repo, bundle)
3939
if gitErr != nil {
4040
return fmt.Errorf("failed to create bundle: %w", gitErr)
4141
}
42+
if !written {
43+
return fmt.Errorf("refused to write empty bundle. Is the repo empty?")
44+
}
4245

4346
list := bundles.SingletonList(bundle)
4447
listErr := bundles.WriteBundleList(list, repo)

cmd/git-bundle-server/subcommand.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ type Subcommand interface {
88
func all() []Subcommand {
99
return []Subcommand{
1010
Init{},
11+
Update{},
1112
}
1213
}

cmd/git-bundle-server/update.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package main
22

33
import (
4+
"errors"
45
"fmt"
6+
"git-bundle-server/internal/bundles"
7+
"git-bundle-server/internal/core"
8+
"git-bundle-server/internal/git"
59
)
610

711
type Update struct{}
@@ -11,10 +15,39 @@ func (Update) subcommand() string {
1115
}
1216

1317
func (Update) run(args []string) error {
14-
fmt.Printf("Found Update method!\n")
18+
if len(args) != 1 {
19+
// TODO: allow parsing <route> out of <url>
20+
return errors.New("usage: git-bundle-server update <route>")
21+
}
22+
23+
route := args[0]
24+
repo := core.GetRepository(route)
25+
26+
list, err := bundles.GetBundleList(repo)
27+
if err != nil {
28+
return fmt.Errorf("failed to load bundle list: %w", err)
29+
}
30+
31+
bundle := bundles.CreateDistinctBundle(repo, *list)
32+
33+
fmt.Printf("Constructing incremental bundle file at %s\n", bundle.Filename)
34+
35+
written, err := git.CreateIncrementalBundle(repo, bundle, *list)
36+
if err != nil {
37+
return fmt.Errorf("failed to create incremental bundle: %w", err)
38+
}
39+
40+
// Nothing to update
41+
if !written {
42+
return nil
43+
}
44+
45+
list.Bundles[bundle.CreationToken] = bundle
1546

16-
for _, arg := range args {
17-
fmt.Printf("%s\n", arg)
47+
fmt.Printf("Writing updated bundle list\n")
48+
listErr := bundles.WriteBundleList(*list, repo)
49+
if listErr != nil {
50+
return fmt.Errorf("failed to write bundle list: %w", listErr)
1851
}
1952

2053
return nil

internal/bundles/bundles.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,24 @@ import (
55
"encoding/json"
66
"fmt"
77
"git-bundle-server/internal/core"
8+
"io"
89
"os"
10+
"strconv"
11+
"strings"
912
"time"
1013
)
1114

15+
type BundleHeader struct {
16+
Version int64
17+
18+
// The Refs map is given as Refs[<refname>] = <oid>.
19+
Refs map[string]string
20+
21+
// The PrereqCommits map is given as
22+
// PrereqCommits[<oid>] = <commit-msg>
23+
PrereqCommits map[string]string
24+
}
25+
1226
type Bundle struct {
1327
URI string
1428
Filename string
@@ -38,6 +52,27 @@ func CreateInitialBundle(repo core.Repository) Bundle {
3852
return bundle
3953
}
4054

55+
func CreateDistinctBundle(repo core.Repository, list BundleList) Bundle {
56+
timestamp := time.Now().UTC().Unix()
57+
58+
_, c := list.Bundles[timestamp]
59+
60+
for c {
61+
timestamp++
62+
_, c = list.Bundles[timestamp]
63+
}
64+
65+
bundleName := "bundle-" + fmt.Sprint(timestamp) + ".bundle"
66+
bundleFile := repo.WebDir + "/" + bundleName
67+
bundle := Bundle{
68+
URI: "./" + bundleName,
69+
Filename: bundleFile,
70+
CreationToken: timestamp,
71+
}
72+
73+
return bundle
74+
}
75+
4176
func SingletonList(bundle Bundle) BundleList {
4277
list := BundleList{1, "all", make(map[int64]Bundle)}
4378

@@ -104,3 +139,104 @@ func WriteBundleList(list BundleList, repo core.Repository) error {
104139

105140
return os.Rename(listFile+".lock", listFile)
106141
}
142+
143+
func GetBundleList(repo core.Repository) (*BundleList, error) {
144+
jsonFile := repo.RepoDir + "/bundle-list.json"
145+
146+
reader, err := os.Open(jsonFile)
147+
if err != nil {
148+
return nil, fmt.Errorf("failed to open file: %w", err)
149+
}
150+
151+
var list BundleList
152+
err = json.NewDecoder(reader).Decode(&list)
153+
if err != nil {
154+
return nil, fmt.Errorf("failed to parse JSON from file: %w", err)
155+
}
156+
157+
return &list, nil
158+
}
159+
160+
func GetBundleHeader(bundle Bundle) (*BundleHeader, error) {
161+
file, err := os.Open(bundle.Filename)
162+
if err != nil {
163+
return nil, fmt.Errorf("failed to open bundle file: %w", err)
164+
}
165+
166+
header := BundleHeader{
167+
Version: 0,
168+
Refs: make(map[string]string),
169+
PrereqCommits: make(map[string]string),
170+
}
171+
172+
reader := bufio.NewReader(file)
173+
line, err := reader.ReadString('\n')
174+
if line == "" ||
175+
(err != nil && err != io.EOF) ||
176+
!strings.HasPrefix(line, "# v") ||
177+
!strings.HasSuffix(line, " git bundle\n") {
178+
return nil, fmt.Errorf("bundle file has invalid header: '%s'", line)
179+
}
180+
181+
header.Version, err = strconv.ParseInt(line[3:len(line)-len(" git bundle\n")], 10, 64)
182+
if err != nil {
183+
return nil, fmt.Errorf(("bundle file has invalid version: %w"), err)
184+
}
185+
186+
// Parse until we reach the "PACK" information
187+
for {
188+
line, err = reader.ReadString('\n')
189+
if line == "" || line[0] == '\n' ||
190+
(err != nil && err != io.EOF) ||
191+
strings.HasPrefix(line, "PACK") {
192+
break
193+
}
194+
195+
if line[0] == '@' {
196+
// This is a capability. Ignore for now.
197+
continue
198+
}
199+
200+
if line[0] == '-' {
201+
// This is a prerequisite
202+
space := strings.Index(line, " ")
203+
if space < 0 {
204+
return nil, fmt.Errorf("failed to parse rerequisite '%s'", line)
205+
}
206+
207+
oid := line[0:space]
208+
message := line[space+1 : len(line)-1]
209+
header.PrereqCommits[oid] = message
210+
} else {
211+
// This is a tip
212+
space := strings.Index(line, " ")
213+
214+
if space < 0 {
215+
return nil, fmt.Errorf("failed to parse tip '%s'", line)
216+
}
217+
218+
oid := line[0:space]
219+
ref := line[space+1 : len(line)-1]
220+
header.Refs[ref] = oid
221+
}
222+
}
223+
224+
return &header, nil
225+
}
226+
227+
func GetAllPrereqsForIncrementalBundle(list BundleList) ([]string, error) {
228+
prereqs := []string{}
229+
230+
for _, bundle := range list.Bundles {
231+
header, err := GetBundleHeader(bundle)
232+
if err != nil {
233+
return nil, fmt.Errorf("failed to parse bundle file %s: %w", bundle.Filename, err)
234+
}
235+
236+
for _, oid := range header.Refs {
237+
prereqs = append(prereqs, "^"+oid)
238+
}
239+
}
240+
241+
return prereqs, nil
242+
}

internal/git/git.go

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package git
22

33
import (
4-
"log"
4+
"bytes"
5+
"fmt"
6+
"git-bundle-server/internal/bundles"
7+
"git-bundle-server/internal/core"
8+
"os"
59
"os/exec"
10+
"strings"
611
)
712

813
func GitCommand(args ...string) error {
@@ -13,15 +18,88 @@ func GitCommand(args ...string) error {
1318
}
1419

1520
cmd := exec.Command(git, args...)
21+
cmd.Stderr = os.Stderr
22+
cmd.Stdout = os.Stdout
23+
1624
err := cmd.Start()
1725
if err != nil {
18-
log.Fatal("Git command failed to start: ", err)
26+
return fmt.Errorf("git command failed to start: %w", err)
1927
}
2028

2129
err = cmd.Wait()
2230
if err != nil {
23-
log.Fatal("Git command returned a failure: ", err)
31+
return fmt.Errorf("git command returned a failure: %w", err)
2432
}
2533

2634
return err
2735
}
36+
37+
func GitCommandWithStdin(stdinLines []string, args ...string) error {
38+
git, lookErr := exec.LookPath("git")
39+
40+
if lookErr != nil {
41+
return lookErr
42+
}
43+
44+
buffer := bytes.Buffer{}
45+
for line := range stdinLines {
46+
buffer.Write([]byte(stdinLines[line] + "\n"))
47+
}
48+
49+
cmd := exec.Command(git, args...)
50+
51+
cmd.Stdin = &buffer
52+
53+
errorBuffer := bytes.Buffer{}
54+
cmd.Stderr = &errorBuffer
55+
cmd.Stdout = os.Stdout
56+
57+
err := cmd.Start()
58+
if err != nil {
59+
return fmt.Errorf("git command failed to start: %w", err)
60+
}
61+
62+
err = cmd.Wait()
63+
if err != nil {
64+
return fmt.Errorf("git command returned a failure: %w\nstderr: %s", err, errorBuffer.String())
65+
}
66+
67+
return err
68+
}
69+
70+
func CreateBundle(repo core.Repository, bundle bundles.Bundle) (bool, error) {
71+
err := GitCommand(
72+
"-C", repo.RepoDir, "bundle", "create",
73+
bundle.Filename, "--all")
74+
if err != nil {
75+
if strings.Contains(err.Error(), "Refusing to create empty bundle") {
76+
return false, nil
77+
}
78+
return false, err
79+
}
80+
81+
return true, nil
82+
}
83+
84+
func CreateIncrementalBundle(repo core.Repository, bundle bundles.Bundle, list bundles.BundleList) (bool, error) {
85+
lines, err := bundles.GetAllPrereqsForIncrementalBundle(list)
86+
if err != nil {
87+
return false, err
88+
}
89+
90+
for _, line := range lines {
91+
fmt.Printf("Sending prereq: %s\n", line)
92+
}
93+
94+
err = GitCommandWithStdin(
95+
lines, "-C", repo.RepoDir, "bundle", "create",
96+
bundle.Filename, "--stdin", "--all")
97+
if err != nil {
98+
if strings.Contains(err.Error(), "Refusing to create empty bundle") {
99+
return false, nil
100+
}
101+
return false, err
102+
}
103+
104+
return true, nil
105+
}

0 commit comments

Comments
 (0)