cmd/gitbayd/maint.go
126 lines · 3674 bytes
1package main
2
3import (
4 "fmt"
5 "os"
6 "os/exec"
7 "time"
8
9 "github.com/spf13/cobra"
10
11 "gitbay.org/gitbay/internal/config"
12 "gitbay.org/gitbay/internal/control"
13 "gitbay.org/gitbay/internal/gitutil"
14 "gitbay.org/gitbay/internal/lfs"
15 "gitbay.org/gitbay/internal/store"
16 "gitbay.org/gitbay/internal/toolpath"
17)
18
19func gcCmd() *cobra.Command {
20 var repoPath string
21 var aggressive, withLFS bool
22 cmd := &cobra.Command{
23 Use: "gc",
24 Short: "repack and prune repositories (git gc); --lfs removes unreferenced LFS objects",
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 := gitutil.DirSize(dir)
51 gcArgs := []string{"-C", dir, "gc", "--quiet"}
52 if aggressive {
53 gcArgs = append(gcArgs, "--aggressive")
54 }
55 if out, err := exec.Command(toolpath.Look("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 := gitutil.DirSize(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 if !withLFS {
65 return nil
66 }
67 return gcLFS(cfg, st)
68 },
69 }
70 cmd.Flags().StringVar(&repoPath, "repo", "", "one repository (owner/name) instead of all")
71 cmd.Flags().BoolVar(&aggressive, "aggressive", false, "more thorough repack (slow; rarely needed)")
72 cmd.Flags().BoolVar(&withLFS, "lfs", false, "also remove LFS objects no repository references (older than a day)")
73 return cmd
74}
75
76func human(b int64) string {
77 switch {
78 case b >= 1<<30:
79 return fmt.Sprintf("%.1f GiB", float64(b)/(1<<30))
80 case b >= 1<<20:
81 return fmt.Sprintf("%.1f MiB", float64(b)/(1<<20))
82 case b >= 1<<10:
83 return fmt.Sprintf("%.1f KiB", float64(b)/(1<<10))
84 default:
85 return fmt.Sprintf("%d B", b)
86 }
87}
88
89// lfsOrphanAge keeps an object uploaded ahead of the push that references
90// it: git lfs uploads first and pushes second.
91const lfsOrphanAge = 24 * time.Hour
92
93// gcLFS removes LFS objects no repository's pointers name. Every
94// repository is scanned, whatever --repo said: an object is shared across
95// repositories by content, so only the whole set says it is unreferenced.
96func gcLFS(cfg config.Config, st *store.Store) error {
97 repos, err := st.ListAllRepos()
98 if err != nil {
99 return err
100 }
101 referenced := map[string]bool{}
102 for _, r := range repos {
103 oids, err := gitutil.LFSPointerOIDs(control.RepoDir(cfg.Server.Root, r.OwnerName, r.Name))
104 if err != nil {
105 return fmt.Errorf("%s: scanning for LFS pointers: %w", r.Path(), err)
106 }
107 for _, o := range oids {
108 referenced[o] = true
109 }
110 }
111 blobs := lfs.LocalStore{Root: lfs.RootFor(cfg.LFS.Root, cfg.Server.Root)}
112 orphans, err := blobs.Orphans(referenced, lfsOrphanAge)
113 if err != nil {
114 return err
115 }
116 var freed int64
117 for _, o := range orphans {
118 if err := blobs.Delete(o.OID); err != nil {
119 fmt.Fprintf(os.Stderr, "lfs %s: %v\n", o.OID, err)
120 continue
121 }
122 freed += o.Size
123 }
124 fmt.Printf("lfs\t%d referenced, removed %d orphans (%s)\n", len(referenced), len(orphans), human(freed))
125 return nil
126}