mirror of
https://github.com/thegeeklab/wp-git-clone.git
synced 2024-11-09 17:50:39 +00:00
208 lines
5.1 KiB
Go
208 lines
5.1 KiB
Go
|
// Copyright (c) 2023, Robert Kaussow <mail@thegeeklab.de>
|
||
|
|
||
|
// Use of this source code is governed by an Apache 2.0 license that can be
|
||
|
// found in the LICENSE file.
|
||
|
|
||
|
package plugin
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"context"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/cenkalti/backoff/v4"
|
||
|
"github.com/rs/zerolog/log"
|
||
|
"github.com/thegeeklab/wp-git-clone/git"
|
||
|
"github.com/thegeeklab/wp-plugin-go/types"
|
||
|
"golang.org/x/sys/execabs"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
daemonBackoffMaxRetries = 3
|
||
|
daemonBackoffInitialInterval = 2 * time.Second
|
||
|
daemonBackoffMultiplier = 3.5
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
ErrGitCloneDestintionNotValid = errors.New("destination not valid")
|
||
|
ErrTypeAssertionFailed = errors.New("type assertion failed")
|
||
|
)
|
||
|
|
||
|
//nolint:revive
|
||
|
func (p *Plugin) run(ctx context.Context) error {
|
||
|
if err := p.FlagsFromContext(); err != nil {
|
||
|
return fmt.Errorf("validation failed: %w", err)
|
||
|
}
|
||
|
|
||
|
if err := p.Validate(); err != nil {
|
||
|
return fmt.Errorf("validation failed: %w", err)
|
||
|
}
|
||
|
|
||
|
if err := p.Execute(); err != nil {
|
||
|
return fmt.Errorf("execution failed: %w", err)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Validate handles the settings validation of the plugin.
|
||
|
func (p *Plugin) Validate() error {
|
||
|
if p.Settings.WorkDir == "" {
|
||
|
var err error
|
||
|
if p.Settings.WorkDir, err = os.Getwd(); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if p.Settings.Pipeline.Event == "tag" && !p.Settings.Tags {
|
||
|
// tags clone not explicit set but pipeline is triggered by a tag
|
||
|
// auto set tags cloning to true
|
||
|
p.Settings.Tags = true
|
||
|
}
|
||
|
|
||
|
if p.Settings.Tags && p.Settings.Partial {
|
||
|
log.Warn().Msg("ignore partial clone as tags are fetched")
|
||
|
|
||
|
// if tag fetching is enabled per event or setting, disable partial clone
|
||
|
p.Settings.Partial = false
|
||
|
}
|
||
|
|
||
|
if p.Settings.Partial {
|
||
|
p.Settings.Depth = 1
|
||
|
p.Settings.Filter = "tree:0"
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Execute provides the implementation of the plugin.
|
||
|
func (p *Plugin) Execute() error {
|
||
|
cmds := make([]*execabs.Cmd, 0)
|
||
|
|
||
|
if err := os.Setenv("GIT_TERMINAL_PROMPT", "0"); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
// prevents git-lfs from retrieving any LFS files
|
||
|
if err := os.Setenv("GIT_LFS_SKIP_SMUDGE", "1"); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Handle init
|
||
|
initPath := filepath.Join(p.Settings.WorkDir, ".git")
|
||
|
|
||
|
if err := os.MkdirAll(p.Settings.WorkDir, os.ModePerm); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
//nolint:nestif
|
||
|
if _, err := os.Stat(initPath); os.IsNotExist(err) {
|
||
|
if err := p.execCmd(git.Init(p.Settings.Repo), new(bytes.Buffer)); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if p.Settings.UseSSH {
|
||
|
cmds = append(cmds, git.RemoteAdd(p.Settings.Repo.RemoteSSH))
|
||
|
if p.Settings.SSHKey != "" {
|
||
|
cmds = append(cmds, git.ConfigSSHCommand(p.Settings.SSHKey))
|
||
|
}
|
||
|
} else {
|
||
|
cmds = append(cmds, git.RemoteAdd(p.Settings.Repo.RemoteURL))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
cmds = append(cmds, git.ConfigSSLVerify(p.Settings.Repo))
|
||
|
|
||
|
if err := git.WriteNetrc(p.Settings.Netrc.Machine, p.Settings.Netrc.Login, p.Settings.Netrc.Password); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Handle clone
|
||
|
|
||
|
if p.Settings.Repo.CommitSha == "" {
|
||
|
// fetch and checkout by ref
|
||
|
log.Info().Msg("no commit information: using head checkout")
|
||
|
|
||
|
cmds = append(cmds, git.FetchSource(p.Settings.Repo.CommitRef, p.Settings.Tags, p.Settings.Depth, p.Settings.Filter))
|
||
|
cmds = append(cmds, git.CheckoutHead())
|
||
|
} else {
|
||
|
cmds = append(cmds, git.FetchSource(p.Settings.Repo.CommitSha, p.Settings.Tags, p.Settings.Depth, p.Settings.Filter))
|
||
|
cmds = append(cmds, git.CheckoutSha(p.Settings.Repo))
|
||
|
}
|
||
|
|
||
|
for name, submoduleURL := range p.Settings.Repo.Submodules {
|
||
|
cmds = append(cmds, git.ConfigRemapSubmodule(name, submoduleURL))
|
||
|
}
|
||
|
|
||
|
if p.Settings.Recursive {
|
||
|
cmds = append(cmds, git.SubmoduleUpdate(p.Settings.Repo))
|
||
|
}
|
||
|
|
||
|
if p.Settings.Lfs {
|
||
|
cmds = append(cmds, git.FetchLFS())
|
||
|
cmds = append(cmds, git.CheckoutLFS())
|
||
|
}
|
||
|
|
||
|
for _, cmd := range cmds {
|
||
|
log.Debug().Msgf("+ %s", strings.Join(cmd.Args, " "))
|
||
|
|
||
|
buf := new(bytes.Buffer)
|
||
|
err := p.execCmd(cmd, buf)
|
||
|
|
||
|
switch {
|
||
|
case err != nil && shouldRetry(buf.String()):
|
||
|
backoffOps := func() error {
|
||
|
// copy the original command
|
||
|
//nolint:gosec
|
||
|
retry := execabs.Command(cmd.Args[0], cmd.Args[1:]...)
|
||
|
retry.Dir = cmd.Dir
|
||
|
retry.Env = cmd.Env
|
||
|
retry.Stdout = os.Stdout
|
||
|
retry.Stderr = os.Stderr
|
||
|
|
||
|
trace(cmd)
|
||
|
|
||
|
return cmd.Run()
|
||
|
}
|
||
|
backoffLog := func(err error, delay time.Duration) {
|
||
|
log.Error().Msgf("failed to find remote ref: %v: retry in %s", err, delay.Truncate(time.Second))
|
||
|
}
|
||
|
|
||
|
if err := backoff.RetryNotify(backoffOps, newBackoff(daemonBackoffMaxRetries), backoffLog); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
case err != nil:
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (p *Plugin) FlagsFromContext() error {
|
||
|
submodules, ok := p.Context.Generic("submodule-override").(*types.MapFlag)
|
||
|
if !ok {
|
||
|
return fmt.Errorf("%w: failed to read submodule-override input", ErrTypeAssertionFailed)
|
||
|
}
|
||
|
|
||
|
p.Settings.Repo.Submodules = submodules.Get()
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (p *Plugin) execCmd(cmd *execabs.Cmd, buf *bytes.Buffer) error {
|
||
|
cmd.Env = os.Environ()
|
||
|
cmd.Stdout = io.MultiWriter(os.Stdout, buf)
|
||
|
cmd.Stderr = io.MultiWriter(os.Stderr, buf)
|
||
|
cmd.Dir = p.Settings.WorkDir
|
||
|
|
||
|
fmt.Println(cmd.Dir)
|
||
|
|
||
|
return cmd.Run()
|
||
|
}
|