github.com/purpleclay/gitz@v0.8.2-0.20240515052600-43f80eea2fe1/commit.go (about)

     1  package git
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  )
     7  
     8  // CommitOption provides a way for setting specific options during a commit
     9  // operation. Each supported option can customize the way the commit is
    10  // created against the current repository (working directory)
    11  type CommitOption func(*commitOptions)
    12  
    13  type commitOptions struct {
    14  	AllowEmpty    bool
    15  	Config        []string
    16  	ForceNoSigned bool
    17  	Signed        bool
    18  	SigningKey    string
    19  }
    20  
    21  // WithAllowEmpty allows a commit to be created without having to track
    22  // any changes. This bypasses the default protection by git, preventing
    23  // a commit from having the exact same tree as its parent
    24  func WithAllowEmpty() CommitOption {
    25  	return func(opts *commitOptions) {
    26  		opts.AllowEmpty = true
    27  	}
    28  }
    29  
    30  // WithCommitConfig allows temporary git config to be set during the
    31  // execution of the commit. Config set using this approach will override
    32  // any config defined within existing git config files. Config must be
    33  // provided as key value pairs, mismatched config will result in an
    34  // [ErrMissingConfigValue] error. Any invalid paths will result in an
    35  // [ErrInvalidConfigPath] error
    36  func WithCommitConfig(kv ...string) CommitOption {
    37  	return func(opts *commitOptions) {
    38  		opts.Config = trim(kv...)
    39  	}
    40  }
    41  
    42  // WithGpgSign will create a GPG-signed commit using the GPG key associated
    43  // with the committers email address. Overriding this behavior is possible
    44  // through the user.signingkey config setting. This option does not need
    45  // to be explicitly called if the commit.gpgSign config setting is set to
    46  // true
    47  func WithGpgSign() CommitOption {
    48  	return func(opts *commitOptions) {
    49  		opts.Signed = true
    50  	}
    51  }
    52  
    53  // WithGpgSigningKey will create a GPG-signed commit using the provided GPG
    54  // key ID, overridding any default GPG key set by the user.signingKey git
    55  // config setting
    56  func WithGpgSigningKey(key string) CommitOption {
    57  	return func(opts *commitOptions) {
    58  		opts.Signed = true
    59  		opts.SigningKey = strings.TrimSpace(key)
    60  	}
    61  }
    62  
    63  // WithNoGpgSign ensures the created commit will not be GPG signed
    64  // regardless of the value assigned to the repositories commit.gpgSign
    65  // git config setting
    66  func WithNoGpgSign() CommitOption {
    67  	return func(opts *commitOptions) {
    68  		opts.ForceNoSigned = true
    69  	}
    70  }
    71  
    72  // Commit a snapshot of changes within the current repository (working directory)
    73  // and describe those changes with a given log message. Commit behavior can be
    74  // customized through the use of options
    75  func (c *Client) Commit(msg string, opts ...CommitOption) (string, error) {
    76  	options := &commitOptions{}
    77  	for _, opt := range opts {
    78  		opt(options)
    79  	}
    80  
    81  	cfg, err := ToInlineConfig(options.Config...)
    82  	if err != nil {
    83  		return "", err
    84  	}
    85  
    86  	var buf strings.Builder
    87  	buf.WriteString("git")
    88  
    89  	if len(cfg) > 0 {
    90  		buf.WriteString(" ")
    91  		buf.WriteString(strings.Join(cfg, " "))
    92  	}
    93  	buf.WriteString(" commit")
    94  
    95  	if options.AllowEmpty {
    96  		buf.WriteString(" --allow-empty")
    97  	}
    98  
    99  	if options.Signed {
   100  		buf.WriteString(" -S")
   101  	}
   102  
   103  	if options.SigningKey != "" {
   104  		buf.WriteString(" --gpg-sign=" + options.SigningKey)
   105  	}
   106  
   107  	if options.ForceNoSigned {
   108  		buf.WriteString(" --no-gpg-sign")
   109  	}
   110  
   111  	buf.WriteString(fmt.Sprintf(" -m '%s'", msg))
   112  	return c.exec(buf.String())
   113  }
   114  
   115  // CommitVerification contains details about a GPG signed commit
   116  type CommitVerification struct {
   117  	// Author represents a person who originally created the files
   118  	// within the repository
   119  	Author Person
   120  
   121  	// Committer represents a person who changed any existing files
   122  	// within the repository
   123  	Committer Person
   124  
   125  	// Hash contains the unique identifier associated with the commit
   126  	Hash string
   127  
   128  	// Message contains the message associated with the commit
   129  	Message string
   130  
   131  	// Signature contains details of the verified GPG signature
   132  	Signature *Signature
   133  }
   134  
   135  // VerifyCommit validates that a given commit has a valid GPG signature
   136  // and returns details about that signature
   137  func (c *Client) VerifyCommit(hash string) (*CommitVerification, error) {
   138  	out, err := c.exec("git verify-commit -v " + hash)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  
   143  	out, _ = until("author ")(out)
   144  	out, pair := separatedPair(tag("author "), ws(), until("committer "))(out)
   145  	author := parsePerson(pair[1])
   146  
   147  	out, pair = separatedPair(tag("committer "), ws(), takeUntil(lineEnding))(out)
   148  	committer := parsePerson(pair[1])
   149  	out, _ = line()(out)
   150  
   151  	out, mesage := until("gpg: ")(out)
   152  
   153  	return &CommitVerification{
   154  		Author:    author,
   155  		Committer: committer,
   156  		Hash:      hash,
   157  		Message:   strings.TrimSpace(mesage),
   158  		Signature: parseSignature(out),
   159  	}, nil
   160  }