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 74func (g *GitRepo) Commits(offset, limit int) ([]*object.Commit, error) { 75 commits := []*object.Commit{} 76 77 output, err := g.revList( 78 g.h.String(), 79 fmt.Sprintf("--skip=%d", offset), 80 fmt.Sprintf("--max-count=%d", limit), 81 ) 82 if err != nil { 83 return nil, fmt.Errorf("commits from ref: %w", err) 84 } 85 86 lines := strings.Split(strings.TrimSpace(string(output)), "\n") 87 if len(lines) == 1 && lines[0] == "" { 88 return commits, nil 89 } 90 91 for _, item := range lines { 92 obj, err := g.r.CommitObject(plumbing.NewHash(item)) 93 if err != nil { 94 continue 95 } 96 commits = append(commits, obj) 97 } 98 99 return commits, nil 100} 101 102func (g *GitRepo) TotalCommits() (int, error) { 103 output, err := g.revList( 104 g.h.String(), 105 fmt.Sprintf("--count"), 106 ) 107 if err != nil { 108 return 0, fmt.Errorf("failed to run rev-list: %w", err) 109 } 110 111 count, err := strconv.Atoi(strings.TrimSpace(string(output))) 112 if err != nil { 113 return 0, err 114 } 115 116 return count, nil 117} 118 119func (g *GitRepo) Commit(h plumbing.Hash) (*object.Commit, error) { 120 return g.r.CommitObject(h) 121} 122 123func (g *GitRepo) LastCommit() (*object.Commit, error) { 124 c, err := g.r.CommitObject(g.h) 125 if err != nil { 126 return nil, fmt.Errorf("last commit: %w", err) 127 } 128 return c, nil 129} 130 131func (g *GitRepo) FileContentN(path string, cap int64) ([]byte, error) { 132 c, err := g.r.CommitObject(g.h) 133 if err != nil { 134 return nil, fmt.Errorf("commit object: %w", err) 135 } 136 137 tree, err := c.Tree() 138 if err != nil { 139 return nil, fmt.Errorf("file tree: %w", err) 140 } 141 142 file, err := tree.File(path) 143 if err != nil { 144 return nil, err 145 } 146 147 isbin, _ := file.IsBinary() 148 if isbin { 149 return nil, ErrBinaryFile 150 } 151 152 reader, err := file.Reader() 153 if err != nil { 154 return nil, err 155 } 156 157 buf := new(bytes.Buffer) 158 if _, err = buf.ReadFrom(io.LimitReader(reader, cap)); err != nil { 159 return nil, err 160 } 161 162 return buf.Bytes(), nil 163} 164 165func (g *GitRepo) FileContent(path string) (string, error) { 166 c, err := g.r.CommitObject(g.h) 167 if err != nil { 168 return "", fmt.Errorf("commit object: %w", err) 169 } 170 171 tree, err := c.Tree() 172 if err != nil { 173 return "", fmt.Errorf("file tree: %w", err) 174 } 175 176 file, err := tree.File(path) 177 if err != nil { 178 return "", err 179 } 180 181 isbin, _ := file.IsBinary() 182 183 if !isbin { 184 return file.Contents() 185 } else { 186 return "", ErrBinaryFile 187 } 188} 189 190func (g *GitRepo) RawContent(path string) ([]byte, error) { 191 c, err := g.r.CommitObject(g.h) 192 if err != nil { 193 return nil, fmt.Errorf("commit object: %w", err) 194 } 195 196 tree, err := c.Tree() 197 if err != nil { 198 return nil, fmt.Errorf("file tree: %w", err) 199 } 200 201 file, err := tree.File(path) 202 if err != nil { 203 return nil, err 204 } 205 206 reader, err := file.Reader() 207 if err != nil { 208 return nil, fmt.Errorf("opening file reader: %w", err) 209 } 210 defer reader.Close() 211 212 return io.ReadAll(reader) 213} 214 215func (g *GitRepo) Branch(name string) (*plumbing.Reference, error) { 216 ref, err := g.r.Reference(plumbing.NewBranchReferenceName(name), false) 217 if err != nil { 218 return nil, fmt.Errorf("branch: %w", err) 219 } 220 221 if !ref.Name().IsBranch() { 222 return nil, fmt.Errorf("branch: %s is not a branch", ref.Name()) 223 } 224 225 return ref, nil 226} 227 228func (g *GitRepo) SetDefaultBranch(branch string) error { 229 ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch)) 230 return g.r.Storer.SetReference(ref) 231} 232 233func (g *GitRepo) FindMainBranch() (string, error) { 234 output, err := g.revParse("--abbrev-ref", "HEAD") 235 if err != nil { 236 return "", fmt.Errorf("failed to find main branch: %w", err) 237 } 238 239 return strings.TrimSpace(string(output)), nil 240} 241 242// WriteTar writes itself from a tree into a binary tar file format. 243// prefix is root folder to be appended. 244func (g *GitRepo) WriteTar(w io.Writer, prefix string) error { 245 tw := tar.NewWriter(w) 246 defer tw.Close() 247 248 c, err := g.r.CommitObject(g.h) 249 if err != nil { 250 return fmt.Errorf("commit object: %w", err) 251 } 252 253 tree, err := c.Tree() 254 if err != nil { 255 return err 256 } 257 258 walker := object.NewTreeWalker(tree, true, nil) 259 defer walker.Close() 260 261 name, entry, err := walker.Next() 262 for ; err == nil; name, entry, err = walker.Next() { 263 info, err := newInfoWrapper(name, prefix, &entry, tree) 264 if err != nil { 265 return err 266 } 267 268 header, err := tar.FileInfoHeader(info, "") 269 if err != nil { 270 return err 271 } 272 273 err = tw.WriteHeader(header) 274 if err != nil { 275 return err 276 } 277 278 if !info.IsDir() { 279 file, err := tree.File(name) 280 if err != nil { 281 return err 282 } 283 284 reader, err := file.Blob.Reader() 285 if err != nil { 286 return err 287 } 288 289 _, err = io.Copy(tw, reader) 290 if err != nil { 291 reader.Close() 292 return err 293 } 294 reader.Close() 295 } 296 } 297 298 return nil 299} 300 301func newInfoWrapper( 302 name string, 303 prefix string, 304 entry *object.TreeEntry, 305 tree *object.Tree, 306) (*infoWrapper, error) { 307 var ( 308 size int64 309 mode fs.FileMode 310 isDir bool 311 ) 312 313 if entry.Mode.IsFile() { 314 file, err := tree.TreeEntryFile(entry) 315 if err != nil { 316 return nil, err 317 } 318 mode = fs.FileMode(file.Mode) 319 320 size, err = tree.Size(name) 321 if err != nil { 322 return nil, err 323 } 324 } else { 325 isDir = true 326 mode = fs.ModeDir | fs.ModePerm 327 } 328 329 fullname := path.Join(prefix, name) 330 return &infoWrapper{ 331 name: fullname, 332 size: size, 333 mode: mode, 334 modTime: time.Unix(0, 0), 335 isDir: isDir, 336 }, nil 337} 338 339func (i *infoWrapper) Name() string { 340 return i.name 341} 342 343func (i *infoWrapper) Size() int64 { 344 return i.size 345} 346 347func (i *infoWrapper) Mode() fs.FileMode { 348 return i.mode 349} 350 351func (i *infoWrapper) ModTime() time.Time { 352 return i.modTime 353} 354 355func (i *infoWrapper) IsDir() bool { 356 return i.isDir 357} 358 359func (i *infoWrapper) Sys() any { 360 return nil 361}