github.com/google/go-github/v71@v71.0.0/github/git_commits.go (about)

     1  // Copyright 2013 The go-github AUTHORS. All rights reserved.
     2  //
     3  // Use of this source code is governed by a BSD-style
     4  // license that can be found in the LICENSE file.
     5  
     6  package github
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"strings"
    15  )
    16  
    17  // SignatureVerification represents GPG signature verification.
    18  type SignatureVerification struct {
    19  	Verified  *bool   `json:"verified,omitempty"`
    20  	Reason    *string `json:"reason,omitempty"`
    21  	Signature *string `json:"signature,omitempty"`
    22  	Payload   *string `json:"payload,omitempty"`
    23  }
    24  
    25  // MessageSigner is used by GitService.CreateCommit to sign a commit.
    26  //
    27  // To create a MessageSigner that signs a commit with a [golang.org/x/crypto/openpgp.Entity],
    28  // or [github.com/ProtonMail/go-crypto/openpgp.Entity], use:
    29  //
    30  //	commit.Signer = github.MessageSignerFunc(func(w io.Writer, r io.Reader) error {
    31  //		return openpgp.ArmoredDetachSign(w, openpgpEntity, r, nil)
    32  //	})
    33  type MessageSigner interface {
    34  	Sign(w io.Writer, r io.Reader) error
    35  }
    36  
    37  // MessageSignerFunc is a single function implementation of MessageSigner.
    38  type MessageSignerFunc func(w io.Writer, r io.Reader) error
    39  
    40  func (f MessageSignerFunc) Sign(w io.Writer, r io.Reader) error {
    41  	return f(w, r)
    42  }
    43  
    44  // Commit represents a GitHub commit.
    45  type Commit struct {
    46  	SHA          *string                `json:"sha,omitempty"`
    47  	Author       *CommitAuthor          `json:"author,omitempty"`
    48  	Committer    *CommitAuthor          `json:"committer,omitempty"`
    49  	Message      *string                `json:"message,omitempty"`
    50  	Tree         *Tree                  `json:"tree,omitempty"`
    51  	Parents      []*Commit              `json:"parents,omitempty"`
    52  	HTMLURL      *string                `json:"html_url,omitempty"`
    53  	URL          *string                `json:"url,omitempty"`
    54  	Verification *SignatureVerification `json:"verification,omitempty"`
    55  	NodeID       *string                `json:"node_id,omitempty"`
    56  
    57  	// CommentCount is the number of GitHub comments on the commit. This
    58  	// is only populated for requests that fetch GitHub data like
    59  	// Pulls.ListCommits, Repositories.ListCommits, etc.
    60  	CommentCount *int `json:"comment_count,omitempty"`
    61  }
    62  
    63  func (c Commit) String() string {
    64  	return Stringify(c)
    65  }
    66  
    67  // CommitAuthor represents the author or committer of a commit. The commit
    68  // author may not correspond to a GitHub User.
    69  type CommitAuthor struct {
    70  	Date  *Timestamp `json:"date,omitempty"`
    71  	Name  *string    `json:"name,omitempty"`
    72  	Email *string    `json:"email,omitempty"`
    73  
    74  	// The following fields are only populated by Webhook events.
    75  	Login *string `json:"username,omitempty"` // Renamed for go-github consistency.
    76  }
    77  
    78  func (c CommitAuthor) String() string {
    79  	return Stringify(c)
    80  }
    81  
    82  // GetCommit fetches the Commit object for a given SHA.
    83  //
    84  // GitHub API docs: https://docs.github.com/rest/git/commits#get-a-commit-object
    85  //
    86  //meta:operation GET /repos/{owner}/{repo}/git/commits/{commit_sha}
    87  func (s *GitService) GetCommit(ctx context.Context, owner string, repo string, sha string) (*Commit, *Response, error) {
    88  	u := fmt.Sprintf("repos/%v/%v/git/commits/%v", owner, repo, sha)
    89  	req, err := s.client.NewRequest("GET", u, nil)
    90  	if err != nil {
    91  		return nil, nil, err
    92  	}
    93  
    94  	c := new(Commit)
    95  	resp, err := s.client.Do(ctx, req, c)
    96  	if err != nil {
    97  		return nil, resp, err
    98  	}
    99  
   100  	return c, resp, nil
   101  }
   102  
   103  // createCommit represents the body of a CreateCommit request.
   104  type createCommit struct {
   105  	Author    *CommitAuthor `json:"author,omitempty"`
   106  	Committer *CommitAuthor `json:"committer,omitempty"`
   107  	Message   *string       `json:"message,omitempty"`
   108  	Tree      *string       `json:"tree,omitempty"`
   109  	Parents   []string      `json:"parents,omitempty"`
   110  	Signature *string       `json:"signature,omitempty"`
   111  }
   112  
   113  type CreateCommitOptions struct {
   114  	// CreateCommit will sign the commit with this signer. See MessageSigner doc for more details.
   115  	// Ignored on commits where Verification.Signature is defined.
   116  	Signer MessageSigner
   117  }
   118  
   119  // CreateCommit creates a new commit in a repository.
   120  // commit must not be nil.
   121  //
   122  // The commit.Committer is optional and will be filled with the commit.Author
   123  // data if omitted. If the commit.Author is omitted, it will be filled in with
   124  // the authenticated user’s information and the current date.
   125  //
   126  // GitHub API docs: https://docs.github.com/rest/git/commits#create-a-commit
   127  //
   128  //meta:operation POST /repos/{owner}/{repo}/git/commits
   129  func (s *GitService) CreateCommit(ctx context.Context, owner string, repo string, commit *Commit, opts *CreateCommitOptions) (*Commit, *Response, error) {
   130  	if commit == nil {
   131  		return nil, nil, errors.New("commit must be provided")
   132  	}
   133  	if opts == nil {
   134  		opts = &CreateCommitOptions{}
   135  	}
   136  
   137  	u := fmt.Sprintf("repos/%v/%v/git/commits", owner, repo)
   138  
   139  	parents := make([]string, len(commit.Parents))
   140  	for i, parent := range commit.Parents {
   141  		parents[i] = *parent.SHA
   142  	}
   143  
   144  	body := &createCommit{
   145  		Author:    commit.Author,
   146  		Committer: commit.Committer,
   147  		Message:   commit.Message,
   148  		Parents:   parents,
   149  	}
   150  	if commit.Tree != nil {
   151  		body.Tree = commit.Tree.SHA
   152  	}
   153  	switch {
   154  	case commit.Verification != nil:
   155  		body.Signature = commit.Verification.Signature
   156  	case opts.Signer != nil:
   157  		signature, err := createSignature(opts.Signer, body)
   158  		if err != nil {
   159  			return nil, nil, err
   160  		}
   161  		body.Signature = &signature
   162  	}
   163  
   164  	req, err := s.client.NewRequest("POST", u, body)
   165  	if err != nil {
   166  		return nil, nil, err
   167  	}
   168  
   169  	c := new(Commit)
   170  	resp, err := s.client.Do(ctx, req, c)
   171  	if err != nil {
   172  		return nil, resp, err
   173  	}
   174  
   175  	return c, resp, nil
   176  }
   177  
   178  func createSignature(signer MessageSigner, commit *createCommit) (string, error) {
   179  	if signer == nil {
   180  		return "", errors.New("createSignature: invalid parameters")
   181  	}
   182  
   183  	message, err := createSignatureMessage(commit)
   184  	if err != nil {
   185  		return "", err
   186  	}
   187  
   188  	var writer bytes.Buffer
   189  	err = signer.Sign(&writer, strings.NewReader(message))
   190  	if err != nil {
   191  		return "", err
   192  	}
   193  
   194  	return writer.String(), nil
   195  }
   196  
   197  func createSignatureMessage(commit *createCommit) (string, error) {
   198  	if commit == nil || commit.Message == nil || *commit.Message == "" || commit.Author == nil {
   199  		return "", errors.New("createSignatureMessage: invalid parameters")
   200  	}
   201  
   202  	var message []string
   203  
   204  	if commit.Tree != nil {
   205  		message = append(message, fmt.Sprintf("tree %s", *commit.Tree))
   206  	}
   207  
   208  	for _, parent := range commit.Parents {
   209  		message = append(message, fmt.Sprintf("parent %s", parent))
   210  	}
   211  
   212  	message = append(message, fmt.Sprintf("author %s <%s> %d %s", commit.Author.GetName(), commit.Author.GetEmail(), commit.Author.GetDate().Unix(), commit.Author.GetDate().Format("-0700")))
   213  
   214  	committer := commit.Committer
   215  	if committer == nil {
   216  		committer = commit.Author
   217  	}
   218  
   219  	// There needs to be a double newline after committer
   220  	message = append(message, fmt.Sprintf("committer %s <%s> %d %s\n", committer.GetName(), committer.GetEmail(), committer.GetDate().Unix(), committer.GetDate().Format("-0700")))
   221  	message = append(message, *commit.Message)
   222  
   223  	return strings.Join(message, "\n"), nil
   224  }