cmd/gitbayd/maint.go

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

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}