cmd/gitbayd/maint.go

v1.13.3
gitbay/cmd/gitbayd/maint.go history · blame · raw

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}