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