Skip to content

Commit 2694cb1

Browse files
committed
git-bundle-server: add 'repair' command
Create a new command 'repair' to fix common issues with the bundle server's internal storage. Start by implementing the 'routes' subcommand, which (by default) removes routes from the 'routes' file if they do not appear (or are not Git repositories) on disk. Include two additional options: - '--dry-run', which gathers repository information and constructs the list of repairs needed, but does not update anything - '--start-all', which adds any unregistered, valid repositories on disk to the 'routes' file (effectively running 'git-bundle-server start' on all) Signed-off-by: Victoria Dye <[email protected]>
1 parent f112462 commit 2694cb1

File tree

6 files changed

+278
-0
lines changed

6 files changed

+278
-0
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ being managed by the bundle server.
136136
* `git-bundle-server list [<options>]`: List each route and associated
137137
information (e.g. Git remote URL) in the bundle server.
138138

139+
* `git-bundle-server repair routes [<options>]`: Correct the contents of the
140+
internal route registry by comparing to bundle server's internal repository
141+
storage.
142+
139143
### Web server management
140144

141145
Independent of the management of the individual repositories hosted by the

cmd/git-bundle-server/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ func all(logger log.TraceLogger) []argparse.Subcommand {
1515
return []argparse.Subcommand{
1616
NewDeleteCommand(logger, container),
1717
NewInitCommand(logger, container),
18+
NewRepairCommand(logger, container),
1819
NewStartCommand(logger, container),
1920
NewStopCommand(logger, container),
2021
NewUpdateCommand(logger, container),

cmd/git-bundle-server/repair.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/github/git-bundle-server/cmd/utils"
8+
"github.com/github/git-bundle-server/internal/argparse"
9+
"github.com/github/git-bundle-server/internal/core"
10+
"github.com/github/git-bundle-server/internal/log"
11+
typeutils "github.com/github/git-bundle-server/internal/utils"
12+
)
13+
14+
type repairCmd struct {
15+
logger log.TraceLogger
16+
container *utils.DependencyContainer
17+
}
18+
19+
func NewRepairCommand(logger log.TraceLogger, container *utils.DependencyContainer) argparse.Subcommand {
20+
return &repairCmd{
21+
logger: logger,
22+
container: container,
23+
}
24+
}
25+
26+
func (repairCmd) Name() string {
27+
return "repair"
28+
}
29+
30+
func (repairCmd) Description() string {
31+
return `
32+
Scan and correct inconsistencies in the bundle server's internal registries and
33+
storage.`
34+
}
35+
36+
func (r *repairCmd) repairRoutes(ctx context.Context, args []string) error {
37+
parser := argparse.NewArgParser(r.logger, "git-bundle-server repair routes [--start-all] [--dry-run]")
38+
enable := parser.Bool("start-all", false, "turn on bundle computation for all repositories found")
39+
dryRun := parser.Bool("dry-run", false, "report the repairs needed, but do not perform them")
40+
// TODO: add a '--cleanup' option to delete non-repo contents inside repo root
41+
parser.Parse(ctx, args)
42+
43+
repoProvider := utils.GetDependency[core.RepositoryProvider](ctx, r.container)
44+
45+
// Read the routes file
46+
repos, err := repoProvider.GetRepositories(ctx)
47+
if err != nil {
48+
// If routes file cannot be read, start over
49+
fmt.Println("warning: cannot load routes file; rebuilding from scratch...")
50+
repos = make(map[string]core.Repository)
51+
}
52+
53+
// Read the repositories as represented by internal storage
54+
storedRepos, err := repoProvider.ReadRepositoryStorage(ctx)
55+
if err != nil {
56+
return r.logger.Errorf(ctx, "could not read internal repository storage: %w", err)
57+
}
58+
59+
_, missingOnDisk, notRegistered := typeutils.SegmentKeys(repos, storedRepos)
60+
61+
// Print the updates to be made
62+
fmt.Print("\n")
63+
64+
if *enable && len(notRegistered) > 0 {
65+
fmt.Println("Unregistered routes to add")
66+
fmt.Println("--------------------------")
67+
for _, route := range notRegistered {
68+
fmt.Printf("* %s\n", route)
69+
repos[route] = storedRepos[route]
70+
}
71+
fmt.Print("\n")
72+
}
73+
74+
if len(missingOnDisk) > 0 {
75+
fmt.Println("Missing or invalid routes to remove")
76+
fmt.Println("-----------------------------------")
77+
for _, route := range missingOnDisk {
78+
fmt.Printf("* %s\n", route)
79+
delete(repos, route)
80+
}
81+
fmt.Print("\n")
82+
}
83+
84+
if (!*enable || len(notRegistered) == 0) && len(missingOnDisk) == 0 {
85+
fmt.Println("No repairs needed.")
86+
return nil
87+
}
88+
89+
if *dryRun {
90+
fmt.Println("Skipping updates (dry run)")
91+
} else {
92+
fmt.Println("Applying route repairs...")
93+
err := repoProvider.WriteAllRoutes(ctx, repos)
94+
if err != nil {
95+
return err
96+
}
97+
98+
// Start the global cron schedule (if it's not already running)
99+
cron := utils.GetDependency[utils.CronHelper](ctx, r.container)
100+
cron.SetCronSchedule(ctx)
101+
fmt.Println("Done")
102+
}
103+
104+
return nil
105+
}
106+
107+
func (r *repairCmd) Run(ctx context.Context, args []string) error {
108+
parser := argparse.NewArgParser(r.logger, "git-bundle-server repair <subcommand> [<options>]")
109+
parser.Subcommand(argparse.NewSubcommand("routes", "Correct the contents of the internal route registry", r.repairRoutes))
110+
parser.Parse(ctx, args)
111+
112+
return parser.InvokeSubcommand(ctx)
113+
}

docs/man/git-bundle-server.adoc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,19 @@ fetching during scheduled bundle updates.
9292
*--name-only*:::
9393
Print only the route name on each line.
9494

95+
*repair* *routes* [*--start-all*] [*--dry-run*]::
96+
Correct the contents of the internal route registry by comparing to bundle
97+
server's internal repository storage.
98+
99+
*--start-all*:::
100+
If any valid repositories are found that are not registered to the bundle
101+
server (for example, those deactivated with *git-bundle-server stop*),
102+
enable them.
103+
104+
*--dry-run*:::
105+
Collect and report the repairs that the command will perform, but do not
106+
perform them.
107+
95108
*web-server* *start* [*-f*|*--force*] [_server-options_]::
96109
Start a background process web server hosting bundle metadata and content. The
97110
web server daemon runs under the calling user's domain, and will continue

internal/utils/maps.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package utils
2+
3+
// SegmentKeys takes two maps with keys of the same type and returns three
4+
// slices (with *no guaranteed order*):
5+
// - the keys common across both maps
6+
// - the keys present in the first set but not the second
7+
// - the keys present in the second set but not the first
8+
func SegmentKeys[T comparable, S any, R any](mapA map[T]S, mapB map[T]R) (intersect, diffAB, diffBA []T) {
9+
intersect = []T{}
10+
diffAB = []T{}
11+
diffBA = []T{}
12+
for a := range mapA {
13+
if _, contains := mapB[a]; contains {
14+
intersect = append(intersect, a)
15+
} else {
16+
diffAB = append(diffAB, a)
17+
}
18+
}
19+
20+
for b := range mapB {
21+
if _, contains := mapA[b]; !contains {
22+
diffBA = append(diffBA, b)
23+
}
24+
}
25+
26+
return
27+
}

internal/utils/maps_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package utils_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/github/git-bundle-server/internal/utils"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
var segmentKeysTests = []struct {
11+
title string
12+
mapA map[string]interface{}
13+
mapB map[string]interface{}
14+
15+
expectedIntersect []string
16+
expectedDiffAB []string
17+
expectedDiffBA []string
18+
}{
19+
{
20+
"all empty",
21+
map[string]interface{}{},
22+
map[string]interface{}{},
23+
24+
[]string{},
25+
[]string{},
26+
[]string{},
27+
},
28+
{
29+
"no overlap",
30+
map[string]interface{}{
31+
"A": nil,
32+
"B": nil,
33+
},
34+
map[string]interface{}{
35+
"C": nil,
36+
"D": nil,
37+
},
38+
39+
[]string{},
40+
[]string{"A", "B"},
41+
[]string{"C", "D"},
42+
},
43+
{
44+
"all overlap",
45+
map[string]interface{}{
46+
"A": nil,
47+
"B": nil,
48+
},
49+
map[string]interface{}{
50+
"B": nil,
51+
"A": nil,
52+
},
53+
54+
[]string{"A", "B"},
55+
[]string{},
56+
[]string{},
57+
},
58+
{
59+
"A superset of B",
60+
map[string]interface{}{
61+
"A": nil,
62+
"B": nil,
63+
"C": nil,
64+
"D": nil,
65+
},
66+
map[string]interface{}{
67+
"B": nil,
68+
"D": nil,
69+
},
70+
71+
[]string{"B", "D"},
72+
[]string{"A", "C"},
73+
[]string{},
74+
},
75+
{
76+
"B superset of A",
77+
map[string]interface{}{
78+
"A": nil,
79+
"C": nil,
80+
},
81+
map[string]interface{}{
82+
"A": nil,
83+
"B": nil,
84+
"C": nil,
85+
"D": nil,
86+
},
87+
88+
[]string{"A", "C"},
89+
[]string{},
90+
[]string{"B", "D"},
91+
},
92+
{
93+
"no empty result sets",
94+
map[string]interface{}{
95+
"A": nil,
96+
"C": nil,
97+
"E": nil,
98+
},
99+
map[string]interface{}{
100+
"B": nil,
101+
"C": nil,
102+
"D": nil,
103+
},
104+
105+
[]string{"C"},
106+
[]string{"A", "E"},
107+
[]string{"B", "D"},
108+
},
109+
}
110+
111+
func TestSegmentKeys(t *testing.T) {
112+
for _, tt := range segmentKeysTests {
113+
t.Run(tt.title, func(t *testing.T) {
114+
intersect, diffAB, diffBA := utils.SegmentKeys(tt.mapA, tt.mapB)
115+
assert.ElementsMatch(t, tt.expectedIntersect, intersect)
116+
assert.ElementsMatch(t, tt.expectedDiffAB, diffAB)
117+
assert.ElementsMatch(t, tt.expectedDiffBA, diffBA)
118+
})
119+
}
120+
}

0 commit comments

Comments
 (0)