Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · server/hook/lfs.go

Line attribution on main · 131 line(s)

64ac5071
Keenan Nemetz
17d ago
1 package hook
2
3 // LFS policy at the gate.
4 //
5 // Two different rules, because they answer two different mistakes:
6 //
7 // - A pointer whose object is missing is a broken repository. The checkout
8 // cannot be completed by anyone, so the push is refused, before the ref moves.
9 // - A large binary committed straight into git is a repository that will get
10 // slow, but it is not broken. The push is accepted and the user is told how
11 // to move it to LFS.
12
13 import (
14 "fmt"
15 "io"
16 "sort"
17 "strings"
18
d37d301f
Keenan Nemetz
15h ago
19 "rodent.software/vole/core/git"
20 "rodent.software/vole/core/lfs"
21 "rodent.software/vole/internal/format"
64ac5071
Keenan Nemetz
17d ago
22 )
23
24 // lfsFindings scans the blobs of the tree a pushed ref will serve. problems
25 // reject the push, warnings are printed and the push proceeds.
c5902451
Keenan Nemetz
16d ago
26 func lfsFindings(g *git.Repo, repo, new string, store *lfs.Store, warnBytes int64) (problems, warnings []string, err error) {
64ac5071
Keenan Nemetz
17d ago
27 if store == nil {
28 return nil, nil, fmt.Errorf("no LFS store configured")
29 }
30 blobs, err := g.TreeBlobs(new)
31 if err != nil {
32 return nil, nil, err
33 }
34 shas := make([]string, 0, len(blobs))
35 for sha := range blobs {
36 shas = append(shas, sha)
37 }
38 sort.Strings(shas) // stable messages, so a test and a human see the same order
39
40 for _, sha := range shas {
41 b := blobs[sha]
42 if b.Size <= lfs.MaxPointerBytes {
43 head, err := g.BlobPrefix(sha, int(lfs.MaxPointerBytes))
44 if err != nil {
45 return nil, nil, err
46 }
47 p, ok := lfs.ParsePointer(head)
48 if !ok {
49 continue
50 }
65b88b3b
Keenan Nemetz
16d ago
51 problems = append(problems, pointerProblem(store, repo, p, b.Path)...)
64ac5071
Keenan Nemetz
17d ago
52 continue
53 }
54 if warnBytes <= 0 || b.Size <= warnBytes {
55 continue
56 }
57 head, err := g.BlobPrefix(sha, 8000)
58 if err != nil {
59 return nil, nil, err
60 }
61 if !looksBinary(head) {
62 continue
63 }
64 warnings = append(warnings, fmt.Sprintf(
65 "%s is %s and stored in git. Track it with LFS instead:\n"+
66 " git lfs track %q && git add .gitattributes %s\n"+
67 " (a file already in history needs `git lfs migrate import --include=%q`)",
c5902451
Keenan Nemetz
16d ago
68 b.Path, format.Bytes(b.Size), trackPattern(b.Path), b.Path, trackPattern(b.Path)))
64ac5071
Keenan Nemetz
17d ago
69 }
70 return problems, warnings, nil
71 }
72
65b88b3b
Keenan Nemetz
16d ago
73 func pointerProblem(store *lfs.Store, repo string, p lfs.Pointer, path string) []string {
74 size, err := store.Stat(repo, p.OID)
64ac5071
Keenan Nemetz
17d ago
75 if err != nil {
76 return []string{fmt.Sprintf(
77 "%s points at LFS object %s (%s), which is not on this server. "+
85cc89fe
Keenan Nemetz
17d ago
78 "Upload it first: git lfs push origin <ref> for a tracked file, "+
79 "or vole release attach <tag> <file> for a release artifact",
c5902451
Keenan Nemetz
16d ago
80 path, oidShort(p.OID), format.Bytes(p.Size))}
64ac5071
Keenan Nemetz
17d ago
81 }
82 if p.Size > 0 && size != p.Size {
83 return []string{fmt.Sprintf(
84 "%s points at LFS object %s and says it is %d bytes, but the stored object is %d",
85 path, oidShort(p.OID), p.Size, size)}
86 }
87 return nil
88 }
89
90 // oidShort is longer than short(): when the message tells someone which object to
91 // upload, twelve hex characters are enough to find it in `git lfs ls-files`
92 // without being a wall of hex.
93 func oidShort(oid string) string {
94 if len(oid) > 12 {
95 return oid[:12]
96 }
97 return oid
98 }
99
100 // looksBinary is git's own heuristic, narrowed to "contains a NUL in the first
101 // 8 KiB". A file that could be a pointer was already handled: only blobs larger
102 // than the pointer cap reach here.
103 func looksBinary(head []byte) bool {
104 for _, c := range head {
105 if c == 0 {
106 return true
107 }
108 }
109 return false
110 }
111
112 // trackPattern suggests the narrowest useful gitattributes pattern: the
113 // extension when there is one, otherwise the path itself.
114 func trackPattern(path string) string {
115 base := path
116 if i := strings.LastIndexByte(base, '/'); i >= 0 {
117 base = base[i+1:]
118 }
119 if i := strings.LastIndexByte(base, '.'); i > 0 && i < len(base)-1 {
120 return "*" + base[i:]
121 }
122 return path
123 }
124
125 // printWarnings writes findings the push accepts anyway. They go to the same
126 // stream as rejections, which git forwards to the client as remote: lines.
127 func printWarnings(w io.Writer, warnings []string) {
128 for _, warn := range warnings {
129 fmt.Fprintf(w, "vole: warning: %s\n", strings.TrimSpace(warn))
130 }
131 }