[VAULT-36201] pipeline(git): add Go git client (#30645)

As I was working on VAULT-34829 it became clear that we needed to solve
the problem of using Git from Go. Initially I tried to use the
go-git/go-git pure Go implementation of Git, but it lacked several
features that we needed.

The next best option seemed to be shelling out to Git. What started as a
simple way to execute Git commands with the requisite environment and
configuration led to the implementation.

A few notes:
- I did not add support for all flags for the implemented sub-commands
- I wanted it to handle automatic inject and configuration of PATs when
  operating against private remote repositories in Github.
- I did not want to rely on the local machine having been configured.
  The client that is built in is capable of configuring everything that
  we need via environment variables.
- I chose to go the environment variable route for configuration as it's
  the only way to not overwrite local configuration or set up our own
  cred store.

As the git client ended up being ~50% of the work for VAULT-34829, I
decided to extract it out into its own PR to reduce the review burden.

NOTE: While we don't use it in CI, there is .golanci.yml present in the
 the tree and it causes problems in editors that expect it to be V2. As
 such I migrated it.

Signed-off-by: Ryan Cragun <me@ryan.ec>
This commit is contained in:
Ryan Cragun 2025-05-16 12:41:31 -06:00 committed by GitHub
parent 0d8f1d30f3
commit cb321ce774
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 3320 additions and 15 deletions

View File

@ -1,20 +1,39 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
linters-settings:
depguard:
rules:
main:
list-mode: lax
files:
- "./sdk/**"
allow:
- "github.com/hashicorp/go-metrics/compat"
deny:
- pkg: "github.com/hashicorp/go-metrics"
desc: not allowed, use github.com/hashicorp/go-metrics/compat instead
- pkg: "github.com/armon/go-metrics"
desc: not allowed, use github.com/hashicorp/go-metrics/compat instead
# SPDX-License-Identifier: BUSL-1.1
version: "2"
linters:
enable:
- depguard
settings:
depguard:
rules:
main:
list-mode: lax
files:
- ./sdk/**
allow:
- github.com/hashicorp/go-metrics/compat
deny:
- pkg: github.com/hashicorp/go-metrics
desc: not allowed, use github.com/hashicorp/go-metrics/compat instead
- pkg: github.com/armon/go-metrics
desc: not allowed, use github.com/hashicorp/go-metrics/compat instead
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@ -0,0 +1,115 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package git
import (
"context"
"fmt"
"strings"
)
// ApplyWhitespaceAction are actions Git can take when encountering whitespace
// conflicts during apply.
type ApplyWhitespaceAction = string
const (
ApplyWhitespaceActionNoWarn ApplyWhitespaceAction = "nowarn"
ApplyWhitespaceActionWarn ApplyWhitespaceAction = "warn"
ApplyWhitespaceActionFix ApplyWhitespaceAction = "fix"
ApplyWhitespaceActionError ApplyWhitespaceAction = "error"
ApplyWhitespaceActionErrorAll ApplyWhitespaceAction = "error-all"
)
// ApplyOpts are the git apply flags and arguments
// See: https://git-scm.com/docs/git-apply
type ApplyOpts struct {
// Options
AllowEmpty bool // --allow-empty
Cached bool // --cached
Check bool // --check
Index bool // --index
Ours bool // --ours
Recount bool // --recount
Stat bool // --stat
Summary bool // --summary
Theirs bool // --theirs
ThreeWayMerge bool // -3way
Union bool // --union
Whitespace ApplyWhitespaceAction // --whitespace=<action>
// Targets, depending on which combination of options you're setting
Patch []string // <patch>
}
// Apply runs the git apply command
func (c *Client) Apply(ctx context.Context, opts *ApplyOpts) (*ExecResponse, error) {
return c.Exec(ctx, "apply", opts)
}
// String returns the options as a string
func (o *ApplyOpts) String() string {
return strings.Join(o.Strings(), " ")
}
// Strings returns the options as a string slice
func (o *ApplyOpts) Strings() []string {
if o == nil {
return nil
}
opts := []string{}
if o.AllowEmpty {
opts = append(opts, "--allow-empty")
}
if o.Cached {
opts = append(opts, "--cached")
}
if o.Check {
opts = append(opts, "--check")
}
if o.Index {
opts = append(opts, "--index")
}
if o.Ours {
opts = append(opts, "--ours")
}
if o.Recount {
opts = append(opts, "--recount")
}
if o.Stat {
opts = append(opts, "--stat")
}
if o.Summary {
opts = append(opts, "--summary")
}
if o.Theirs {
opts = append(opts, "--theirs")
}
if o.ThreeWayMerge {
opts = append(opts, "--3way")
}
if o.Union {
opts = append(opts, "--union")
}
if o.Whitespace != "" {
opts = append(opts, fmt.Sprintf("--whitespace=%s", string(o.Whitespace)))
}
if len(o.Patch) > 0 {
opts = append(opts, o.Patch...)
}
return opts
}

View File

@ -0,0 +1,216 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package git
import (
"context"
"fmt"
"strings"
)
// BranchTrack are supported branch tracking options
type BranchTrack = string
const (
BranchTrackDirect BranchTrack = "direct"
BranchTrackInherit BranchTrack = "inherit"
)
// BranchOpts are the git branch flags and arguments
// See: https://git-scm.com/docs/git-branch
type BranchOpts struct {
// Options
Abbrev uint // --abbrev=<n>
All bool // --all
Contains string // --contains <commit>
Copy bool // --copy
CreateReflog bool // --create-reflog
Delete bool // --delete
Force bool // -f
Format string // --format <format>
IgnoreCase bool // --ignore-case
List bool // --list
Merged string // --merged <commit>
Move bool // --move
NoAbbrev bool // --no-abbrev
NoColor bool // --no-color
NoColumn bool // --no-column
NoContains string // --no-contains <commit>
NoMerged string // --no-merged <commit>
NoTrack bool // --no-track
OmitEmpty bool // --omit-empty
PointsAt string // --points-at <object>
Remotes bool // --remotes
Quiet bool // --quiet
SetUpstream bool // --set-upstream
SetUpstreamTo string // --set-upstream-to=<upstream>
ShowCurrent bool // --show-current
Sort string // --sort=<key>
Track BranchTrack // --track
UnsetUpstream bool // --unset-upstream
// Targets. The branch command has several different modules. Set the correct
// targets depending on which combination of options you're setting.
BranchName string // <branchname>
StartPoint string // <start-point>
OldBranch string // <oldbranch>
NewBranch string // <newbranch>
Pattern []string // <pattern>
}
// Branch runs the git branch command
func (c *Client) Branch(ctx context.Context, opts *BranchOpts) (*ExecResponse, error) {
return c.Exec(ctx, "branch", opts)
}
// String returns the options as a string
func (o *BranchOpts) String() string {
return strings.Join(o.Strings(), " ")
}
// Strings returns the options as a string slice
func (o *BranchOpts) Strings() []string {
if o == nil {
return nil
}
opts := []string{}
if o.Abbrev > 0 {
opts = append(opts, fmt.Sprintf("--abbrev=%d", o.Abbrev))
}
if o.All {
opts = append(opts, "--all")
}
if o.Contains != "" {
opts = append(opts, fmt.Sprintf("--contains=%s", string(o.Contains)))
}
if o.Copy {
opts = append(opts, "--copy")
}
if o.CreateReflog {
opts = append(opts, "--create-reflog")
}
if o.Delete {
opts = append(opts, "--delete")
}
if o.Force {
opts = append(opts, "--force")
}
if o.Format != "" {
opts = append(opts, fmt.Sprintf("--format=%s", string(o.Format)))
}
if o.IgnoreCase {
opts = append(opts, "--ignore-case")
}
if o.List {
opts = append(opts, "--list")
}
if o.Merged != "" {
opts = append(opts, fmt.Sprintf("--merged=%s", string(o.Merged)))
}
if o.Move {
opts = append(opts, "--move")
}
if o.NoAbbrev {
opts = append(opts, "--no-abbrev")
}
if o.NoColor {
opts = append(opts, "--no-color")
}
if o.NoColumn {
opts = append(opts, "--no-column")
}
if o.NoTrack {
opts = append(opts, "--no-track")
}
if o.NoContains != "" {
opts = append(opts, fmt.Sprintf("--no-contains=%s", string(o.NoContains)))
}
if o.NoMerged != "" {
opts = append(opts, fmt.Sprintf("--no-merged=%s", string(o.NoMerged)))
}
if o.OmitEmpty {
opts = append(opts, "--omit-empty")
}
if o.PointsAt != "" {
opts = append(opts, fmt.Sprintf("--points-at=%s", string(o.PointsAt)))
}
if o.Quiet {
opts = append(opts, "--quiet")
}
if o.Remotes {
opts = append(opts, "--remotes")
}
if o.SetUpstream {
opts = append(opts, "--set-upstream")
}
if o.SetUpstreamTo != "" {
opts = append(opts, fmt.Sprintf("--set-upstream-to=%s", string(o.SetUpstreamTo)))
}
if o.ShowCurrent {
opts = append(opts, "--show-current")
}
if o.Sort != "" {
opts = append(opts, fmt.Sprintf("--sort=%s", string(o.Sort)))
}
if o.Track != "" {
opts = append(opts, fmt.Sprintf("--track=%s", string(o.Track)))
}
if o.UnsetUpstream {
opts = append(opts, "--unset-upstream")
}
// Not all of these can be used at once but we try to put them in an order
// where we won't cause problems if the correct flags and targets are set.
if o.BranchName != "" {
opts = append(opts, o.BranchName)
}
if o.OldBranch != "" {
opts = append(opts, o.OldBranch)
}
if o.NewBranch != "" {
opts = append(opts, o.NewBranch)
}
if o.StartPoint != "" {
opts = append(opts, o.StartPoint)
}
if len(o.Pattern) > 0 {
opts = append(opts, o.Pattern...)
}
return opts
}

View File

@ -0,0 +1,122 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package git
import (
"context"
"fmt"
"strings"
)
// CheckoutOpts are the git checkout flags and arguments
// See: https://git-scm.com/docs/git-checkout
type CheckoutOpts struct {
// Options
NewBranch string // -b
NewBranchForceCheckout string // -B
Detach bool // --detach
Force bool // -f
Guess bool // --guess
Progress bool // --progress
NoTrack bool // --no-track
Orphan string // --orphan
Ours bool // --ours
Quiet bool // --quiet
Theirs bool // --theirs
Track BranchTrack // --track
// Targets
Branch string // <new-branch>
StartPoint string // <start-point>
Treeish string // <tree-ish>
// Paths
PathSpec []string // -- <pathspec>
}
// Branch runs the git checkout command
func (c *Client) Checkout(ctx context.Context, opts *CheckoutOpts) (*ExecResponse, error) {
return c.Exec(ctx, "checkout", opts)
}
// String returns the options as a string
func (o *CheckoutOpts) String() string {
return strings.Join(o.Strings(), " ")
}
// Strings returns the options as a string slice
func (o *CheckoutOpts) Strings() []string {
if o == nil {
return nil
}
opts := []string{}
if o.NewBranch != "" {
opts = append(opts, "-b", o.NewBranch)
}
if o.NewBranchForceCheckout != "" {
opts = append(opts, "-B", o.NewBranchForceCheckout)
}
if o.Detach {
opts = append(opts, "--detach")
}
if o.Force {
opts = append(opts, "--force")
}
if o.Guess {
opts = append(opts, "--guess")
}
if o.NoTrack {
opts = append(opts, "--no-track")
}
if o.Orphan != "" {
opts = append(opts, "--orphan", string(o.Orphan))
}
if o.Ours {
opts = append(opts, "--ours")
}
if o.Progress {
opts = append(opts, "--progress")
}
if o.Quiet {
opts = append(opts, "--quiet")
}
if o.Theirs {
opts = append(opts, "--theirs")
}
if o.Track != "" {
opts = append(opts, fmt.Sprintf("--track=%s", string(o.Track)))
}
// Do the <branch>, <start-point>, and <tree-ish> before pathspec
if o.Branch != "" {
opts = append(opts, o.Branch)
}
if o.StartPoint != "" {
opts = append(opts, o.StartPoint)
}
if o.Treeish != "" {
opts = append(opts, o.Treeish)
}
// If there's a pathspec always set it last
if len(o.PathSpec) > 0 {
opts = append(append(opts, "--"), o.PathSpec...)
}
return opts
}

View File

@ -0,0 +1,147 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package git
import (
"context"
"fmt"
"strings"
)
// EmptyCommit are supported empty commit handling options
type EmptyCommit = string
const (
EmptyCommitDrop EmptyCommit = "drop"
EmptyCommitKeep EmptyCommit = "keep"
EmptyCommitStop EmptyCommit = "stop"
)
// CherryPickOpts are the git cherry-pick flags and arguments
// See: https://git-scm.com/docs/git-cherry-pick
type CherryPickOpts struct {
// Options
AllowEmpty bool // --allow-empty
AllowEmptyMessage bool // --allow-empty-message
Empty EmptyCommit // --empty=
FF bool // --ff
GPGSign bool // --gpgsign
GPGSignKeyID string // --gpgsign=<key-id>
Mainline string // --mainline
NoReReReAutoupdate bool // --no-rerere-autoupdate
Record bool // -x
ReReReAutoupdate bool // --rerere-autoupdate
Signoff bool // --signoff
Strategy MergeStrategy // --strategy
StrategyOptions []MergeStrategyOption // --strategy-option=<option>
// Target
Commit string // <commit>
// Sequences
Continue bool // --continue
Abort bool // --abort
Quit bool // --quit
}
// CherryPick runs the git cherry-pick command
func (c *Client) CherryPick(ctx context.Context, opts *CherryPickOpts) (*ExecResponse, error) {
return c.Exec(ctx, "cherry-pick", opts)
}
// CherryPickAbort aborts an in-progress cherry-pick
func (c *Client) CherryPickAbort(ctx context.Context) (*ExecResponse, error) {
return c.CherryPick(ctx, &CherryPickOpts{Abort: true})
}
// CherryPickContinue continues an in-progress cherry-pick
func (c *Client) CherryPickContinue(ctx context.Context) (*ExecResponse, error) {
return c.CherryPick(ctx, &CherryPickOpts{Continue: true})
}
// CherryPickQuit quits an in-progress cherry-pick
func (c *Client) CherryPickQuit(ctx context.Context) (*ExecResponse, error) {
return c.CherryPick(ctx, &CherryPickOpts{Quit: true})
}
// String returns the options as a string
func (o *CherryPickOpts) String() string {
return strings.Join(o.Strings(), " ")
}
// Strings returns the options as a string slice
func (o *CherryPickOpts) Strings() []string {
if o == nil {
return nil
}
switch {
case o.Abort:
return []string{"--abort"}
case o.Continue:
return []string{"--continue"}
case o.Quit:
return []string{"--quit"}
}
opts := []string{}
if o.AllowEmpty {
opts = append(opts, "--allow-empty")
}
if o.AllowEmptyMessage {
opts = append(opts, "--allow-empty-message")
}
if o.Empty != "" {
opts = append(opts, fmt.Sprintf("--empty=%s", string(o.Empty)))
}
if o.FF {
opts = append(opts, "--ff")
}
if o.GPGSign {
opts = append(opts, "--gpg-sign")
}
if o.GPGSignKeyID != "" {
opts = append(opts, fmt.Sprintf("--gpg-sign=%s", o.GPGSignKeyID))
}
if o.Mainline != "" {
opts = append(opts, fmt.Sprintf("--mainline=%s", o.Mainline))
}
if o.NoReReReAutoupdate {
opts = append(opts, "--no-rerere-autoupdate")
}
if o.Record {
opts = append(opts, "-x")
}
if o.ReReReAutoupdate {
opts = append(opts, "--rerere-autoupdate")
}
if o.Signoff {
opts = append(opts, "--signoff")
}
if o.Strategy != "" {
opts = append(opts, fmt.Sprintf("--strategy=%s", string(o.Strategy)))
}
for _, opt := range o.StrategyOptions {
opts = append(opts, fmt.Sprintf("--strategy-option=%s", string(opt)))
}
if o.Commit != "" {
opts = append(opts, o.Commit)
}
return opts
}

View File

@ -0,0 +1,182 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package git
import (
"context"
"errors"
"fmt"
"log/slog"
"maps"
"net/url"
"os"
"os/exec"
oexec "os/exec"
"strings"
"sync"
slogctx "github.com/veqryn/slog-context"
)
// Client is the local git client.
type Client struct {
Token string
envOnce sync.Once
envVal []string
config map[string]string
}
// OptStringer is an interface that all sub-command configuration options must
// implement.
type OptStringer interface {
String() string
Strings() []string
}
// ExecResponse is the response from the client running a sub-command with Exec()
type ExecResponse struct {
Cmd string
Env []string
Stdout []byte
Stderr []byte
}
// NewClientOpt is a NewClient() functional option
type NewClientOpt func(*Client)
// NewClient takes variable options and returns a default Client.
func NewClient(opts ...NewClientOpt) *Client {
client := &Client{
config: map[string]string{
"core.pager": "",
"user.name": "hc-github-team-secure-vault-core",
"user.email": "github-team-secure-vault-core@hashicorp.com",
},
}
for _, opt := range opts {
opt(client)
}
return client
}
// WithToken sets the Token in NewClient()
func WithToken(token string) NewClientOpt {
return func(client *Client) {
client.Token = token
}
}
// WithToken sets additional gitconfig in NewClient()
func WithConfig(config map[string]string) NewClientOpt {
return func(client *Client) {
maps.Copy(client.config, config)
}
}
// WithLoadTokenFromEnv sets the Token from known env vars in NewClient()
func WithLoadTokenFromEnv() NewClientOpt {
return func(client *Client) {
if token, ok := os.LookupEnv("GITHUB_TOKEN"); ok {
client.Token = token
return
}
if token, ok := os.LookupEnv("GH_TOKEN"); ok {
client.Token = token
return
}
}
}
// Exec executes a git sub-command.
func (c *Client) Exec(ctx context.Context, subCmd string, opts OptStringer) (*ExecResponse, error) {
env := os.Environ()
res := &ExecResponse{Env: os.Environ()}
if c.Token != "" {
res.Env = c.configEnv()
env = append(env, res.Env...)
}
cmd := oexec.Command("git", append([]string{subCmd}, opts.Strings()...)...)
cmd.Env = env
res.Cmd = cmd.String()
ctx = slogctx.Append(ctx, slog.String("cmd", cmd.String()))
slog.Default().DebugContext(ctx, "executing git command")
var err error
res.Stdout, err = cmd.Output()
if err != nil {
slog.Default().ErrorContext(slogctx.Append(ctx,
slog.String("error", err.Error()),
), "executing git command failed")
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
res.Stderr = exitErr.Stderr
}
}
return res, err
}
// String returns the ExecResponse command and output as a string
func (e *ExecResponse) String() string {
if e == nil {
return ""
}
b := strings.Builder{}
b.WriteString(e.Cmd)
b.WriteString("\n")
for _, line := range strings.Split(string(e.Stdout), "\n") {
b.WriteString(line)
}
b.WriteString("\n")
for _, line := range strings.Split(string(e.Stderr), "\n") {
b.WriteString(line)
}
b.WriteString("\n")
return b.String()
}
// configEnv creates a slice of all git configuration as environment variables
// to avoid:
// - modifying local or global gitconfig
// - relying on preconfigured gitconfig
// - requiring a credstore
// - sensitive values like tokens being passed via flags and thus potentially
// bleeding into STDOUT
//
// As this is relatively expensive it's only done once and cached so subsequent
// requests can reuse the same configuration.
func (c *Client) configEnv() []string {
c.envOnce.Do(func() {
env := c.config
if c.Token != "" {
// NOTE: This basic auth token probably only works with Github right now,
// which is fine because our pipeline only supports Github. Other SCM repos
// have different rules around the user in the auth portion of the URL.
// Github doesn't care what the username is but requires one to be set so
// we always set it to user.
token := url.UserPassword("user", c.Token).String()
env[fmt.Sprintf("url.https://%s@github.com.insteadOf", token)] = "https://github.com"
}
vars := []string{fmt.Sprintf("GIT_CONFIG_COUNT=%d", len(env))}
count := 0
for k, v := range env {
vars = append(
vars,
fmt.Sprintf("GIT_CONFIG_KEY_%d=%s", count, k),
fmt.Sprintf("GIT_CONFIG_VALUE_%d=%s", count, v),
)
count++
}
c.envVal = vars
})
return c.envVal
}

View File

@ -0,0 +1,107 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package git
import (
"context"
"strconv"
"strings"
)
// CloneOpts are the git clone flags and arguments
// See: https://git-scm.com/docs/git-clone
type CloneOpts struct {
// Options
Branch string // --branch
Depth uint // --depth
NoCheckout bool // --no-checkout
NoTags bool // --no-tags
Origin string // --origin
Progress bool // --progress
Quiet bool // --quiet
Revision string // --revision
SingleBranch bool // --single-branch
Sparse bool // --sparse
Verbose bool // --verbose
// Targets
Repository string // <repository>
Directory string // <directory>
}
// Clone runs the git clone command
func (c *Client) Clone(ctx context.Context, opts *CloneOpts) (*ExecResponse, error) {
return c.Exec(ctx, "clone", opts)
}
// String returns the options as a string
func (o *CloneOpts) String() string {
return strings.Join(o.Strings(), " ")
}
// Strings returns the options as a string slice
func (o *CloneOpts) Strings() []string {
if o == nil {
return nil
}
opts := []string{}
if o.Branch != "" {
opts = append(opts, "--branch", o.Branch)
}
if o.Depth > 0 {
opts = append(opts, "--depth", strconv.FormatUint(uint64(o.Depth), 10))
}
if o.NoCheckout {
opts = append(opts, "--no-checkout")
}
if o.NoTags {
opts = append(opts, "--no-tags")
}
if o.Origin != "" {
opts = append(opts, "--origin", o.Origin)
}
if o.Progress {
opts = append(opts, "--progress")
}
if o.Quiet {
opts = append(opts, "--quiet")
}
if o.Revision != "" {
opts = append(opts, "--revision", o.Revision)
}
if o.SingleBranch {
opts = append(opts, "--single-branch")
}
if o.Sparse {
opts = append(opts, "--sparse")
}
if o.Verbose {
opts = append(opts, "--verbose")
}
if o.Repository != "" || o.Directory != "" {
opts = append(opts, "--")
if o.Repository != "" {
opts = append(opts, o.Repository)
}
if o.Directory != "" {
opts = append(opts, o.Directory)
}
}
return opts
}

View File

@ -0,0 +1,218 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package git
import (
"context"
"fmt"
"strings"
)
type (
// Cleanup are cleanup modes
Cleanup string
// FixupLog configures how to fix up a commit
FixupLog string
// UntrackedFiles are how to show untracked files
UntrackedFiles string
)
const (
CommitCleanupModeString Cleanup = "strip"
CommitCleanupModeWhitespace Cleanup = "whitespace"
CommitCleanupModeVerbatim Cleanup = "verbatim"
CommitCleanupModeScissors Cleanup = "scissors"
CommitCleanupModeDefault Cleanup = "default"
CommitFixupLogNone FixupLog = ""
CommitFixupLogAmend FixupLog = "amend"
CommitFixupLogReword FixupLog = "reword"
UntrackedFilesNo UntrackedFiles = "no"
UntrackedFilesNormal UntrackedFiles = "normal"
UntrackedFilesAll UntrackedFiles = "all"
)
// CommitFixup is how to fixup a commit
type CommitFixup struct {
FixupLog
Commit string
}
// CommitOpts are the git commit flags and arguments
// See: https://git-scm.com/docs/git-commit
type CommitOpts struct {
// Options
All bool // --all
AllowEmpty bool // --allow-empty
AllowEmptyMessage bool // --allow-empty-message
Amend bool // --amend
Author string // --author=<author>
Branch bool // --branch
Cleanup Cleanup // --cleanup=<mode>
Date string // --date=<date>
DryRun bool // --dry-run
File string // --file=<file>
Fixup *CommitFixup // --fixup=
GPGSign bool // --gpgsign
GPGSignKeyID string // --gpgsign=<key-id>
Long bool // --long
Patch bool // --patch
Porcelain bool // --porcelain
Message string // --message=<message>
NoEdit bool // --no-edit
NoPostRewrite bool // --no-post-rewrite
NoVerify bool // --no-verify
Null bool // --null
Only bool // --only
Quiet bool // --quiet
ResetAuthor bool // --reset-author
ReuseMessage string // --reuse-message=<commit>
Short bool // --short
Signoff bool // --signoff
Status bool // --status
Verbose bool // --verbose
// Target
PathSpec []string // <pathspec>
}
// Commit runs the git commit command
func (c *Client) Commit(ctx context.Context, opts *CommitOpts) (*ExecResponse, error) {
return c.Exec(ctx, "commit", opts)
}
// String returns the options as a string
func (o *CommitOpts) String() string {
return strings.Join(o.Strings(), " ")
}
// Strings returns the options as a string slice
func (o *CommitOpts) Strings() []string {
if o == nil {
return nil
}
opts := []string{}
if o.All {
opts = append(opts, "--all")
}
if o.AllowEmpty {
opts = append(opts, "--allow-empty")
}
if o.AllowEmptyMessage {
opts = append(opts, "--allow-empty-message")
}
if o.Amend {
opts = append(opts, "--amend")
}
if o.Author != "" {
opts = append(opts, fmt.Sprintf("--author=%s", o.Author))
}
if o.Branch {
opts = append(opts, "--branch")
}
if o.Cleanup != "" {
opts = append(opts, fmt.Sprintf("--cleanup=%s", string(o.Cleanup)))
}
if o.Date != "" {
opts = append(opts, fmt.Sprintf("--date=%s", o.Date))
}
if o.DryRun {
opts = append(opts, "--dry-run")
}
if o.File != "" {
opts = append(opts, fmt.Sprintf("--file=%s", o.File))
}
if o.Fixup != nil {
if o.Fixup.FixupLog == CommitFixupLogNone {
opts = append(opts, fmt.Sprintf("--fixup=%s", string(o.Fixup.Commit)))
} else {
opts = append(opts, fmt.Sprintf("--fixup=%s:%s", string(o.Fixup.FixupLog), string(o.Fixup.Commit)))
}
}
if o.GPGSign {
opts = append(opts, "--gpg-sign")
}
if o.GPGSignKeyID != "" {
opts = append(opts, fmt.Sprintf("--gpg-sign=%s", o.GPGSignKeyID))
}
if o.Long {
opts = append(opts, "--long")
}
if o.Patch {
opts = append(opts, "--patch")
}
if o.Porcelain {
opts = append(opts, "--porcelain")
}
if o.Message != "" {
opts = append(opts, fmt.Sprintf("--message=%s", o.Message))
}
if o.NoEdit {
opts = append(opts, "--no-edit")
}
if o.NoPostRewrite {
opts = append(opts, "--no-post-rewrite")
}
if o.NoVerify {
opts = append(opts, "--no-verify")
}
if o.Null {
opts = append(opts, "--null")
}
if o.Only {
opts = append(opts, "--only")
}
if o.Quiet {
opts = append(opts, "--quiet")
}
if o.ResetAuthor {
opts = append(opts, "--reset-author")
}
if o.ReuseMessage != "" {
opts = append(opts, fmt.Sprintf("--reuse-message=%s", o.ReuseMessage))
}
if o.Short {
opts = append(opts, "--short")
}
if o.Status {
opts = append(opts, "--status")
}
if o.Verbose {
opts = append(opts, "--verbose")
}
// If there's a pathspec, append the paths at the very end
if len(o.PathSpec) > 0 {
opts = append(append(opts, "--"), o.PathSpec...)
}
return opts
}

View File

@ -0,0 +1,123 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package git
import (
"context"
"strconv"
"strings"
)
// FetchOpts are the git fetch flags and arguments
// See: https://git-scm.com/docs/git-fetch
type FetchOpts struct {
// Options
All bool // --all
Atomic bool // --atomic
Depth uint // --depth
Deepen uint // --deepen
Force bool // --force
NoTags bool // --no-tags
Porcelain bool // --porcelain
Progress bool // --progress
Prune bool // --prune
PruneTags bool // --prune-tags
Quiet bool // --quiet
SetUpstream bool // --set-upstream
Unshallow bool // --unshallow
Verbose bool // --verbose
// Targets
Repository string // <repository>
Refspec []string // <refspec>
}
// Fetch runs the git fetch command
func (c *Client) Fetch(ctx context.Context, opts *FetchOpts) (*ExecResponse, error) {
return c.Exec(ctx, "fetch", opts)
}
// String returns the options as a string
func (o *FetchOpts) String() string {
if o == nil {
return ""
}
return strings.Join(o.Strings(), " ")
}
// Strings returns the options as a string slice
func (o *FetchOpts) Strings() []string {
if o == nil {
return nil
}
opts := []string{}
if o.All {
opts = append(opts, "--all")
}
if o.Atomic {
opts = append(opts, "--atomic")
}
if o.Depth > 0 {
opts = append(opts, "--depth", strconv.FormatUint(uint64(o.Depth), 10))
}
if o.Deepen > 0 {
opts = append(opts, "--deepen", strconv.FormatUint(uint64(o.Deepen), 10))
}
if o.Force {
opts = append(opts, "--force")
}
if o.NoTags {
opts = append(opts, "--no-tags")
}
if o.Porcelain {
opts = append(opts, "--porcelain")
}
if o.Progress {
opts = append(opts, "--progress")
}
if o.Prune {
opts = append(opts, "--prune")
}
if o.PruneTags {
opts = append(opts, "--prune-tags")
}
if o.Quiet {
opts = append(opts, "--quiet")
}
if o.SetUpstream {
opts = append(opts, "--set-upstream")
}
if o.Unshallow {
opts = append(opts, "--unshallow")
}
if o.Verbose {
opts = append(opts, "--verbose")
}
if o.Repository != "" {
opts = append(opts, o.Repository)
}
if len(o.Refspec) > 0 {
opts = append(opts, o.Refspec...)
}
return opts
}

View File

@ -0,0 +1,220 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package git
import (
"context"
"fmt"
"strings"
)
type (
// MergeStrategy are the merge strategy to use
MergeStrategy = string
// MergeStrategy are merge strategy options
MergeStrategyOption = string
)
const (
MergeStrategyORT MergeStrategy = "ort"
MergeStrategyRecursive MergeStrategy = "recursive"
MergeStrategyResolve MergeStrategy = "resolve"
MergeStrategyOctopus MergeStrategy = "octopus"
MergeStrategyOurs MergeStrategy = "ours"
MergeStrategySubtree MergeStrategy = "subtree"
// Ort
MergeStrategyOptionOurs MergeStrategy = "ours"
MergeStrategyOptionTheirs MergeStrategy = "theirs"
MergeStrategyOptionIgnoreSpaceChange MergeStrategy = "ignore-space-change"
MergeStrategyOptionIgnoreAllSpace MergeStrategy = "ignore-all-space"
MergeStrategyOptionIgnoreSpaceAtEOL MergeStrategy = "ignore-space-at-eol"
MergeStrategyOptionIgnoreCRAtEOL MergeStrategy = "ignore-cr-at-eol"
MergeStrategyOptionRenormalize MergeStrategy = "renormalize"
MergeStrategyOptionNoRenormalize MergeStrategy = "no-renormalize"
MergeStrategyOptionFindRenames MergeStrategy = "find-renames"
// Recursive
MergeStrategyOptionDiffAlgorithmPatience MergeStrategy = "diff-algorithm=patience"
MergeStrategyOptionDiffAlgorithmMinimal MergeStrategy = "diff-algorithm=minimal"
MergeStrategyOptionDiffAlgorithmHistogram MergeStrategy = "diff-algorithm=histogram"
MergeStrategyOptionDiffAlgorithmMyers MergeStrategy = "diff-algorithm=myers"
)
// MergeOpts are the git merge flags and arguments
// See: https://git-scm.com/docs/git-merge
type MergeOpts struct {
// Options
Autostash bool // --autostash
DoCommit bool // --commit
FF bool // --ff
FFOnly bool // --ff-onnly
IntoName string // --into-name
Log uint // --log=<n>
Message string // -m
NoAutostash bool // --no-autostash
NoDoCommit bool // --no-commit
NoFF bool // --no-ff
NoLog bool // --no-log
NoOverwrite bool // --no-overwrite
NoProgress bool // --no-progress
NoRebase bool // --no-rebase
NoReReReAutoupdate bool // --no-rerere-autoupdate
NoSquash bool // --no-squash
NoStat bool // --no-stat
NoVerify bool // --no-verify
Progress bool // --progress
Quiet bool // --quiet
ReReReAutoupdate bool // --rerere-autoupdate
Squash bool // --squash
Stat bool // --stat
Strategy MergeStrategy // --stategy=<strategy>
StragegyOptions []MergeStrategyOption // --strategy-option=<option>
Verbose bool // --verbose
// Targets
Commit string // <commit>
// Sequences
Continue bool // --continue
Abort bool // --abort
Quit bool // --quit
}
// Merge runs the git merge command
func (c *Client) Merge(ctx context.Context, opts *MergeOpts) (*ExecResponse, error) {
return c.Exec(ctx, "merge", opts)
}
// MergeAbort aborts an in-progress merge
func (c *Client) MergeAbort(ctx context.Context) (*ExecResponse, error) {
return c.Merge(ctx, &MergeOpts{Abort: true})
}
// MergeContinue continues an in-progress merge
func (c *Client) MergeContinue(ctx context.Context) (*ExecResponse, error) {
return c.Merge(ctx, &MergeOpts{Continue: true})
}
// MergeQuit quits an in-progress merge
func (c *Client) MergeQuit(ctx context.Context) (*ExecResponse, error) {
return c.Merge(ctx, &MergeOpts{Quit: true})
}
// String returns the options as a string
func (m *MergeOpts) String() string {
return strings.Join(m.Strings(), " ")
}
// Strings returns the options as a string slice
func (m *MergeOpts) Strings() []string {
if m == nil {
return nil
}
switch {
case m.Abort:
return []string{"--abort"}
case m.Continue:
return []string{"--continue"}
case m.Quit:
return []string{"--quit"}
}
opts := []string{}
if m.Autostash {
opts = append(opts, "--autostash")
}
if m.DoCommit {
opts = append(opts, "--commit")
}
if m.FF {
opts = append(opts, "--ff")
}
if m.FFOnly {
opts = append(opts, "--ff-only")
}
if m.IntoName != "" {
opts = append(opts, "--into-name", m.IntoName)
}
if m.Log > 0 {
opts = append(opts, fmt.Sprintf("--log=%d", m.Log))
}
if m.ReReReAutoupdate {
opts = append(opts, "--rerere-autoupdate")
}
if m.Squash {
opts = append(opts, "--squash")
}
if m.Stat {
opts = append(opts, "--stat")
}
if m.Strategy != "" {
opts = append(opts, fmt.Sprintf("--strategy=%s", string(m.Strategy)))
}
for _, opt := range m.StragegyOptions {
opts = append(opts, fmt.Sprintf("--strategy-option=%s", string(opt)))
}
if m.NoAutostash {
opts = append(opts, "--no-autostash")
}
if m.NoDoCommit {
opts = append(opts, "--no-commit")
}
if m.NoFF {
opts = append(opts, "--no-ff")
}
if m.NoLog {
opts = append(opts, "--no-log")
}
if m.NoProgress {
opts = append(opts, "--no-progress")
}
if m.NoRebase {
opts = append(opts, "--no-rebase")
}
if m.NoReReReAutoupdate {
opts = append(opts, "--no-rerere-autoupdate")
}
if m.NoSquash {
opts = append(opts, "--no-squash")
}
if m.NoStat {
opts = append(opts, "--no-stat")
}
if m.NoStat {
opts = append(opts, "--no-stat")
}
if m.NoVerify {
opts = append(opts, "--no-verify")
}
if m.Commit != "" {
opts = append(opts, m.Commit)
}
return opts
}

View File

@ -0,0 +1,632 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package git
import (
"testing"
"github.com/stretchr/testify/require"
)
// Test our opts structs ability to render the correct flags in the correct order
// NOTE: Many of thests use incompatible options but that's not what we care about,
// we're simply asserting that the rendered string matches what ought to be there
// give the config.
// We have chosen not to try and very flag combinations. Instead we render it
// and execute it and rely on git to handle validation of options.
func TestOptsStringers(t *testing.T) {
t.Parallel()
for name, expect := range map[string]struct {
opts OptStringer
expected string
}{
"apply": {
&ApplyOpts{
AllowEmpty: true,
Cached: true,
Check: true,
Index: true,
Ours: true,
Recount: true,
Stat: true,
Summary: true,
Theirs: true,
ThreeWayMerge: true,
Union: true,
Whitespace: ApplyWhitespaceActionFix,
Patch: []string{"/path/to/my.diff"},
},
"--allow-empty --cached --check --index --ours --recount --stat --summary --theirs --3way --union --whitespace=fix /path/to/my.diff",
},
"branch copy": {
&BranchOpts{
Copy: true,
Force: true,
OldBranch: "my-old-branch",
NewBranch: "my-new-branch",
},
"--copy --force my-old-branch my-new-branch",
},
"branch delete": {
&BranchOpts{
Delete: true,
Remotes: true,
BranchName: "my-branch",
},
"--delete --remotes my-branch",
},
"branch move": {
&BranchOpts{
Move: true,
OldBranch: "my-old-branch",
NewBranch: "my-new-branch",
},
"--move my-old-branch my-new-branch",
},
"branch upstream set": {
&BranchOpts{
SetUpstream: true,
SetUpstreamTo: "my-upstream",
BranchName: "my-branch",
},
"--set-upstream --set-upstream-to=my-upstream my-branch",
},
"branch upstream unset": {
&BranchOpts{
UnsetUpstream: true,
BranchName: "my-branch",
},
"--unset-upstream my-branch",
},
"branch track": {
&BranchOpts{
Track: BranchTrackInherit,
NoTrack: true,
Force: true,
BranchName: "my-branch",
StartPoint: "HEAD~2",
},
"--force --no-track --track=inherit my-branch HEAD~2",
},
"branch with pattern": {
// Everything else in branch..
&BranchOpts{
Abbrev: 7,
All: true,
Contains: "abcd1234",
Format: "%%",
List: true,
Merged: "1234abcd",
NoColor: true,
NoColumn: true,
PointsAt: "12ab34cd",
Remotes: true,
ShowCurrent: true,
Sort: "key",
Pattern: []string{"my/dir", "another/dir"},
},
"--abbrev=7 --all --contains=abcd1234 --format=%% --list --merged=1234abcd --no-color --no-column --points-at=12ab34cd --remotes --show-current --sort=key my/dir another/dir",
},
"checkout 1/2 opts": {
&CheckoutOpts{
Branch: "source",
NewBranchForceCheckout: "new",
Force: true,
NoTrack: true,
Ours: true,
Quiet: true,
},
"-B new --force --no-track --ours --quiet source",
},
"checkout 2/2 opts": {
&CheckoutOpts{
Branch: "source",
NewBranch: "new",
Guess: true,
Orphan: "bar",
Progress: true,
Theirs: true,
Track: BranchTrackDirect,
StartPoint: "HEAD~1",
},
"-b new --guess --orphan bar --progress --theirs --track=direct source HEAD~1",
},
"checkout path spec": {
&CheckoutOpts{
Branch: "main",
StartPoint: "HEAD~1",
PathSpec: []string{"go.mod", "go.sum"},
},
"main HEAD~1 -- go.mod go.sum",
},
"cherry-pick 1/2 opts": {
&CherryPickOpts{
AllowEmpty: true,
AllowEmptyMessage: true,
Empty: EmptyCommitKeep,
FF: true,
GPGSign: true,
Mainline: "ABCDEFGH",
Record: true,
Signoff: true,
Commit: "1234ABCD",
},
"--allow-empty --allow-empty-message --empty=keep --ff --gpg-sign --mainline=ABCDEFGH -x --signoff 1234ABCD",
},
"cherry-pick: 2/2 opts": {
&CherryPickOpts{
GPGSignKeyID: "4321DCBA",
ReReReAutoupdate: true,
Strategy: MergeStrategyResolve,
StrategyOptions: []MergeStrategyOption{
MergeStrategyOptionDiffAlgorithmHistogram,
MergeStrategyOptionIgnoreSpaceChange,
},
Commit: "1234ABCD",
},
"--gpg-sign=4321DCBA --rerere-autoupdate --strategy=resolve --strategy-option=diff-algorithm=histogram --strategy-option=ignore-space-change 1234ABCD",
},
"cherry-pick --continue": {
&CherryPickOpts{
Continue: true,
// Options are ignored
Commit: "1234ABCD",
GPGSignKeyID: "4321DCBA",
},
"--continue",
},
"cherry-pick --abort": {
&CherryPickOpts{
Abort: true,
// Options are ignored
Commit: "1234ABCD",
GPGSignKeyID: "4321DCBA",
},
"--abort",
},
"cherry-pick --quit": {
&CherryPickOpts{
Quit: true,
// Options are ignored
Commit: "1234ABCD",
GPGSignKeyID: "4321DCBA",
},
"--quit",
},
"clone 1/2 opts": {
&CloneOpts{
Branch: "my-branch",
Depth: 3,
NoCheckout: true,
NoTags: true,
Origin: "my-fork",
Quiet: true,
Directory: "some-dir",
},
"--branch my-branch --depth 3 --no-checkout --no-tags --origin my-fork --quiet -- some-dir",
},
"clone 2/2 opts": {
&CloneOpts{
Branch: "my-branch",
Progress: true,
Sparse: true,
SingleBranch: true,
Repository: "my-repo",
Directory: "some-dir",
},
"--branch my-branch --progress --single-branch --sparse -- my-repo some-dir",
},
"commit 1/2 opts": {
&CommitOpts{
All: true,
AllowEmpty: true,
AllowEmptyMessage: true,
Amend: true,
Author: "example@hashicorp.com",
Branch: true,
Cleanup: CommitCleanupModeWhitespace,
Date: "1 day ago",
DryRun: true,
File: "path/to/message/file",
Fixup: &CommitFixup{
FixupLog: CommitFixupLogReword,
Commit: "1234ABCD",
},
GPGSign: true,
Long: true,
NoEdit: true,
PathSpec: []string{
"file/a",
"another/b",
},
},
"--all --allow-empty --allow-empty-message --amend --author=example@hashicorp.com --branch --cleanup=whitespace --date=1 day ago --dry-run --file=path/to/message/file --fixup=reword:1234ABCD --gpg-sign --long --no-edit -- file/a another/b",
},
"commit 2/2 opts": {
&CommitOpts{
GPGSignKeyID: "4321DCBA",
Patch: true,
Porcelain: true,
Message: "my commit message",
NoPostRewrite: true,
NoVerify: true,
Null: true,
Only: true,
ResetAuthor: true,
ReuseMessage: "1234ABCD",
Short: true,
Signoff: true,
Status: true,
Verbose: true,
PathSpec: []string{
"file/a",
"another/b",
},
},
"--gpg-sign=4321DCBA --patch --porcelain --message=my commit message --no-post-rewrite --no-verify --null --only --reset-author --reuse-message=1234ABCD --short --status --verbose -- file/a another/b",
},
"fetch": {
&FetchOpts{
All: true,
Atomic: true,
Depth: 5,
Deepen: 6,
Force: true,
NoTags: true,
Porcelain: true,
Progress: true,
Prune: true,
Quiet: true,
SetUpstream: true,
Unshallow: true,
Verbose: true,
Repository: "my-repo",
Refspec: []string{"my-branch"},
},
"--all --atomic --depth 5 --deepen 6 --force --no-tags --porcelain --progress --prune --quiet --set-upstream --unshallow --verbose my-repo my-branch",
},
"merge 1/2 opts": {
&MergeOpts{
Autostash: true,
DoCommit: true,
Commit: "1234ABCD",
FF: true,
FFOnly: true,
IntoName: "my-other-branch",
Log: 2,
Message: "merging my branch",
Progress: true,
ReReReAutoupdate: true,
Squash: true,
Stat: true,
Strategy: MergeStrategyORT,
StragegyOptions: []MergeStrategyOption{
MergeStrategyOptionDiffAlgorithmMyers,
MergeStrategyOptionFindRenames,
},
Verbose: true,
},
"--autostash --commit --ff --ff-only --into-name my-other-branch --log=2 --rerere-autoupdate --squash --stat --strategy=ort --strategy-option=diff-algorithm=myers --strategy-option=find-renames 1234ABCD",
},
"merge 2/2 opts": {
&MergeOpts{
NoAutostash: true,
NoDoCommit: true,
NoFF: true,
NoLog: true,
NoProgress: true,
NoRebase: true,
NoReReReAutoupdate: true,
NoSquash: true,
NoStat: true,
NoVerify: true,
},
"--no-autostash --no-commit --no-ff --no-log --no-progress --no-rebase --no-rerere-autoupdate --no-squash --no-stat --no-stat --no-verify",
},
"merge --continue": {
&MergeOpts{
Continue: true,
// Options are ignored
Commit: "1234ABCD",
Message: "merging my branch",
},
"--continue",
},
"merge --abort": {
&MergeOpts{
Abort: true,
// Options are ignored
Commit: "1234ABCD",
Message: "merging my branch",
},
"--abort",
},
"merge --quit": {
&MergeOpts{
Quit: true,
// Options are ignored
Commit: "1234ABCD",
Message: "merging my branch",
},
"--quit",
},
"pull 1/3 opts": {
&PullOpts{
Atomic: true,
Autostash: true,
Depth: 4,
DoCommit: true,
FF: true,
GPGSign: true,
NoLog: true,
NoStat: true,
Quiet: true,
Prune: true,
SetUpstream: true,
Squash: true,
Rebase: RebaseStrategyTrue,
Refspec: []string{"my-branch"},
Repository: "my-repo",
UpdateShallow: true,
}, "--atomic --autostash --commit --depth 4 --ff --gpg-sign --squash --no-log --no-stat --no-stat --prune --quiet --rebase=true --set-upstream my-repo my-branch",
},
"pull 2/3 opts": {
&PullOpts{
AllowUnrelatedHistories: true,
Append: true,
Deepen: 3,
FFOnly: true,
GPGSignKeyID: "4321DCBA",
Log: 5,
NoRebase: true,
NoRecurseSubmodules: true,
Porcelain: true,
Progress: true,
PruneTags: true,
Refspec: []string{"my-branch"},
Repository: "my-repo",
Stat: true,
Strategy: MergeStrategyOctopus,
StragegyOptions: []MergeStrategyOption{
MergeStrategyOptionDiffAlgorithmMinimal,
MergeStrategyOptionIgnoreCRAtEOL,
},
Verbose: true,
Verify: true,
}, "--deepen 3 --ff-only --gpg-sign=4321DCBA --log=5 --stat -X diff-algorithm=minimal -X ignore-cr-at-eol --no-rebase --no-recurse-submodules --porcelain --progress --prune-tags --verbose my-repo my-branch",
},
"pull 3/3 opts": {
&PullOpts{
All: true,
Cleanup: CommitCleanupModeDefault,
Force: true,
NoAutostash: true,
NoDoCommit: true,
NoFF: true,
NoSquash: true,
NoTags: true,
NoVerify: true,
RecurseSubmodules: RecurseSubmodulesOnDemand,
Refspec: []string{"my-branch"},
Repository: "my-repo",
Stat: true,
Unshallow: true,
}, "--all --force --stat --no-autostash --no-commit --no-ff --no-squash --no-tags --no-verify --unshallow my-repo my-branch",
},
"push 1/2 opts": {
&PushOpts{
All: true,
Branches: true,
DryRun: true,
FollowTags: true,
ForceIfIncludes: true,
Mirror: true,
NoForceWithLease: true,
NoRecurseSubmodules: true,
NoThin: true,
Porcelain: true,
Prune: true,
Quiet: true,
Refspec: []string{"my-branch"},
Repository: "my-repo",
SetUpstream: true,
Tags: true,
Verify: true,
}, "--all --branches --dry-run --follow-tags --force-if-includes --mirror --no-force-with-lease --no-recurse-submodules --no-thin --porcelain --prune --quiet --set-upstream --tags --verify my-repo my-branch",
},
"push 2/2 opts": {
&PushOpts{
Atomic: true,
Delete: true,
Exec: "/path/to/git-receive-pack",
Force: true,
ForceWithLease: "another-branch",
NoAtomic: true,
NoForceIfIncludes: true,
NoSigned: true,
NoVerify: true,
Progress: true,
PushOption: "a",
RecurseSubmodules: PushRecurseSubmodulesCheck,
Refspec: []string{"my-branch"},
Repository: "my-repo",
Signed: PushSignedTrue,
Verbose: true,
}, "--atomic --delete --exec=/path/to/git-receive-pack --force --force-with-lease=another-branch --no-atomic --no-force-if-includes --no-signed --no-verify --progress --push-option=a --recurse-submodules=check --signed=true --verbose my-repo my-branch",
},
"rebase 1/3 opts": {
&RebaseOpts{
AllowEmptyMessage: true,
Autosquash: true,
Branch: "my-branch",
Empty: EmptyCommitDrop,
ForkPoint: true,
IgnoreDate: true,
Merge: true,
NoAutostash: true,
NoRebaseMerges: true,
NoReReReAutoupdate: true,
NoVerify: true,
RescheduleFailedExec: true,
Root: true,
UpdateRefs: true,
}, "--allow-empty-message --autosquash --empty=drop --fork-point --ignore-date --merge --no-autostash --no-rebase-merges --no-rerere-autoupdate --no-verify --reschedule-failed-exec --root --update-refs my-branch",
},
"rebase 2/3 opts": {
&RebaseOpts{
Apply: true,
Branch: "my-branch",
CommitterDateIsAuthorDate: true,
Exec: "/path/to/git-receive-pack",
GPGSign: true,
IgnoreWhitespace: true,
KeepEmpty: true,
NoReapplyCherryPicks: true,
NoStat: true,
Onto: "new-base",
Quiet: true,
ResetAuthorDate: true,
Stat: true,
Whitespace: WhitespaceActionFix,
}, "--apply --committer-date-is-author-date --exec=/path/to/git-receive-pack --gpg-sign --ignore-whitespace --keep-empty --no-reapply-cherry-picks --no-stat --onto=new-base --quiet --reset-author-date --stat --whitespace=fix my-branch",
},
"rebase 3/3 opts": {
&RebaseOpts{
Autostash: true,
Branch: "my-branch",
Context: 3,
ForceRebase: true,
GPGSignKeyID: "4321DCBA",
KeepBase: "another-upstream",
NoAutosquash: true,
NoKeepEmpty: true,
NoRescheduleFailedExec: true,
NoUpdateRefs: true,
ReapplyCherryPicks: true,
RebaseMerges: RebaseMergesCousins,
ReReReAutoupdate: true,
Strategy: MergeStrategySubtree,
StragegyOptions: []MergeStrategyOption{
MergeStrategyOptionDiffAlgorithmPatience,
MergeStrategyOptionNoRenormalize,
},
Verbose: true,
}, "--autostash -C 3 --force-rebase --gpg-sign=4321DCBA --keep-base=another-upstream --no-autosquash --no-keep-empty --no-reschedule-failed-exec --no-update-refs --reapply-cherry-picks --rebase-merges=rebase-cousins --rerere-autoupdate --strategy=subtree --strategy-option=diff-algorithm=patience --strategy-option=no-renormalize --verbose my-branch",
},
"rebase --continue": {
&RebaseOpts{
Continue: true,
// Options are ignored
Branch: "my-branch",
GPGSignKeyID: "4321DCBA",
},
"--continue",
},
"rebase --abort": {
&RebaseOpts{
Abort: true,
// Options are ignored
Branch: "my-branch",
GPGSignKeyID: "4321DCBA",
},
"--abort",
},
"rebase --quit": {
&RebaseOpts{
Quit: true,
// Options are ignored
Branch: "my-branch",
GPGSignKeyID: "4321DCBA",
},
"--quit",
},
"rebase --skip": {
&RebaseOpts{
Skip: true,
// Options are ignored
Branch: "my-branch",
GPGSignKeyID: "4321DCBA",
},
"--skip",
},
"rebase --show-current-patch": {
&RebaseOpts{
ShowCurrentPatch: true,
// Options are ignored
Branch: "my-branch",
GPGSignKeyID: "4321DCBA",
},
"--show-current-patch",
},
"reset": {
&ResetOpts{
Mode: ResetModeHard,
NoRefresh: true,
Patch: true,
Quiet: true,
Refresh: true,
Commit: "abcd1234",
Treeish: "HEAD~2",
PathSpec: []string{"vault/something_ent.go", "vault/cli/another_ent.go"},
},
"--hard --no-refresh --quiet --refresh --patch abcd1234 HEAD~2 -- vault/something_ent.go vault/cli/another_ent.go",
},
"rm": {
&RmOpts{
Cached: true,
DryRun: true,
Force: true,
IgnoreUnmatched: true,
Quiet: true,
Recursive: true,
Sparse: true,
PathSpec: []string{"vault/something_ent.go", "vault/cli/another_ent.go"},
},
"--cached --dry-run --force --ignore-unmatched --quiet -r --sparse -- vault/something_ent.go vault/cli/another_ent.go",
},
"show": {
&ShowOpts{
DiffAlgorithm: DiffAlgorithmHistogram,
DiffMerges: DiffMergeFormatDenseCombined,
Format: "medium",
NoColor: true,
NoPatch: true,
Output: "/path/to/my.diff",
Patch: true,
Raw: true,
Object: "HEAD",
PathSpec: []string{"go.mod", "go.sum"},
},
"--diff-algorithm=histogram --diff-merges=dense-combined --format=medium --no-color --no-patch --output=/path/to/my.diff --patch --raw HEAD -- go.mod go.sum",
},
"status": {
&StatusOpts{
AheadBehind: true,
Branch: true,
Column: "always",
FindRenames: 12,
Ignored: IgnoredModeMatching,
IgnoreSubmodules: IgnoreSubmodulesWhenDirty,
Long: true,
NoAheadBehind: true,
NoColumn: true,
NoRenames: true,
Porcelain: true,
Renames: true,
Short: true,
ShowStash: true,
UntrackedFiles: UntrackedFilesAll,
Verbose: true,
PathSpec: []string{"go.mod", "go.sum"},
},
"--ahead-behind --branch --column=always --find-renames=12 --ignored=matching --ignore-submodules=dirty --long --no-ahead-behind --no-column --no-renames --porcelain --renames --short --show-stash --untracked-files=all --verbose -- go.mod go.sum",
},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
require.Equal(t, expect.expected, expect.opts.String())
})
}
}

View File

@ -0,0 +1,244 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package git
import (
"context"
"fmt"
"strconv"
"strings"
)
// RecurseSubmodules is a sub-module recurse mode
type RecurseSubmodules = string
const (
RecurseSubmodulesYes RecurseSubmodules = "yes"
RecurseSubmodulesOnDemand RecurseSubmodules = "on-demand"
RecurseSubmodulesNo RecurseSubmodules = "no"
)
// PullOpts are the git pull flags and arguments
// See: https://git-scm.com/docs/git-pull
type PullOpts struct {
// Options
Quiet bool // --quiet
Verbose bool // --verbose
RecurseSubmodules RecurseSubmodules // --recurse-submodules=
NoRecurseSubmodules bool // --no-recurse-submodules
// Merge options
Autostash bool // --autostash
AllowUnrelatedHistories bool // --allow-unrelated-histories
DoCommit bool // --commit
NoDoCommit bool // --no-commit
Cleanup Cleanup // --cleanup=
FF bool // --ff
FFOnly bool // --ff-onnly
NoFF bool // --no-ff
GPGSign bool // --gpgsign
GPGSignKeyID string // --gpgsign=<key-id>
Log uint // --log=
NoAutostash bool // --no-autostash
NoLog bool // --no-log
NoRebase bool // --no-rebase
NoStat bool // --no-stat
NoSquash bool // --no-squash
NoVerify bool // --no-verify
Stat bool // --stat
Squash bool // --squash
Strategy MergeStrategy // --stategy=
StragegyOptions []MergeStrategyOption // --strategy-option=
Rebase RebaseStrategy // --rebase=
Verify bool // --verify
// Fetch options
All bool // --all
Append bool // --append
Atomic bool // --atomic
Depth uint // --depth
Deepen uint // --deepen
Force bool // --force
NoTags bool // --no-tags
Porcelain bool // --porcelain
Progress bool // --progress
Prune bool // --prune
PruneTags bool // --prune-tags
SetUpstream bool // --set-upstream
Unshallow bool // --unshallow
UpdateShallow bool // --update-shallow
// Targets
Repository string // <repository>
Refspec []string // <refspec>
}
// Pull runs the git pull command
func (c *Client) Pull(ctx context.Context, opts *PullOpts) (*ExecResponse, error) {
return c.Exec(ctx, "pull", opts)
}
// String returns the options as a string
func (o *PullOpts) String() string {
return strings.Join(o.Strings(), " ")
}
// Strings returns the options as a string slice
func (o *PullOpts) Strings() []string {
if o == nil {
return nil
}
opts := []string{}
if o.All {
opts = append(opts, "--all")
}
if o.Atomic {
opts = append(opts, "--atomic")
}
if o.Autostash {
opts = append(opts, "--autostash")
}
if o.DoCommit {
opts = append(opts, "--commit")
}
if o.Depth > 0 {
opts = append(opts, "--depth", strconv.FormatUint(uint64(o.Depth), 10))
}
if o.Deepen > 0 {
opts = append(opts, "--deepen", strconv.FormatUint(uint64(o.Deepen), 10))
}
if o.FF {
opts = append(opts, "--ff")
}
if o.FFOnly {
opts = append(opts, "--ff-only")
}
if o.Force {
opts = append(opts, "--force")
}
if o.GPGSign {
opts = append(opts, "--gpg-sign")
}
if o.GPGSignKeyID != "" {
opts = append(opts, fmt.Sprintf("--gpg-sign=%s", o.GPGSignKeyID))
}
if o.Log > 0 {
opts = append(opts, fmt.Sprintf("--log=%d", o.Log))
}
if o.Squash {
opts = append(opts, "--squash")
}
if o.Stat {
opts = append(opts, "--stat")
}
for _, opt := range o.StragegyOptions {
opts = append(opts, "-X", string(opt))
}
if o.NoAutostash {
opts = append(opts, "--no-autostash")
}
if o.NoDoCommit {
opts = append(opts, "--no-commit")
}
if o.NoFF {
opts = append(opts, "--no-ff")
}
if o.NoLog {
opts = append(opts, "--no-log")
}
if o.NoRebase {
opts = append(opts, "--no-rebase")
}
if o.NoRecurseSubmodules {
opts = append(opts, "--no-recurse-submodules")
}
if o.NoSquash {
opts = append(opts, "--no-squash")
}
if o.NoStat {
opts = append(opts, "--no-stat")
}
if o.NoStat {
opts = append(opts, "--no-stat")
}
if o.NoTags {
opts = append(opts, "--no-tags")
}
if o.NoVerify {
opts = append(opts, "--no-verify")
}
if o.Porcelain {
opts = append(opts, "--porcelain")
}
if o.Progress {
opts = append(opts, "--progress")
}
if o.Prune {
opts = append(opts, "--prune")
}
if o.PruneTags {
opts = append(opts, "--prune-tags")
}
if o.Quiet {
opts = append(opts, "--quiet")
}
if o.Rebase != "" {
opts = append(opts, fmt.Sprintf("--rebase=%s", string(o.Rebase)))
}
if o.SetUpstream {
opts = append(opts, "--set-upstream")
}
if o.Unshallow {
opts = append(opts, "--unshallow")
}
if o.Verbose {
opts = append(opts, "--verbose")
}
if o.Repository != "" {
opts = append(opts, o.Repository)
}
if len(o.Refspec) > 0 {
opts = append(opts, o.Refspec...)
}
return opts
}

View File

@ -0,0 +1,217 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package git
import (
"context"
"fmt"
"strings"
)
type (
// PushRecurseSubmodules is how to handle recursive sub-modules
PushRecurseSubmodules = string
// PushSigned sets gpg sign mode
PushSigned = string
)
const (
PushRecurseSubmodulesCheck PushRecurseSubmodules = "check"
PushRecurseSubmodulesOnDemand PushRecurseSubmodules = "on-demand"
PushRecurseSubmodulesOnly PushRecurseSubmodules = "only"
PushRecurseSubmodulesNo PushRecurseSubmodules = "no"
PushSignedTrue PushSigned = "true"
PushSignedFalse PushSigned = "false"
PushSignedIfAsked PushSigned = "if-asked"
)
// PushOpts are the git push flags and arguments
// See: https://git-scm.com/docs/git-push
type PushOpts struct {
// Options
All bool // --all
Atomic bool // --atomic
Branches bool // --branches
Delete bool // --delete
DryRun bool // --dry-run
Exec string // --exec=<git-receive-pack>
FollowTags bool // --follow-tags
Force bool // --force
ForceIfIncludes bool // --force-if-includes
ForceWithLease string // --force-with-lease=<refname>
Mirror bool // --mirror
NoAtomic bool // --no-atomic
NoForceIfIncludes bool // --no-force-if-includes
NoForceWithLease bool // --no-force-with-lease
NoRecurseSubmodules bool // --no-recurse-submodules
NoSigned bool // --no-signed
NoThin bool // --no-thin
NoVerify bool // --no-verify
Porcelain bool // --porcelain
Progress bool // --progress
Prune bool // --prune
PushOption string // --push-option
Quiet bool // --quiet
RecurseSubmodules PushRecurseSubmodules // --recurse-submodules=<mode>
SetUpstream bool // --set-upstream
Signed PushSigned // --signed=<mode>
Tags bool // --tags
Thin bool // --thin
Verbose bool // --verbose
Verify bool // --verify
// Targets
Repository string // <repository>
Refspec []string // <refspec>
}
// Push runs the git push command
func (c *Client) Push(ctx context.Context, opts *PushOpts) (*ExecResponse, error) {
return c.Exec(ctx, "push", opts)
}
// String returns the options as a string
func (o *PushOpts) String() string {
return strings.Join(o.Strings(), " ")
}
// Strings returns the options as a string slice
func (o *PushOpts) Strings() []string {
if o == nil {
return nil
}
opts := []string{}
if o.All {
opts = append(opts, "--all")
}
if o.Atomic {
opts = append(opts, "--atomic")
}
if o.Branches {
opts = append(opts, "--branches")
}
if o.Delete {
opts = append(opts, "--delete")
}
if o.DryRun {
opts = append(opts, "--dry-run")
}
if o.Exec != "" {
opts = append(opts, fmt.Sprintf("--exec=%s", o.Exec))
}
if o.FollowTags {
opts = append(opts, "--follow-tags")
}
if o.Force {
opts = append(opts, "--force")
}
if o.ForceIfIncludes {
opts = append(opts, "--force-if-includes")
}
if o.ForceWithLease != "" {
opts = append(opts, fmt.Sprintf("--force-with-lease=%s", o.ForceWithLease))
}
if o.Mirror {
opts = append(opts, "--mirror")
}
if o.NoAtomic {
opts = append(opts, "--no-atomic")
}
if o.NoForceIfIncludes {
opts = append(opts, "--no-force-if-includes")
}
if o.NoForceWithLease {
opts = append(opts, "--no-force-with-lease")
}
if o.NoRecurseSubmodules {
opts = append(opts, "--no-recurse-submodules")
}
if o.NoSigned {
opts = append(opts, "--no-signed")
}
if o.NoThin {
opts = append(opts, "--no-thin")
}
if o.NoVerify {
opts = append(opts, "--no-verify")
}
if o.Porcelain {
opts = append(opts, "--porcelain")
}
if o.Progress {
opts = append(opts, "--progress")
}
if o.Prune {
opts = append(opts, "--prune")
}
if o.PushOption != "" {
opts = append(opts, fmt.Sprintf("--push-option=%s", o.PushOption))
}
if o.Quiet {
opts = append(opts, "--quiet")
}
if o.RecurseSubmodules != "" {
opts = append(opts, fmt.Sprintf("--recurse-submodules=%s", string(o.RecurseSubmodules)))
}
if o.SetUpstream {
opts = append(opts, "--set-upstream")
}
if o.Signed != "" {
opts = append(opts, fmt.Sprintf("--signed=%s", string(o.Signed)))
}
if o.Tags {
opts = append(opts, "--tags")
}
if o.Thin {
opts = append(opts, "--thin")
}
if o.Verbose {
opts = append(opts, "--verbose")
}
if o.Verify {
opts = append(opts, "--verify")
}
if o.Repository != "" {
opts = append(opts, o.Repository)
}
if len(o.Refspec) > 0 {
opts = append(opts, o.Refspec...)
}
return opts
}

View File

@ -0,0 +1,324 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package git
import (
"context"
"fmt"
"strconv"
"strings"
)
type (
// RebaseMerges is the strategy for handling merge commits in rebases
RebaseMerges = string
// RebaseMerges is the strategy for rebasing
RebaseStrategy = string
// RebaseMerges is the strategy for handling whitespace during rebasing
WhitespaceAction = string
)
const (
RebaseMergesCousins RebaseMerges = "rebase-cousins"
RebaseMergesNoCousins RebaseMerges = "no-rebase-cousins"
RebaseStrategyTrue RebaseStrategy = "true"
RebaseStrategyFalse RebaseStrategy = "false"
RebaseStrategyMerges RebaseStrategy = "merges"
RebaseStrategyInteractive RebaseStrategy = "interactive"
WhitespaceActionNoWarn WhitespaceAction = "nowarn"
WhitespaceActionWarn WhitespaceAction = "warn"
WhitespaceActionFix WhitespaceAction = "fix"
WhitespaceActionError WhitespaceAction = "error"
WhitespaceActionErrorAll WhitespaceAction = "error-all"
)
// RebaseOpts are the git rebase flags and arguments
// See: https://git-scm.com/docs/git-rebase
type RebaseOpts struct {
// Options
AllowEmptyMessage bool // --allow-empty-message
Apply bool // --apply
Autosquash bool // --autosquash
Autostash bool // --autostash
CommitterDateIsAuthorDate bool // --committer-date-is-author-date
Context uint // -C
Empty EmptyCommit // --empty=
Exec string // --exec=
ForceRebase bool // --force-rebase
ForkPoint bool // --fork-point
GPGSign bool // --gpgsign
GPGSignKeyID string // --gpgsign=<key-id>
IgnoreDate bool // --ignore-date
IgnoreWhitespace bool // --ignore-whitespace
KeepBase string // --keep-base <upstream|branch>
KeepEmpty bool // --keep-empty
Merge bool // --merge
NoAutosquash bool // --no-autosquash
NoAutostash bool // --no-autostash
NoKeepEmpty bool // --no-keep-empty
NoReapplyCherryPicks bool // --no-reapply-cherry-picks
NoRebaseMerges bool // --no-rebase-merges
NoRescheduleFailedExec bool // --no-reschedule-failed-exec
NoReReReAutoupdate bool // --no-rerere-autoupdate
NoStat bool // --no-stat
NoUpdateRefs bool // --no-update-refs
NoVerify bool // --no-verify
Onto string // --onto
Quiet bool // --quiet
ReapplyCherryPicks bool // --reapply-cherry-picks
RebaseMerges RebaseMerges // --rebase-merges=<strategy>
RescheduleFailedExec bool // --reschedule-failed-exec
ResetAuthorDate bool // --reset-author-date
ReReReAutoupdate bool // --rerere-autoupdate
Root bool // --root
Stat bool // --stat
Strategy MergeStrategy // --strategy
StragegyOptions []MergeStrategyOption // --strategy-option=<option>
UpdateRefs bool // --update-refs
Verbose bool // --verbose
Verify bool // --verify
Whitespace WhitespaceAction //--whitespace=<handler>
// Args
Branch string // <branch>
// Mode Options
Continue bool // --continue
Skip bool // --skip
Abort bool // --abort
Quit bool // --quit
ShowCurrentPatch bool // --show-current-patch
}
// Rebase runs the git rebase command
func (c *Client) Rebase(ctx context.Context, opts *RebaseOpts) (*ExecResponse, error) {
return c.Exec(ctx, "rebase", opts)
}
// RebaseAbort aborts an in-progress rebase
func (c *Client) RebaseAbort(ctx context.Context) (*ExecResponse, error) {
return c.Rebase(ctx, &RebaseOpts{Abort: true})
}
// RebaseContinue continues an in-progress rebase
func (c *Client) RebaseContinue(ctx context.Context) (*ExecResponse, error) {
return c.Rebase(ctx, &RebaseOpts{Continue: true})
}
// RebaseQuit quits an in-progress rebase
func (c *Client) RebaseQuit(ctx context.Context) (*ExecResponse, error) {
return c.Rebase(ctx, &RebaseOpts{Quit: true})
}
// RebaseSkip skips an in-progress rebase
func (c *Client) RebaseSkip(ctx context.Context) (*ExecResponse, error) {
return c.Rebase(ctx, &RebaseOpts{Skip: true})
}
// RebaseShowCurrentPatch shows the current patch an in-progress rebase
func (c *Client) RebaseShowCurrentPatch(ctx context.Context) (*ExecResponse, error) {
return c.Rebase(ctx, &RebaseOpts{ShowCurrentPatch: true})
}
// String returns the options as a string
func (o *RebaseOpts) String() string {
return strings.Join(o.Strings(), " ")
}
// Strings returns the set options as a string slice
func (o *RebaseOpts) Strings() []string {
if o == nil {
return nil
}
switch {
case o.Abort:
return []string{"--abort"}
case o.Continue:
return []string{"--continue"}
case o.Quit:
return []string{"--quit"}
case o.Skip:
return []string{"--skip"}
case o.ShowCurrentPatch:
return []string{"--show-current-patch"}
}
opts := []string{}
if o.AllowEmptyMessage {
opts = append(opts, "--allow-empty-message")
}
if o.Apply {
opts = append(opts, "--apply")
}
if o.Autosquash {
opts = append(opts, "--autosquash")
}
if o.Autostash {
opts = append(opts, "--autostash")
}
if o.CommitterDateIsAuthorDate {
opts = append(opts, "--committer-date-is-author-date")
}
if o.Context > 0 {
opts = append(opts, "-C", strconv.FormatUint(uint64(o.Context), 10))
}
if o.Empty != "" {
opts = append(opts, fmt.Sprintf("--empty=%s", string(o.Empty)))
}
if o.Exec != "" {
opts = append(opts, fmt.Sprintf("--exec=%s", o.Exec))
}
if o.ForceRebase {
opts = append(opts, "--force-rebase")
}
if o.ForkPoint {
opts = append(opts, "--fork-point")
}
if o.GPGSign {
opts = append(opts, "--gpg-sign")
}
if o.GPGSignKeyID != "" {
opts = append(opts, fmt.Sprintf("--gpg-sign=%s", o.GPGSignKeyID))
}
if o.IgnoreDate {
opts = append(opts, "--ignore-date")
}
if o.IgnoreWhitespace {
opts = append(opts, "--ignore-whitespace")
}
if o.KeepBase != "" {
opts = append(opts, fmt.Sprintf("--keep-base=%s", o.KeepBase))
}
if o.KeepEmpty {
opts = append(opts, "--keep-empty")
}
if o.Merge {
opts = append(opts, "--merge")
}
if o.NoAutosquash {
opts = append(opts, "--no-autosquash")
}
if o.NoAutostash {
opts = append(opts, "--no-autostash")
}
if o.NoKeepEmpty {
opts = append(opts, "--no-keep-empty")
}
if o.NoReapplyCherryPicks {
opts = append(opts, "--no-reapply-cherry-picks")
}
if o.NoRebaseMerges {
opts = append(opts, "--no-rebase-merges")
}
if o.NoRescheduleFailedExec {
opts = append(opts, "--no-reschedule-failed-exec")
}
if o.NoReReReAutoupdate {
opts = append(opts, "--no-rerere-autoupdate")
}
if o.NoStat {
opts = append(opts, "--no-stat")
}
if o.NoUpdateRefs {
opts = append(opts, "--no-update-refs")
}
if o.NoVerify {
opts = append(opts, "--no-verify")
}
if o.Onto != "" {
opts = append(opts, fmt.Sprintf("--onto=%s", o.Onto))
}
if o.Quiet {
opts = append(opts, "--quiet")
}
if o.ReapplyCherryPicks {
opts = append(opts, "--reapply-cherry-picks")
}
if o.RebaseMerges != "" {
opts = append(opts, fmt.Sprintf("--rebase-merges=%s", string(o.RebaseMerges)))
}
if o.RescheduleFailedExec {
opts = append(opts, "--reschedule-failed-exec")
}
if o.ResetAuthorDate {
opts = append(opts, "--reset-author-date")
}
if o.ReReReAutoupdate {
opts = append(opts, "--rerere-autoupdate")
}
if o.Root {
opts = append(opts, "--root")
}
if o.Stat {
opts = append(opts, "--stat")
}
if o.Strategy != "" {
opts = append(opts, fmt.Sprintf("--strategy=%s", string(o.Strategy)))
}
for _, opt := range o.StragegyOptions {
opts = append(opts, fmt.Sprintf("--strategy-option=%s", string(opt)))
}
if o.UpdateRefs {
opts = append(opts, "--update-refs")
}
if o.Verbose {
opts = append(opts, "--verbose")
}
if o.Verify {
opts = append(opts, "--verify")
}
if o.Whitespace != "" {
opts = append(opts, fmt.Sprintf("--whitespace=%s", string(o.Whitespace)))
}
if o.Branch != "" {
opts = append(opts, o.Branch)
}
return opts
}

View File

@ -0,0 +1,94 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package git
import (
"context"
"strings"
)
// ResetMode is is mode to use when resetting the repository
type ResetMode string
const (
ResetModeSoft ResetMode = "soft"
ResetModeMixed ResetMode = "mixed"
ResetModeHard ResetMode = "hard"
ResetModeMerge ResetMode = "merge"
ResetModeKeep ResetMode = "keep"
)
// ResetOpts are the git reset flags and arguments
// See: https://git-scm.com/docs/git-reset
type ResetOpts struct {
// Options
Mode ResetMode // [--soft, --hard, etc..]
NoRefresh bool // --no-refresh
Patch bool // --patch
Quiet bool // --quiet
Refresh bool // --refresh
// Targets
Commit string // <commit>
Treeish string // <tree-ish>
PathSpec []string // <pathspec>
}
// Reset runs the git reset command
func (c *Client) Reset(ctx context.Context, opts *ResetOpts) (*ExecResponse, error) {
return c.Exec(ctx, "reset", opts)
}
// String returns the options as a string
func (o *ResetOpts) String() string {
return strings.Join(o.Strings(), " ")
}
// Strings returns the options as a string slice
func (o *ResetOpts) Strings() []string {
if o == nil {
return nil
}
opts := []string{}
// Do mode before flags if it set
if o.Mode != "" {
opts = append(opts, "--"+string(o.Mode))
}
// Flags
if o.NoRefresh {
opts = append(opts, "--no-refresh")
}
if o.Quiet {
opts = append(opts, "--quiet")
}
if o.Refresh {
opts = append(opts, "--refresh")
}
// Do Patch after flags but before our targets
if o.Patch {
opts = append(opts, "--patch")
}
// Do our targets
if o.Commit != "" {
opts = append(opts, o.Commit)
}
if o.Treeish != "" {
opts = append(opts, o.Treeish)
}
// If there's a pathspec, append the paths at the very end
if len(o.PathSpec) > 0 {
opts = append(append(opts, "--"), o.PathSpec...)
}
return opts
}

View File

@ -0,0 +1,75 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package git
import (
"context"
"strings"
)
// RmOpts are the git rm flags and arguments
type RmOpts struct {
Cached bool // --cached
DryRun bool // --dry-run
Force bool // --force
IgnoreUnmatched bool // --ignore-unmatched
Quiet bool // --quiet
Recursive bool // -r
Sparse bool // --sparse
PathSpec []string // <pathspec>
}
// Rm runs the git rm command
func (c *Client) Rm(ctx context.Context, opts *RmOpts) (*ExecResponse, error) {
return c.Exec(ctx, "rm", opts)
}
// String returns the options as a string
func (o *RmOpts) String() string {
return strings.Join(o.Strings(), " ")
}
// Strings returns the options as a string slice
func (o *RmOpts) Strings() []string {
if o == nil {
return nil
}
opts := []string{}
if o.Cached {
opts = append(opts, "--cached")
}
if o.DryRun {
opts = append(opts, "--dry-run")
}
if o.Force {
opts = append(opts, "--force")
}
if o.IgnoreUnmatched {
opts = append(opts, "--ignore-unmatched")
}
if o.Quiet {
opts = append(opts, "--quiet")
}
if o.Recursive {
opts = append(opts, "-r")
}
if o.Sparse {
opts = append(opts, "--sparse")
}
// If there's a pathspec, append the paths at the very end
if len(o.PathSpec) > 0 {
opts = append(append(opts, "--"), o.PathSpec...)
}
return opts
}

View File

@ -0,0 +1,108 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package git
import (
"context"
"fmt"
"strings"
)
type (
DiffAlgorithm = string
DiffMergeFormat = string
)
const (
DiffAlgorithmPatience DiffAlgorithm = "patience"
DiffAlgorithmMinimal DiffAlgorithm = "minimal"
DiffAlgorithmHistogram DiffAlgorithm = "histogram"
DiffAlgorithmMyers DiffAlgorithm = "myers"
DiffMergeFormatOff DiffMergeFormat = "off"
DiffMergeFormatNone DiffMergeFormat = "none"
DiffMergeFormatFirstParent DiffMergeFormat = "first-parent"
DiffMergeFormatSeparate DiffMergeFormat = "separate"
DiffMergeFormatCombined DiffMergeFormat = "combined"
DiffMergeFormatDenseCombined DiffMergeFormat = "dense-combined"
DiffMergeFormatRemerge DiffMergeFormat = "remerge"
)
// ShowOpts are the git show flags and arguments
// See: https://git-scm.com/docs/git-show
type ShowOpts struct {
// Options
DiffAlgorithm DiffAlgorithm // --diff-algorithm=<algo>
DiffMerges DiffMergeFormat // --diff-merges=<format>
Format string // --format <format>
NoColor bool // --no-color
NoPatch bool // --no-patch
Patch bool // --patch
Output string // --output=<file>
Raw bool // --raw
// Targets
Object string // <object>
PathSpec []string // <pathspec>
}
// Show runs the git show command
func (c *Client) Show(ctx context.Context, opts *ShowOpts) (*ExecResponse, error) {
return c.Exec(ctx, "show", opts)
}
// String returns the options as a string
func (o *ShowOpts) String() string {
return strings.Join(o.Strings(), " ")
}
// Strings returns the options as a string slice
func (o *ShowOpts) Strings() []string {
if o == nil {
return nil
}
opts := []string{}
if o.DiffAlgorithm != "" {
opts = append(opts, fmt.Sprintf("--diff-algorithm=%s", string(o.DiffAlgorithm)))
}
if o.DiffMerges != "" {
opts = append(opts, fmt.Sprintf("--diff-merges=%s", string(o.DiffMerges)))
}
if o.Format != "" {
opts = append(opts, fmt.Sprintf("--format=%s", string(o.Format)))
}
if o.NoColor {
opts = append(opts, "--no-color")
}
if o.NoPatch {
opts = append(opts, "--no-patch")
}
if o.Output != "" {
opts = append(opts, fmt.Sprintf("--output=%s", o.Output))
}
if o.Patch {
opts = append(opts, "--patch")
}
if o.Raw {
opts = append(opts, "--raw")
}
opts = append(opts, o.Object)
// If there's a pathspec, append the paths at the very end
if len(o.PathSpec) > 0 {
opts = append(append(opts, "--"), o.PathSpec...)
}
return opts
}

View File

@ -0,0 +1,142 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package git
import (
"context"
"fmt"
"strings"
)
type (
// IgnoredMode determines how to handle ignored files
IgnoredMode = string
// IgnoredMode determines how to handle changes to submodules
IgnoreSubmodulesWhen = string
)
const (
IgnoredModeTraditional IgnoredMode = "traditional"
IgnoredModeNo IgnoredMode = "no"
IgnoredModeMatching IgnoredMode = "matching"
IgnoreSubmodulesWhenNone IgnoreSubmodulesWhen = "none"
IgnoreSubmodulesWhenUntracked IgnoreSubmodulesWhen = "untracked"
IgnoreSubmodulesWhenDirty IgnoreSubmodulesWhen = "dirty"
IgnoreSubmodulesWhenAll IgnoreSubmodulesWhen = "all"
)
// StatusOpts are the git status flags and arguments
// See: https://git-scm.com/docs/git-status
type StatusOpts struct {
// Options
AheadBehind bool // --ahead-behind
Branch bool // --branch
Column string // --column=
FindRenames uint // --find-renames=
Ignored IgnoredMode // --ignored=
IgnoreSubmodules IgnoreSubmodulesWhen // --ignore-submodules=<when>
Long bool // --long
NoAheadBehind bool // --no-ahead-behind
NoColumn bool // --no-column
NoRenames bool // --no-renames
Porcelain bool // --porcelain
Renames bool // --renames
Short bool // --short
ShowStash bool // --show-stash
UntrackedFiles UntrackedFiles // --untracked-files=<mode>
Verbose bool // --verbose
// Targets
PathSpec []string // <pathspec>
}
// Status runs the git status command
func (c *Client) Status(ctx context.Context, opts *StatusOpts) (*ExecResponse, error) {
return c.Exec(ctx, "status", opts)
}
// String returns the options as a string
func (o *StatusOpts) String() string {
return strings.Join(o.Strings(), " ")
}
// Strings returns the options as a string slice
func (o *StatusOpts) Strings() []string {
if o == nil {
return nil
}
opts := []string{}
if o.AheadBehind {
opts = append(opts, "--ahead-behind")
}
if o.Branch {
opts = append(opts, "--branch")
}
if o.Column != "" {
opts = append(opts, fmt.Sprintf("--column=%s", o.Column))
}
if o.FindRenames > 0 {
opts = append(opts, fmt.Sprintf("--find-renames=%d", o.FindRenames))
}
if o.Ignored != "" {
opts = append(opts, fmt.Sprintf("--ignored=%s", string(o.Ignored)))
}
if o.IgnoreSubmodules != "" {
opts = append(opts, fmt.Sprintf("--ignore-submodules=%s", string(o.IgnoreSubmodules)))
}
if o.Long {
opts = append(opts, "--long")
}
if o.NoAheadBehind {
opts = append(opts, "--no-ahead-behind")
}
if o.NoColumn {
opts = append(opts, "--no-column")
}
if o.NoRenames {
opts = append(opts, "--no-renames")
}
if o.Porcelain {
opts = append(opts, "--porcelain")
}
if o.Renames {
opts = append(opts, "--renames")
}
if o.Short {
opts = append(opts, "--short")
}
if o.ShowStash {
opts = append(opts, "--show-stash")
}
if o.UntrackedFiles != "" {
opts = append(opts, fmt.Sprintf("--untracked-files=%s", string(o.UntrackedFiles)))
}
if o.Verbose {
opts = append(opts, "--verbose")
}
// If there's a pathspec, append the paths at the very end
if len(o.PathSpec) > 0 {
opts = append(append(opts, "--"), o.PathSpec...)
}
return opts
}