Skip to content

Commit 4ba9ca2

Browse files
authored
Merge pull request #38 from github/vdye/list-repair
Add `list` and `repair` commands
2 parents a4f4087 + 2694cb1 commit 4ba9ca2

File tree

15 files changed

+785
-25
lines changed

15 files changed

+785
-25
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ being managed by the bundle server.
133133
* `git-bundle-server delete <route>`: Remove the configuration for the given
134134
`<route>` and delete its repository data.
135135

136+
* `git-bundle-server list [<options>]`: List each route and associated
137+
information (e.g. Git remote URL) in the bundle server.
138+
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+
136143
### Web server management
137144

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

cmd/git-bundle-server/list.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/github/git-bundle-server/cmd/utils"
9+
"github.com/github/git-bundle-server/internal/argparse"
10+
"github.com/github/git-bundle-server/internal/core"
11+
"github.com/github/git-bundle-server/internal/git"
12+
"github.com/github/git-bundle-server/internal/log"
13+
)
14+
15+
type listCmd struct {
16+
logger log.TraceLogger
17+
container *utils.DependencyContainer
18+
}
19+
20+
func NewListCommand(logger log.TraceLogger, container *utils.DependencyContainer) argparse.Subcommand {
21+
return &listCmd{
22+
logger: logger,
23+
container: container,
24+
}
25+
}
26+
27+
func (listCmd) Name() string {
28+
return "list"
29+
}
30+
31+
func (listCmd) Description() string {
32+
return `
33+
List the routes registered to the bundle server.`
34+
}
35+
36+
func (l *listCmd) Run(ctx context.Context, args []string) error {
37+
parser := argparse.NewArgParser(l.logger, "git-bundle-server list [--name-only]")
38+
nameOnly := parser.Bool("name-only", false, "print only the names of configured routes")
39+
parser.Parse(ctx, args)
40+
41+
repoProvider := utils.GetDependency[core.RepositoryProvider](ctx, l.container)
42+
gitHelper := utils.GetDependency[git.GitHelper](ctx, l.container)
43+
44+
repos, err := repoProvider.GetRepositories(ctx)
45+
if err != nil {
46+
return l.logger.Error(ctx, err)
47+
}
48+
49+
for _, repo := range repos {
50+
info := []string{repo.Route}
51+
if !*nameOnly {
52+
remote, err := gitHelper.GetRemoteUrl(ctx, repo.RepoDir)
53+
if err != nil {
54+
return l.logger.Error(ctx, err)
55+
}
56+
info = append(info, remote)
57+
}
58+
59+
// Join with space & tab to ensure each element of the info array is
60+
// separated by at least two spaces (for better readability).
61+
fmt.Println(strings.Join(info, " \t"))
62+
}
63+
64+
return nil
65+
}

cmd/git-bundle-server/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ 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),
2122
NewUpdateAllCommand(logger, container),
23+
NewListCommand(logger, container),
2224
NewWebServerCommand(logger, container),
2325
}
2426
}

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+
}

cmd/git-bundle-web-server/bundle-server.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ import (
1313
"time"
1414

1515
"github.com/github/git-bundle-server/internal/bundles"
16+
"github.com/github/git-bundle-server/internal/cmd"
1617
"github.com/github/git-bundle-server/internal/common"
1718
"github.com/github/git-bundle-server/internal/core"
19+
"github.com/github/git-bundle-server/internal/git"
1820
"github.com/github/git-bundle-server/internal/log"
1921
)
2022

@@ -84,7 +86,9 @@ func (b *bundleWebServer) serve(w http.ResponseWriter, r *http.Request) {
8486

8587
userProvider := common.NewUserProvider()
8688
fileSystem := common.NewFileSystem()
87-
repoProvider := core.NewRepositoryProvider(b.logger, userProvider, fileSystem)
89+
commandExecutor := cmd.NewCommandExecutor(b.logger)
90+
gitHelper := git.NewGitHelper(b.logger, commandExecutor)
91+
repoProvider := core.NewRepositoryProvider(b.logger, userProvider, fileSystem, gitHelper)
8892

8993
repos, err := repoProvider.GetRepositories(ctx)
9094
if err != nil {

cmd/utils/container-helpers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ func BuildGitBundleServerContainer(logger log.TraceLogger) *DependencyContainer
2828
logger,
2929
GetDependency[common.UserProvider](ctx, container),
3030
GetDependency[common.FileSystem](ctx, container),
31+
GetDependency[git.GitHelper](ctx, container),
3132
)
3233
})
3334
registerDependency(container, func(ctx context.Context) bundles.BundleProvider {

docs/man/git-bundle-server.adoc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,27 @@ fetching during scheduled bundle updates.
8484
*delete* _route_::
8585
Remove a repository configuration and delete its data on disk.
8686

87+
*list* [*--name-only*]::
88+
List the routes registered to the bundle server. Each line in the output
89+
represents a unique route and includes (in order) the route name and the Git
90+
remote URL associated with that route.
91+
92+
*--name-only*:::
93+
Print only the route name on each line.
94+
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+
87108
*web-server* *start* [*-f*|*--force*] [_server-options_]::
88109
Start a background process web server hosting bundle metadata and content. The
89110
web server daemon runs under the calling user's domain, and will continue

internal/common/filesystem.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"path"
1111
"path/filepath"
1212
"syscall"
13+
14+
"github.com/github/git-bundle-server/internal/utils"
1315
)
1416

1517
const (
@@ -35,6 +37,29 @@ func (l *lockFile) Rollback() error {
3537
return os.Remove(l.lockFilename)
3638
}
3739

40+
type ReadDirEntry interface {
41+
Path() string
42+
fs.DirEntry
43+
}
44+
45+
type fsEntry struct {
46+
root string
47+
fs.DirEntry
48+
}
49+
50+
func (e *fsEntry) Path() string {
51+
return filepath.Join(e.root, e.Name())
52+
}
53+
54+
func mapDirEntry(root string) func(fs.DirEntry) ReadDirEntry {
55+
return func(e fs.DirEntry) ReadDirEntry {
56+
return &fsEntry{
57+
root: root,
58+
DirEntry: e,
59+
}
60+
}
61+
}
62+
3863
type FileSystem interface {
3964
GetLocalExecutable(name string) (string, error)
4065

@@ -43,6 +68,15 @@ type FileSystem interface {
4368
WriteLockFileFunc(filename string, writeFunc func(io.Writer) error) (LockFile, error)
4469
DeleteFile(filename string) (bool, error)
4570
ReadFileLines(filename string) ([]string, error)
71+
72+
// ReadDirRecursive recurses into a given directory ('path') up to 'depth'
73+
// levels deep. If 'strictDepth' is true, only the entries at *exactly* the
74+
// given depth are returned (if any). If 'strictDepth' is false, though, the
75+
// results will also include any files or empty directories for a depth <
76+
// 'depth'.
77+
//
78+
// If 'depth' is <= 0, ReadDirRecursive returns an empty list.
79+
ReadDirRecursive(path string, depth int, strictDepth bool) ([]ReadDirEntry, error)
4680
}
4781

4882
type fileSystem struct{}
@@ -173,3 +207,41 @@ func (f *fileSystem) ReadFileLines(filename string) ([]string, error) {
173207

174208
return l, nil
175209
}
210+
211+
func (f *fileSystem) ReadDirRecursive(path string, depth int, strictDepth bool) ([]ReadDirEntry, error) {
212+
if depth <= 0 {
213+
return []ReadDirEntry{}, nil
214+
}
215+
216+
dirEntries, err := os.ReadDir(path)
217+
if err != nil {
218+
return nil, err
219+
}
220+
221+
entries := utils.Map(dirEntries, mapDirEntry(path))
222+
if depth == 1 {
223+
return entries, nil
224+
}
225+
226+
out := []ReadDirEntry{}
227+
for _, entry := range entries {
228+
if !entry.IsDir() {
229+
if !strictDepth {
230+
out = append(out, entry)
231+
}
232+
continue
233+
}
234+
235+
subEntries, err := f.ReadDirRecursive(entry.Path(), depth-1, strictDepth)
236+
if err != nil {
237+
return nil, err
238+
}
239+
if !strictDepth && len(subEntries) == 0 {
240+
out = append(out, entry)
241+
continue
242+
}
243+
out = append(out, subEntries...)
244+
}
245+
246+
return out, nil
247+
}

internal/core/paths.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,21 @@ package core
22

33
import (
44
"os/user"
5+
"path/filepath"
56
)
67

78
func bundleroot(user *user.User) string {
8-
return user.HomeDir + "/git-bundle-server/"
9+
return filepath.Join(user.HomeDir, "git-bundle-server")
910
}
1011

1112
func webroot(user *user.User) string {
12-
return bundleroot(user) + "www/"
13+
return filepath.Join(bundleroot(user), "www")
1314
}
1415

1516
func reporoot(user *user.User) string {
16-
return bundleroot(user) + "git/"
17+
return filepath.Join(bundleroot(user), "git")
1718
}
1819

1920
func CrontabFile(user *user.User) string {
20-
return bundleroot(user) + "cron-schedule"
21+
return filepath.Join(bundleroot(user), "cron-schedule")
2122
}

0 commit comments

Comments
 (0)