mirror of https://github.com/fluxcd/flux2.git
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
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
|
|
}
|