this repo has no description
1package git 2 3import ( 4 "archive/tar" 5 "fmt" 6 "io" 7 "io/fs" 8 "path" 9 "sort" 10 "sync" 11 "time" 12 13 "github.com/dgraph-io/ristretto" 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 commitCache *ristretto.Cache 21 cacheMu sync.RWMutex 22) 23 24func init() { 25 cache, _ := ristretto.NewCache(&ristretto.Config{ 26 NumCounters: 1e7, 27 MaxCost: 1 << 30, 28 BufferItems: 64, 29 }) 30 commitCache = cache 31} 32 33var ( 34 ErrBinaryFile = fmt.Errorf("binary file") 35) 36 37type GitRepo struct { 38 r *git.Repository 39 h plumbing.Hash 40} 41 42type TagList struct { 43 refs []*TagReference 44 r *git.Repository 45} 46 47// TagReference is used to list both tag and non-annotated tags. 48// Non-annotated tags should only contains a reference. 49// Annotated tags should contain its reference and its tag information. 50type TagReference struct { 51 ref *plumbing.Reference 52 tag *object.Tag 53} 54 55// infoWrapper wraps the property of a TreeEntry so it can export fs.FileInfo 56// to tar WriteHeader 57type infoWrapper struct { 58 name string 59 size int64 60 mode fs.FileMode 61 modTime time.Time 62 isDir bool 63} 64 65func (self *TagList) Len() int { 66 return len(self.refs) 67} 68 69func (self *TagList) Swap(i, j int) { 70 self.refs[i], self.refs[j] = self.refs[j], self.refs[i] 71} 72 73// sorting tags in reverse chronological order 74func (self *TagList) Less(i, j int) bool { 75 var dateI time.Time 76 var dateJ time.Time 77 78 if self.refs[i].tag != nil { 79 dateI = self.refs[i].tag.Tagger.When 80 } else { 81 c, err := self.r.CommitObject(self.refs[i].ref.Hash()) 82 if err != nil { 83 dateI = time.Now() 84 } else { 85 dateI = c.Committer.When 86 } 87 } 88 89 if self.refs[j].tag != nil { 90 dateJ = self.refs[j].tag.Tagger.When 91 } else { 92 c, err := self.r.CommitObject(self.refs[j].ref.Hash()) 93 if err != nil { 94 dateJ = time.Now() 95 } else { 96 dateJ = c.Committer.When 97 } 98 } 99 100 return dateI.After(dateJ) 101} 102 103func Open(path string, ref string) (*GitRepo, error) { 104 var err error 105 g := GitRepo{} 106 g.r, err = git.PlainOpen(path) 107 if err != nil { 108 return nil, fmt.Errorf("opening %s: %w", path, err) 109 } 110 111 if ref == "" { 112 head, err := g.r.Head() 113 if err != nil { 114 return nil, fmt.Errorf("getting head of %s: %w", path, err) 115 } 116 g.h = head.Hash() 117 } else { 118 hash, err := g.r.ResolveRevision(plumbing.Revision(ref)) 119 if err != nil { 120 return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err) 121 } 122 g.h = *hash 123 } 124 return &g, nil 125} 126 127func (g *GitRepo) Commits() ([]*object.Commit, error) { 128 ci, err := g.r.Log(&git.LogOptions{From: g.h}) 129 if err != nil { 130 return nil, fmt.Errorf("commits from ref: %w", err) 131 } 132 133 commits := []*object.Commit{} 134 ci.ForEach(func(c *object.Commit) error { 135 commits = append(commits, c) 136 return nil 137 }) 138 139 return commits, nil 140} 141 142func (g *GitRepo) LastCommit() (*object.Commit, error) { 143 c, err := g.r.CommitObject(g.h) 144 if err != nil { 145 return nil, fmt.Errorf("last commit: %w", err) 146 } 147 return c, nil 148} 149 150func (g *GitRepo) FileContent(path string) (string, error) { 151 c, err := g.r.CommitObject(g.h) 152 if err != nil { 153 return "", fmt.Errorf("commit object: %w", err) 154 } 155 156 tree, err := c.Tree() 157 if err != nil { 158 return "", fmt.Errorf("file tree: %w", err) 159 } 160 161 file, err := tree.File(path) 162 if err != nil { 163 return "", err 164 } 165 166 isbin, _ := file.IsBinary() 167 168 if !isbin { 169 return file.Contents() 170 } else { 171 return "", ErrBinaryFile 172 } 173} 174 175func (g *GitRepo) Tags() ([]*TagReference, error) { 176 iter, err := g.r.Tags() 177 if err != nil { 178 return nil, fmt.Errorf("tag objects: %w", err) 179 } 180 181 tags := make([]*TagReference, 0) 182 183 if err := iter.ForEach(func(ref *plumbing.Reference) error { 184 obj, err := g.r.TagObject(ref.Hash()) 185 switch err { 186 case nil: 187 tags = append(tags, &TagReference{ 188 ref: ref, 189 tag: obj, 190 }) 191 case plumbing.ErrObjectNotFound: 192 tags = append(tags, &TagReference{ 193 ref: ref, 194 }) 195 default: 196 return err 197 } 198 return nil 199 }); err != nil { 200 return nil, err 201 } 202 203 tagList := &TagList{r: g.r, refs: tags} 204 sort.Sort(tagList) 205 return tags, nil 206} 207 208func (g *GitRepo) Branches() ([]*plumbing.Reference, error) { 209 bi, err := g.r.Branches() 210 if err != nil { 211 return nil, fmt.Errorf("branchs: %w", err) 212 } 213 214 branches := []*plumbing.Reference{} 215 216 _ = bi.ForEach(func(ref *plumbing.Reference) error { 217 branches = append(branches, ref) 218 return nil 219 }) 220 221 return branches, nil 222} 223 224func (g *GitRepo) FindMainBranch(branches []string) (string, error) { 225 branches = append(branches, []string{ 226 "main", 227 "master", 228 "trunk", 229 }...) 230 for _, b := range branches { 231 _, err := g.r.ResolveRevision(plumbing.Revision(b)) 232 if err == nil { 233 return b, nil 234 } 235 } 236 return "", fmt.Errorf("unable to find main branch") 237} 238 239// WriteTar writes itself from a tree into a binary tar file format. 240// prefix is root folder to be appended. 241func (g *GitRepo) WriteTar(w io.Writer, prefix string) error { 242 tw := tar.NewWriter(w) 243 defer tw.Close() 244 245 c, err := g.r.CommitObject(g.h) 246 if err != nil { 247 return fmt.Errorf("commit object: %w", err) 248 } 249 250 tree, err := c.Tree() 251 if err != nil { 252 return err 253 } 254 255 walker := object.NewTreeWalker(tree, true, nil) 256 defer walker.Close() 257 258 name, entry, err := walker.Next() 259 for ; err == nil; name, entry, err = walker.Next() { 260 info, err := newInfoWrapper(name, prefix, &entry, tree) 261 if err != nil { 262 return err 263 } 264 265 header, err := tar.FileInfoHeader(info, "") 266 if err != nil { 267 return err 268 } 269 270 err = tw.WriteHeader(header) 271 if err != nil { 272 return err 273 } 274 275 if !info.IsDir() { 276 file, err := tree.File(name) 277 if err != nil { 278 return err 279 } 280 281 reader, err := file.Blob.Reader() 282 if err != nil { 283 return err 284 } 285 286 _, err = io.Copy(tw, reader) 287 if err != nil { 288 reader.Close() 289 return err 290 } 291 reader.Close() 292 } 293 } 294 295 return nil 296} 297 298func (g *GitRepo) LastCommitTime(filePath string) (*object.Commit, error) { 299 cacheMu.RLock() 300 if commit, exists := commitCache.Get(filePath); exists { 301 cacheMu.RUnlock() 302 return commit.(*object.Commit), nil 303 } 304 cacheMu.RUnlock() 305 306 commitIter, err := g.r.Log(&git.LogOptions{ 307 From: g.h, 308 PathFilter: func(s string) bool { 309 return s == filePath 310 }, 311 Order: git.LogOrderCommitterTime, 312 }) 313 314 if err != nil { 315 return nil, fmt.Errorf("failed to get commit log for %s: %w", filePath, err) 316 } 317 318 commit, err := commitIter.Next() 319 if err != nil { 320 return nil, fmt.Errorf("no commit found for %s", filePath) 321 } 322 323 cacheMu.Lock() 324 commitCache.Set(filePath, commit, 1) 325 cacheMu.Unlock() 326 327 return commit, nil 328} 329 330func newInfoWrapper( 331 name string, 332 prefix string, 333 entry *object.TreeEntry, 334 tree *object.Tree, 335) (*infoWrapper, error) { 336 var ( 337 size int64 338 mode fs.FileMode 339 isDir bool 340 ) 341 342 if entry.Mode.IsFile() { 343 file, err := tree.TreeEntryFile(entry) 344 if err != nil { 345 return nil, err 346 } 347 mode = fs.FileMode(file.Mode) 348 349 size, err = tree.Size(name) 350 if err != nil { 351 return nil, err 352 } 353 } else { 354 isDir = true 355 mode = fs.ModeDir | fs.ModePerm 356 } 357 358 fullname := path.Join(prefix, name) 359 return &infoWrapper{ 360 name: fullname, 361 size: size, 362 mode: mode, 363 modTime: time.Unix(0, 0), 364 isDir: isDir, 365 }, nil 366} 367 368func (i *infoWrapper) Name() string { 369 return i.name 370} 371 372func (i *infoWrapper) Size() int64 { 373 return i.size 374} 375 376func (i *infoWrapper) Mode() fs.FileMode { 377 return i.mode 378} 379 380func (i *infoWrapper) ModTime() time.Time { 381 return i.modTime 382} 383 384func (i *infoWrapper) IsDir() bool { 385 return i.isDir 386} 387 388func (i *infoWrapper) Sys() any { 389 return nil 390} 391 392func (t *TagReference) Name() string { 393 return t.ref.Name().Short() 394} 395 396func (t *TagReference) Message() string { 397 if t.tag != nil { 398 return t.tag.Message 399 } 400 return "" 401} 402 403func (t *TagReference) TagObject() *object.Tag { 404 return t.tag 405} 406 407func (t *TagReference) Hash() plumbing.Hash { 408 return t.ref.Hash() 409}