mirror of https://github.com/harness/drone.git
[feat] command package - initial work (#993)
parent
87cae05747
commit
26c06b65a0
|
@ -25,9 +25,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/command"
|
||||
"github.com/harness/gitness/git/types"
|
||||
|
||||
gitea "code.gitea.io/gitea/modules/git"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -46,8 +45,11 @@ func (a Adapter) Blame(
|
|||
lineTo int,
|
||||
) types.BlameReader {
|
||||
// prepare the git command line arguments
|
||||
args := make([]string, 0, 8)
|
||||
args = append(args, "blame", "--porcelain", "--encoding=UTF-8")
|
||||
cmd := command.New(
|
||||
"blame",
|
||||
command.WithFlag("--porcelain"),
|
||||
command.WithFlag("--encoding", "UTF-8"),
|
||||
)
|
||||
if lineFrom > 0 || lineTo > 0 {
|
||||
var lines string
|
||||
if lineFrom > 0 {
|
||||
|
@ -57,9 +59,11 @@ func (a Adapter) Blame(
|
|||
lines += "," + strconv.Itoa(lineTo)
|
||||
}
|
||||
|
||||
args = append(args, "-L", lines)
|
||||
cmd.Add(command.WithFlag("-L", lines))
|
||||
}
|
||||
args = append(args, rev, "--", file)
|
||||
|
||||
cmd.Add(command.WithArg(rev))
|
||||
cmd.Add(command.WithPostSepArg(file))
|
||||
|
||||
pipeRead, pipeWrite := io.Pipe()
|
||||
stderr := &bytes.Buffer{}
|
||||
|
@ -71,12 +75,11 @@ func (a Adapter) Blame(
|
|||
_ = pipeWrite.CloseWithError(err)
|
||||
}()
|
||||
|
||||
cmd := gitea.NewCommand(ctx, args...)
|
||||
err = cmd.Run(&gitea.RunOpts{
|
||||
Dir: repoPath,
|
||||
Stdout: pipeWrite,
|
||||
Stderr: stderr, // We capture stderr output in a buffer.
|
||||
})
|
||||
err = cmd.Run(ctx,
|
||||
command.WithDir(repoPath),
|
||||
command.WithStdout(pipeWrite),
|
||||
command.WithStderr(stderr),
|
||||
)
|
||||
}()
|
||||
|
||||
return &BlameReader{
|
||||
|
|
|
@ -21,8 +21,6 @@ import (
|
|||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/types"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
)
|
||||
|
||||
// GetBlob returns the blob for the given object sha.
|
||||
|
@ -32,7 +30,7 @@ func (a Adapter) GetBlob(
|
|||
sha string,
|
||||
sizeLimit int64,
|
||||
) (*types.BlobReader, error) {
|
||||
stdIn, stdOut, cancel := git.CatFileBatch(ctx, repoPath)
|
||||
stdIn, stdOut, cancel := CatFileBatch(ctx, repoPath)
|
||||
|
||||
_, err := stdIn.Write([]byte(sha + "\n"))
|
||||
if err != nil {
|
||||
|
@ -40,7 +38,7 @@ func (a Adapter) GetBlob(
|
|||
return nil, fmt.Errorf("failed to write blob sha to git stdin: %w", err)
|
||||
}
|
||||
|
||||
objectSHA, objectType, objectSize, err := git.ReadBatchLine(stdOut)
|
||||
objectSHA, objectType, objectSize, err := ReadBatchHeaderLine(stdOut)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, processGiteaErrorf(err, "failed to read cat-file batch line")
|
||||
|
@ -50,10 +48,10 @@ func (a Adapter) GetBlob(
|
|||
cancel()
|
||||
return nil, fmt.Errorf("cat-file returned object sha '%s' but expected '%s'", objectSHA, sha)
|
||||
}
|
||||
if objectType != string(git.ObjectBlob) {
|
||||
if objectType != string(ObjectBlob) {
|
||||
cancel()
|
||||
return nil, errors.InvalidArgument(
|
||||
"cat-file returned object type '%s' but expected '%s'", objectType, git.ObjectBlob)
|
||||
"cat-file returned object type '%s' but expected '%s'", objectType, ObjectBlob)
|
||||
}
|
||||
|
||||
contentSize := objectSize
|
||||
|
|
|
@ -15,13 +15,13 @@
|
|||
package adapter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/harness/gitness/git/command"
|
||||
"github.com/harness/gitness/git/types"
|
||||
|
||||
gitea "code.gitea.io/gitea/modules/git"
|
||||
)
|
||||
|
||||
// GetBranch gets an existing branch.
|
||||
|
@ -62,11 +62,14 @@ func (a Adapter) HasBranches(
|
|||
}
|
||||
// repo has branches IFF there's at least one commit that is reachable via a branch
|
||||
// (every existing branch points to a commit)
|
||||
stdout, _, runErr := gitea.NewCommand(ctx, "rev-list", "--max-count", "1", "--branches").
|
||||
RunStdBytes(&gitea.RunOpts{Dir: repoPath})
|
||||
if runErr != nil {
|
||||
return false, processGiteaErrorf(runErr, "failed to trigger rev-list command")
|
||||
cmd := command.New("rev-list",
|
||||
command.WithFlag("--max-count", "1"),
|
||||
command.WithFlag("--branches"),
|
||||
)
|
||||
output := &bytes.Buffer{}
|
||||
if err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output)); err != nil {
|
||||
return false, processGiteaErrorf(err, "failed to trigger rev-list command")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(stdout)) == "", nil
|
||||
return strings.TrimSpace(output.String()) == "", nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// 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 adapter
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/command"
|
||||
|
||||
"github.com/djherbis/buffer"
|
||||
"github.com/djherbis/nio/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// WriteCloserError wraps an io.WriteCloser with an additional CloseWithError function.
|
||||
type WriteCloserError interface {
|
||||
io.WriteCloser
|
||||
CloseWithError(err error) error
|
||||
}
|
||||
|
||||
// CatFileBatch opens git cat-file --batch in the provided repo and returns a stdin pipe,
|
||||
// a stdout reader and cancel function.
|
||||
func CatFileBatch(
|
||||
ctx context.Context,
|
||||
repoPath string,
|
||||
) (WriteCloserError, *bufio.Reader, func()) {
|
||||
const bufferSize = 32 * 1024
|
||||
// We often want to feed the commits in order into cat-file --batch,
|
||||
// followed by their trees and sub trees as necessary.
|
||||
batchStdinReader, batchStdinWriter := io.Pipe()
|
||||
batchStdoutReader, batchStdoutWriter := nio.Pipe(buffer.New(bufferSize))
|
||||
ctx, ctxCancel := context.WithCancel(ctx)
|
||||
closed := make(chan struct{})
|
||||
cancel := func() {
|
||||
ctxCancel()
|
||||
_ = batchStdinWriter.Close()
|
||||
_ = batchStdoutReader.Close()
|
||||
<-closed
|
||||
}
|
||||
|
||||
// Ensure cancel is called as soon as the provided context is cancelled
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
stderr := bytes.Buffer{}
|
||||
cmd := command.New("cat-file", command.WithFlag("--batch"))
|
||||
err := cmd.Run(ctx,
|
||||
command.WithDir(repoPath),
|
||||
command.WithStdin(batchStdinReader),
|
||||
command.WithStdout(batchStdoutWriter),
|
||||
command.WithStderr(&stderr),
|
||||
)
|
||||
if err != nil {
|
||||
_ = batchStdoutWriter.CloseWithError(command.NewError(err, stderr.Bytes()))
|
||||
_ = batchStdinReader.CloseWithError(command.NewError(err, stderr.Bytes()))
|
||||
} else {
|
||||
_ = batchStdoutWriter.Close()
|
||||
_ = batchStdinReader.Close()
|
||||
}
|
||||
close(closed)
|
||||
}()
|
||||
|
||||
// For simplicities sake we'll us a buffered reader to read from the cat-file --batch
|
||||
batchReader := bufio.NewReaderSize(batchStdoutReader, bufferSize)
|
||||
|
||||
return batchStdinWriter, batchReader, cancel
|
||||
}
|
||||
|
||||
// ReadBatchHeaderLine reads the header line from cat-file --batch
|
||||
// We expect:
|
||||
// <sha> SP <type> SP <size> LF
|
||||
// sha is a 40byte not 20byte here.
|
||||
func ReadBatchHeaderLine(rd *bufio.Reader) (sha []byte, objType string, size int64, err error) {
|
||||
objType, err = rd.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, "", 0, err
|
||||
}
|
||||
if len(objType) == 1 {
|
||||
objType, err = rd.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, "", 0, err
|
||||
}
|
||||
}
|
||||
idx := strings.IndexByte(objType, ' ')
|
||||
if idx < 0 {
|
||||
log.Debug().Msgf("missing space type: %s", objType)
|
||||
err = errors.NotFound("sha '%s' not found", sha)
|
||||
return nil, "", 0, err
|
||||
}
|
||||
sha = []byte(objType[:idx])
|
||||
objType = objType[idx+1:]
|
||||
|
||||
idx = strings.IndexByte(objType, ' ')
|
||||
if idx < 0 {
|
||||
err = errors.NotFound("sha '%s' not found", sha)
|
||||
return nil, "", 0, err
|
||||
}
|
||||
|
||||
sizeStr := objType[idx+1 : len(objType)-1]
|
||||
objType = objType[:idx]
|
||||
|
||||
size, err = strconv.ParseInt(sizeStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, "", 0, err
|
||||
}
|
||||
return sha, objType, size, nil
|
||||
}
|
|
@ -23,6 +23,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git/command"
|
||||
"github.com/harness/gitness/git/types"
|
||||
|
||||
gitea "code.gitea.io/gitea/modules/git"
|
||||
|
@ -71,51 +72,50 @@ func (a Adapter) listCommitSHAs(
|
|||
limit int,
|
||||
filter types.CommitFilter,
|
||||
) ([]string, error) {
|
||||
args := make([]string, 0, 16)
|
||||
args = append(args, "rev-list")
|
||||
cmd := command.New("rev-list")
|
||||
|
||||
// return commits only up to a certain reference if requested
|
||||
if filter.AfterRef != "" {
|
||||
// ^REF tells the rev-list command to return only commits that aren't reachable by SHA
|
||||
args = append(args, fmt.Sprintf("^%s", filter.AfterRef))
|
||||
cmd.Add(command.WithArg(fmt.Sprintf("^%s", filter.AfterRef)))
|
||||
}
|
||||
// add refCommitSHA as starting point
|
||||
args = append(args, ref)
|
||||
cmd.Add(command.WithArg(ref))
|
||||
|
||||
if len(filter.Path) != 0 {
|
||||
args = append(args, "--", filter.Path)
|
||||
cmd.Add(command.WithPostSepArg(filter.Path))
|
||||
}
|
||||
|
||||
// add pagination if requested
|
||||
// TODO: we should add absolut limits to protect git (return error)
|
||||
if limit > 0 {
|
||||
args = append(args, "--max-count", fmt.Sprint(limit))
|
||||
cmd.Add(command.WithFlag("--max-count", strconv.Itoa(limit)))
|
||||
|
||||
if page > 1 {
|
||||
args = append(args, "--skip", fmt.Sprint((page-1)*limit))
|
||||
cmd.Add(command.WithFlag("--skip", strconv.Itoa((page-1)*limit)))
|
||||
}
|
||||
}
|
||||
|
||||
if filter.Since > 0 || filter.Until > 0 {
|
||||
args = append(args, "--date", "unix")
|
||||
cmd.Add(command.WithFlag("--date", "unix"))
|
||||
}
|
||||
if filter.Since > 0 {
|
||||
args = append(args, "--since", strconv.FormatInt(filter.Since, 10))
|
||||
cmd.Add(command.WithFlag("--since", strconv.FormatInt(filter.Since, 10)))
|
||||
}
|
||||
if filter.Until > 0 {
|
||||
args = append(args, "--until", strconv.FormatInt(filter.Until, 10))
|
||||
cmd.Add(command.WithFlag("--until", strconv.FormatInt(filter.Until, 10)))
|
||||
}
|
||||
if filter.Committer != "" {
|
||||
args = append(args, "--committer", filter.Committer)
|
||||
cmd.Add(command.WithFlag("--committer", filter.Committer))
|
||||
}
|
||||
|
||||
stdout, _, runErr := gitea.NewCommand(ctx, args...).RunStdBytes(&gitea.RunOpts{Dir: repoPath})
|
||||
if runErr != nil {
|
||||
output := &bytes.Buffer{}
|
||||
err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output))
|
||||
if err != nil {
|
||||
// TODO: handle error in case they don't have a common merge base!
|
||||
return nil, processGiteaErrorf(runErr, "failed to trigger rev-list command")
|
||||
return nil, processGiteaErrorf(err, "failed to trigger rev-list command")
|
||||
}
|
||||
|
||||
return parseLinesToSlice(stdout), nil
|
||||
return parseLinesToSlice(output.Bytes()), nil
|
||||
}
|
||||
|
||||
// ListCommitSHAs lists the commits reachable from ref.
|
||||
|
@ -243,13 +243,18 @@ func giteaGetRenameDetails(
|
|||
ref string,
|
||||
path string,
|
||||
) (*types.PathRenameDetails, error) {
|
||||
stdout, _, runErr := gitea.NewCommand(giteaRepo.Ctx, "log", ref, "--name-status", "--pretty=format:", "-1").
|
||||
RunStdBytes(&gitea.RunOpts{Dir: giteaRepo.Path})
|
||||
if runErr != nil {
|
||||
return nil, fmt.Errorf("failed to trigger log command: %w", runErr)
|
||||
cmd := command.New("log",
|
||||
command.WithArg(ref),
|
||||
command.WithFlag("--name-status"),
|
||||
command.WithFlag("--pretty=format:", "-1"),
|
||||
)
|
||||
output := &bytes.Buffer{}
|
||||
err := cmd.Run(giteaRepo.Ctx, command.WithDir(giteaRepo.Path), command.WithStdout(output))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to trigger log command: %w", err)
|
||||
}
|
||||
|
||||
lines := parseLinesToSlice(stdout)
|
||||
lines := parseLinesToSlice(output.Bytes())
|
||||
|
||||
changeType, oldPath, newPath, err := getFileChangeTypeFromLog(lines, path)
|
||||
if err != nil {
|
||||
|
@ -303,7 +308,18 @@ func (a Adapter) GetFullCommitID(
|
|||
if repoPath == "" {
|
||||
return "", ErrRepositoryPathEmpty
|
||||
}
|
||||
return gitea.GetFullCommitID(ctx, repoPath, shortID)
|
||||
cmd := command.New("rev-parse",
|
||||
command.WithArg(shortID),
|
||||
)
|
||||
output := &bytes.Buffer{}
|
||||
err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "exit status 128") {
|
||||
return "", errors.NotFound("commit not found %s", shortID)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(output.String()), nil
|
||||
}
|
||||
|
||||
// GetCommits returns the (latest) commits for a specific list of refs.
|
||||
|
@ -462,19 +478,25 @@ func getCommit(
|
|||
fmtSubject + fmtZero + // 7
|
||||
fmtBody // 8
|
||||
|
||||
args := []string{"log", "--max-count=1", "--format=" + format, rev}
|
||||
cmd := command.New("log",
|
||||
command.WithFlag("--max-count", "1"),
|
||||
command.WithFlag("--format="+format),
|
||||
command.WithArg(rev),
|
||||
)
|
||||
if path != "" {
|
||||
args = append(args, "--", path)
|
||||
}
|
||||
|
||||
commitLine, stderr, err := gitea.NewCommand(ctx, args...).RunStdString(&gitea.RunOpts{Dir: repoPath})
|
||||
if strings.Contains(stderr, "ambiguous argument") {
|
||||
return nil, errors.NotFound("revision %q not found", rev)
|
||||
cmd.Add(command.WithPostSepArg(path))
|
||||
}
|
||||
output := &bytes.Buffer{}
|
||||
err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output))
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "ambiguous argument") {
|
||||
return nil, errors.NotFound("revision %q not found", rev)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to run git to get commit data: %w", err)
|
||||
}
|
||||
|
||||
commitLine := output.String()
|
||||
|
||||
if commitLine == "" {
|
||||
return nil, errors.InvalidArgument("path %q not found in %s", path, rev)
|
||||
}
|
||||
|
|
|
@ -20,8 +20,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"github.com/harness/gitness/git/command"
|
||||
)
|
||||
|
||||
// Config set local git key and value configuration.
|
||||
|
@ -38,12 +37,15 @@ func (a Adapter) Config(
|
|||
return errors.InvalidArgument("key cannot be empty")
|
||||
}
|
||||
var outbuf, errbuf strings.Builder
|
||||
if err := git.NewCommand(ctx, "config", "--local").AddArguments(key, value).
|
||||
Run(&git.RunOpts{
|
||||
Dir: repoPath,
|
||||
Stdout: &outbuf,
|
||||
Stderr: &errbuf,
|
||||
}); err != nil {
|
||||
cmd := command.New("config",
|
||||
command.WithFlag("--local"),
|
||||
command.WithArg(key, value),
|
||||
)
|
||||
err := cmd.Run(ctx, command.WithDir(repoPath),
|
||||
command.WithStdout(&outbuf),
|
||||
command.WithStderr(&errbuf),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("git config [%s -> <%s> ]: %w\n%s\n%s",
|
||||
key, value, err, outbuf.String(), errbuf.String())
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// 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 adapter
|
||||
|
||||
// ObjectType git object type.
|
||||
type ObjectType string
|
||||
|
||||
const (
|
||||
// ObjectCommit commit object type.
|
||||
ObjectCommit ObjectType = "commit"
|
||||
// ObjectTree tree object type.
|
||||
ObjectTree ObjectType = "tree"
|
||||
// ObjectBlob blob object type.
|
||||
ObjectBlob ObjectType = "blob"
|
||||
// ObjectTag tag object type.
|
||||
ObjectTag ObjectType = "tag"
|
||||
// ObjectBranch branch object type.
|
||||
ObjectBranch ObjectType = "branch"
|
||||
)
|
||||
|
||||
// Bytes returns the byte array for the Object Type.
|
||||
func (o ObjectType) Bytes() []byte {
|
||||
return []byte(o)
|
||||
}
|
|
@ -17,10 +17,12 @@ package adapter
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/git/command"
|
||||
"github.com/harness/gitness/git/types"
|
||||
|
||||
gitea "code.gitea.io/gitea/modules/git"
|
||||
|
@ -44,7 +46,16 @@ func (a Adapter) InitRepository(
|
|||
if repoPath == "" {
|
||||
return ErrRepositoryPathEmpty
|
||||
}
|
||||
return gitea.InitRepository(ctx, repoPath, bare)
|
||||
err := os.MkdirAll(repoPath, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create directory '%s', err: %w", repoPath, err)
|
||||
}
|
||||
|
||||
cmd := command.New("init")
|
||||
if bare {
|
||||
cmd.Add(command.WithFlag("--bare"))
|
||||
}
|
||||
return cmd.Run(ctx, command.WithDir(repoPath))
|
||||
}
|
||||
|
||||
func (a Adapter) OpenRepository(
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// 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 command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
GitExecutable = "git"
|
||||
|
||||
actionRegex = regexp.MustCompile(`^[[:alnum:]]+[-[:alnum:]]*$`)
|
||||
)
|
||||
|
||||
// Command contains options for running a git command.
|
||||
type Command struct {
|
||||
// Name is the name of the Git command to run, e.g. "log", "cat-file" or "worktree".
|
||||
Name string
|
||||
|
||||
// Action is the action of the Git command, e.g. "set-url" in `git remote set-url`
|
||||
Action string
|
||||
|
||||
// Flags is the number of optional flags to pass before positional arguments, e.g.
|
||||
// `--oneline` or `--format=fuller`.
|
||||
Flags []string
|
||||
|
||||
// Args is the arguments that shall be passed after all flags. These arguments must not be
|
||||
// flags and thus cannot start with `-`. Note that it may be unsafe to use this field in the
|
||||
// case where arguments are directly user-controlled. In that case it is advisable to use
|
||||
// `PostSepArgs` instead.
|
||||
Args []string
|
||||
|
||||
// PostSepArgs is the arguments that shall be passed as positional arguments after the `--`
|
||||
// separator. Git recognizes that separator as the point where it should stop expecting any
|
||||
// options and treat the remaining arguments as positionals. This should be used when
|
||||
// passing user-controlled input of arbitrary form like for example paths, which may start
|
||||
// with a `-`.
|
||||
PostSepArgs []string
|
||||
|
||||
// Git environment variables
|
||||
Envs Envs
|
||||
|
||||
// internal counter for GIT_CONFIG_COUNT environment variable.
|
||||
// more info: [link](https://git-scm.com/docs/git-config#Documentation/git-config.txt-GITCONFIGCOUNT)
|
||||
configEnvCounter int
|
||||
}
|
||||
|
||||
// New creates new command for interacting with the git process.
|
||||
func New(name string, options ...CmdOptionFunc) *Command {
|
||||
c := &Command{
|
||||
Name: name,
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
opt(c)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Add appends given options to the command.
|
||||
func (c *Command) Add(options ...CmdOptionFunc) *Command {
|
||||
for _, opt := range options {
|
||||
opt(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Run executes the git command with optional configuration using WithXxx functions.
|
||||
func (c *Command) Run(ctx context.Context, opts ...RunOptionFunc) (err error) {
|
||||
options := &RunOption{}
|
||||
for _, f := range opts {
|
||||
f(options)
|
||||
}
|
||||
|
||||
if options.Stdout == nil {
|
||||
options.Stdout = io.Discard
|
||||
}
|
||||
errAsBuff := false
|
||||
if options.Stderr == nil {
|
||||
options.Stderr = new(bytes.Buffer)
|
||||
errAsBuff = true
|
||||
}
|
||||
|
||||
args, err := c.makeArgs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build argument list: %w", err)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, GitExecutable, args...)
|
||||
if len(c.Envs) > 0 {
|
||||
cmd.Env = c.Envs.Args()
|
||||
}
|
||||
cmd.Dir = options.Dir
|
||||
cmd.Stdin = options.Stdin
|
||||
cmd.Stdout = options.Stdout
|
||||
cmd.Stderr = options.Stderr
|
||||
if err = cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result := make(chan error)
|
||||
go func() {
|
||||
result <- cmd.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
<-result
|
||||
if cmd.Process != nil && cmd.ProcessState != nil && !cmd.ProcessState.Exited() {
|
||||
if err := cmd.Process.Kill(); err != nil && !errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return fmt.Errorf("kill process: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.Err()
|
||||
case err = <-result:
|
||||
if err != nil && errAsBuff {
|
||||
buff, ok := options.Stderr.(*bytes.Buffer)
|
||||
if ok {
|
||||
return NewError(err, buff.Bytes())
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Command) makeArgs() ([]string, error) {
|
||||
var safeArgs []string
|
||||
|
||||
commandDescription, ok := descriptions[c.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid sub command name %q: %w", c.Name, ErrInvalidArg)
|
||||
}
|
||||
safeArgs = append(safeArgs, c.Name)
|
||||
|
||||
if c.Action != "" {
|
||||
if !actionRegex.MatchString(c.Action) {
|
||||
return nil, fmt.Errorf("invalid action %q: %w", c.Action, ErrInvalidArg)
|
||||
}
|
||||
safeArgs = append(safeArgs, c.Action)
|
||||
}
|
||||
|
||||
commandArgs, err := commandDescription.args(c.Flags, c.Args, c.PostSepArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
safeArgs = append(safeArgs, commandArgs...)
|
||||
|
||||
return safeArgs, nil
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// 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 command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/harness/gitness/errors"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func TestCreateBareRepository(t *testing.T) {
|
||||
cmd := New("init", WithFlag("--bare"), WithArg("samplerepo"))
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
err := cmd.Run(ctx)
|
||||
defer os.RemoveAll("samplerepo")
|
||||
if err != nil {
|
||||
t.Errorf("expected: %v error, got: %v", nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
cmd = New("rev-parse", WithFlag("--is-bare-repository"))
|
||||
output := &bytes.Buffer{}
|
||||
err = cmd.Run(context.Background(), WithDir("samplerepo"), WithStdout(output))
|
||||
if err != nil {
|
||||
t.Errorf("expected: %v error, got: %v", nil, err)
|
||||
return
|
||||
}
|
||||
got := strings.TrimSpace(output.String())
|
||||
exp := "true"
|
||||
if got != exp {
|
||||
t.Errorf("expected value: %s, got: %s", exp, got)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandContextTimeout(t *testing.T) {
|
||||
cmd := New("init", WithFlag("--bare"), WithArg("samplerepo"))
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := cmd.Run(ctx)
|
||||
defer os.RemoveAll("samplerepo")
|
||||
if err != nil {
|
||||
t.Errorf("expected: %v error, got: %v", nil, err)
|
||||
}
|
||||
|
||||
inbuff := &bytes.Buffer{}
|
||||
inbuff.WriteString("some content")
|
||||
outbuffer := &bytes.Buffer{}
|
||||
|
||||
cmd = New("hash-object", WithFlag("--stdin"))
|
||||
err = cmd.Run(ctx,
|
||||
WithDir("./samplerepo"),
|
||||
WithStdin(inbuff),
|
||||
WithStdout(outbuffer),
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("hashing object failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().Msgf("outbuffer %s", outbuffer.String())
|
||||
|
||||
cmd = New("cat-file", WithFlag("--batch"))
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
defer pr.Close()
|
||||
|
||||
outbuffer.Reset()
|
||||
|
||||
go func() {
|
||||
defer pw.Close()
|
||||
for i := 0; i < 3; i++ {
|
||||
_, _ = pw.Write(outbuffer.Bytes())
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}()
|
||||
|
||||
runCtx, runCancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer runCancel()
|
||||
|
||||
err = cmd.Run(runCtx,
|
||||
WithDir("./samplerepo"),
|
||||
WithStdin(pr),
|
||||
WithStdout(outbuffer),
|
||||
)
|
||||
|
||||
if !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Errorf("expected: %v error, got: %v", context.DeadlineExceeded, err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// 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 command
|
||||
|
||||
const (
|
||||
GitCommitterName = "GIT_COMMITTER_NAME"
|
||||
GitCommitterEmail = "GIT_COMMITTER_EMAIL"
|
||||
GitAuthorName = "GIT_AUTHOR_NAME"
|
||||
GitAuthorEmail = "GIT_AUTHOR_EMAIL"
|
||||
|
||||
GitTrace = "GIT_TRACE"
|
||||
GitTracePack = "GIT_TRACE_PACK_ACCESS"
|
||||
GitTracePackAccess = "GIT_TRACE_PACKET"
|
||||
GitTracePerformance = "GIT_TRACE_PERFORMANCE"
|
||||
GitTraceSetup = "GIT_TRACE_SETUP"
|
||||
GitExecPath = "GIT_EXEC_PATH" // tells Git where to find its binaries.
|
||||
)
|
||||
|
||||
// Envs custom key value store for environment variables.
|
||||
type Envs map[string]string
|
||||
|
||||
func (e Envs) Args() []string {
|
||||
slice := make([]string, 0, len(e))
|
||||
for key, val := range e {
|
||||
slice = append(slice, key+"="+val)
|
||||
}
|
||||
return slice
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// 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 command
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEnvs_Args(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
e Envs
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "test envs",
|
||||
e: Envs{
|
||||
"GIT_TRACE": "true",
|
||||
},
|
||||
want: []string{"GIT_TRACE=true"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.e.Args(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Args() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// 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 command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidArg represent family of errors to report about bad argument used to make a call.
|
||||
ErrInvalidArg = errors.New("invalid argument")
|
||||
)
|
||||
|
||||
// Error type with optional Stderr payload.
|
||||
type Error struct {
|
||||
Err error
|
||||
StdErr []byte
|
||||
}
|
||||
|
||||
// NewError creates error with source err and stderr payload.
|
||||
func NewError(err error, stderr []byte) *Error {
|
||||
return &Error{
|
||||
Err: err,
|
||||
StdErr: stderr,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
if len(e.StdErr) != 0 {
|
||||
return fmt.Sprintf("%s: %s", e.Err.Error(), e.StdErr)
|
||||
}
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
// AsError unwraps Error otherwise return nil.
|
||||
func AsError(err error) (e *Error) {
|
||||
if errors.As(err, &e) {
|
||||
return
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,280 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// 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 command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// NoRefUpdates denotes a command which will never update refs.
|
||||
NoRefUpdates = 1 << iota
|
||||
// NoEndOfOptions denotes a command which doesn't know --end-of-options.
|
||||
NoEndOfOptions
|
||||
)
|
||||
|
||||
type description struct {
|
||||
flags uint
|
||||
validatePositionalArgs func([]string) error
|
||||
}
|
||||
|
||||
// supportsEndOfOptions indicates whether a command can handle the
|
||||
// `--end-of-options` option.
|
||||
func (c description) supportsEndOfOptions() bool {
|
||||
return c.flags&NoEndOfOptions == 0
|
||||
}
|
||||
|
||||
// descriptions is a curated list of Git command descriptions.
|
||||
var descriptions = map[string]description{
|
||||
"am": {},
|
||||
"apply": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"archive": {
|
||||
// git-archive(1) does not support disambiguating options from paths from revisions.
|
||||
flags: NoRefUpdates | NoEndOfOptions,
|
||||
},
|
||||
"blame": {
|
||||
// git-blame(1) does not support disambiguating options from paths from revisions.
|
||||
flags: NoRefUpdates | NoEndOfOptions,
|
||||
},
|
||||
"bundle": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"cat-file": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"check-attr": {
|
||||
flags: NoRefUpdates | NoEndOfOptions,
|
||||
},
|
||||
"check-ref-format": {
|
||||
// git-check-ref-format(1) uses a hand-rolled option parser which doesn't support
|
||||
// `--end-of-options`.
|
||||
flags: NoRefUpdates | NoEndOfOptions,
|
||||
},
|
||||
"checkout": {
|
||||
// git-checkout(1) does not support disambiguating options from paths from
|
||||
// revisions.
|
||||
flags: NoEndOfOptions,
|
||||
},
|
||||
"clone": {},
|
||||
"commit": {
|
||||
flags: 0,
|
||||
},
|
||||
"commit-graph": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"commit-tree": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"config": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"count-objects": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"diff": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"diff-tree": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"fetch": {
|
||||
flags: 0,
|
||||
},
|
||||
"for-each-ref": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"format-patch": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"fsck": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"gc": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"grep": {
|
||||
// git-grep(1) does not support disambiguating options from paths from
|
||||
// revisions.
|
||||
flags: NoRefUpdates | NoEndOfOptions,
|
||||
},
|
||||
"hash-object": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"index-pack": {
|
||||
flags: NoRefUpdates | NoEndOfOptions,
|
||||
},
|
||||
"init": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"log": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"ls-remote": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"ls-tree": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"merge-base": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"merge-file": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"merge-tree": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"mktag": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"mktree": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"multi-pack-index": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"pack-refs": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"pack-objects": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"patch-id": {
|
||||
flags: NoRefUpdates | NoEndOfOptions,
|
||||
},
|
||||
"prune": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"prune-packed": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"push": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"receive-pack": {
|
||||
flags: 0,
|
||||
},
|
||||
"remote": {
|
||||
// While git-remote(1)'s `add` subcommand does support `--end-of-options`,
|
||||
// `remove` doesn't.
|
||||
flags: NoEndOfOptions,
|
||||
},
|
||||
"repack": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"rev-list": {
|
||||
// We cannot use --end-of-options here because pseudo revisions like `--all`
|
||||
// and `--not` count as options.
|
||||
flags: NoRefUpdates | NoEndOfOptions,
|
||||
validatePositionalArgs: func(args []string) error {
|
||||
for _, arg := range args {
|
||||
// git-rev-list(1) supports pseudo-revision arguments which can be
|
||||
// intermingled with normal positional arguments. Given that these
|
||||
// pseudo-revisions have leading dashes, normal validation would
|
||||
// refuse them as positional arguments. We thus override validation
|
||||
// for two of these which we are using in our codebase. There are
|
||||
// more, but we can add them at a later point if they're ever
|
||||
// required.
|
||||
if arg == "--all" || arg == "--not" {
|
||||
continue
|
||||
}
|
||||
if err := validatePositionalArg(arg); err != nil {
|
||||
return fmt.Errorf("rev-list: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
"rev-parse": {
|
||||
// --end-of-options is echoed by git-rev-parse(1) if used without
|
||||
// `--verify`.
|
||||
flags: NoRefUpdates | NoEndOfOptions,
|
||||
},
|
||||
"show": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"show-ref": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"symbolic-ref": {
|
||||
flags: 0,
|
||||
},
|
||||
"tag": {
|
||||
flags: 0,
|
||||
},
|
||||
"unpack-objects": {
|
||||
flags: NoRefUpdates | NoEndOfOptions,
|
||||
},
|
||||
"update-ref": {
|
||||
flags: 0,
|
||||
},
|
||||
"upload-archive": {
|
||||
// git-upload-archive(1) has a handrolled parser which always interprets the
|
||||
// first argument as directory, so we cannot use `--end-of-options`.
|
||||
flags: NoRefUpdates | NoEndOfOptions,
|
||||
},
|
||||
"upload-pack": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"version": {
|
||||
flags: NoRefUpdates,
|
||||
},
|
||||
"worktree": {
|
||||
flags: 0,
|
||||
},
|
||||
}
|
||||
|
||||
// args validates the given flags and arguments and, if valid, returns the complete command line.
|
||||
func (c description) args(flags []string, args []string, postSepArgs []string) ([]string, error) {
|
||||
var cmdArgs []string
|
||||
|
||||
cmdArgs = append(cmdArgs, flags...)
|
||||
|
||||
if c.supportsEndOfOptions() {
|
||||
cmdArgs = append(cmdArgs, "--end-of-options")
|
||||
}
|
||||
|
||||
if c.validatePositionalArgs != nil {
|
||||
if err := c.validatePositionalArgs(args); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
for _, a := range args {
|
||||
if err := validatePositionalArg(a); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
cmdArgs = append(cmdArgs, args...)
|
||||
|
||||
if len(postSepArgs) > 0 {
|
||||
cmdArgs = append(cmdArgs, "--")
|
||||
}
|
||||
|
||||
// post separator args do not need any validation
|
||||
cmdArgs = append(cmdArgs, postSepArgs...)
|
||||
|
||||
return cmdArgs, nil
|
||||
}
|
||||
|
||||
func validatePositionalArg(arg string) error {
|
||||
if strings.HasPrefix(arg, "-") {
|
||||
return fmt.Errorf("positional arg %q cannot start with dash '-': %w", arg, ErrInvalidArg)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// 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 command
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type CmdOptionFunc func(c *Command)
|
||||
|
||||
// WithAction set the action of the Git command, e.g. "set-url" in `git remote set-url`.
|
||||
func WithAction(action string) CmdOptionFunc {
|
||||
return func(c *Command) {
|
||||
c.Action = action
|
||||
}
|
||||
}
|
||||
|
||||
// WithFlag set optional flags to pass before positional arguments.
|
||||
func WithFlag(flags ...string) CmdOptionFunc {
|
||||
return func(c *Command) {
|
||||
c.Flags = append(c.Flags, flags...)
|
||||
}
|
||||
}
|
||||
|
||||
// WithArg add arguments that shall be passed after all flags.
|
||||
func WithArg(args ...string) CmdOptionFunc {
|
||||
return func(c *Command) {
|
||||
c.Args = append(c.Args, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// WithPostSepArg set arguments that shall be passed as positional arguments after the `--`.
|
||||
func WithPostSepArg(args ...string) CmdOptionFunc {
|
||||
return func(c *Command) {
|
||||
c.PostSepArgs = append(c.PostSepArgs, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// WithEnv sets environment variable using key value pair
|
||||
// for example: WithEnv("GIT_TRACE", "true").
|
||||
func WithEnv(keyValPairs ...string) CmdOptionFunc {
|
||||
return func(c *Command) {
|
||||
for i := 0; i < len(keyValPairs); i += 2 {
|
||||
k, v := keyValPairs[i], keyValPairs[i+1]
|
||||
c.Envs[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithCommitter sets given committer to the command.
|
||||
func WithCommitter(name, email string) CmdOptionFunc {
|
||||
return func(c *Command) {
|
||||
c.Envs[GitCommitterName] = name
|
||||
c.Envs[GitCommitterEmail] = email
|
||||
}
|
||||
}
|
||||
|
||||
// WithAuthor sets given author to the command.
|
||||
func WithAuthor(name, email string) CmdOptionFunc {
|
||||
return func(c *Command) {
|
||||
c.Envs[GitAuthorName] = name
|
||||
c.Envs[GitAuthorEmail] = email
|
||||
}
|
||||
}
|
||||
|
||||
// WithConfig function sets key and value for config command.
|
||||
func WithConfig(key, value string) CmdOptionFunc {
|
||||
return func(c *Command) {
|
||||
c.Envs["GIT_CONFIG_KEY_"+strconv.Itoa(c.configEnvCounter)] = key
|
||||
c.Envs["GIT_CONFIG_VALUE_"+strconv.Itoa(c.configEnvCounter)] = value
|
||||
c.configEnvCounter++
|
||||
c.Envs["GIT_CONFIG_COUNT"] = strconv.Itoa(c.configEnvCounter)
|
||||
}
|
||||
}
|
||||
|
||||
// RunOption contains option for running a command.
|
||||
type RunOption struct {
|
||||
// Dir is location of repo.
|
||||
Dir string
|
||||
// Stdin is the input to the command.
|
||||
Stdin io.Reader
|
||||
// Stdout is the outputs from the command.
|
||||
Stdout io.Writer
|
||||
// Stderr is the error output from the command.
|
||||
Stderr io.Writer
|
||||
}
|
||||
|
||||
type RunOptionFunc func(option *RunOption)
|
||||
|
||||
// WithDir set directory RunOption.Dir, this is repository dir
|
||||
// where git command should be running.
|
||||
func WithDir(dir string) RunOptionFunc {
|
||||
return func(option *RunOption) {
|
||||
option.Dir = dir
|
||||
}
|
||||
}
|
||||
|
||||
// WithStdin set RunOption.Stdin reader.
|
||||
func WithStdin(stdin io.Reader) RunOptionFunc {
|
||||
return func(option *RunOption) {
|
||||
option.Stdin = stdin
|
||||
}
|
||||
}
|
||||
|
||||
// WithStdout set RunOption.Stdout writer.
|
||||
func WithStdout(stdout io.Writer) RunOptionFunc {
|
||||
return func(option *RunOption) {
|
||||
option.Stdout = stdout
|
||||
}
|
||||
}
|
||||
|
||||
// WithStderr set RunOption.Stderr writer.
|
||||
func WithStderr(stderr io.Writer) RunOptionFunc {
|
||||
return func(option *RunOption) {
|
||||
option.Stderr = stderr
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue