forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
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 cacheMu.RLock() 304 if commit, found := commitCache.Get(path); found { 305 cacheMu.RUnlock() 306 return commit.(*object.Commit), nil 307 } 308 cacheMu.RUnlock() 309 310 cmd := exec.Command("git", "-C", g.path, "log", "-1", "--format=%H", "--", path) 311 312 var out bytes.Buffer 313 cmd.Stdout = &out 314 cmd.Stderr = &out 315 316 if err := cmd.Run(); err != nil { 317 return nil, fmt.Errorf("failed to get commit hash: %w", err) 318 } 319 320 commitHash := strings.TrimSpace(out.String()) 321 if commitHash == "" { 322 return nil, fmt.Errorf("no commits found for path: %s", path) 323 } 324 325 hash := plumbing.NewHash(commitHash) 326 327 commit, err := g.r.CommitObject(hash) 328 if err != nil { 329 return nil, err 330 } 331 332 cacheMu.Lock() 333 commitCache.Set(path, commit, 1) 334 cacheMu.Unlock() 335 336 return commit, nil 337} 338 339func newInfoWrapper( 340 name string, 341 prefix string, 342 entry *object.TreeEntry, 343 tree *object.Tree, 344) (*infoWrapper, error) { 345 var ( 346 size int64 347 mode fs.FileMode 348 isDir bool 349 ) 350 351 if entry.Mode.IsFile() { 352 file, err := tree.TreeEntryFile(entry) 353 if err != nil { 354 return nil, err 355 } 356 mode = fs.FileMode(file.Mode) 357 358 size, err = tree.Size(name) 359 if err != nil { 360 return nil, err 361 } 362 } else { 363 isDir = true 364 mode = fs.ModeDir | fs.ModePerm 365 } 366 367 fullname := path.Join(prefix, name) 368 return &infoWrapper{ 369 name: fullname, 370 size: size, 371 mode: mode, 372 modTime: time.Unix(0, 0), 373 isDir: isDir, 374 }, nil 375} 376 377func (i *infoWrapper) Name() string { 378 return i.name 379} 380 381func (i *infoWrapper) Size() int64 { 382 return i.size 383} 384 385func (i *infoWrapper) Mode() fs.FileMode { 386 return i.mode 387} 388 389func (i *infoWrapper) ModTime() time.Time { 390 return i.modTime 391} 392 393func (i *infoWrapper) IsDir() bool { 394 return i.isDir 395} 396 397func (i *infoWrapper) Sys() any { 398 return nil 399} 400 401func (t *TagReference) Name() string { 402 return t.ref.Name().Short() 403} 404 405func (t *TagReference) Message() string { 406 if t.tag != nil { 407 return t.tag.Message 408 } 409 return "" 410} 411 412func (t *TagReference) TagObject() *object.Tag { 413 return t.tag 414} 415 416func (t *TagReference) Hash() plumbing.Hash { 417 return t.ref.Hash() 418}