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