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 "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(branches []string) (string, error) { 229 branches = append(branches, []string{ 230 "main", 231 "master", 232 "trunk", 233 }...) 234 for _, b := range branches { 235 _, err := g.r.ResolveRevision(plumbing.Revision(b)) 236 if err == nil { 237 return b, nil 238 } 239 } 240 return "", fmt.Errorf("unable to find main branch") 241} 242 243// WriteTar writes itself from a tree into a binary tar file format. 244// prefix is root folder to be appended. 245func (g *GitRepo) WriteTar(w io.Writer, prefix string) error { 246 tw := tar.NewWriter(w) 247 defer tw.Close() 248 249 c, err := g.r.CommitObject(g.h) 250 if err != nil { 251 return fmt.Errorf("commit object: %w", err) 252 } 253 254 tree, err := c.Tree() 255 if err != nil { 256 return err 257 } 258 259 walker := object.NewTreeWalker(tree, true, nil) 260 defer walker.Close() 261 262 name, entry, err := walker.Next() 263 for ; err == nil; name, entry, err = walker.Next() { 264 info, err := newInfoWrapper(name, prefix, &entry, tree) 265 if err != nil { 266 return err 267 } 268 269 header, err := tar.FileInfoHeader(info, "") 270 if err != nil { 271 return err 272 } 273 274 err = tw.WriteHeader(header) 275 if err != nil { 276 return err 277 } 278 279 if !info.IsDir() { 280 file, err := tree.File(name) 281 if err != nil { 282 return err 283 } 284 285 reader, err := file.Blob.Reader() 286 if err != nil { 287 return err 288 } 289 290 _, err = io.Copy(tw, reader) 291 if err != nil { 292 reader.Close() 293 return err 294 } 295 reader.Close() 296 } 297 } 298 299 return nil 300} 301 302func (g *GitRepo) LastCommitForPath(path string) (*object.Commit, error) { 303 cacheKey := fmt.Sprintf("%s:%s", g.h.String(), path) 304 cacheMu.RLock() 305 if commit, found := commitCache.Get(cacheKey); found { 306 cacheMu.RUnlock() 307 return commit.(*object.Commit), nil 308 } 309 cacheMu.RUnlock() 310 311 cmd := exec.Command("git", "-C", g.path, "log", "-1", "--format=%H", "--", path) 312 313 var out bytes.Buffer 314 cmd.Stdout = &out 315 cmd.Stderr = &out 316 317 if err := cmd.Run(); err != nil { 318 return nil, fmt.Errorf("failed to get commit hash: %w", err) 319 } 320 321 commitHash := strings.TrimSpace(out.String()) 322 if commitHash == "" { 323 return nil, fmt.Errorf("no commits found for path: %s", path) 324 } 325 326 hash := plumbing.NewHash(commitHash) 327 328 commit, err := g.r.CommitObject(hash) 329 if err != nil { 330 return nil, err 331 } 332 333 cacheMu.Lock() 334 commitCache.Set(cacheKey, commit, 1) 335 cacheMu.Unlock() 336 337 return commit, nil 338} 339 340func newInfoWrapper( 341 name string, 342 prefix string, 343 entry *object.TreeEntry, 344 tree *object.Tree, 345) (*infoWrapper, error) { 346 var ( 347 size int64 348 mode fs.FileMode 349 isDir bool 350 ) 351 352 if entry.Mode.IsFile() { 353 file, err := tree.TreeEntryFile(entry) 354 if err != nil { 355 return nil, err 356 } 357 mode = fs.FileMode(file.Mode) 358 359 size, err = tree.Size(name) 360 if err != nil { 361 return nil, err 362 } 363 } else { 364 isDir = true 365 mode = fs.ModeDir | fs.ModePerm 366 } 367 368 fullname := path.Join(prefix, name) 369 return &infoWrapper{ 370 name: fullname, 371 size: size, 372 mode: mode, 373 modTime: time.Unix(0, 0), 374 isDir: isDir, 375 }, nil 376} 377 378func (i *infoWrapper) Name() string { 379 return i.name 380} 381 382func (i *infoWrapper) Size() int64 { 383 return i.size 384} 385 386func (i *infoWrapper) Mode() fs.FileMode { 387 return i.mode 388} 389 390func (i *infoWrapper) ModTime() time.Time { 391 return i.modTime 392} 393 394func (i *infoWrapper) IsDir() bool { 395 return i.isDir 396} 397 398func (i *infoWrapper) Sys() any { 399 return nil 400} 401 402func (t *TagReference) Name() string { 403 return t.ref.Name().Short() 404} 405 406func (t *TagReference) Message() string { 407 if t.tag != nil { 408 return t.tag.Message 409 } 410 return "" 411} 412 413func (t *TagReference) TagObject() *object.Tag { 414 return t.tag 415} 416 417func (t *TagReference) Hash() plumbing.Hash { 418 return t.ref.Hash() 419}