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