Skip to content

Commit 1b1f540

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 1b1f540

File tree

5 files changed

+260
-7
lines changed

5 files changed

+260
-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: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,22 @@ import (
66
"fmt"
77
"git-bundle-server/internal/core"
88
"os"
9+
"strconv"
10+
"strings"
911
"time"
1012
)
1113

14+
type BundleHeader struct {
15+
Version int64
16+
17+
// The Refs map is given as Refs[<refname>] = <oid>.
18+
Refs map[string]string
19+
20+
// The PrereqCommits map is given as
21+
// PrereqCommits[<oid>] = <commit-msg>
22+
PrereqCommits map[string]string
23+
}
24+
1225
type Bundle struct {
1326
URI string
1427
Filename string
@@ -38,6 +51,27 @@ func CreateInitialBundle(repo core.Repository) Bundle {
3851
return bundle
3952
}
4053

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

@@ -104,3 +138,107 @@ func WriteBundleList(list BundleList, repo core.Repository) error {
104138

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

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)