@@ -29,13 +29,11 @@ import (
2929 "time"
3030
3131 "github.com/Masterminds/sprig/v3"
32- gogit "github.com/go-git/go-git/v5"
3332 libgit2 "github.com/libgit2/git2go/v33"
3433
3534 "github.com/ProtonMail/go-crypto/openpgp"
35+ "github.com/ProtonMail/go-crypto/openpgp/packet"
3636 securejoin "github.com/cyphar/filepath-securejoin"
37- "github.com/go-git/go-git/v5/plumbing"
38- "github.com/go-git/go-git/v5/plumbing/object"
3937 "github.com/go-logr/logr"
4038 corev1 "k8s.io/api/core/v1"
4139 apimeta "k8s.io/apimachinery/pkg/api/meta"
@@ -253,10 +251,11 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
253251 // Use the git operations timeout for the repo.
254252 cloneCtx , cancel := context .WithTimeout (ctx , origin .Spec .Timeout .Duration )
255253 defer cancel ()
256- var repo * gogit .Repository
254+ var repo * libgit2 .Repository
257255 if repo , err = cloneInto (cloneCtx , access , ref , tmp ); err != nil {
258256 return failWithError (err )
259257 }
258+ defer repo .Free ()
260259
261260 // When there's a push spec, the pushed-to branch is where commits
262261 // shall be made
@@ -333,13 +332,13 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
333332 // The status message depends on what happens next. Since there's
334333 // more than one way to succeed, there's some if..else below, and
335334 // early returns only on failure.
336- author := & object .Signature {
335+ signature := & libgit2 .Signature {
337336 Name : gitSpec .Commit .Author .Name ,
338337 Email : gitSpec .Commit .Author .Email ,
339338 When : time .Now (),
340339 }
341340
342- if rev , err := commitChangedManifests (tracelog , repo , tmp , signingEntity , author , message ); err != nil {
341+ if rev , err := commitChangedManifests (tracelog , repo , tmp , signingEntity , signature , message ); err != nil {
343342 if err != errNoChanges {
344343 return failWithError (err )
345344 }
@@ -514,9 +513,9 @@ func (r repoAccess) remoteCallbacks(ctx context.Context) libgit2.RemoteCallbacks
514513}
515514
516515// cloneInto clones the upstream repository at the `ref` given (which
517- // can be `nil`). It returns a `*gogit .Repository` since that is used
516+ // can be `nil`). It returns a `*libgit2 .Repository` since that is used
518517// for committing changes.
519- func cloneInto (ctx context.Context , access repoAccess , ref * sourcev1.GitRepositoryRef , path string ) (* gogit .Repository , error ) {
518+ func cloneInto (ctx context.Context , access repoAccess , ref * sourcev1.GitRepositoryRef , path string ) (* libgit2 .Repository , error ) {
520519 opts := git.CheckoutOptions {}
521520 if ref != nil {
522521 opts .Tag = ref .Tag
@@ -532,90 +531,164 @@ func cloneInto(ctx context.Context, access repoAccess, ref *sourcev1.GitReposito
532531 return nil , err
533532 }
534533
535- return gogit . PlainOpen (path )
534+ return libgit2 . OpenRepository (path )
536535}
537536
538537// switchBranch switches the repo from the current branch to the
539538// branch given. If the branch does not exist, it is created using the
540539// head as the starting point.
541- func switchBranch (repo * gogit.Repository , pushBranch string ) error {
542- localBranch := plumbing .NewBranchReferenceName (pushBranch )
540+ func switchBranch (repo * libgit2.Repository , pushBranch string ) error {
541+ if err := repo .SetHead (fmt .Sprintf ("refs/heads/%s" , pushBranch )); err != nil {
542+ head , err := headCommit (repo )
543+ if err != nil {
544+ return err
545+ }
546+ defer head .Free ()
543547
544- // is the branch already present?
545- _ , err := repo .Reference (localBranch , true )
546- var create bool
547- switch {
548- case err == plumbing .ErrReferenceNotFound :
549- // make a new branch, starting at HEAD
550- create = true
551- case err != nil :
548+ _ , err = repo .CreateBranch (pushBranch , head , false )
552549 return err
553- default :
554- // local branch found, great
555- break
556550 }
557551
558- tree , err := repo .Worktree ()
552+ return nil
553+ }
554+
555+ func headCommit (repo * libgit2.Repository ) (* libgit2.Commit , error ) {
556+ head , err := repo .Head ()
559557 if err != nil {
560- return err
558+ return nil , err
561559 }
562-
563- return tree .Checkout (& gogit.CheckoutOptions {
564- Branch : localBranch ,
565- Create : create ,
566- })
560+ defer head .Free ()
561+ c , err := repo .LookupCommit (head .Target ())
562+ if err != nil {
563+ return nil , err
564+ }
565+ return c , nil
567566}
568567
569568var errNoChanges error = errors .New ("no changes made to working directory" )
570569
571- func commitChangedManifests (tracelog logr.Logger , repo * gogit.Repository , absRepoPath string , ent * openpgp.Entity , author * object.Signature , message string ) (string , error ) {
572- working , err := repo .Worktree ()
570+ func commitChangedManifests (tracelog logr.Logger , repo * libgit2.Repository , absRepoPath string , ent * openpgp.Entity , sig * libgit2.Signature , message string ) (string , error ) {
571+ sl , err := repo .StatusList (& libgit2.StatusOptions {
572+ Show : libgit2 .StatusShowIndexAndWorkdir ,
573+ })
573574 if err != nil {
574575 return "" , err
575576 }
576- status , err := working .Status ()
577+ defer sl .Free ()
578+
579+ count , err := sl .EntryCount ()
577580 if err != nil {
578581 return "" , err
579582 }
580583
581- // go-git has [a bug](https://github.com/go-git/go-git/issues/253)
582- // whereby it thinks broken symlinks to absolute paths are
583- // modified. There's no circumstance in which we want to commit a
584- // change to a broken symlink: so, detect and skip those.
585- var changed bool
586- for file , _ := range status {
587- abspath := filepath .Join (absRepoPath , file )
588- info , err := os .Lstat (abspath )
589- if err != nil {
590- return "" , fmt .Errorf ("checking if %s is a symlink: %w" , file , err )
591- }
592- if info .Mode ()& os .ModeSymlink > 0 {
593- // symlinks are OK; broken symlinks are probably a result
594- // of the bug mentioned above, but not of interest in any
595- // case.
596- if _ , err := os .Stat (abspath ); os .IsNotExist (err ) {
597- tracelog .Info ("apparently broken symlink found; ignoring" , "path" , abspath )
598- continue
584+ if count == 0 {
585+ return "" , errNoChanges
586+ }
587+
588+ var parentC []* libgit2.Commit
589+ head , err := headCommit (repo )
590+ if err == nil {
591+ defer head .Free ()
592+ parentC = append (parentC , head )
593+ }
594+
595+ index , err := repo .Index ()
596+ if err != nil {
597+ return "" , err
598+ }
599+ defer index .Free ()
600+
601+ // add to index any files that are not within .git/
602+ if err = filepath .Walk (repo .Workdir (),
603+ func (path string , info os.FileInfo , err error ) error {
604+ if err != nil {
605+ return err
606+ }
607+ rel , err := filepath .Rel (repo .Workdir (), path )
608+ if err != nil {
609+ return err
610+ }
611+ f , err := os .Stat (path )
612+ if err != nil {
613+ return err
599614 }
615+ if f .IsDir () || strings .HasPrefix (rel , ".git" ) || rel == "." {
616+ return nil
617+ }
618+ if err := index .AddByPath (rel ); err != nil {
619+ tracelog .Info ("adding file" , "file" , rel )
620+ return err
621+ }
622+ return nil
623+ }); err != nil {
624+ return "" , err
625+ }
626+
627+ if err := index .Write (); err != nil {
628+ return "" , err
629+ }
630+
631+ treeID , err := index .WriteTree ()
632+ if err != nil {
633+ return "" , err
634+ }
635+
636+ tree , err := repo .LookupTree (treeID )
637+ if err != nil {
638+ return "" , err
639+ }
640+ defer tree .Free ()
641+
642+ commitID , err := repo .CreateCommit ("HEAD" , sig , sig , message , tree , parentC ... )
643+ if err != nil {
644+ return "" , err
645+ }
646+
647+ // return unsigned commit if pgp entity is not provided
648+ if ent == nil {
649+ return commitID .String (), nil
650+ }
651+
652+ commit , err := repo .LookupCommit (commitID )
653+ if err != nil {
654+ return "" , err
655+ }
656+
657+ signedCommitID , err := commit .WithSignatureUsing (func (commitContent string ) (string , string , error ) {
658+ cipherText := new (bytes.Buffer )
659+ err := openpgp .ArmoredDetachSignText (cipherText , ent , strings .NewReader (commitContent ), & packet.Config {})
660+ if err != nil {
661+ return "" , "" , errors .New ("error signing payload" )
600662 }
601- tracelog .Info ("adding file" , "file" , file )
602- working .Add (file )
603- changed = true
663+
664+ return cipherText .String (), "" , nil
665+ })
666+ if err != nil {
667+ return "" , err
604668 }
669+ signedCommit , err := repo .LookupCommit (signedCommitID )
670+ if err != nil {
671+ return "" , err
672+ }
673+ defer signedCommit .Free ()
605674
606- if ! changed {
607- return "" , errNoChanges
675+ newHead , err := repo .Head ()
676+ if err != nil {
677+ return "" , err
608678 }
679+ defer newHead .Free ()
609680
610- var rev plumbing.Hash
611- if rev , err = working .Commit (message , & gogit.CommitOptions {
612- Author : author ,
613- SignKey : ent ,
614- }); err != nil {
681+ _ , err = repo .References .Create (
682+ newHead .Name (),
683+ signedCommit .Id (),
684+ true ,
685+ "repoint to signed commit" ,
686+ )
687+ if err != nil {
615688 return "" , err
616689 }
617690
618- return rev .String (), nil
691+ return signedCommitID .String (), nil
619692}
620693
621694// getSigningEntity retrieves an OpenPGP entity referenced by the
@@ -683,8 +756,8 @@ func fetch(ctx context.Context, path string, branch string, access repoAccess) e
683756
684757// push pushes the branch given to the origin using the git library
685758// indicated by `impl`. It's passed both the path to the repo and a
686- // gogit .Repository value, since the latter may as well be used if the
687- // implementation is GoGit .
759+ // libgit2 .Repository value, since the latter may as well be used if the
760+ // implementation is libgit2 .
688761func push (ctx context.Context , path , branch string , access repoAccess ) error {
689762 repo , err := libgit2 .OpenRepository (path )
690763 if err != nil {
0 commit comments