1package shared
2
3import (
4	"encoding/json"
5	"errors"
6	"fmt"
7	"io/ioutil"
8	"net/http"
9	"reflect"
10	"strings"
11	"time"
12
13	"github.com/cli/cli/v2/api"
14	"github.com/cli/cli/v2/internal/ghinstance"
15	"github.com/cli/cli/v2/internal/ghrepo"
16)
17
18var ReleaseFields = []string{
19	"url",
20	"apiUrl",
21	"uploadUrl",
22	"tarballUrl",
23	"zipballUrl",
24	"id",
25	"tagName",
26	"name",
27	"body",
28	"isDraft",
29	"isPrerelease",
30	"createdAt",
31	"publishedAt",
32	"targetCommitish",
33	"author",
34	"assets",
35}
36
37type Release struct {
38	ID           string     `json:"node_id"`
39	TagName      string     `json:"tag_name"`
40	Name         string     `json:"name"`
41	Body         string     `json:"body"`
42	IsDraft      bool       `json:"draft"`
43	IsPrerelease bool       `json:"prerelease"`
44	CreatedAt    time.Time  `json:"created_at"`
45	PublishedAt  *time.Time `json:"published_at"`
46
47	TargetCommitish string `json:"target_commitish"`
48
49	APIURL     string `json:"url"`
50	UploadURL  string `json:"upload_url"`
51	TarballURL string `json:"tarball_url"`
52	ZipballURL string `json:"zipball_url"`
53	URL        string `json:"html_url"`
54	Assets     []ReleaseAsset
55
56	Author struct {
57		ID    string `json:"node_id"`
58		Login string `json:"login"`
59	}
60}
61
62type ReleaseAsset struct {
63	ID     string `json:"node_id"`
64	Name   string
65	Label  string
66	Size   int64
67	State  string
68	APIURL string `json:"url"`
69
70	CreatedAt          time.Time `json:"created_at"`
71	UpdatedAt          time.Time `json:"updated_at"`
72	DownloadCount      int       `json:"download_count"`
73	ContentType        string    `json:"content_type"`
74	BrowserDownloadURL string    `json:"browser_download_url"`
75}
76
77func (rel *Release) ExportData(fields []string) map[string]interface{} {
78	v := reflect.ValueOf(rel).Elem()
79	fieldByName := func(v reflect.Value, field string) reflect.Value {
80		return v.FieldByNameFunc(func(s string) bool {
81			return strings.EqualFold(field, s)
82		})
83	}
84	data := map[string]interface{}{}
85
86	for _, f := range fields {
87		switch f {
88		case "author":
89			data[f] = map[string]interface{}{
90				"id":    rel.Author.ID,
91				"login": rel.Author.Login,
92			}
93		case "assets":
94			assets := make([]interface{}, 0, len(rel.Assets))
95			for _, a := range rel.Assets {
96				assets = append(assets, map[string]interface{}{
97					"url":           a.BrowserDownloadURL,
98					"apiUrl":        a.APIURL,
99					"id":            a.ID,
100					"name":          a.Name,
101					"label":         a.Label,
102					"size":          a.Size,
103					"state":         a.State,
104					"createdAt":     a.CreatedAt,
105					"updatedAt":     a.UpdatedAt,
106					"downloadCount": a.DownloadCount,
107					"contentType":   a.ContentType,
108				})
109			}
110			data[f] = assets
111		default:
112			sf := fieldByName(v, f)
113			data[f] = sf.Interface()
114		}
115	}
116
117	return data
118}
119
120// FetchRelease finds a repository release by its tagName.
121func FetchRelease(httpClient *http.Client, baseRepo ghrepo.Interface, tagName string) (*Release, error) {
122	path := fmt.Sprintf("repos/%s/%s/releases/tags/%s", baseRepo.RepoOwner(), baseRepo.RepoName(), tagName)
123	url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path
124	req, err := http.NewRequest("GET", url, nil)
125	if err != nil {
126		return nil, err
127	}
128
129	resp, err := httpClient.Do(req)
130	if err != nil {
131		return nil, err
132	}
133	defer resp.Body.Close()
134
135	if resp.StatusCode == 404 {
136		return FindDraftRelease(httpClient, baseRepo, tagName)
137	}
138
139	if resp.StatusCode > 299 {
140		return nil, api.HandleHTTPError(resp)
141	}
142
143	b, err := ioutil.ReadAll(resp.Body)
144	if err != nil {
145		return nil, err
146	}
147
148	var release Release
149	err = json.Unmarshal(b, &release)
150	if err != nil {
151		return nil, err
152	}
153
154	return &release, nil
155}
156
157// FetchLatestRelease finds the latest published release for a repository.
158func FetchLatestRelease(httpClient *http.Client, baseRepo ghrepo.Interface) (*Release, error) {
159	path := fmt.Sprintf("repos/%s/%s/releases/latest", baseRepo.RepoOwner(), baseRepo.RepoName())
160	url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path
161	req, err := http.NewRequest("GET", url, nil)
162	if err != nil {
163		return nil, err
164	}
165
166	resp, err := httpClient.Do(req)
167	if err != nil {
168		return nil, err
169	}
170	defer resp.Body.Close()
171
172	if resp.StatusCode > 299 {
173		return nil, api.HandleHTTPError(resp)
174	}
175
176	b, err := ioutil.ReadAll(resp.Body)
177	if err != nil {
178		return nil, err
179	}
180
181	var release Release
182	err = json.Unmarshal(b, &release)
183	if err != nil {
184		return nil, err
185	}
186
187	return &release, nil
188}
189
190// FindDraftRelease returns the latest draft release that matches tagName.
191func FindDraftRelease(httpClient *http.Client, baseRepo ghrepo.Interface, tagName string) (*Release, error) {
192	path := fmt.Sprintf("repos/%s/%s/releases", baseRepo.RepoOwner(), baseRepo.RepoName())
193	url := ghinstance.RESTPrefix(baseRepo.RepoHost()) + path
194
195	perPage := 100
196	page := 1
197	for {
198		req, err := http.NewRequest("GET", fmt.Sprintf("%s?per_page=%d&page=%d", url, perPage, page), nil)
199		if err != nil {
200			return nil, err
201		}
202
203		resp, err := httpClient.Do(req)
204		if err != nil {
205			return nil, err
206		}
207		defer resp.Body.Close()
208
209		if resp.StatusCode > 299 {
210			return nil, api.HandleHTTPError(resp)
211		}
212
213		b, err := ioutil.ReadAll(resp.Body)
214		if err != nil {
215			return nil, err
216		}
217
218		var releases []Release
219		err = json.Unmarshal(b, &releases)
220		if err != nil {
221			return nil, err
222		}
223
224		for _, r := range releases {
225			if r.IsDraft && r.TagName == tagName {
226				return &r, nil
227			}
228		}
229		//nolint:staticcheck
230		break
231	}
232
233	return nil, errors.New("release not found")
234}
235