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