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