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 "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() ([]*object.Commit, error) { 146 ci, err := g.r.Log(&git.LogOptions{From: g.h}) 147 if err != nil { 148 return nil, fmt.Errorf("commits from ref: %w", err) 149 } 150 151 commits := []*object.Commit{} 152 ci.ForEach(func(c *object.Commit) error { 153 commits = append(commits, c) 154 return nil 155 }) 156 157 return commits, nil 158} 159 160func (g *GitRepo) Commit(h plumbing.Hash) (*object.Commit, error) { 161 return g.r.CommitObject(h) 162} 163 164func (g *GitRepo) LastCommit() (*object.Commit, error) { 165 c, err := g.r.CommitObject(g.h) 166 if err != nil { 167 return nil, fmt.Errorf("last commit: %w", err) 168 } 169 return c, nil 170} 171 172func (g *GitRepo) FileContentN(path string, cap int64) ([]byte, error) { 173 buf := []byte{} 174 175 c, err := g.r.CommitObject(g.h) 176 if err != nil { 177 return nil, fmt.Errorf("commit object: %w", err) 178 } 179 180 tree, err := c.Tree() 181 if err != nil { 182 return nil, fmt.Errorf("file tree: %w", err) 183 } 184 185 file, err := tree.File(path) 186 if err != nil { 187 return nil, err 188 } 189 190 isbin, _ := file.IsBinary() 191 192 if !isbin { 193 reader, err := file.Reader() 194 if err != nil { 195 return nil, err 196 } 197 bufReader := io.LimitReader(reader, cap) 198 _, err = bufReader.Read(buf) 199 if err != nil { 200 return nil, err 201 } 202 return buf, nil 203 } else { 204 return nil, ErrBinaryFile 205 } 206} 207 208func (g *GitRepo) FileContent(path string) (string, error) { 209 c, err := g.r.CommitObject(g.h) 210 if err != nil { 211 return "", fmt.Errorf("commit object: %w", err) 212 } 213 214 tree, err := c.Tree() 215 if err != nil { 216 return "", fmt.Errorf("file tree: %w", err) 217 } 218 219 file, err := tree.File(path) 220 if err != nil { 221 return "", err 222 } 223 224 isbin, _ := file.IsBinary() 225 226 if !isbin { 227 return file.Contents() 228 } else { 229 return "", ErrBinaryFile 230 } 231} 232 233func (g *GitRepo) RawContent(path string) ([]byte, error) { 234 c, err := g.r.CommitObject(g.h) 235 if err != nil { 236 return nil, fmt.Errorf("commit object: %w", err) 237 } 238 239 tree, err := c.Tree() 240 if err != nil { 241 return nil, fmt.Errorf("file tree: %w", err) 242 } 243 244 file, err := tree.File(path) 245 if err != nil { 246 return nil, err 247 } 248 249 reader, err := file.Reader() 250 if err != nil { 251 return nil, fmt.Errorf("opening file reader: %w", err) 252 } 253 defer reader.Close() 254 255 return io.ReadAll(reader) 256} 257 258func (g *GitRepo) Tags() ([]*TagReference, error) { 259 iter, err := g.r.Tags() 260 if err != nil { 261 return nil, fmt.Errorf("tag objects: %w", err) 262 } 263 264 tags := make([]*TagReference, 0) 265 266 if err := iter.ForEach(func(ref *plumbing.Reference) error { 267 obj, err := g.r.TagObject(ref.Hash()) 268 switch err { 269 case nil: 270 tags = append(tags, &TagReference{ 271 ref: ref, 272 tag: obj, 273 }) 274 case plumbing.ErrObjectNotFound: 275 tags = append(tags, &TagReference{ 276 ref: ref, 277 }) 278 default: 279 return err 280 } 281 return nil 282 }); err != nil { 283 return nil, err 284 } 285 286 tagList := &TagList{r: g.r, refs: tags} 287 sort.Sort(tagList) 288 return tags, nil 289} 290 291func (g *GitRepo) Branches() ([]types.Branch, error) { 292 bi, err := g.r.Branches() 293 if err != nil { 294 return nil, fmt.Errorf("branchs: %w", err) 295 } 296 297 branches := []types.Branch{} 298 299 defaultBranch, err := g.FindMainBranch() 300 301 _ = bi.ForEach(func(ref *plumbing.Reference) error { 302 b := types.Branch{} 303 b.Hash = ref.Hash().String() 304 b.Name = ref.Name().Short() 305 306 // resolve commit that this branch points to 307 commit, _ := g.Commit(ref.Hash()) 308 if commit != nil { 309 b.Commit = commit 310 } 311 312 if defaultBranch != "" && defaultBranch == b.Name { 313 b.IsDefault = true 314 } 315 316 branches = append(branches, b) 317 318 return nil 319 }) 320 321 return branches, nil 322} 323 324func (g *GitRepo) Branch(name string) (*plumbing.Reference, error) { 325 ref, err := g.r.Reference(plumbing.NewBranchReferenceName(name), false) 326 if err != nil { 327 return nil, fmt.Errorf("branch: %w", err) 328 } 329 330 if !ref.Name().IsBranch() { 331 return nil, fmt.Errorf("branch: %s is not a branch", ref.Name()) 332 } 333 334 return ref, nil 335} 336 337func (g *GitRepo) SetDefaultBranch(branch string) error { 338 ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch)) 339 return g.r.Storer.SetReference(ref) 340} 341 342func (g *GitRepo) FindMainBranch() (string, error) { 343 ref, err := g.r.Head() 344 if err != nil { 345 return "", fmt.Errorf("unable to find main branch: %w", err) 346 } 347 if ref.Name().IsBranch() { 348 return strings.TrimPrefix(string(ref.Name()), "refs/heads/"), nil 349 } 350 351 return "", fmt.Errorf("unable to find main branch: %w", err) 352} 353 354// WriteTar writes itself from a tree into a binary tar file format. 355// prefix is root folder to be appended. 356func (g *GitRepo) WriteTar(w io.Writer, prefix string) error { 357 tw := tar.NewWriter(w) 358 defer tw.Close() 359 360 c, err := g.r.CommitObject(g.h) 361 if err != nil { 362 return fmt.Errorf("commit object: %w", err) 363 } 364 365 tree, err := c.Tree() 366 if err != nil { 367 return err 368 } 369 370 walker := object.NewTreeWalker(tree, true, nil) 371 defer walker.Close() 372 373 name, entry, err := walker.Next() 374 for ; err == nil; name, entry, err = walker.Next() { 375 info, err := newInfoWrapper(name, prefix, &entry, tree) 376 if err != nil { 377 return err 378 } 379 380 header, err := tar.FileInfoHeader(info, "") 381 if err != nil { 382 return err 383 } 384 385 err = tw.WriteHeader(header) 386 if err != nil { 387 return err 388 } 389 390 if !info.IsDir() { 391 file, err := tree.File(name) 392 if err != nil { 393 return err 394 } 395 396 reader, err := file.Blob.Reader() 397 if err != nil { 398 return err 399 } 400 401 _, err = io.Copy(tw, reader) 402 if err != nil { 403 reader.Close() 404 return err 405 } 406 reader.Close() 407 } 408 } 409 410 return nil 411} 412 413func (g *GitRepo) LastCommitForPath(path string) (*types.LastCommitInfo, error) { 414 cacheKey := fmt.Sprintf("%s:%s", g.h.String(), path) 415 cacheMu.RLock() 416 if commitInfo, found := commitCache.Get(cacheKey); found { 417 cacheMu.RUnlock() 418 return commitInfo.(*types.LastCommitInfo), nil 419 } 420 cacheMu.RUnlock() 421 422 cmd := exec.Command("git", "-C", g.path, "log", g.h.String(), "-1", "--format=%H %ct", "--", path) 423 424 var out bytes.Buffer 425 cmd.Stdout = &out 426 cmd.Stderr = &out 427 428 if err := cmd.Run(); err != nil { 429 return nil, fmt.Errorf("failed to get commit hash: %w", err) 430 } 431 432 output := strings.TrimSpace(out.String()) 433 if output == "" { 434 return nil, fmt.Errorf("no commits found for path: %s", path) 435 } 436 437 parts := strings.SplitN(output, " ", 2) 438 if len(parts) < 2 { 439 return nil, fmt.Errorf("unexpected commit log format") 440 } 441 442 commitHash := parts[0] 443 commitTimeUnix, err := strconv.ParseInt(parts[1], 10, 64) 444 if err != nil { 445 return nil, fmt.Errorf("parsing commit time: %w", err) 446 } 447 commitTime := time.Unix(commitTimeUnix, 0) 448 449 hash := plumbing.NewHash(commitHash) 450 451 commitInfo := &types.LastCommitInfo{ 452 Hash: hash, 453 Message: "", 454 When: commitTime, 455 } 456 457 cacheMu.Lock() 458 commitCache.Set(cacheKey, commitInfo, 1) 459 cacheMu.Unlock() 460 461 return commitInfo, nil 462} 463 464func newInfoWrapper( 465 name string, 466 prefix string, 467 entry *object.TreeEntry, 468 tree *object.Tree, 469) (*infoWrapper, error) { 470 var ( 471 size int64 472 mode fs.FileMode 473 isDir bool 474 ) 475 476 if entry.Mode.IsFile() { 477 file, err := tree.TreeEntryFile(entry) 478 if err != nil { 479 return nil, err 480 } 481 mode = fs.FileMode(file.Mode) 482 483 size, err = tree.Size(name) 484 if err != nil { 485 return nil, err 486 } 487 } else { 488 isDir = true 489 mode = fs.ModeDir | fs.ModePerm 490 } 491 492 fullname := path.Join(prefix, name) 493 return &infoWrapper{ 494 name: fullname, 495 size: size, 496 mode: mode, 497 modTime: time.Unix(0, 0), 498 isDir: isDir, 499 }, nil 500} 501 502func (i *infoWrapper) Name() string { 503 return i.name 504} 505 506func (i *infoWrapper) Size() int64 { 507 return i.size 508} 509 510func (i *infoWrapper) Mode() fs.FileMode { 511 return i.mode 512} 513 514func (i *infoWrapper) ModTime() time.Time { 515 return i.modTime 516} 517 518func (i *infoWrapper) IsDir() bool { 519 return i.isDir 520} 521 522func (i *infoWrapper) Sys() any { 523 return nil 524} 525 526func (t *TagReference) Name() string { 527 return t.ref.Name().Short() 528} 529 530func (t *TagReference) Message() string { 531 if t.tag != nil { 532 return t.tag.Message 533 } 534 return "" 535} 536 537func (t *TagReference) TagObject() *object.Tag { 538 return t.tag 539} 540 541func (t *TagReference) Hash() plumbing.Hash { 542 return t.ref.Hash() 543}