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