Raw
  1package replica
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"io/fs"
  7	"os"
  8	"path/filepath"
  9	"sort"
 10	"strings"
 11
 12	"github.com/go-git/go-git/v5"
 13	"github.com/go-git/go-git/v5/plumbing"
 14	"github.com/go-git/go-git/v5/plumbing/object"
 15)
 16
 17var ErrPruneKeepMissing = errors.New("replica: an object the prune must keep is not in the local repository")
 18
 19var pruneHook = func() error { return nil }
 20
 21func Prune(dir string, keep ...plumbing.Hash) (*git.Repository, error) {
 22	r, err := git.PlainOpen(dir)
 23	if err != nil {
 24		return nil, err
 25	}
 26	head, err := localHead(r)
 27	if err != nil {
 28		return nil, err
 29	}
 30	roots := append([]plumbing.Hash{}, keep...)
 31	if !head.IsZero() {
 32		roots = append(roots, head)
 33	}
 34	critical := map[plumbing.Hash]bool{}
 35	for _, id := range roots {
 36		if err := addFull(r, id, critical); err != nil {
 37			return nil, err
 38		}
 39	}
 40	set := map[plumbing.Hash]bool{}
 41	for h := range critical {
 42		set[h] = true
 43	}
 44	addCommitChain(r, roots, set)
 45	objects := filepath.Join(dir, "objects")
 46	for h := range set {
 47		p := loosePath(objects, h)
 48		written := false
 49		if _, err := os.Stat(p); errors.Is(err, fs.ErrNotExist) {
 50			obj, err := r.Storer.EncodedObject(plumbing.AnyObject, h)
 51			if err != nil {
 52				return nil, err
 53			}
 54			if _, err := r.Storer.SetEncodedObject(obj); err != nil {
 55				return nil, err
 56			}
 57			written = true
 58		} else if err != nil {
 59			return nil, err
 60		}
 61		if !written && !critical[h] {
 62			continue
 63		}
 64		if err := syncPath(p); err != nil {
 65			return nil, err
 66		}
 67		if err := syncPath(filepath.Dir(p)); err != nil {
 68			return nil, err
 69		}
 70	}
 71	if err := syncPath(objects); err != nil {
 72		return nil, err
 73	}
 74	if err := removeOtherRefs(r); err != nil {
 75		return nil, err
 76	}
 77	if err := removePacks(filepath.Join(objects, "pack")); err != nil {
 78		return nil, err
 79	}
 80	if err := removeLoose(objects, set); err != nil {
 81		return nil, err
 82	}
 83	return git.PlainOpen(dir)
 84}
 85
 86func addFull(r *git.Repository, id plumbing.Hash, set map[plumbing.Hash]bool) error {
 87	c, err := object.GetCommit(r.Storer, id)
 88	if err != nil {
 89		return fmt.Errorf("%w: commit %s: %v", ErrPruneKeepMissing, id, err)
 90	}
 91	tree, err := c.Tree()
 92	if err != nil {
 93		return fmt.Errorf("%w: tree of %s: %v", ErrPruneKeepMissing, id, err)
 94	}
 95	for _, e := range tree.Entries {
 96		if _, err := r.Storer.EncodedObject(plumbing.AnyObject, e.Hash); err != nil {
 97			return fmt.Errorf("%w: blob of %s: %v", ErrPruneKeepMissing, id, err)
 98		}
 99		set[e.Hash] = true
100	}
101	set[id] = true
102	set[tree.Hash] = true
103	return nil
104}
105
106func addCommitChain(r *git.Repository, roots []plumbing.Hash, set map[plumbing.Hash]bool) {
107	seen := map[plumbing.Hash]bool{}
108	stack := append([]plumbing.Hash{}, roots...)
109	for len(stack) > 0 {
110		id := stack[len(stack)-1]
111		stack = stack[:len(stack)-1]
112		if seen[id] {
113			continue
114		}
115		seen[id] = true
116		c, err := object.GetCommit(r.Storer, id)
117		if err != nil {
118			continue
119		}
120		set[id] = true
121		stack = append(stack, c.ParentHashes...)
122	}
123}
124
125func loosePath(objects string, h plumbing.Hash) string {
126	s := h.String()
127	return filepath.Join(objects, s[:2], s[2:])
128}
129
130func syncPath(p string) error {
131	f, err := os.Open(p)
132	if err != nil {
133		return err
134	}
135	defer f.Close()
136	return f.Sync()
137}
138
139func removeOtherRefs(r *git.Repository) error {
140	iter, err := r.Storer.IterReferences()
141	if err != nil {
142		return err
143	}
144	var names []plumbing.ReferenceName
145	if err := iter.ForEach(func(ref *plumbing.Reference) error {
146		if ref.Name() != Branch && ref.Name() != plumbing.HEAD {
147			names = append(names, ref.Name())
148		}
149		return nil
150	}); err != nil {
151		return err
152	}
153	for _, n := range names {
154		if err := r.Storer.RemoveReference(n); err != nil {
155			return err
156		}
157	}
158	return nil
159}
160
161func removePacks(dir string) error {
162	entries, err := os.ReadDir(dir)
163	if errors.Is(err, fs.ErrNotExist) {
164		return nil
165	}
166	if err != nil {
167		return err
168	}
169	var packs, rest []string
170	for _, e := range entries {
171		if strings.HasSuffix(e.Name(), ".pack") {
172			packs = append(packs, e.Name())
173		} else {
174			rest = append(rest, e.Name())
175		}
176	}
177	sort.Strings(packs)
178	sort.Strings(rest)
179	for _, name := range append(packs, rest...) {
180		if err := pruneHook(); err != nil {
181			return err
182		}
183		if err := os.Remove(filepath.Join(dir, name)); err != nil {
184			return err
185		}
186	}
187	return nil
188}
189
190func removeLoose(objects string, keep map[plumbing.Hash]bool) error {
191	dirs, err := os.ReadDir(objects)
192	if err != nil {
193		return err
194	}
195	for _, d := range dirs {
196		if !d.IsDir() || len(d.Name()) != 2 {
197			continue
198		}
199		sub := filepath.Join(objects, d.Name())
200		files, err := os.ReadDir(sub)
201		if err != nil {
202			return err
203		}
204		left := 0
205		for _, f := range files {
206			h := plumbing.NewHash(d.Name() + f.Name())
207			if len(d.Name()+f.Name()) != 40 || h.IsZero() || keep[h] {
208				left++
209				continue
210			}
211			if err := pruneHook(); err != nil {
212				return err
213			}
214			if err := os.Remove(filepath.Join(sub, f.Name())); err != nil {
215				return err
216			}
217		}
218		if left == 0 {
219			_ = os.Remove(sub)
220		}
221	}
222	return nil
223}