forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package git 2 3import ( 4 "archive/tar" 5 "bytes" 6 "fmt" 7 "io" 8 "io/fs" 9 "path" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/go-git/go-git/v5" 15 "github.com/go-git/go-git/v5/plumbing" 16 "github.com/go-git/go-git/v5/plumbing/object" 17) 18 19var ( 20 ErrBinaryFile = fmt.Errorf("binary file") 21 ErrNotBinaryFile = fmt.Errorf("not binary file") 22) 23 24type GitRepo struct { 25 path string 26 r *git.Repository 27 h plumbing.Hash 28} 29 30// infoWrapper wraps the property of a TreeEntry so it can export fs.FileInfo 31// to tar WriteHeader 32type infoWrapper struct { 33 name string 34 size int64 35 mode fs.FileMode 36 modTime time.Time 37 isDir bool 38} 39 40func Open(path string, ref string) (*GitRepo, error) { 41 var err error 42 g := GitRepo{path: path} 43 g.r, err = git.PlainOpen(path) 44 if err != nil { 45 return nil, fmt.Errorf("opening %s: %w", path, err) 46 } 47 48 if ref == "" { 49 head, err := g.r.Head() 50 if err != nil { 51 return nil, fmt.Errorf("getting head of %s: %w", path, err) 52 } 53 g.h = head.Hash() 54 } else { 55 hash, err := g.r.ResolveRevision(plumbing.Revision(ref)) 56 if err != nil { 57 return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err) 58 } 59 g.h = *hash 60 } 61 return &g, nil 62} 63 64func PlainOpen(path string) (*GitRepo, error) { 65 var err error 66 g := GitRepo{path: path} 67 g.r, err = git.PlainOpen(path) 68 if err != nil { 69 return nil, fmt.Errorf("opening %s: %w", path, err) 70 } 71 return &g, nil 72} 73 74// re-open a repository and update references 75func (g *GitRepo) Refresh() error { 76 refreshed, err := PlainOpen(g.path) 77 if err != nil { 78 return err 79 } 80 81 *g = *refreshed 82 return nil 83} 84 85func (g *GitRepo) Commits(offset, limit int) ([]*object.Commit, error) { 86 commits := []*object.Commit{} 87 88 output, err := g.revList( 89 g.h.String(), 90 fmt.Sprintf("--skip=%d", offset), 91 fmt.Sprintf("--max-count=%d", limit), 92 ) 93 if err != nil { 94 return nil, fmt.Errorf("commits from ref: %w", err) 95 } 96 97 lines := strings.Split(strings.TrimSpace(string(output)), "\n") 98 if len(lines) == 1 && lines[0] == "" { 99 return commits, nil 100 } 101 102 for _, item := range lines { 103 obj, err := g.r.CommitObject(plumbing.NewHash(item)) 104 if err != nil { 105 continue 106 } 107 commits = append(commits, obj) 108 } 109 110 return commits, nil 111} 112 113func (g *GitRepo) TotalCommits() (int, error) { 114 output, err := g.revList( 115 g.h.String(), 116 fmt.Sprintf("--count"), 117 ) 118 if err != nil { 119 return 0, fmt.Errorf("failed to run rev-list: %w", err) 120 } 121 122 count, err := strconv.Atoi(strings.TrimSpace(string(output))) 123 if err != nil { 124 return 0, err 125 } 126 127 return count, nil 128} 129 130func (g *GitRepo) Commit(h plumbing.Hash) (*object.Commit, error) { 131 return g.r.CommitObject(h) 132} 133 134func (g *GitRepo) FileContentN(path string, cap int64) ([]byte, error) { 135 c, err := g.r.CommitObject(g.h) 136 if err != nil { 137 return nil, fmt.Errorf("commit object: %w", err) 138 } 139 140 tree, err := c.Tree() 141 if err != nil { 142 return nil, fmt.Errorf("file tree: %w", err) 143 } 144 145 file, err := tree.File(path) 146 if err != nil { 147 return nil, err 148 } 149 150 isbin, _ := file.IsBinary() 151 if isbin { 152 return nil, ErrBinaryFile 153 } 154 155 reader, err := file.Reader() 156 if err != nil { 157 return nil, err 158 } 159 160 buf := new(bytes.Buffer) 161 if _, err = buf.ReadFrom(io.LimitReader(reader, cap)); err != nil { 162 return nil, err 163 } 164 165 return buf.Bytes(), nil 166} 167 168func (g *GitRepo) RawContent(path string) ([]byte, error) { 169 c, err := g.r.CommitObject(g.h) 170 if err != nil { 171 return nil, fmt.Errorf("commit object: %w", err) 172 } 173 174 tree, err := c.Tree() 175 if err != nil { 176 return nil, fmt.Errorf("file tree: %w", err) 177 } 178 179 file, err := tree.File(path) 180 if err != nil { 181 return nil, err 182 } 183 184 reader, err := file.Reader() 185 if err != nil { 186 return nil, fmt.Errorf("opening file reader: %w", err) 187 } 188 defer reader.Close() 189 190 return io.ReadAll(reader) 191} 192 193func (g *GitRepo) Branch(name string) (*plumbing.Reference, error) { 194 ref, err := g.r.Reference(plumbing.NewBranchReferenceName(name), false) 195 if err != nil { 196 return nil, fmt.Errorf("branch: %w", err) 197 } 198 199 if !ref.Name().IsBranch() { 200 return nil, fmt.Errorf("branch: %s is not a branch", ref.Name()) 201 } 202 203 return ref, nil 204} 205 206func (g *GitRepo) SetDefaultBranch(branch string) error { 207 ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch)) 208 return g.r.Storer.SetReference(ref) 209} 210 211func (g *GitRepo) FindMainBranch() (string, error) { 212 output, err := g.revParse("--abbrev-ref", "HEAD") 213 if err != nil { 214 return "", fmt.Errorf("failed to find main branch: %w", err) 215 } 216 217 return strings.TrimSpace(string(output)), nil 218} 219 220// WriteTar writes itself from a tree into a binary tar file format. 221// prefix is root folder to be appended. 222func (g *GitRepo) WriteTar(w io.Writer, prefix string) error { 223 tw := tar.NewWriter(w) 224 defer tw.Close() 225 226 c, err := g.r.CommitObject(g.h) 227 if err != nil { 228 return fmt.Errorf("commit object: %w", err) 229 } 230 231 tree, err := c.Tree() 232 if err != nil { 233 return err 234 } 235 236 walker := object.NewTreeWalker(tree, true, nil) 237 defer walker.Close() 238 239 name, entry, err := walker.Next() 240 for ; err == nil; name, entry, err = walker.Next() { 241 info, err := newInfoWrapper(name, prefix, &entry, tree) 242 if err != nil { 243 return err 244 } 245 246 header, err := tar.FileInfoHeader(info, "") 247 if err != nil { 248 return err 249 } 250 251 err = tw.WriteHeader(header) 252 if err != nil { 253 return err 254 } 255 256 if !info.IsDir() { 257 file, err := tree.File(name) 258 if err != nil { 259 return err 260 } 261 262 reader, err := file.Blob.Reader() 263 if err != nil { 264 return err 265 } 266 267 _, err = io.Copy(tw, reader) 268 if err != nil { 269 reader.Close() 270 return err 271 } 272 reader.Close() 273 } 274 } 275 276 return nil 277} 278 279func newInfoWrapper( 280 name string, 281 prefix string, 282 entry *object.TreeEntry, 283 tree *object.Tree, 284) (*infoWrapper, error) { 285 var ( 286 size int64 287 mode fs.FileMode 288 isDir bool 289 ) 290 291 if entry.Mode.IsFile() { 292 file, err := tree.TreeEntryFile(entry) 293 if err != nil { 294 return nil, err 295 } 296 mode = fs.FileMode(file.Mode) 297 298 size, err = tree.Size(name) 299 if err != nil { 300 return nil, err 301 } 302 } else { 303 isDir = true 304 mode = fs.ModeDir | fs.ModePerm 305 } 306 307 fullname := path.Join(prefix, name) 308 return &infoWrapper{ 309 name: fullname, 310 size: size, 311 mode: mode, 312 modTime: time.Unix(0, 0), 313 isDir: isDir, 314 }, nil 315} 316 317func (i *infoWrapper) Name() string { 318 return i.name 319} 320 321func (i *infoWrapper) Size() int64 { 322 return i.size 323} 324 325func (i *infoWrapper) Mode() fs.FileMode { 326 return i.mode 327} 328 329func (i *infoWrapper) ModTime() time.Time { 330 return i.modTime 331} 332 333func (i *infoWrapper) IsDir() bool { 334 return i.isDir 335} 336 337func (i *infoWrapper) Sys() any { 338 return nil 339}