Skip to content

Commit b10b284

Browse files
Merge pull request #3: git-bundle-server CLI Part 2: collapse base bundle and the update-all subcommand
Based on #2. The `update` subcommand now notices when there are more than 5 bundles and squashes the oldest ones into a new "base" bundle. The bundle is renamed but keeps the maximum creation token from that group of bundles. Modified some of the parameters, especially those in the `git` package to avoid cyclic package dependencies. Also used struct pointers more often. Repository routes are now stored in a new `routes` file (currently plaintext with line-separated list of routes). The new `update-all` subcommand runs the `update` subcommand on all registered routes. It also passes any remaining arguments down to the subcommand, which will help when we add the `--daily` and `--hourly` options.
2 parents a7329d3 + 3d0746a commit b10b284

File tree

8 files changed

+284
-52
lines changed

8 files changed

+284
-52
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ being managed by the bundle server.
2727
`https://github.com/git-for-windows/git` is assigned the route
2828
`/git-for-windows/git`. Run `git-bundle-server update` to initialize bundle
2929
information. Configure the web server to recognize this repository at that
30-
route. Configure scheduler to run `git-bundle-server update --all` as
30+
route. Configure scheduler to run `git-bundle-server update-all` as
3131
necessary.
3232

3333
* `git-bundle-server update [--daily|--hourly] <route>`: For the

cmd/git-bundle-server/init.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ func (Init) run(args []string) error {
2323
url := args[0]
2424
route := args[1]
2525

26-
repo := core.GetRepository(route)
26+
repo, err := core.CreateRepository(route)
27+
if err != nil {
28+
return err
29+
}
2730

2831
fmt.Printf("Cloning repository from %s\n", url)
2932
gitErr := git.GitCommand("clone", "--mirror", url, repo.RepoDir)
@@ -35,15 +38,15 @@ func (Init) run(args []string) error {
3538
bundle := bundles.CreateInitialBundle(repo)
3639
fmt.Printf("Constructing base bundle file at %s\n", bundle.Filename)
3740

38-
written, gitErr := git.CreateBundle(repo, bundle)
41+
written, gitErr := git.CreateBundle(repo.RepoDir, bundle.Filename)
3942
if gitErr != nil {
4043
return fmt.Errorf("failed to create bundle: %w", gitErr)
4144
}
4245
if !written {
4346
return fmt.Errorf("refused to write empty bundle. Is the repo empty?")
4447
}
4548

46-
list := bundles.SingletonList(bundle)
49+
list := bundles.CreateSingletonList(bundle)
4750
listErr := bundles.WriteBundleList(list, repo)
4851
if listErr != nil {
4952
return fmt.Errorf("failed to write bundle list: %w", listErr)

cmd/git-bundle-server/subcommand.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ func all() []Subcommand {
99
return []Subcommand{
1010
Init{},
1111
Update{},
12+
UpdateAll{},
1213
}
1314
}

cmd/git-bundle-server/update-all.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"git-bundle-server/internal/core"
6+
"os"
7+
"os/exec"
8+
)
9+
10+
type UpdateAll struct{}
11+
12+
func (UpdateAll) subcommand() string {
13+
return "update-all"
14+
}
15+
16+
func (UpdateAll) run(args []string) error {
17+
exe, err := os.Executable()
18+
if err != nil {
19+
return fmt.Errorf("failed to get path to execuable: %w", err)
20+
}
21+
22+
repos, err := core.GetRepositories()
23+
if err != nil {
24+
return err
25+
}
26+
27+
subargs := []string{"update", ""}
28+
subargs = append(subargs, args...)
29+
30+
for route := range repos {
31+
subargs[1] = route
32+
cmd := exec.Command(exe, subargs...)
33+
cmd.Stderr = os.Stderr
34+
cmd.Stdout = os.Stdout
35+
36+
err := cmd.Start()
37+
if err != nil {
38+
return fmt.Errorf("git command failed to start: %w", err)
39+
}
40+
41+
err = cmd.Wait()
42+
if err != nil {
43+
return fmt.Errorf("git command returned a failure: %w", err)
44+
}
45+
}
46+
47+
return nil
48+
}

cmd/git-bundle-server/update.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"fmt"
66
"git-bundle-server/internal/bundles"
77
"git-bundle-server/internal/core"
8-
"git-bundle-server/internal/git"
98
)
109

1110
type Update struct{}
@@ -21,31 +20,37 @@ func (Update) run(args []string) error {
2120
}
2221

2322
route := args[0]
24-
repo := core.GetRepository(route)
23+
repo, err := core.CreateRepository(route)
24+
if err != nil {
25+
return err
26+
}
2527

2628
list, err := bundles.GetBundleList(repo)
2729
if err != nil {
2830
return fmt.Errorf("failed to load bundle list: %w", err)
2931
}
3032

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)
33+
fmt.Printf("Creating new incremental bundle\n")
34+
bundle, err := bundles.CreateIncrementalBundle(repo, list)
3635
if err != nil {
37-
return fmt.Errorf("failed to create incremental bundle: %w", err)
36+
return err
3837
}
3938

40-
// Nothing to update
41-
if !written {
39+
// Nothing new!
40+
if bundle == nil {
4241
return nil
4342
}
4443

45-
list.Bundles[bundle.CreationToken] = bundle
44+
list.Bundles[bundle.CreationToken] = *bundle
45+
46+
fmt.Printf("Collapsing bundle list\n")
47+
err = bundles.CollapseList(repo, list)
48+
if err != nil {
49+
return err
50+
}
4651

4752
fmt.Printf("Writing updated bundle list\n")
48-
listErr := bundles.WriteBundleList(*list, repo)
53+
listErr := bundles.WriteBundleList(list, repo)
4954
if listErr != nil {
5055
return fmt.Errorf("failed to write bundle list: %w", listErr)
5156
}

internal/bundles/bundles.go

Lines changed: 113 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import (
55
"encoding/json"
66
"fmt"
77
"git-bundle-server/internal/core"
8+
"git-bundle-server/internal/git"
89
"os"
10+
"sort"
911
"strconv"
1012
"strings"
1113
"time"
@@ -34,11 +36,11 @@ type BundleList struct {
3436
Bundles map[int64]Bundle
3537
}
3638

37-
func addBundleToList(bundle Bundle, list BundleList) {
39+
func addBundleToList(bundle Bundle, list *BundleList) {
3840
list.Bundles[bundle.CreationToken] = bundle
3941
}
4042

41-
func CreateInitialBundle(repo core.Repository) Bundle {
43+
func CreateInitialBundle(repo *core.Repository) Bundle {
4244
timestamp := time.Now().UTC().Unix()
4345
bundleName := "bundle-" + fmt.Sprint(timestamp) + ".bundle"
4446
bundleFile := repo.WebDir + "/" + bundleName
@@ -51,14 +53,14 @@ func CreateInitialBundle(repo core.Repository) Bundle {
5153
return bundle
5254
}
5355

54-
func CreateDistinctBundle(repo core.Repository, list BundleList) Bundle {
56+
func CreateDistinctBundle(repo *core.Repository, list *BundleList) Bundle {
5557
timestamp := time.Now().UTC().Unix()
5658

57-
_, c := list.Bundles[timestamp]
59+
keys := GetSortedCreationTokens(list)
5860

59-
for c {
60-
timestamp++
61-
_, c = list.Bundles[timestamp]
61+
maxTimestamp := keys[len(keys)-1]
62+
if timestamp <= maxTimestamp {
63+
timestamp = maxTimestamp + 1
6264
}
6365

6466
bundleName := "bundle-" + fmt.Sprint(timestamp) + ".bundle"
@@ -72,21 +74,21 @@ func CreateDistinctBundle(repo core.Repository, list BundleList) Bundle {
7274
return bundle
7375
}
7476

75-
func SingletonList(bundle Bundle) BundleList {
77+
func CreateSingletonList(bundle Bundle) *BundleList {
7678
list := BundleList{1, "all", make(map[int64]Bundle)}
7779

78-
addBundleToList(bundle, list)
80+
addBundleToList(bundle, &list)
7981

80-
return list
82+
return &list
8183
}
8284

8385
// Given a BundleList
84-
func WriteBundleList(list BundleList, repo core.Repository) error {
86+
func WriteBundleList(list *BundleList, repo *core.Repository) error {
8587
listFile := repo.WebDir + "/bundle-list"
8688
jsonFile := repo.RepoDir + "/bundle-list.json"
8789

8890
// TODO: Formalize lockfile concept.
89-
f, err := os.OpenFile(listFile+".lock", os.O_WRONLY|os.O_CREATE, 0600)
91+
f, err := os.OpenFile(listFile+".lock", os.O_WRONLY|os.O_CREATE, 0o600)
9092
if err != nil {
9193
return fmt.Errorf("failure to open file: %w", err)
9294
}
@@ -97,7 +99,10 @@ func WriteBundleList(list BundleList, repo core.Repository) error {
9799
out, "[bundle]\n\tversion = %d\n\tmode = %s\n\n",
98100
list.Version, list.Mode)
99101

100-
for token, bundle := range list.Bundles {
102+
keys := GetSortedCreationTokens(list)
103+
104+
for _, token := range keys {
105+
bundle := list.Bundles[token]
101106
fmt.Fprintf(
102107
out, "[bundle \"%d\"]\n\turi = %s\n\tcreationToken = %d\n\n",
103108
token, bundle.URI, token)
@@ -109,7 +114,7 @@ func WriteBundleList(list BundleList, repo core.Repository) error {
109114
return fmt.Errorf("failed to close lock file: %w", err)
110115
}
111116

112-
f, err = os.OpenFile(jsonFile+".lock", os.O_WRONLY|os.O_CREATE, 0600)
117+
f, err = os.OpenFile(jsonFile+".lock", os.O_WRONLY|os.O_CREATE, 0o600)
113118
if err != nil {
114119
return fmt.Errorf("failed to open JSON file: %w", err)
115120
}
@@ -139,7 +144,7 @@ func WriteBundleList(list BundleList, repo core.Repository) error {
139144
return os.Rename(listFile+".lock", listFile)
140145
}
141146

142-
func GetBundleList(repo core.Repository) (*BundleList, error) {
147+
func GetBundleList(repo *core.Repository) (*BundleList, error) {
143148
jsonFile := repo.RepoDir + "/bundle-list.json"
144149

145150
reader, err := os.Open(jsonFile)
@@ -182,8 +187,8 @@ func GetBundleHeader(bundle Bundle) (*BundleHeader, error) {
182187

183188
if line[0] == '#' &&
184189
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)
190+
strings.HasSuffix(line, " git bundle") {
191+
header.Version, err = strconv.ParseInt(line[3:len(line)-len(" git bundle")], 10, 64)
187192
if err != nil {
188193
return nil, fmt.Errorf("failed to parse bundle version: %s", err)
189194
}
@@ -226,7 +231,7 @@ func GetBundleHeader(bundle Bundle) (*BundleHeader, error) {
226231
return &header, nil
227232
}
228233

229-
func GetAllPrereqsForIncrementalBundle(list BundleList) ([]string, error) {
234+
func GetAllPrereqsForIncrementalBundle(list *BundleList) ([]string, error) {
230235
prereqs := []string{}
231236

232237
for _, bundle := range list.Bundles {
@@ -242,3 +247,93 @@ func GetAllPrereqsForIncrementalBundle(list BundleList) ([]string, error) {
242247

243248
return prereqs, nil
244249
}
250+
251+
func CreateIncrementalBundle(repo *core.Repository, list *BundleList) (*Bundle, error) {
252+
bundle := CreateDistinctBundle(repo, list)
253+
254+
lines, err := GetAllPrereqsForIncrementalBundle(list)
255+
if err != nil {
256+
return nil, err
257+
}
258+
259+
written, err := git.CreateIncrementalBundle(repo.RepoDir, bundle.Filename, lines)
260+
if err != nil {
261+
return nil, fmt.Errorf("failed to create incremental bundle: %w", err)
262+
}
263+
264+
if !written {
265+
return nil, nil
266+
}
267+
268+
return &bundle, nil
269+
}
270+
271+
func CollapseList(repo *core.Repository, list *BundleList) error {
272+
maxBundles := 5
273+
274+
if len(list.Bundles) <= maxBundles {
275+
return nil
276+
}
277+
278+
keys := GetSortedCreationTokens(list)
279+
280+
refs := make(map[string]string)
281+
282+
maxTimestamp := int64(0)
283+
284+
for i := range keys[0 : len(keys)-maxBundles+1] {
285+
bundle := list.Bundles[keys[i]]
286+
287+
if bundle.CreationToken > maxTimestamp {
288+
maxTimestamp = bundle.CreationToken
289+
}
290+
291+
header, err := GetBundleHeader(bundle)
292+
if err != nil {
293+
return fmt.Errorf("failed to parse bundle file %s: %w", bundle.Filename, err)
294+
}
295+
296+
// Ignore the old ref name and instead use the OID
297+
// to generate the ref name. This allows us to create new
298+
// refs that point to exactly these objects without disturbing
299+
// refs/heads/ which is tracking the remote refs.
300+
for _, oid := range header.Refs {
301+
refs["refs/base/"+oid] = oid
302+
}
303+
304+
delete(list.Bundles, keys[i])
305+
}
306+
307+
// TODO: Use Git to determine which OIDs are "maximal" in the set
308+
// and which are not implied by the previous ones.
309+
310+
// TODO: Use Git to determine which OIDs are required as prerequisites
311+
// of the remaining bundles and latest ref tips, so we can "GC" the
312+
// branches that were never merged and may have been force-pushed or
313+
// deleted.
314+
315+
bundle := Bundle{
316+
CreationToken: maxTimestamp,
317+
Filename: fmt.Sprintf("%s/base-%d.bundle", repo.WebDir, maxTimestamp),
318+
URI: fmt.Sprintf("./base-%d.bundle", maxTimestamp),
319+
}
320+
321+
err := git.CreateBundleFromRefs(repo.RepoDir, bundle.Filename, refs)
322+
if err != nil {
323+
return err
324+
}
325+
326+
list.Bundles[maxTimestamp] = bundle
327+
return nil
328+
}
329+
330+
func GetSortedCreationTokens(list *BundleList) []int64 {
331+
keys := make([]int64, 0, len(list.Bundles))
332+
for timestamp := range list.Bundles {
333+
keys = append(keys, timestamp)
334+
}
335+
336+
sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
337+
338+
return keys
339+
}

0 commit comments

Comments
 (0)