github.com/google/go-github/v57@v57.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  	Stats        *CommitStats           `json:"stats,omitempty"`
    53  	HTMLURL      *string                `json:"html_url,omitempty"`
    54  	URL          *string                `json:"url,omitempty"`
    55  	Verification *SignatureVerification `json:"verification,omitempty"`
    56  	NodeID       *string                `json:"node_id,omitempty"`
    57  
    58  	// CommentCount is the number of GitHub comments on the commit. This
    59  	// is only populated for requests that fetch GitHub data like
    60  	// Pulls.ListCommits, Repositories.ListCommits, etc.
    61  	CommentCount *int `json:"comment_count,omitempty"`
    62  }
    63  
    64  func (c Commit) String() string {
    65  	return Stringify(c)
    66  }
    67  
    68  // CommitAuthor represents the author or committer of a commit. The commit
    69  // author may not correspond to a GitHub User.
    70  type CommitAuthor struct {
    71  	Date  *Timestamp `json:"date,omitempty"`
    72  	Name  *string    `json:"name,omitempty"`
    73  	Email *string    `json:"email,omitempty"`
    74  
    75  	// The following fields are only populated by Webhook events.
    76  	Login *string `json:"username,omitempty"` // Renamed for go-github consistency.
    77  }
    78  
    79  func (c CommitAuthor) String() string {
    80  	return Stringify(c)
    81  }
    82  
    83  // GetCommit fetches the Commit object for a given SHA.
    84  //
    85  // GitHub API docs: https://docs.github.com/rest/git/commits#get-a-commit-object
    86  //
    87  //meta:operation GET /repos/{owner}/{repo}/git/commits/{commit_sha}
    88  func (s *GitService) GetCommit(ctx context.Context, owner string, repo string, sha string) (*Commit, *Response, error) {
    89  	u := fmt.Sprintf("repos/%v/%v/git/commits/%v", owner, repo, sha)
    90  	req, err := s.client.NewRequest("GET", u, nil)
    91  	if err != nil {
    92  		return nil, nil, err
    93  	}
    94  
    95  	c := new(Commit)
    96  	resp, err := s.client.Do(ctx, req, c)
    97  	if err != nil {
    98  		return nil, resp, err
    99  	}
   100  
   101  	return c, resp, nil
   102  }
   103  
   104  // createCommit represents the body of a CreateCommit request.
   105  type createCommit struct {
   106  	Author    *CommitAuthor `json:"author,omitempty"`
   107  	Committer *CommitAuthor `json:"committer,omitempty"`
   108  	Message   *string       `json:"message,omitempty"`
   109  	Tree      *string       `json:"tree,omitempty"`
   110  	Parents   []string      `json:"parents,omitempty"`
   111  	Signature *string       `json:"signature,omitempty"`
   112  }
   113  
   114  type CreateCommitOptions struct {
   115  	// CreateCommit will sign the commit with this signer. See MessageSigner doc for more details.
   116  	// Ignored on commits where Verification.Signature is defined.
   117  	Signer MessageSigner
   118  }
   119  
   120  // CreateCommit creates a new commit in a repository.
   121  // commit must not be nil.
   122  //
   123  // The commit.Committer is optional and will be filled with the commit.Author
   124  // data if omitted. If the commit.Author is omitted, it will be filled in with
   125  // the authenticated user’s information and the current date.
   126  //
   127  // GitHub API docs: https://docs.github.com/rest/git/commits#create-a-commit
   128  //
   129  //meta:operation POST /repos/{owner}/{repo}/git/commits
   130  func (s *GitService) CreateCommit(ctx context.Context, owner string, repo string, commit *Commit, opts *CreateCommitOptions) (*Commit, *Response, error) {
   131  	if commit == nil {
   132  		return nil, nil, fmt.Errorf("commit must be provided")
   133  	}
   134  	if opts == nil {
   135  		opts = &CreateCommitOptions{}
   136  	}
   137  
   138  	u := fmt.Sprintf("repos/%v/%v/git/commits", owner, repo)
   139  
   140  	parents := make([]string, len(commit.Parents))
   141  	for i, parent := range commit.Parents {
   142  		parents[i] = *parent.SHA
   143  	}
   144  
   145  	body := &createCommit{
   146  		Author:    commit.Author,
   147  		Committer: commit.Committer,
   148  		Message:   commit.Message,
   149  		Parents:   parents,
   150  	}
   151  	if commit.Tree != nil {
   152  		body.Tree = commit.Tree.SHA
   153  	}
   154  	switch {
   155  	case commit.Verification != nil:
   156  		body.Signature = commit.Verification.Signature
   157  	case opts.Signer != nil:
   158  		signature, err := createSignature(opts.Signer, body)
   159  		if err != nil {
   160  			return nil, nil, err
   161  		}
   162  		body.Signature = &signature
   163  	}
   164  
   165  	req, err := s.client.NewRequest("POST", u, body)
   166  	if err != nil {
   167  		return nil, nil, err
   168  	}
   169  
   170  	c := new(Commit)
   171  	resp, err := s.client.Do(ctx, req, c)
   172  	if err != nil {
   173  		return nil, resp, err
   174  	}
   175  
   176  	return c, resp, nil
   177  }
   178  
   179  func createSignature(signer MessageSigner, commit *createCommit) (string, error) {
   180  	if signer == nil {
   181  		return "", errors.New("createSignature: invalid parameters")
   182  	}
   183  
   184  	message, err := createSignatureMessage(commit)
   185  	if err != nil {
   186  		return "", err
   187  	}
   188  
   189  	var writer bytes.Buffer
   190  	err = signer.Sign(&writer, strings.NewReader(message))
   191  	if err != nil {
   192  		return "", err
   193  	}
   194  
   195  	return writer.String(), nil
   196  }
   197  
   198  func createSignatureMessage(commit *createCommit) (string, error) {
   199  	if commit == nil || commit.Message == nil || *commit.Message == "" || commit.Author == nil {
   200  		return "", errors.New("createSignatureMessage: invalid parameters")
   201  	}
   202  
   203  	var message []string
   204  
   205  	if commit.Tree != nil {
   206  		message = append(message, fmt.Sprintf("tree %s", *commit.Tree))
   207  	}
   208  
   209  	for _, parent := range commit.Parents {
   210  		message = append(message, fmt.Sprintf("parent %s", parent))
   211  	}
   212  
   213  	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")))
   214  
   215  	committer := commit.Committer
   216  	if committer == nil {
   217  		committer = commit.Author
   218  	}
   219  
   220  	// There needs to be a double newline after committer
   221  	message = append(message, fmt.Sprintf("committer %s <%s> %d %s\n", committer.GetName(), committer.GetEmail(), committer.GetDate().Unix(), committer.GetDate().Format("-0700")))
   222  	message = append(message, *commit.Message)
   223  
   224  	return strings.Join(message, "\n"), nil
   225  }