cmd/gitbayd/maint.go

b4684c088d2b99f0f42c4e95231c4c0edc122323
gitbay/cmd/gitbayd/maint.go history · blame · raw

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}