github.com/erikjuhani/git-gong@v0.0.0-20220213141213-6b9fa82d4e7c/gong/repository.go (about) 1 package gong 2 3 import ( 4 "errors" 5 "fmt" 6 "io/ioutil" 7 "log" 8 "net/url" 9 "os" 10 "strings" 11 "time" 12 13 "github.com/erikjuhani/git-gong/config" 14 git "github.com/libgit2/git2go/v31" 15 ) 16 17 // Repository represents is an abstraction of the underlying *git.Repository. 18 // To access the essence (*git.Repository) call Essence() function. 19 type Repository struct { 20 Head *Head 21 Path string 22 GitPath string 23 Index *git.Index 24 Tree *git.Tree 25 essence *git.Repository 26 Stashes *StashCollection 27 } 28 29 // Free frees git repository pointer. 30 func (repo *Repository) Free() { 31 repo.Essence().Free() 32 } 33 34 // Essence returns *git.Repository. 35 func (repo *Repository) Essence() *git.Repository { 36 return repo.essence 37 } 38 39 func (repo *Repository) FindTree(treeID *git.Oid) (*git.Tree, error) { 40 return repo.Essence().LookupTree(treeID) 41 } 42 43 func NewRepository(gitRepo *git.Repository, index *git.Index) *Repository { 44 return &Repository{ 45 Head: NewHead(gitRepo), 46 Path: gitRepo.Workdir(), 47 GitPath: gitRepo.Path(), 48 Index: index, 49 Stashes: NewStashCollection(&gitRepo.Stashes), 50 essence: gitRepo, 51 } 52 } 53 54 // Init initializes the repository. 55 // TODO: use the git config to set the initial default branch. 56 // more info: https://github.blog/2020-07-27-highlights-from-git-2-28/#introducing-init-defaultbranch 57 func Init(path string, bare bool, initialReference string) (*Repository, error) { 58 gitRepo, err := git.InitRepository(path, bare) 59 if err != nil { 60 return nil, err 61 } 62 63 if checkEmptyString(initialReference) { 64 initialReference = DefaultReference 65 } 66 67 initRef := fmt.Sprintf("%s%s", headRef, initialReference) 68 if err := ioutil.WriteFile(fmt.Sprintf("%s/HEAD", gitRepo.Path()), []byte("ref: "+initRef), 0644); err != nil { 69 return nil, err 70 } 71 72 index, err := gitRepo.Index() 73 if err != nil { 74 return nil, err 75 } 76 defer Free(index) 77 78 return NewRepository(gitRepo, index), nil 79 } 80 81 func (repo *Repository) DiffTreeToTree(oldTree *git.Tree, newTree *git.Tree) (*git.Diff, error) { 82 return repo.Essence().DiffTreeToTree(oldTree, newTree, nil) 83 } 84 85 func (repo *Repository) FindBranch(branchName string, branchType git.BranchType) (*Branch, error) { 86 gitBranch, err := repo.Essence().LookupBranch(branchName, branchType) 87 if err != nil { 88 return nil, err 89 } 90 91 return NewBranch(branchName, gitBranch), nil 92 } 93 94 func (repo *Repository) Merge(branchName string) error { 95 destinationbranch, err := repo.Head.Branch() 96 if err != nil { 97 return err 98 } 99 100 sourceBranch, err := repo.FindBranch(branchName, git.BranchLocal) 101 if err != nil { 102 return err 103 } 104 105 theirAnnCommit, err := sourceBranch.AnnotatedCommit() 106 if err != nil { 107 return err 108 } 109 defer Free(theirAnnCommit) 110 111 mergeHeads := make([]*git.AnnotatedCommit, 1) 112 mergeHeads[0] = theirAnnCommit 113 114 analysis, _, err := repo.essence.MergeAnalysis(mergeHeads) 115 log.Println(err) 116 if err != nil { 117 return err 118 } 119 120 checkoutOpts := git.CheckoutOpts{ 121 Strategy: git.CheckoutSafe | git.CheckoutRecreateMissing | git.CheckoutUseTheirs, 122 } 123 124 switch { 125 case analysis&git.MergeAnalysisUnborn != 0: 126 // Head is unborn, merge is impossible 127 return errors.New("cannot merge head doest not exist") 128 case analysis&git.MergeAnalysisUpToDate != 0: 129 // Nothing to merge 130 return errors.New("merge failed, nothing to merge") 131 case analysis&git.MergeAnalysisFastForward != 0: 132 // History has not diverted so we fast forward and just add the commits on top 133 134 sourceCommit, err := repo.FindCommit(sourceBranch.ReferenceID) 135 if err != nil { 136 return err 137 } 138 139 tree, err := repo.FindTree(sourceCommit.Essence().TreeId()) 140 log.Println(err) 141 if err != nil { 142 return err 143 } 144 145 if err := repo.CheckoutTree(tree, &checkoutOpts); err != nil { 146 return err 147 } 148 149 if err := repo.Head.SetReference(sourceBranch.RefName); err != nil { 150 return err 151 } 152 153 return nil 154 case analysis&git.MergeAnalysisNormal != 0: 155 mergeOpts, err := git.DefaultMergeOptions() 156 if err != nil { 157 return err 158 } 159 160 mergeOpts.FileFavor = git.MergeFileFavorNormal 161 mergeOpts.TreeFlags = git.MergeTreeFailOnConflict 162 163 if err := repo.Essence().Merge(mergeHeads, &mergeOpts, &checkoutOpts); err != nil { 164 return err 165 } 166 167 index, err := repo.Essence().Index() 168 if err != nil { 169 return err 170 } 171 defer Free(index) 172 173 if index.HasConflicts() { 174 return errors.New("merge conflict, cannot merge. Fix conflicts then commit before merge") 175 } 176 177 theirCommit, err := repo.FindCommit(sourceBranch.ReferenceID) 178 if err != nil { 179 return err 180 } 181 defer Free(theirCommit) 182 183 treeID, err := index.WriteTree() 184 if err != nil { 185 return err 186 } 187 188 tree, err := repo.FindTree(treeID) 189 if err != nil { 190 return err 191 } 192 defer Free(tree) 193 194 ourCommit, err := repo.Head.Commit() 195 if err != nil { 196 return err 197 } 198 defer Free(ourCommit) 199 200 mergeMessage := fmt.Sprintf("Merge %s into %s", sourceBranch.Name, destinationbranch.Name) 201 202 mergeCommit, err := repo.CreateCommit(tree, mergeMessage, ourCommit, theirCommit) 203 if err != nil { 204 return err 205 } 206 defer Free(mergeCommit) 207 208 return repo.Essence().StateCleanup() 209 } 210 211 return nil 212 } 213 214 func (repo *Repository) Info() (string, error) { 215 currentBranch, err := repo.CurrentBranch() 216 if err != nil { 217 return "", err 218 } 219 220 currentTip, err := repo.Head.Commit() 221 if err != nil { 222 return "", err 223 } 224 225 sb := strings.Builder{} 226 227 sb.WriteString(fmt.Sprintf("Branch %s\n", currentBranch.Name)) 228 sb.WriteString(fmt.Sprintf("Commit %s\n\n", currentTip.ID.String())) 229 230 entries, err := repo.StatusEntries() 231 if err != nil { 232 return "", err 233 } 234 235 if len(entries) == 0 { 236 sb.WriteString("No changes.") 237 return strings.TrimSuffix(sb.String(), "\n"), nil 238 } 239 240 sb.WriteString(fmt.Sprintf("Changes (%d):\n", len(entries))) 241 for _, e := range entries { 242 switch e.IndexToWorkdir.Status { 243 case git.DeltaAdded: 244 sb.WriteString(" A ") 245 case git.DeltaModified: 246 sb.WriteString(" M ") 247 case git.DeltaRenamed: 248 sb.WriteString(" R ") 249 case git.DeltaDeleted: 250 sb.WriteString(" D ") 251 case git.DeltaUntracked: 252 sb.WriteString("?? ") 253 } 254 255 sb.WriteString(fmt.Sprintf("%s\n", e.IndexToWorkdir.NewFile.Path)) 256 } 257 258 return strings.TrimSuffix(sb.String(), "\n"), nil 259 } 260 261 func (repo *Repository) StatusEntries() ([]git.StatusEntry, error) { 262 opts := &git.StatusOptions{ 263 Show: git.StatusShowIndexAndWorkdir, 264 Flags: git.StatusOptIncludeUntracked | git.StatusOptRenamesHeadToIndex | git.StatusOptSortCaseSensitively, 265 } 266 267 var entries []git.StatusEntry 268 269 statusList, err := repo.Essence().StatusList(opts) 270 if err != nil { 271 return entries, err 272 } 273 defer Free(statusList) 274 275 entryCount, err := statusList.EntryCount() 276 if err != nil { 277 return entries, err 278 } 279 280 for i := 0; i < entryCount; i++ { 281 entry, err := statusList.ByIndex(i) 282 if err != nil { 283 continue 284 } 285 entries = append(entries, entry) 286 } 287 288 return entries, nil 289 } 290 291 func (repo *Repository) UndoLastCommit() (*Commit, error) { 292 commits, err := repo.Commits() 293 if err != nil { 294 return nil, err 295 } 296 297 var idx int 298 299 if len(commits) > 1 { 300 idx = 1 301 } 302 303 tip := commits[0] 304 305 if err := repo.Essence().ResetToCommit(commits[idx].Essence(), git.ResetSoft, &git.CheckoutOptions{}); err != nil { 306 return nil, err 307 } 308 309 return tip, nil 310 } 311 312 func (repo *Repository) CurrentBranch() (*Branch, error) { 313 return repo.Head.Branch() 314 } 315 316 func (repo *Repository) Tags() ([]*Tag, error) { 317 var tags []*Tag 318 err := repo.Essence().Tags.Foreach(func(name string, _ *git.Oid) error { 319 ref, err := repo.Essence().References.Lookup(name) 320 if err != nil { 321 return err 322 } 323 defer Free(ref) 324 325 if ref.IsTag() { 326 tagObj, err := ref.Peel(git.ObjectTag) 327 if err != nil { 328 return err 329 } 330 331 tag, err := tagObj.AsTag() 332 if err != nil { 333 return err 334 } 335 defer Free(tag) 336 337 tags = append(tags, NewTag(tag)) 338 } 339 340 return nil 341 }) 342 343 if err != nil { 344 return tags, err 345 } 346 347 return tags, nil 348 } 349 350 func (repo *Repository) FindTag(tagName string) (*Tag, error) { 351 tags, err := repo.Tags() 352 if err != nil { 353 return nil, err 354 } 355 356 var tag *Tag 357 358 for _, tag = range tags { 359 if tag.Name == tagName { 360 break 361 } 362 } 363 364 if tag == nil { 365 return nil, fmt.Errorf("no tag found by tag name %s", tagName) 366 } 367 defer Free(tag) 368 369 return tag, nil 370 } 371 372 func (repo *Repository) FindCommit(commitID *git.Oid) (*Commit, error) { 373 gitCommit, err := repo.Essence().LookupCommit(commitID) 374 if err != nil { 375 return nil, err 376 } 377 378 return NewCommit(gitCommit), nil 379 } 380 381 func (repo *Repository) CheckoutTree(tree *git.Tree, opts *git.CheckoutOptions) error { 382 return repo.Essence().CheckoutTree(tree, opts) 383 } 384 385 func (repo *Repository) CheckoutTag(tagName string) (*Tag, error) { 386 checkoutOpts := &git.CheckoutOpts{ 387 Strategy: git.CheckoutSafe | git.CheckoutRecreateMissing | git.CheckoutAllowConflicts | git.CheckoutUseTheirs, 388 } 389 390 tags, err := repo.Tags() 391 if err != nil { 392 return nil, err 393 } 394 395 var tag *Tag 396 397 for _, tag = range tags { 398 if tag.Name == tagName { 399 break 400 } 401 } 402 403 if tag == nil { 404 return nil, fmt.Errorf("no tag found by tag name %s", tagName) 405 } 406 defer Free(tag) 407 408 commit, err := repo.FindCommit(tag.Essence().TargetId()) 409 if err != nil { 410 return nil, err 411 } 412 defer Free(commit) 413 414 tree, err := commit.Tree() 415 if err != nil { 416 return nil, err 417 } 418 defer Free(tree) 419 420 if err := repo.CheckoutTree(tree, checkoutOpts); err != nil { 421 return nil, err 422 } 423 424 if err := repo.Head.Detach(commit.ID); err != nil { 425 return nil, err 426 } 427 428 return tag, nil 429 } 430 431 func (repo *Repository) CheckoutCommit(hash string) (*Commit, error) { 432 checkoutOpts := &git.CheckoutOpts{ 433 Strategy: git.CheckoutSafe | git.CheckoutRecreateMissing | git.CheckoutAllowConflicts | git.CheckoutUseTheirs, 434 } 435 436 commits, err := repo.Commits() 437 if err != nil { 438 return nil, err 439 } 440 441 var commit *Commit 442 for _, commit = range commits { 443 if commit.ID.String() == hash { 444 break 445 } 446 } 447 448 if commit == nil { 449 return nil, fmt.Errorf("no commit found by hash %s", hash) 450 } 451 defer Free(commit) 452 453 tree, err := commit.Tree() 454 if err != nil { 455 return nil, err 456 } 457 defer Free(tree) 458 459 if err := repo.Essence().CheckoutTree(tree, checkoutOpts); err != nil { 460 return nil, err 461 } 462 463 if err := repo.Head.Detach(commit.ID); err != nil { 464 return nil, err 465 } 466 467 return commit, err 468 } 469 470 func (repo *Repository) CheckoutBranch(branchName string) (*Branch, error) { 471 detached, err := repo.Head.IsDetached() 472 if err != nil { 473 return nil, err 474 } 475 476 if detached { 477 ref, err := repo.Essence().References.Lookup(fmt.Sprintf("%s%s", headRef, branchName)) 478 if err != nil { 479 return nil, err 480 } 481 defer Free(ref) 482 483 if err := repo.Head.SetReference(ref.Name()); err != nil { 484 return nil, err 485 } 486 487 if err := repo.Head.Checkout(); err != nil { 488 return nil, err 489 } 490 } 491 492 branch, err := repo.FindBranch(branchName, git.BranchLocal) 493 494 // Branch does not exist, create it first 495 if branch == nil || err != nil { 496 branch, err = repo.CreateLocalBranch(branchName) 497 if err != nil { 498 return nil, err 499 } 500 } 501 502 currentBranch, err := repo.CurrentBranch() 503 if err != nil { 504 return nil, err 505 } 506 507 changed, err := repo.Changed() 508 if err != nil { 509 return nil, err 510 } 511 512 if changed { 513 _, err = repo.Stashes.Create(currentBranch) 514 if err != nil { 515 return nil, err 516 } 517 } 518 519 checkoutOpts := &git.CheckoutOpts{ 520 Strategy: git.CheckoutSafe | git.CheckoutRecreateMissing | git.CheckoutAllowConflicts | git.CheckoutUseTheirs, 521 } 522 523 localCommit, err := repo.FindCommit(branch.ReferenceID) 524 if err != nil { 525 return nil, err 526 } 527 defer Free(localCommit) 528 529 tree, err := localCommit.Tree() 530 if err != nil { 531 return nil, err 532 } 533 defer Free(tree) 534 535 if err := repo.Essence().CheckoutTree(tree, checkoutOpts); err != nil { 536 return nil, err 537 } 538 539 if err := repo.Head.SetReference(headRef + branchName); err != nil { 540 return nil, err 541 } 542 543 // No existing stash. 544 if !repo.Stashes.Has(branch) { 545 return branch, nil 546 } 547 548 if err := repo.Stashes.Pop(branch); err != nil { 549 return nil, err 550 } 551 552 return branch, nil 553 } 554 555 // Clone clones a git repository from source location to a target location. 556 // If target location is an empty string clone to a directory named after source. 557 func Clone(source string, target string) (*Repository, error) { 558 opts := git.CloneOptions{} 559 560 // Check that the source is a valid url. 561 u, err := url.Parse(source) 562 if err != nil { 563 return nil, err 564 } 565 566 src := strings.TrimSuffix(u.String(), ".git") 567 568 gitRepo, err := git.Clone(src, target, &opts) 569 if err != nil { 570 return nil, err 571 } 572 defer Free(gitRepo) 573 574 return NewRepository(gitRepo, nil), nil 575 576 } 577 578 func Open() (repo *Repository, err error) { 579 wd, err := os.Getwd() 580 if err != nil { 581 return 582 } 583 584 gitRepo, err := git.OpenRepository(wd) 585 if err != nil { 586 return 587 } 588 589 index, err := gitRepo.Index() 590 if err != nil { 591 return 592 } 593 defer Free(index) 594 595 return NewRepository(gitRepo, index), nil 596 } 597 598 func (repo *Repository) Changed() (bool, error) { 599 diff, err := repo.Essence().DiffIndexToWorkdir( 600 repo.Index, 601 &git.DiffOptions{Flags: git.DiffIncludeUntracked}, 602 ) 603 if err != nil { 604 return false, err 605 } 606 defer diff.Free() 607 608 stats, err := diff.Stats() 609 if err != nil { 610 return false, err 611 } 612 defer stats.Free() 613 614 changeCount := stats.FilesChanged() 615 616 status, err := repo.Essence().StatusList(&git.StatusOptions{}) 617 if err != nil { 618 return false, err 619 } 620 defer Free(status) 621 622 entryCount, err := status.EntryCount() 623 if err != nil { 624 return false, err 625 } 626 627 if changeCount == 0 && entryCount == 0 { 628 return false, nil 629 } 630 631 return true, nil 632 } 633 634 func (repo *Repository) AddToIndex(pathspec []string) (*git.Tree, error) { 635 branch, err := repo.Head.Branch() 636 if err != nil { 637 return nil, err 638 } 639 640 if config.ProtectedBranchPatterns.Match(branch.Name) { 641 return nil, errors.New("trying to commit on a protected branch, operation aborted") 642 } 643 644 changed, err := repo.Changed() 645 if err != nil { 646 return nil, err 647 } 648 649 if !changed { 650 return nil, fmt.Errorf("no files changed, %w", ErrNothingToCommit) 651 } 652 653 if err := repo.Index.AddAll(pathspec, git.IndexAddDefault, nil); err != nil { 654 return nil, err 655 } 656 657 treeID, err := repo.Index.WriteTree() 658 if err != nil { 659 return nil, err 660 } 661 662 if err = repo.Index.Write(); err != nil { 663 return nil, err 664 } 665 666 return repo.FindTree(treeID) 667 } 668 669 func (repo *Repository) CreateCommit(tree *git.Tree, message string, parents ...*Commit) (*Commit, error) { 670 // Implement later. 671 /* 672 if checkEmptyString(msg) { 673 input, cliErr := cli.CaptureInput() 674 if cliErr != nil { 675 return nil, cliErr 676 } 677 678 msg = string(input) 679 } 680 */ 681 682 if checkEmptyString(message) { 683 return nil, ErrEmptyCommitMsg 684 } 685 686 exists, err := repo.Head.Exists() 687 if err != nil { 688 return nil, err 689 } 690 691 var commitID *git.Oid 692 693 if exists { 694 headCommit, err := repo.Head.Commit() 695 defer Free(headCommit) 696 697 if err != nil { 698 return nil, err 699 } 700 701 gitCommits := []*git.Commit{headCommit.Essence()} 702 for _, c := range parents { 703 gitCommits = append(gitCommits, c.Essence()) 704 } 705 706 commitID, err = repo.Essence().CreateCommit( 707 repo.Head.RefName, 708 signature(), 709 signature(), 710 message, 711 tree, 712 gitCommits..., 713 ) 714 if err != nil { 715 return nil, err 716 } 717 } else { 718 // Initial commit. 719 commitID, err = repo.Essence().CreateCommit(repo.Head.RefName, signature(), signature(), message, tree) 720 if err != nil { 721 return nil, err 722 } 723 } 724 725 if err := repo.Head.Checkout(); err != nil { 726 return nil, err 727 } 728 729 return repo.FindCommit(commitID) 730 } 731 732 func (repo *Repository) References() ([]string, error) { 733 iter, err := repo.Essence().NewReferenceIterator() 734 if err != nil { 735 return nil, err 736 } 737 defer Free(iter) 738 739 var list []string 740 741 nameIter := iter.Names() 742 name, err := nameIter.Next() 743 for err == nil { 744 list = append(list, name) 745 name, err = nameIter.Next() 746 } 747 748 return list, err 749 } 750 751 func (repo *Repository) Commits() ([]*Commit, error) { 752 currentTip, err := repo.Head.Commit() 753 if err != nil { 754 return nil, err 755 } 756 defer Free(currentTip) 757 758 commits := []*Commit{currentTip} 759 760 parent := currentTip 761 for parent.HasChildren() { 762 parent = parent.Parent() 763 commits = append(commits, parent) 764 } 765 766 return commits, nil 767 } 768 769 // CreateTag creates a git tag. 770 func (repo *Repository) CreateTag(tagname string, message string) (tag *Tag, err error) { 771 headCommit, err := repo.Head.Commit() 772 if err != nil { 773 return 774 } 775 defer Free(headCommit) 776 777 gitTag, err := repo.Essence().Tags.Create(tagname, headCommit.Essence(), signature(), message) 778 if err != nil { 779 return 780 } 781 782 return &Tag{ID: gitTag, Name: tagname}, nil 783 } 784 785 // CreateLocalBranch creates a local branch to repository. 786 func (repo *Repository) CreateLocalBranch(branchName string) (branch *Branch, err error) { 787 // Check if branch already exists 788 localBranch, err := repo.FindBranch(branchName, git.BranchLocal) 789 if localBranch != nil && err != nil { 790 return 791 } 792 793 // Branch already exists return existing branch and an error stating branch already exists. 794 if localBranch != nil { 795 return localBranch, fmt.Errorf("branch %s already exists", branchName) 796 } 797 798 headCommit, err := repo.Head.Commit() 799 if err != nil { 800 return 801 } 802 defer Free(headCommit) 803 804 return repo.createBranch(branchName, headCommit, false) 805 } 806 807 func (repo *Repository) createBranch(branchName string, commit *Commit, force bool) (*Branch, error) { 808 if !config.AllowedBranchPatterns.Match(branchName) { 809 return nil, errors.New("error branch name did not match allowed template patterns") 810 } 811 812 gitBranch, err := repo.Essence().CreateBranch(branchName, commit.Essence(), force) 813 if err != nil { 814 return nil, err 815 } 816 817 return NewBranch(branchName, gitBranch), nil 818 } 819 820 // TODO get signature from git configuration 821 func signature() *git.Signature { 822 return &git.Signature{ 823 Name: "gong tester", 824 Email: "gong@tester.com", 825 When: time.Now(), 826 } 827 }