github.com/arsham/gitrelease@v0.3.2-0.20221207124258-6867180b2c2d/commit/git.go (about)

     1  // Package commit contains the logic for interacting with git, commits and
     2  // github.
     3  package commit
     4  
     5  import (
     6  	"bytes"
     7  	"context"
     8  	"encoding/json"
     9  	"fmt"
    10  	"net/http"
    11  	"os/exec"
    12  	"regexp"
    13  	"strings"
    14  
    15  	"github.com/github-release/github-release/github"
    16  	"github.com/pkg/errors"
    17  )
    18  
    19  const baseURL = "https://api.github.com"
    20  
    21  // Git executes git processes targeted at a directory. If the Dir property is
    22  // empty, all calls will be on the current folder.
    23  type Git struct {
    24  	Dir    string
    25  	Remote string
    26  }
    27  
    28  // LatestTag returns the last tag in the repository.
    29  func (g Git) LatestTag(ctx context.Context) (string, error) {
    30  	args := []string{
    31  		"describe",
    32  		"--tags",
    33  		"--abbrev=0",
    34  	}
    35  	cmd := exec.CommandContext(ctx, "git", args...)
    36  	cmd.Dir = g.Dir
    37  	out, err := cmd.CombinedOutput()
    38  	if err != nil {
    39  		return "", errors.Wrap(err, string(out))
    40  	}
    41  
    42  	return strings.Trim(string(out), "\n"), nil
    43  }
    44  
    45  // PreviousTag returns the previous tag of the given tag.
    46  func (g Git) PreviousTag(ctx context.Context, tag string) (string, error) {
    47  	args := []string{
    48  		"describe",
    49  		"--tags",
    50  		"--abbrev=0",
    51  		tag + "^",
    52  	}
    53  	// nolint:gosec // we don't have any other way to get the previous tag.
    54  	cmd := exec.CommandContext(ctx, "git", args...)
    55  	cmd.Dir = g.Dir
    56  	out, err := cmd.CombinedOutput()
    57  	if err != nil {
    58  		return "", errors.Wrap(err, string(out))
    59  	}
    60  
    61  	return strings.Trim(string(out), "\n"), nil
    62  }
    63  
    64  // Commits returns the contents of all commits between two tags.
    65  func (g Git) Commits(ctx context.Context, tag1, tag2 string) ([]string, error) {
    66  	separator := "00000000000000000000000000000000000"
    67  	args := []string{
    68  		"log",
    69  		"--oneline",
    70  		fmt.Sprintf("%s..%s", tag1, tag2),
    71  		fmt.Sprintf("--pretty=%s%%B", separator),
    72  	}
    73  	// nolint:gosec // we need these variables.
    74  	cmd := exec.CommandContext(ctx, "git", args...)
    75  	cmd.Dir = g.Dir
    76  	out, err := cmd.CombinedOutput()
    77  	if err != nil {
    78  		return nil, errors.Wrap(err, string(out))
    79  	}
    80  	logs := strings.Split(string(out), separator)
    81  	return logs, nil
    82  }
    83  
    84  var infoRe = regexp.MustCompile(`github\.com[:/](?P<user>[^/]+)/(?P<repo>.+?)(?:.git)?\n?$`)
    85  
    86  // RepoInfo returns some information about the repository.
    87  func (g Git) RepoInfo(ctx context.Context) (user, repo string, err error) {
    88  	if g.Remote == "" {
    89  		g.Remote = "origin"
    90  	}
    91  	args := []string{
    92  		"config",
    93  		"--get",
    94  		fmt.Sprintf("remote.%s.url", g.Remote),
    95  	}
    96  	// nolint:gosec // it is required.
    97  	cmd := exec.CommandContext(ctx, "git", args...)
    98  	cmd.Dir = g.Dir
    99  	out, err := cmd.CombinedOutput()
   100  	if err != nil {
   101  		return "", "", errors.Wrap(err, string(out))
   102  	}
   103  
   104  	info := infoRe.FindStringSubmatch(string(out))
   105  	if len(info) != 3 {
   106  		return "", "", fmt.Errorf("could not parse repository info: %s", string(out))
   107  	}
   108  	user = info[1]
   109  	repo = info[2]
   110  
   111  	return user, repo, nil
   112  }
   113  
   114  type releaseCreate struct {
   115  	TagName         string `json:"tag_name"`
   116  	TargetCommitish string `json:"target_commitish,omitempty"`
   117  	Name            string `json:"name"`
   118  	Body            string `json:"body"`
   119  	Draft           bool   `json:"draft"`
   120  	Prerelease      bool   `json:"prerelease"`
   121  }
   122  
   123  // Release publishes the release for the user on the repo.
   124  func (g Git) Release(ctx context.Context, token, user, repo, tag, desc string) error {
   125  	params := releaseCreate{
   126  		TagName: tag,
   127  		Body:    desc,
   128  	}
   129  
   130  	payload, err := json.Marshal(params)
   131  	if err != nil {
   132  		return errors.Wrap(err, "marshalling values")
   133  	}
   134  
   135  	client := github.NewClient(repo, token, nil)
   136  	client.SetBaseURL(baseURL)
   137  	reader := bytes.NewReader(payload)
   138  	req, err := client.NewRequest("POST", fmt.Sprintf("/repos/%s/%s/releases", user, repo), reader)
   139  	if err != nil {
   140  		return errors.Wrapf(err, "creating request to the API: %q", string(payload))
   141  	}
   142  	req = req.WithContext(ctx)
   143  
   144  	resp, err := client.Do(req)
   145  	if err != nil {
   146  		return errors.Wrapf(err, "submitting to the API: %q", string(payload))
   147  	}
   148  	// nolint:errcheck // it's ok.
   149  	defer resp.Body.Close()
   150  
   151  	if resp.StatusCode != http.StatusCreated {
   152  		if resp.StatusCode == 422 {
   153  			return errors.New("release already exists")
   154  		}
   155  		return fmt.Errorf("error publishing release with code: %q", resp.Status)
   156  	}
   157  	return nil
   158  }