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