cmd/gitbayd/maint.go
139 lines · 4038 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/backuplock"
12 "gitbay.org/gitbay/internal/config"
13 "gitbay.org/gitbay/internal/control"
14 "gitbay.org/gitbay/internal/gitutil"
15 "gitbay.org/gitbay/internal/lfs"
16 "gitbay.org/gitbay/internal/store"
17 "gitbay.org/gitbay/internal/toolpath"
18)
19
20func gcCmd() *cobra.Command {
21 var repoPath string
22 var aggressive, withLFS bool
23 cmd := &cobra.Command{
24 Use: "gc",
25 Short: "repack and prune repositories (git gc); --lfs removes unreferenced LFS objects",
26 RunE: func(cmd *cobra.Command, args []string) error {
27 cfg, err := config.Load(configPath)
28 if err != nil {
29 return err
30 }
31 return runGC(cfg, repoPath, aggressive, withLFS)
32 },
33 }
34 cmd.Flags().StringVar(&repoPath, "repo", "", "one repository (owner/name) instead of all")
35 cmd.Flags().BoolVar(&aggressive, "aggressive", false, "more thorough repack (slow; rarely needed)")
36 cmd.Flags().BoolVar(&withLFS, "lfs", false, "also remove LFS objects no repository references (older than a day)")
37 return cmd
38}
39
40// runGC refuses while a full backup runs: a prune removing a pack the
41// backup has listed but not yet read would leave it out of the archive
42// (#259).
43func runGC(cfg config.Config, repoPath string, aggressive, withLFS bool) error {
44 release, err := backuplock.TryShared(cfg.Server.Root)
45 if err != nil {
46 return err
47 }
48 defer release()
49 st, err := openStore(cfg)
50 if err != nil {
51 return err
52 }
53 defer st.Close()
54
55 var repos []store.Repo
56 if repoPath != "" {
57 r, err := st.RepoByPath(repoPath)
58 if err != nil {
59 return fmt.Errorf("no repository %q", repoPath)
60 }
61 repos = []store.Repo{r}
62 } else if repos, err = st.ListAllRepos(); err != nil {
63 return err
64 }
65
66 var before, after int64
67 for _, r := range repos {
68 dir := control.RepoDir(cfg.Server.Root, r.OwnerName, r.Name)
69 b := gitutil.DirSize(dir)
70 gcArgs := []string{"-C", dir, "gc", "--quiet"}
71 if aggressive {
72 gcArgs = append(gcArgs, "--aggressive")
73 }
74 if out, err := exec.Command(toolpath.Look("git"), gcArgs...).CombinedOutput(); err != nil {
75 fmt.Fprintf(os.Stderr, "%s: gc failed: %v\n%s", r.Path(), err, out)
76 continue
77 }
78 a := gitutil.DirSize(dir)
79 before, after = before+b, after+a
80 fmt.Printf("%s\t%s -> %s\n", r.Path(), human(b), human(a))
81 }
82 fmt.Printf("total\t%s -> %s (freed %s)\n", human(before), human(after), human(before-after))
83 if !withLFS {
84 return nil
85 }
86 return gcLFS(cfg, st)
87}
88
89func human(b int64) string {
90 switch {
91 case b >= 1<<30:
92 return fmt.Sprintf("%.1f GiB", float64(b)/(1<<30))
93 case b >= 1<<20:
94 return fmt.Sprintf("%.1f MiB", float64(b)/(1<<20))
95 case b >= 1<<10:
96 return fmt.Sprintf("%.1f KiB", float64(b)/(1<<10))
97 default:
98 return fmt.Sprintf("%d B", b)
99 }
100}
101
102// lfsOrphanAge keeps an object uploaded ahead of the push that references
103// it: git lfs uploads first and pushes second.
104const lfsOrphanAge = 24 * time.Hour
105
106// gcLFS removes LFS objects no repository's pointers name. Every
107// repository is scanned, whatever --repo said: an object is shared across
108// repositories by content, so only the whole set says it is unreferenced.
109func gcLFS(cfg config.Config, st *store.Store) error {
110 repos, err := st.ListAllRepos()
111 if err != nil {
112 return err
113 }
114 referenced := map[string]bool{}
115 for _, r := range repos {
116 oids, err := gitutil.LFSPointerOIDs(control.RepoDir(cfg.Server.Root, r.OwnerName, r.Name))
117 if err != nil {
118 return fmt.Errorf("%s: scanning for LFS pointers: %w", r.Path(), err)
119 }
120 for _, o := range oids {
121 referenced[o] = true
122 }
123 }
124 blobs := lfs.LocalStore{Root: lfs.RootFor(cfg.LFS.Root, cfg.Server.Root)}
125 orphans, err := blobs.Orphans(referenced, lfsOrphanAge)
126 if err != nil {
127 return err
128 }
129 var freed int64
130 for _, o := range orphans {
131 if err := blobs.Delete(o.OID); err != nil {
132 fmt.Fprintf(os.Stderr, "lfs %s: %v\n", o.OID, err)
133 continue
134 }
135 freed += o.Size
136 }
137 fmt.Printf("lfs\t%d referenced, removed %d orphans (%s)\n", len(referenced), len(orphans), human(freed))
138 return nil
139}