mirror of
https://github.com/harness/drone.git
synced 2025-05-04 14:43:15 +08:00
[feat] command package - initial work (#993)
This commit is contained in:
parent
87cae05747
commit
26c06b65a0
@ -25,9 +25,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/harness/gitness/errors"
|
"github.com/harness/gitness/errors"
|
||||||
|
"github.com/harness/gitness/git/command"
|
||||||
"github.com/harness/gitness/git/types"
|
"github.com/harness/gitness/git/types"
|
||||||
|
|
||||||
gitea "code.gitea.io/gitea/modules/git"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -46,8 +45,11 @@ func (a Adapter) Blame(
|
|||||||
lineTo int,
|
lineTo int,
|
||||||
) types.BlameReader {
|
) types.BlameReader {
|
||||||
// prepare the git command line arguments
|
// prepare the git command line arguments
|
||||||
args := make([]string, 0, 8)
|
cmd := command.New(
|
||||||
args = append(args, "blame", "--porcelain", "--encoding=UTF-8")
|
"blame",
|
||||||
|
command.WithFlag("--porcelain"),
|
||||||
|
command.WithFlag("--encoding", "UTF-8"),
|
||||||
|
)
|
||||||
if lineFrom > 0 || lineTo > 0 {
|
if lineFrom > 0 || lineTo > 0 {
|
||||||
var lines string
|
var lines string
|
||||||
if lineFrom > 0 {
|
if lineFrom > 0 {
|
||||||
@ -57,9 +59,11 @@ func (a Adapter) Blame(
|
|||||||
lines += "," + strconv.Itoa(lineTo)
|
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()
|
pipeRead, pipeWrite := io.Pipe()
|
||||||
stderr := &bytes.Buffer{}
|
stderr := &bytes.Buffer{}
|
||||||
@ -71,12 +75,11 @@ func (a Adapter) Blame(
|
|||||||
_ = pipeWrite.CloseWithError(err)
|
_ = pipeWrite.CloseWithError(err)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
cmd := gitea.NewCommand(ctx, args...)
|
err = cmd.Run(ctx,
|
||||||
err = cmd.Run(&gitea.RunOpts{
|
command.WithDir(repoPath),
|
||||||
Dir: repoPath,
|
command.WithStdout(pipeWrite),
|
||||||
Stdout: pipeWrite,
|
command.WithStderr(stderr),
|
||||||
Stderr: stderr, // We capture stderr output in a buffer.
|
)
|
||||||
})
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return &BlameReader{
|
return &BlameReader{
|
||||||
|
@ -21,8 +21,6 @@ import (
|
|||||||
|
|
||||||
"github.com/harness/gitness/errors"
|
"github.com/harness/gitness/errors"
|
||||||
"github.com/harness/gitness/git/types"
|
"github.com/harness/gitness/git/types"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetBlob returns the blob for the given object sha.
|
// GetBlob returns the blob for the given object sha.
|
||||||
@ -32,7 +30,7 @@ func (a Adapter) GetBlob(
|
|||||||
sha string,
|
sha string,
|
||||||
sizeLimit int64,
|
sizeLimit int64,
|
||||||
) (*types.BlobReader, error) {
|
) (*types.BlobReader, error) {
|
||||||
stdIn, stdOut, cancel := git.CatFileBatch(ctx, repoPath)
|
stdIn, stdOut, cancel := CatFileBatch(ctx, repoPath)
|
||||||
|
|
||||||
_, err := stdIn.Write([]byte(sha + "\n"))
|
_, err := stdIn.Write([]byte(sha + "\n"))
|
||||||
if err != nil {
|
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)
|
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 {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
return nil, processGiteaErrorf(err, "failed to read cat-file batch line")
|
return nil, processGiteaErrorf(err, "failed to read cat-file batch line")
|
||||||
@ -50,10 +48,10 @@ func (a Adapter) GetBlob(
|
|||||||
cancel()
|
cancel()
|
||||||
return nil, fmt.Errorf("cat-file returned object sha '%s' but expected '%s'", objectSHA, sha)
|
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()
|
cancel()
|
||||||
return nil, errors.InvalidArgument(
|
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
|
contentSize := objectSize
|
||||||
|
@ -15,13 +15,13 @@
|
|||||||
package adapter
|
package adapter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/git/command"
|
||||||
"github.com/harness/gitness/git/types"
|
"github.com/harness/gitness/git/types"
|
||||||
|
|
||||||
gitea "code.gitea.io/gitea/modules/git"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetBranch gets an existing branch.
|
// 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
|
// repo has branches IFF there's at least one commit that is reachable via a branch
|
||||||
// (every existing branch points to a commit)
|
// (every existing branch points to a commit)
|
||||||
stdout, _, runErr := gitea.NewCommand(ctx, "rev-list", "--max-count", "1", "--branches").
|
cmd := command.New("rev-list",
|
||||||
RunStdBytes(&gitea.RunOpts{Dir: repoPath})
|
command.WithFlag("--max-count", "1"),
|
||||||
if runErr != nil {
|
command.WithFlag("--branches"),
|
||||||
return false, processGiteaErrorf(runErr, "failed to trigger rev-list command")
|
)
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
128
git/adapter/cat-file.go
Normal file
128
git/adapter/cat-file.go
Normal file
@ -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"
|
"time"
|
||||||
|
|
||||||
"github.com/harness/gitness/errors"
|
"github.com/harness/gitness/errors"
|
||||||
|
"github.com/harness/gitness/git/command"
|
||||||
"github.com/harness/gitness/git/types"
|
"github.com/harness/gitness/git/types"
|
||||||
|
|
||||||
gitea "code.gitea.io/gitea/modules/git"
|
gitea "code.gitea.io/gitea/modules/git"
|
||||||
@ -71,51 +72,50 @@ func (a Adapter) listCommitSHAs(
|
|||||||
limit int,
|
limit int,
|
||||||
filter types.CommitFilter,
|
filter types.CommitFilter,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
args := make([]string, 0, 16)
|
cmd := command.New("rev-list")
|
||||||
args = append(args, "rev-list")
|
|
||||||
|
|
||||||
// return commits only up to a certain reference if requested
|
// return commits only up to a certain reference if requested
|
||||||
if filter.AfterRef != "" {
|
if filter.AfterRef != "" {
|
||||||
// ^REF tells the rev-list command to return only commits that aren't reachable by SHA
|
// ^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
|
// add refCommitSHA as starting point
|
||||||
args = append(args, ref)
|
cmd.Add(command.WithArg(ref))
|
||||||
|
|
||||||
if len(filter.Path) != 0 {
|
if len(filter.Path) != 0 {
|
||||||
args = append(args, "--", filter.Path)
|
cmd.Add(command.WithPostSepArg(filter.Path))
|
||||||
}
|
}
|
||||||
|
|
||||||
// add pagination if requested
|
// add pagination if requested
|
||||||
// TODO: we should add absolut limits to protect git (return error)
|
// TODO: we should add absolut limits to protect git (return error)
|
||||||
if limit > 0 {
|
if limit > 0 {
|
||||||
args = append(args, "--max-count", fmt.Sprint(limit))
|
cmd.Add(command.WithFlag("--max-count", strconv.Itoa(limit)))
|
||||||
|
|
||||||
if page > 1 {
|
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 {
|
if filter.Since > 0 || filter.Until > 0 {
|
||||||
args = append(args, "--date", "unix")
|
cmd.Add(command.WithFlag("--date", "unix"))
|
||||||
}
|
}
|
||||||
if filter.Since > 0 {
|
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 {
|
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 != "" {
|
if filter.Committer != "" {
|
||||||
args = append(args, "--committer", filter.Committer)
|
cmd.Add(command.WithFlag("--committer", filter.Committer))
|
||||||
}
|
}
|
||||||
|
output := &bytes.Buffer{}
|
||||||
stdout, _, runErr := gitea.NewCommand(ctx, args...).RunStdBytes(&gitea.RunOpts{Dir: repoPath})
|
err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output))
|
||||||
if runErr != nil {
|
if err != nil {
|
||||||
// TODO: handle error in case they don't have a common merge base!
|
// 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.
|
// ListCommitSHAs lists the commits reachable from ref.
|
||||||
@ -243,13 +243,18 @@ func giteaGetRenameDetails(
|
|||||||
ref string,
|
ref string,
|
||||||
path string,
|
path string,
|
||||||
) (*types.PathRenameDetails, error) {
|
) (*types.PathRenameDetails, error) {
|
||||||
stdout, _, runErr := gitea.NewCommand(giteaRepo.Ctx, "log", ref, "--name-status", "--pretty=format:", "-1").
|
cmd := command.New("log",
|
||||||
RunStdBytes(&gitea.RunOpts{Dir: giteaRepo.Path})
|
command.WithArg(ref),
|
||||||
if runErr != nil {
|
command.WithFlag("--name-status"),
|
||||||
return nil, fmt.Errorf("failed to trigger log command: %w", runErr)
|
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)
|
changeType, oldPath, newPath, err := getFileChangeTypeFromLog(lines, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -303,7 +308,18 @@ func (a Adapter) GetFullCommitID(
|
|||||||
if repoPath == "" {
|
if repoPath == "" {
|
||||||
return "", ErrRepositoryPathEmpty
|
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.
|
// GetCommits returns the (latest) commits for a specific list of refs.
|
||||||
@ -462,19 +478,25 @@ func getCommit(
|
|||||||
fmtSubject + fmtZero + // 7
|
fmtSubject + fmtZero + // 7
|
||||||
fmtBody // 8
|
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 != "" {
|
if path != "" {
|
||||||
args = append(args, "--", path)
|
cmd.Add(command.WithPostSepArg(path))
|
||||||
}
|
}
|
||||||
|
output := &bytes.Buffer{}
|
||||||
commitLine, stderr, err := gitea.NewCommand(ctx, args...).RunStdString(&gitea.RunOpts{Dir: repoPath})
|
err := cmd.Run(ctx, command.WithDir(repoPath), command.WithStdout(output))
|
||||||
if strings.Contains(stderr, "ambiguous argument") {
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "ambiguous argument") {
|
||||||
return nil, errors.NotFound("revision %q not found", rev)
|
return nil, errors.NotFound("revision %q not found", rev)
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to run git to get commit data: %w", err)
|
return nil, fmt.Errorf("failed to run git to get commit data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
commitLine := output.String()
|
||||||
|
|
||||||
if commitLine == "" {
|
if commitLine == "" {
|
||||||
return nil, errors.InvalidArgument("path %q not found in %s", path, rev)
|
return nil, errors.InvalidArgument("path %q not found in %s", path, rev)
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/harness/gitness/errors"
|
"github.com/harness/gitness/errors"
|
||||||
|
"github.com/harness/gitness/git/command"
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config set local git key and value configuration.
|
// Config set local git key and value configuration.
|
||||||
@ -38,12 +37,15 @@ func (a Adapter) Config(
|
|||||||
return errors.InvalidArgument("key cannot be empty")
|
return errors.InvalidArgument("key cannot be empty")
|
||||||
}
|
}
|
||||||
var outbuf, errbuf strings.Builder
|
var outbuf, errbuf strings.Builder
|
||||||
if err := git.NewCommand(ctx, "config", "--local").AddArguments(key, value).
|
cmd := command.New("config",
|
||||||
Run(&git.RunOpts{
|
command.WithFlag("--local"),
|
||||||
Dir: repoPath,
|
command.WithArg(key, value),
|
||||||
Stdout: &outbuf,
|
)
|
||||||
Stderr: &errbuf,
|
err := cmd.Run(ctx, command.WithDir(repoPath),
|
||||||
}); err != nil {
|
command.WithStdout(&outbuf),
|
||||||
|
command.WithStderr(&errbuf),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("git config [%s -> <%s> ]: %w\n%s\n%s",
|
return fmt.Errorf("git config [%s -> <%s> ]: %w\n%s\n%s",
|
||||||
key, value, err, outbuf.String(), errbuf.String())
|
key, value, err, outbuf.String(), errbuf.String())
|
||||||
}
|
}
|
||||||
|
36
git/adapter/object.go
Normal file
36
git/adapter/object.go
Normal file
@ -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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/harness/gitness/git/command"
|
||||||
"github.com/harness/gitness/git/types"
|
"github.com/harness/gitness/git/types"
|
||||||
|
|
||||||
gitea "code.gitea.io/gitea/modules/git"
|
gitea "code.gitea.io/gitea/modules/git"
|
||||||
@ -44,7 +46,16 @@ func (a Adapter) InitRepository(
|
|||||||
if repoPath == "" {
|
if repoPath == "" {
|
||||||
return ErrRepositoryPathEmpty
|
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(
|
func (a Adapter) OpenRepository(
|
||||||
|
168
git/command/command.go
Normal file
168
git/command/command.go
Normal file
@ -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
|
||||||
|
}
|
114
git/command/command_test.go
Normal file
114
git/command/command_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
40
git/command/env.go
Normal file
40
git/command/env.go
Normal file
@ -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
|
||||||
|
}
|
43
git/command/env_test.go
Normal file
43
git/command/env_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
54
git/command/error.go
Normal file
54
git/command/error.go
Normal file
@ -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
|
||||||
|
}
|
280
git/command/name.go
Normal file
280
git/command/name.go
Normal file
@ -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
|
||||||
|
}
|
130
git/command/option.go
Normal file
130
git/command/option.go
Normal file
@ -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
Block a user