forked from tangled.org/core
this repo has no description
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) FileContentN(path string, cap int64) ([]byte, error) { 124 c, err := g.r.CommitObject(g.h) 125 if err != nil { 126 return nil, fmt.Errorf("commit object: %w", err) 127 } 128 129 tree, err := c.Tree() 130 if err != nil { 131 return nil, fmt.Errorf("file tree: %w", err) 132 } 133 134 file, err := tree.File(path) 135 if err != nil { 136 return nil, err 137 } 138 139 isbin, _ := file.IsBinary() 140 if isbin { 141 return nil, ErrBinaryFile 142 } 143 144 reader, err := file.Reader() 145 if err != nil { 146 return nil, err 147 } 148 149 buf := new(bytes.Buffer) 150 if _, err = buf.ReadFrom(io.LimitReader(reader, cap)); err != nil { 151 return nil, err 152 } 153 154 return buf.Bytes(), nil 155} 156 157func (g *GitRepo) RawContent(path string) ([]byte, error) { 158 c, err := g.r.CommitObject(g.h) 159 if err != nil { 160 return nil, fmt.Errorf("commit object: %w", err) 161 } 162 163 tree, err := c.Tree() 164 if err != nil { 165 return nil, fmt.Errorf("file tree: %w", err) 166 } 167 168 file, err := tree.File(path) 169 if err != nil { 170 return nil, err 171 } 172 173 reader, err := file.Reader() 174 if err != nil { 175 return nil, fmt.Errorf("opening file reader: %w", err) 176 } 177 defer reader.Close() 178 179 return io.ReadAll(reader) 180} 181 182func (g *GitRepo) Branch(name string) (*plumbing.Reference, error) { 183 ref, err := g.r.Reference(plumbing.NewBranchReferenceName(name), false) 184 if err != nil { 185 return nil, fmt.Errorf("branch: %w", err) 186 } 187 188 if !ref.Name().IsBranch() { 189 return nil, fmt.Errorf("branch: %s is not a branch", ref.Name()) 190 } 191 192 return ref, nil 193} 194 195func (g *GitRepo) SetDefaultBranch(branch string) error { 196 ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch)) 197 return g.r.Storer.SetReference(ref) 198} 199 200func (g *GitRepo) FindMainBranch() (string, error) { 201 output, err := g.revParse("--abbrev-ref", "HEAD") 202 if err != nil { 203 return "", fmt.Errorf("failed to find main branch: %w", err) 204 } 205 206 return strings.TrimSpace(string(output)), nil 207} 208 209// WriteTar writes itself from a tree into a binary tar file format. 210// prefix is root folder to be appended. 211func (g *GitRepo) WriteTar(w io.Writer, prefix string) error { 212 tw := tar.NewWriter(w) 213 defer tw.Close() 214 215 c, err := g.r.CommitObject(g.h) 216 if err != nil { 217 return fmt.Errorf("commit object: %w", err) 218 } 219 220 tree, err := c.Tree() 221 if err != nil { 222 return err 223 } 224 225 walker := object.NewTreeWalker(tree, true, nil) 226 defer walker.Close() 227 228 name, entry, err := walker.Next() 229 for ; err == nil; name, entry, err = walker.Next() { 230 info, err := newInfoWrapper(name, prefix, &entry, tree) 231 if err != nil { 232 return err 233 } 234 235 header, err := tar.FileInfoHeader(info, "") 236 if err != nil { 237 return err 238 } 239 240 err = tw.WriteHeader(header) 241 if err != nil { 242 return err 243 } 244 245 if !info.IsDir() { 246 file, err := tree.File(name) 247 if err != nil { 248 return err 249 } 250 251 reader, err := file.Blob.Reader() 252 if err != nil { 253 return err 254 } 255 256 _, err = io.Copy(tw, reader) 257 if err != nil { 258 reader.Close() 259 return err 260 } 261 reader.Close() 262 } 263 } 264 265 return nil 266} 267 268func newInfoWrapper( 269 name string, 270 prefix string, 271 entry *object.TreeEntry, 272 tree *object.Tree, 273) (*infoWrapper, error) { 274 var ( 275 size int64 276 mode fs.FileMode 277 isDir bool 278 ) 279 280 if entry.Mode.IsFile() { 281 file, err := tree.TreeEntryFile(entry) 282 if err != nil { 283 return nil, err 284 } 285 mode = fs.FileMode(file.Mode) 286 287 size, err = tree.Size(name) 288 if err != nil { 289 return nil, err 290 } 291 } else { 292 isDir = true 293 mode = fs.ModeDir | fs.ModePerm 294 } 295 296 fullname := path.Join(prefix, name) 297 return &infoWrapper{ 298 name: fullname, 299 size: size, 300 mode: mode, 301 modTime: time.Unix(0, 0), 302 isDir: isDir, 303 }, nil 304} 305 306func (i *infoWrapper) Name() string { 307 return i.name 308} 309 310func (i *infoWrapper) Size() int64 { 311 return i.size 312} 313 314func (i *infoWrapper) Mode() fs.FileMode { 315 return i.mode 316} 317 318func (i *infoWrapper) ModTime() time.Time { 319 return i.modTime 320} 321 322func (i *infoWrapper) IsDir() bool { 323 return i.isDir 324} 325 326func (i *infoWrapper) Sys() any { 327 return nil 328}