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