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 }