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