mirror of
				https://github.com/fluxcd/flux2.git
				synced 2025-10-31 16:26:36 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			297 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			297 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| Copyright 2021 The Flux authors
 | |
| 
 | |
| Licensed under the Apache License, Version 2.0 (the "License");
 | |
| you may not use this file except in compliance with the License.
 | |
| You may obtain a copy of the License at
 | |
| 
 | |
|     http://www.apache.org/licenses/LICENSE-2.0
 | |
| 
 | |
| Unless required by applicable law or agreed to in writing, software
 | |
| distributed under the License is distributed on an "AS IS" BASIS,
 | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| See the License for the specific language governing permissions and
 | |
| limitations under the License.
 | |
| */
 | |
| 
 | |
| package gogit
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/ProtonMail/go-crypto/openpgp"
 | |
| 	gogit "github.com/go-git/go-git/v5"
 | |
| 	"github.com/go-git/go-git/v5/config"
 | |
| 	"github.com/go-git/go-git/v5/plumbing"
 | |
| 	"github.com/go-git/go-git/v5/plumbing/object"
 | |
| 	"github.com/go-git/go-git/v5/plumbing/transport"
 | |
| 
 | |
| 	"github.com/fluxcd/flux2/pkg/bootstrap/git"
 | |
| )
 | |
| 
 | |
| type GoGit struct {
 | |
| 	path       string
 | |
| 	auth       transport.AuthMethod
 | |
| 	repository *gogit.Repository
 | |
| }
 | |
| 
 | |
| type CommitOptions struct {
 | |
| 	GpgKeyPath       string
 | |
| 	GpgKeyPassphrase string
 | |
| 	KeyID            string
 | |
| }
 | |
| 
 | |
| func New(path string, auth transport.AuthMethod) *GoGit {
 | |
| 	return &GoGit{
 | |
| 		path: path,
 | |
| 		auth: auth,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (g *GoGit) Init(url, branch string) (bool, error) {
 | |
| 	if g.repository != nil {
 | |
| 		return false, nil
 | |
| 	}
 | |
| 
 | |
| 	r, err := gogit.PlainInit(g.path, false)
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 	if _, err = r.CreateRemote(&config.RemoteConfig{
 | |
| 		Name: gogit.DefaultRemoteName,
 | |
| 		URLs: []string{url},
 | |
| 	}); err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 	branchRef := plumbing.NewBranchReferenceName(branch)
 | |
| 	if err = r.CreateBranch(&config.Branch{
 | |
| 		Name:   branch,
 | |
| 		Remote: gogit.DefaultRemoteName,
 | |
| 		Merge:  branchRef,
 | |
| 	}); err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 	// PlainInit assumes the initial branch to always be master, we can
 | |
| 	// overwrite this by setting the reference of the Storer to a new
 | |
| 	// symbolic reference (as there are no commits yet) that points
 | |
| 	// the HEAD to our new branch.
 | |
| 	if err = r.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, branchRef)); err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 
 | |
| 	g.repository = r
 | |
| 	return true, nil
 | |
| }
 | |
| 
 | |
| func (g *GoGit) Clone(ctx context.Context, url, branch string, caBundle []byte) (bool, error) {
 | |
| 	branchRef := plumbing.NewBranchReferenceName(branch)
 | |
| 	r, err := gogit.PlainCloneContext(ctx, g.path, false, &gogit.CloneOptions{
 | |
| 		URL:           url,
 | |
| 		Auth:          g.auth,
 | |
| 		RemoteName:    gogit.DefaultRemoteName,
 | |
| 		ReferenceName: branchRef,
 | |
| 		SingleBranch:  true,
 | |
| 
 | |
| 		NoCheckout: false,
 | |
| 		Progress:   nil,
 | |
| 		Tags:       gogit.NoTags,
 | |
| 		CABundle:   caBundle,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		if err == transport.ErrEmptyRemoteRepository || isRemoteBranchNotFoundErr(err, branchRef.String()) {
 | |
| 			return g.Init(url, branch)
 | |
| 		}
 | |
| 		return false, err
 | |
| 	}
 | |
| 
 | |
| 	g.repository = r
 | |
| 	return true, nil
 | |
| }
 | |
| 
 | |
| func (g *GoGit) Write(path string, reader io.Reader) error {
 | |
| 	if g.repository == nil {
 | |
| 		return git.ErrNoGitRepository
 | |
| 	}
 | |
| 
 | |
| 	wt, err := g.repository.Worktree()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	f, err := wt.Filesystem.Create(path)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	defer f.Close()
 | |
| 
 | |
| 	_, err = io.Copy(f, reader)
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| func (g *GoGit) Commit(message git.Commit, opts ...git.Option) (string, error) {
 | |
| 	if g.repository == nil {
 | |
| 		return "", git.ErrNoGitRepository
 | |
| 	}
 | |
| 
 | |
| 	wt, err := g.repository.Worktree()
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	status, err := wt.Status()
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	// apply the options
 | |
| 	options := &git.CommitOptions{}
 | |
| 	for _, opt := range opts {
 | |
| 		opt.ApplyToCommit(options)
 | |
| 	}
 | |
| 
 | |
| 	// go-git has [a bug](https://github.com/go-git/go-git/issues/253)
 | |
| 	// whereby it thinks broken symlinks to absolute paths are
 | |
| 	// modified. There's no circumstance in which we want to commit a
 | |
| 	// change to a broken symlink: so, detect and skip those.
 | |
| 	var changed bool
 | |
| 	for file, _ := range status {
 | |
| 		abspath := filepath.Join(g.path, file)
 | |
| 		info, err := os.Lstat(abspath)
 | |
| 		if err != nil {
 | |
| 			return "", fmt.Errorf("checking if %s is a symlink: %w", file, err)
 | |
| 		}
 | |
| 		if info.Mode()&os.ModeSymlink > 0 {
 | |
| 			// symlinks are OK; broken symlinks are probably a result
 | |
| 			// of the bug mentioned above, but not of interest in any
 | |
| 			// case.
 | |
| 			if _, err := os.Stat(abspath); os.IsNotExist(err) {
 | |
| 				continue
 | |
| 			}
 | |
| 		}
 | |
| 		_, _ = wt.Add(file)
 | |
| 		changed = true
 | |
| 	}
 | |
| 
 | |
| 	if !changed {
 | |
| 		head, err := g.repository.Head()
 | |
| 		if err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 		return head.Hash().String(), git.ErrNoStagedFiles
 | |
| 	}
 | |
| 
 | |
| 	commitOpts := &gogit.CommitOptions{
 | |
| 		Author: &object.Signature{
 | |
| 			Name:  message.Name,
 | |
| 			Email: message.Email,
 | |
| 			When:  time.Now(),
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	if options.GPGSigningInfo != nil {
 | |
| 		entity, err := getOpenPgpEntity(*options.GPGSigningInfo)
 | |
| 		if err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 
 | |
| 		commitOpts.SignKey = entity
 | |
| 	}
 | |
| 
 | |
| 	commit, err := wt.Commit(message.Message, commitOpts)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	return commit.String(), nil
 | |
| }
 | |
| 
 | |
| func (g *GoGit) Push(ctx context.Context, caBundle []byte) error {
 | |
| 	if g.repository == nil {
 | |
| 		return git.ErrNoGitRepository
 | |
| 	}
 | |
| 
 | |
| 	return g.repository.PushContext(ctx, &gogit.PushOptions{
 | |
| 		RemoteName: gogit.DefaultRemoteName,
 | |
| 		Auth:       g.auth,
 | |
| 		Progress:   nil,
 | |
| 		CABundle:   caBundle,
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func (g *GoGit) Status() (bool, error) {
 | |
| 	if g.repository == nil {
 | |
| 		return false, git.ErrNoGitRepository
 | |
| 	}
 | |
| 	wt, err := g.repository.Worktree()
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 	status, err := wt.Status()
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 	return status.IsClean(), nil
 | |
| }
 | |
| 
 | |
| func (g *GoGit) Head() (string, error) {
 | |
| 	if g.repository == nil {
 | |
| 		return "", git.ErrNoGitRepository
 | |
| 	}
 | |
| 	head, err := g.repository.Head()
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	return head.Hash().String(), nil
 | |
| }
 | |
| 
 | |
| func (g *GoGit) Path() string {
 | |
| 	return g.path
 | |
| }
 | |
| 
 | |
| func isRemoteBranchNotFoundErr(err error, ref string) bool {
 | |
| 	return strings.Contains(err.Error(), fmt.Sprintf("couldn't find remote ref %q", ref))
 | |
| }
 | |
| 
 | |
| func getOpenPgpEntity(info git.GPGSigningInfo) (*openpgp.Entity, error) {
 | |
| 	r, err := os.Open(info.KeyRingPath)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("unable to open GPG key ring: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	entityList, err := openpgp.ReadKeyRing(r)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	if len(entityList) == 0 {
 | |
| 		return nil, fmt.Errorf("empty GPG key ring")
 | |
| 	}
 | |
| 
 | |
| 	var entity *openpgp.Entity
 | |
| 	if info.KeyID != "" {
 | |
| 		for _, ent := range entityList {
 | |
| 			if ent.PrimaryKey.KeyIdString() == info.KeyID {
 | |
| 				entity = ent
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if entity == nil {
 | |
| 			return nil, fmt.Errorf("no GPG private key matching key id '%s' found", info.KeyID)
 | |
| 		}
 | |
| 	} else {
 | |
| 		entity = entityList[0]
 | |
| 	}
 | |
| 
 | |
| 	err = entity.PrivateKey.Decrypt([]byte(info.Passphrase))
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("unable to decrypt GPG private key: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	return entity, nil
 | |
| }
 | 
