cmd/gitbayd/maint.go
159 lines · 4122 bytes
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "io/fs"
7 "os"
8 "os/exec"
9 "path/filepath"
10 "text/tabwriter"
11
12 "github.com/spf13/cobra"
13
14 "gitbay.org/gitbay/internal/config"
15 "gitbay.org/gitbay/internal/control"
16 "gitbay.org/gitbay/internal/store"
17)
18
19func gcCmd() *cobra.Command {
20 var repoPath string
21 var aggressive bool
22 cmd := &cobra.Command{
23 Use: "gc",
24 Short: "repack and prune repositories (git gc)",
25 RunE: func(cmd *cobra.Command, args []string) error {
26 cfg, err := config.Load(configPath)
27 if err != nil {
28 return err
29 }
30 st, err := openStore(cfg)
31 if err != nil {
32 return err
33 }
34 defer st.Close()
35
36 var repos []store.Repo
37 if repoPath != "" {
38 r, err := st.RepoByPath(repoPath)
39 if err != nil {
40 return fmt.Errorf("no repository %q", repoPath)
41 }
42 repos = []store.Repo{r}
43 } else if repos, err = st.ListAllRepos(); err != nil {
44 return err
45 }
46
47 var before, after int64
48 for _, r := range repos {
49 dir := control.RepoDir(cfg.Server.Root, r.OwnerName, r.Name)
50 b := duDir(dir)
51 gcArgs := []string{"-C", dir, "gc", "--quiet"}
52 if aggressive {
53 gcArgs = append(gcArgs, "--aggressive")
54 }
55 if out, err := exec.Command("git", gcArgs...).CombinedOutput(); err != nil {
56 fmt.Fprintf(os.Stderr, "%s: gc failed: %v\n%s", r.Path(), err, out)
57 continue
58 }
59 a := duDir(dir)
60 before, after = before+b, after+a
61 fmt.Printf("%s\t%s -> %s\n", r.Path(), human(b), human(a))
62 }
63 fmt.Printf("total\t%s -> %s (freed %s)\n", human(before), human(after), human(before-after))
64 return nil
65 },
66 }
67 cmd.Flags().StringVar(&repoPath, "repo", "", "one repository (owner/name) instead of all")
68 cmd.Flags().BoolVar(&aggressive, "aggressive", false, "more thorough repack (slow; rarely needed)")
69 return cmd
70}
71
72func statsCmd() *cobra.Command {
73 var asJSON bool
74 cmd := &cobra.Command{
75 Use: "stats",
76 Short: "instance statistics: counts and per-repository disk usage",
77 RunE: func(cmd *cobra.Command, args []string) error {
78 cfg, err := config.Load(configPath)
79 if err != nil {
80 return err
81 }
82 st, err := openStore(cfg)
83 if err != nil {
84 return err
85 }
86 defer st.Close()
87
88 counts, err := st.InstanceCounts()
89 if err != nil {
90 return err
91 }
92 repos, err := st.ListAllRepos()
93 if err != nil {
94 return err
95 }
96 type repoDisk struct {
97 Path string `json:"path"`
98 Bytes int64 `json:"bytes"`
99 }
100 var disks []repoDisk
101 var totalDisk int64
102 for _, r := range repos {
103 b := duDir(control.RepoDir(cfg.Server.Root, r.OwnerName, r.Name))
104 disks = append(disks, repoDisk{r.Path(), b})
105 totalDisk += b
106 }
107 var dbBytes int64
108 if fi, err := os.Stat(cfg.Server.Root + "/gitbay.db"); err == nil {
109 dbBytes = fi.Size()
110 }
111
112 if asJSON {
113 return json.NewEncoder(os.Stdout).Encode(map[string]any{
114 "counts": counts, "db_bytes": dbBytes,
115 "repo_bytes": totalDisk, "repos": disks,
116 })
117 }
118 fmt.Printf("users %d · orgs %d · repos %d · issues %d (%d open) · MRs %d (%d open)\n",
119 counts.Users, counts.Orgs, counts.Repos,
120 counts.Issues, counts.OpenIssues, counts.MRs, counts.OpenMRs)
121 fmt.Printf("database %s · repositories %s\n\n", human(dbBytes), human(totalDisk))
122 w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
123 for _, d := range disks {
124 fmt.Fprintf(w, "%s\t%s\n", d.Path, human(d.Bytes))
125 }
126 return w.Flush()
127 },
128 }
129 cmd.Flags().BoolVar(&asJSON, "json", false, "machine-readable output")
130 return cmd
131}
132
133// duDir sums file sizes under dir; errors count as zero.
134func duDir(dir string) int64 {
135 var total int64
136 filepath.WalkDir(dir, func(_ string, d fs.DirEntry, err error) error {
137 if err != nil || d.IsDir() {
138 return nil
139 }
140 if fi, err := d.Info(); err == nil {
141 total += fi.Size()
142 }
143 return nil
144 })
145 return total
146}
147
148func human(b int64) string {
149 switch {
150 case b >= 1<<30:
151 return fmt.Sprintf("%.1f GiB", float64(b)/(1<<30))
152 case b >= 1<<20:
153 return fmt.Sprintf("%.1f MiB", float64(b)/(1<<20))
154 case b >= 1<<10:
155 return fmt.Sprintf("%.1f KiB", float64(b)/(1<<10))
156 default:
157 return fmt.Sprintf("%d B", b)
158 }
159}