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