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 "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 ErrNotBinaryFile = fmt.Errorf("not binary file") 42) 43 44type GitRepo struct { 45 path string 46 r *git.Repository 47 h plumbing.Hash 48} 49 50type TagList struct { 51 refs []*TagReference 52 r *git.Repository 53} 54 55// TagReference is used to list both tag and non-annotated tags. 56// Non-annotated tags should only contains a reference. 57// Annotated tags should contain its reference and its tag information. 58type TagReference struct { 59 ref *plumbing.Reference 60 tag *object.Tag 61} 62 63// infoWrapper wraps the property of a TreeEntry so it can export fs.FileInfo 64// to tar WriteHeader 65type infoWrapper struct { 66 name string 67 size int64 68 mode fs.FileMode 69 modTime time.Time 70 isDir bool 71} 72 73func (self *TagList) Len() int { 74 return len(self.refs) 75} 76 77func (self *TagList) Swap(i, j int) { 78 self.refs[i], self.refs[j] = self.refs[j], self.refs[i] 79} 80 81// sorting tags in reverse chronological order 82func (self *TagList) Less(i, j int) bool { 83 var dateI time.Time 84 var dateJ time.Time 85 86 if self.refs[i].tag != nil { 87 dateI = self.refs[i].tag.Tagger.When 88 } else { 89 c, err := self.r.CommitObject(self.refs[i].ref.Hash()) 90 if err != nil { 91 dateI = time.Now() 92 } else { 93 dateI = c.Committer.When 94 } 95 } 96 97 if self.refs[j].tag != nil { 98 dateJ = self.refs[j].tag.Tagger.When 99 } else { 100 c, err := self.r.CommitObject(self.refs[j].ref.Hash()) 101 if err != nil { 102 dateJ = time.Now() 103 } else { 104 dateJ = c.Committer.When 105 } 106 } 107 108 return dateI.After(dateJ) 109} 110 111func Open(path string, ref string) (*GitRepo, error) { 112 var err error 113 g := GitRepo{path: path} 114 g.r, err = git.PlainOpen(path) 115 if err != nil { 116 return nil, fmt.Errorf("opening %s: %w", path, err) 117 } 118 119 if ref == "" { 120 head, err := g.r.Head() 121 if err != nil { 122 return nil, fmt.Errorf("getting head of %s: %w", path, err) 123 } 124 g.h = head.Hash() 125 } else { 126 hash, err := g.r.ResolveRevision(plumbing.Revision(ref)) 127 if err != nil { 128 return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err) 129 } 130 g.h = *hash 131 } 132 return &g, nil 133} 134 135func PlainOpen(path string) (*GitRepo, error) { 136 var err error 137 g := GitRepo{path: path} 138 g.r, err = git.PlainOpen(path) 139 if err != nil { 140 return nil, fmt.Errorf("opening %s: %w", path, err) 141 } 142 return &g, nil 143} 144 145func (g *GitRepo) Commits(offset, limit int) ([]*object.Commit, error) { 146 commits := []*object.Commit{} 147 148 output, err := g.revList( 149 fmt.Sprintf("--skip=%d", offset), 150 fmt.Sprintf("--max-count=%d", limit), 151 ) 152 if err != nil { 153 return nil, fmt.Errorf("commits from ref: %w", err) 154 } 155 156 lines := strings.Split(strings.TrimSpace(string(output)), "\n") 157 if len(lines) == 1 && lines[0] == "" { 158 return commits, nil 159 } 160 161 for _, item := range lines { 162 obj, err := g.r.CommitObject(plumbing.NewHash(item)) 163 if err != nil { 164 continue 165 } 166 commits = append(commits, obj) 167 } 168 169 return commits, nil 170} 171 172func (g *GitRepo) TotalCommits() (int, error) { 173 output, err := g.revList( 174 fmt.Sprintf("--count"), 175 ) 176 if err != nil { 177 return 0, fmt.Errorf("failed to run rev-list", err) 178 } 179 180 count, err := strconv.Atoi(strings.TrimSpace(string(output))) 181 if err != nil { 182 return 0, err 183 } 184 185 return count, nil 186} 187 188func (g *GitRepo) revList(extraArgs ...string) ([]byte, error) { 189 var args []string 190 args = append(args, "rev-list") 191 args = append(args, g.h.String()) 192 args = append(args, extraArgs...) 193 194 cmd := exec.Command("git", args...) 195 cmd.Dir = g.path 196 197 return cmd.Output() 198} 199 200func (g *GitRepo) Commit(h plumbing.Hash) (*object.Commit, error) { 201 return g.r.CommitObject(h) 202} 203 204func (g *GitRepo) LastCommit() (*object.Commit, error) { 205 c, err := g.r.CommitObject(g.h) 206 if err != nil { 207 return nil, fmt.Errorf("last commit: %w", err) 208 } 209 return c, nil 210} 211 212func (g *GitRepo) FileContentN(path string, cap int64) ([]byte, error) { 213 buf := []byte{} 214 215 c, err := g.r.CommitObject(g.h) 216 if err != nil { 217 return nil, fmt.Errorf("commit object: %w", err) 218 } 219 220 tree, err := c.Tree() 221 if err != nil { 222 return nil, fmt.Errorf("file tree: %w", err) 223 } 224 225 file, err := tree.File(path) 226 if err != nil { 227 return nil, err 228 } 229 230 isbin, _ := file.IsBinary() 231 232 if !isbin { 233 reader, err := file.Reader() 234 if err != nil { 235 return nil, err 236 } 237 bufReader := io.LimitReader(reader, cap) 238 _, err = bufReader.Read(buf) 239 if err != nil { 240 return nil, err 241 } 242 return buf, nil 243 } else { 244 return nil, ErrBinaryFile 245 } 246} 247 248func (g *GitRepo) FileContent(path string) (string, error) { 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 "", fmt.Errorf("file tree: %w", err) 257 } 258 259 file, err := tree.File(path) 260 if err != nil { 261 return "", err 262 } 263 264 isbin, _ := file.IsBinary() 265 266 if !isbin { 267 return file.Contents() 268 } else { 269 return "", ErrBinaryFile 270 } 271} 272 273func (g *GitRepo) RawContent(path string) ([]byte, error) { 274 c, err := g.r.CommitObject(g.h) 275 if err != nil { 276 return nil, fmt.Errorf("commit object: %w", err) 277 } 278 279 tree, err := c.Tree() 280 if err != nil { 281 return nil, fmt.Errorf("file tree: %w", err) 282 } 283 284 file, err := tree.File(path) 285 if err != nil { 286 return nil, err 287 } 288 289 reader, err := file.Reader() 290 if err != nil { 291 return nil, fmt.Errorf("opening file reader: %w", err) 292 } 293 defer reader.Close() 294 295 return io.ReadAll(reader) 296} 297 298func (g *GitRepo) Tags() ([]*TagReference, error) { 299 iter, err := g.r.Tags() 300 if err != nil { 301 return nil, fmt.Errorf("tag objects: %w", err) 302 } 303 304 tags := make([]*TagReference, 0) 305 306 if err := iter.ForEach(func(ref *plumbing.Reference) error { 307 obj, err := g.r.TagObject(ref.Hash()) 308 switch err { 309 case nil: 310 tags = append(tags, &TagReference{ 311 ref: ref, 312 tag: obj, 313 }) 314 case plumbing.ErrObjectNotFound: 315 tags = append(tags, &TagReference{ 316 ref: ref, 317 }) 318 default: 319 return err 320 } 321 return nil 322 }); err != nil { 323 return nil, err 324 } 325 326 tagList := &TagList{r: g.r, refs: tags} 327 sort.Sort(tagList) 328 return tags, nil 329} 330 331func (g *GitRepo) Branches() ([]types.Branch, error) { 332 bi, err := g.r.Branches() 333 if err != nil { 334 return nil, fmt.Errorf("branchs: %w", err) 335 } 336 337 branches := []types.Branch{} 338 339 defaultBranch, err := g.FindMainBranch() 340 341 _ = bi.ForEach(func(ref *plumbing.Reference) error { 342 b := types.Branch{} 343 b.Hash = ref.Hash().String() 344 b.Name = ref.Name().Short() 345 346 // resolve commit that this branch points to 347 commit, _ := g.Commit(ref.Hash()) 348 if commit != nil { 349 b.Commit = commit 350 } 351 352 if defaultBranch != "" && defaultBranch == b.Name { 353 b.IsDefault = true 354 } 355 356 branches = append(branches, b) 357 358 return nil 359 }) 360 361 return branches, nil 362} 363 364func (g *GitRepo) Branch(name string) (*plumbing.Reference, error) { 365 ref, err := g.r.Reference(plumbing.NewBranchReferenceName(name), false) 366 if err != nil { 367 return nil, fmt.Errorf("branch: %w", err) 368 } 369 370 if !ref.Name().IsBranch() { 371 return nil, fmt.Errorf("branch: %s is not a branch", ref.Name()) 372 } 373 374 return ref, nil 375} 376 377func (g *GitRepo) SetDefaultBranch(branch string) error { 378 ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch)) 379 return g.r.Storer.SetReference(ref) 380} 381 382func (g *GitRepo) FindMainBranch() (string, error) { 383 ref, err := g.r.Head() 384 if err != nil { 385 return "", fmt.Errorf("unable to find main branch: %w", err) 386 } 387 if ref.Name().IsBranch() { 388 return strings.TrimPrefix(string(ref.Name()), "refs/heads/"), nil 389 } 390 391 return "", fmt.Errorf("unable to find main branch: %w", err) 392} 393 394// WriteTar writes itself from a tree into a binary tar file format. 395// prefix is root folder to be appended. 396func (g *GitRepo) WriteTar(w io.Writer, prefix string) error { 397 tw := tar.NewWriter(w) 398 defer tw.Close() 399 400 c, err := g.r.CommitObject(g.h) 401 if err != nil { 402 return fmt.Errorf("commit object: %w", err) 403 } 404 405 tree, err := c.Tree() 406 if err != nil { 407 return err 408 } 409 410 walker := object.NewTreeWalker(tree, true, nil) 411 defer walker.Close() 412 413 name, entry, err := walker.Next() 414 for ; err == nil; name, entry, err = walker.Next() { 415 info, err := newInfoWrapper(name, prefix, &entry, tree) 416 if err != nil { 417 return err 418 } 419 420 header, err := tar.FileInfoHeader(info, "") 421 if err != nil { 422 return err 423 } 424 425 err = tw.WriteHeader(header) 426 if err != nil { 427 return err 428 } 429 430 if !info.IsDir() { 431 file, err := tree.File(name) 432 if err != nil { 433 return err 434 } 435 436 reader, err := file.Blob.Reader() 437 if err != nil { 438 return err 439 } 440 441 _, err = io.Copy(tw, reader) 442 if err != nil { 443 reader.Close() 444 return err 445 } 446 reader.Close() 447 } 448 } 449 450 return nil 451} 452 453func (g *GitRepo) LastCommitForPath(path string) (*types.LastCommitInfo, error) { 454 cacheKey := fmt.Sprintf("%s:%s", g.h.String(), path) 455 cacheMu.RLock() 456 if commitInfo, found := commitCache.Get(cacheKey); found { 457 cacheMu.RUnlock() 458 return commitInfo.(*types.LastCommitInfo), nil 459 } 460 cacheMu.RUnlock() 461 462 cmd := exec.Command("git", "-C", g.path, "log", g.h.String(), "-1", "--format=%H %ct", "--", path) 463 464 var out bytes.Buffer 465 cmd.Stdout = &out 466 cmd.Stderr = &out 467 468 if err := cmd.Run(); err != nil { 469 return nil, fmt.Errorf("failed to get commit hash: %w", err) 470 } 471 472 output := strings.TrimSpace(out.String()) 473 if output == "" { 474 return nil, fmt.Errorf("no commits found for path: %s", path) 475 } 476 477 parts := strings.SplitN(output, " ", 2) 478 if len(parts) < 2 { 479 return nil, fmt.Errorf("unexpected commit log format") 480 } 481 482 commitHash := parts[0] 483 commitTimeUnix, err := strconv.ParseInt(parts[1], 10, 64) 484 if err != nil { 485 return nil, fmt.Errorf("parsing commit time: %w", err) 486 } 487 commitTime := time.Unix(commitTimeUnix, 0) 488 489 hash := plumbing.NewHash(commitHash) 490 491 commitInfo := &types.LastCommitInfo{ 492 Hash: hash, 493 Message: "", 494 When: commitTime, 495 } 496 497 cacheMu.Lock() 498 commitCache.Set(cacheKey, commitInfo, 1) 499 cacheMu.Unlock() 500 501 return commitInfo, nil 502} 503 504func newInfoWrapper( 505 name string, 506 prefix string, 507 entry *object.TreeEntry, 508 tree *object.Tree, 509) (*infoWrapper, error) { 510 var ( 511 size int64 512 mode fs.FileMode 513 isDir bool 514 ) 515 516 if entry.Mode.IsFile() { 517 file, err := tree.TreeEntryFile(entry) 518 if err != nil { 519 return nil, err 520 } 521 mode = fs.FileMode(file.Mode) 522 523 size, err = tree.Size(name) 524 if err != nil { 525 return nil, err 526 } 527 } else { 528 isDir = true 529 mode = fs.ModeDir | fs.ModePerm 530 } 531 532 fullname := path.Join(prefix, name) 533 return &infoWrapper{ 534 name: fullname, 535 size: size, 536 mode: mode, 537 modTime: time.Unix(0, 0), 538 isDir: isDir, 539 }, nil 540} 541 542func (i *infoWrapper) Name() string { 543 return i.name 544} 545 546func (i *infoWrapper) Size() int64 { 547 return i.size 548} 549 550func (i *infoWrapper) Mode() fs.FileMode { 551 return i.mode 552} 553 554func (i *infoWrapper) ModTime() time.Time { 555 return i.modTime 556} 557 558func (i *infoWrapper) IsDir() bool { 559 return i.isDir 560} 561 562func (i *infoWrapper) Sys() any { 563 return nil 564} 565 566func (t *TagReference) Name() string { 567 return t.ref.Name().Short() 568} 569 570func (t *TagReference) Message() string { 571 if t.tag != nil { 572 return t.tag.Message 573 } 574 return "" 575} 576 577func (t *TagReference) TagObject() *object.Tag { 578 return t.tag 579} 580 581func (t *TagReference) Hash() plumbing.Hash { 582 return t.ref.Hash() 583}