webctx 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +97 -0
- package/CONTRIBUTORS.md +126 -0
- package/LICENSE +202 -0
- package/Makefile +69 -0
- package/README.md +121 -0
- package/bin/webctx.js +28 -0
- package/cmd/webctx/main.go +11 -0
- package/docs/porting-status.md +173 -0
- package/go.mod +3 -0
- package/internal/app/app.go +139 -0
- package/internal/app/app_test.go +194 -0
- package/internal/app/github.go +2084 -0
- package/internal/app/github_acceptance_test.go +166 -0
- package/internal/app/github_actions.go +1008 -0
- package/internal/app/github_actions_test.go +484 -0
- package/internal/app/github_activity_deployments.go +503 -0
- package/internal/app/github_activity_deployments_test.go +321 -0
- package/internal/app/github_commits.go +1004 -0
- package/internal/app/github_commits_test.go +503 -0
- package/internal/app/github_discussions_gists.go +1156 -0
- package/internal/app/github_discussions_gists_test.go +425 -0
- package/internal/app/github_issues.go +1616 -0
- package/internal/app/github_issues_test.go +731 -0
- package/internal/app/github_packages_projects.go +429 -0
- package/internal/app/github_packages_projects_test.go +168 -0
- package/internal/app/github_phase9_overview_stress_test.go +178 -0
- package/internal/app/github_pull_views.go +1141 -0
- package/internal/app/github_pull_views_test.go +603 -0
- package/internal/app/github_pulls.go +1459 -0
- package/internal/app/github_pulls_test.go +715 -0
- package/internal/app/github_refs.go +621 -0
- package/internal/app/github_refs_test.go +260 -0
- package/internal/app/github_search_profiles.go +505 -0
- package/internal/app/github_search_profiles_test.go +352 -0
- package/internal/app/github_test.go +888 -0
- package/internal/app/scrape.go +264 -0
- package/internal/app/tools.go +670 -0
- package/internal/buildinfo/buildinfo.go +16 -0
- package/package.json +66 -0
- package/scripts/postinstall.js +137 -0
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
package app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"net/http"
|
|
8
|
+
"net/url"
|
|
9
|
+
"sort"
|
|
10
|
+
"strconv"
|
|
11
|
+
"strings"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
type githubBranch struct {
|
|
15
|
+
Name string `json:"name"`
|
|
16
|
+
Protected bool `json:"protected"`
|
|
17
|
+
Commit struct {
|
|
18
|
+
SHA string `json:"sha"`
|
|
19
|
+
URL string `json:"url"`
|
|
20
|
+
} `json:"commit"`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type githubTag struct {
|
|
24
|
+
Name string `json:"name"`
|
|
25
|
+
ZipballURL string `json:"zipball_url"`
|
|
26
|
+
TarballURL string `json:"tarball_url"`
|
|
27
|
+
Commit struct {
|
|
28
|
+
SHA string `json:"sha"`
|
|
29
|
+
URL string `json:"url"`
|
|
30
|
+
} `json:"commit"`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type githubReleaseAsset struct {
|
|
34
|
+
ID int64 `json:"id"`
|
|
35
|
+
Name string `json:"name"`
|
|
36
|
+
Label string `json:"label"`
|
|
37
|
+
ContentType string `json:"content_type"`
|
|
38
|
+
State string `json:"state"`
|
|
39
|
+
Size int64 `json:"size"`
|
|
40
|
+
DownloadCount int `json:"download_count"`
|
|
41
|
+
BrowserDownloadURL string `json:"browser_download_url"`
|
|
42
|
+
CreatedAt string `json:"created_at"`
|
|
43
|
+
UpdatedAt string `json:"updated_at"`
|
|
44
|
+
Uploader githubUser `json:"uploader"`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type githubRelease struct {
|
|
48
|
+
ID int64 `json:"id"`
|
|
49
|
+
TagName string `json:"tag_name"`
|
|
50
|
+
Target string `json:"target_commitish"`
|
|
51
|
+
Name string `json:"name"`
|
|
52
|
+
Body *string `json:"body"`
|
|
53
|
+
HTMLURL string `json:"html_url"`
|
|
54
|
+
Draft bool `json:"draft"`
|
|
55
|
+
Prerelease bool `json:"prerelease"`
|
|
56
|
+
CreatedAt string `json:"created_at"`
|
|
57
|
+
PublishedAt string `json:"published_at"`
|
|
58
|
+
Author githubUser `json:"author"`
|
|
59
|
+
Assets []githubReleaseAsset `json:"assets"`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type githubReleaseAssetsAvailability struct {
|
|
63
|
+
ProviderMore bool
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type githubFork struct {
|
|
67
|
+
FullName string `json:"full_name"`
|
|
68
|
+
HTMLURL string `json:"html_url"`
|
|
69
|
+
Description *string `json:"description"`
|
|
70
|
+
Language *string `json:"language"`
|
|
71
|
+
DefaultBranch string `json:"default_branch"`
|
|
72
|
+
StargazersCount int `json:"stargazers_count"`
|
|
73
|
+
ForksCount int `json:"forks_count"`
|
|
74
|
+
Archived bool `json:"archived"`
|
|
75
|
+
UpdatedAt string `json:"updated_at"`
|
|
76
|
+
Owner githubUser `json:"owner"`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type githubStar struct {
|
|
80
|
+
StarredAt string `json:"starred_at"`
|
|
81
|
+
User githubUser `json:"user"`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func readGitHubBranches(ctx context.Context, client *GitHubClient, target *GitHubTarget) (string, error) {
|
|
85
|
+
if target.Fragment != "" {
|
|
86
|
+
return "", fmt.Errorf("GitHub branches fragment %q is not a supported native selector", target.Fragment)
|
|
87
|
+
}
|
|
88
|
+
query, err := boundedListQuery(target.Query, []string{"page", "protected"})
|
|
89
|
+
if err != nil {
|
|
90
|
+
return "", err
|
|
91
|
+
}
|
|
92
|
+
endpoint := fmt.Sprintf("/repos/%s/%s/branches?%s", url.PathEscape(target.Owner), url.PathEscape(target.Repo), query.Encode())
|
|
93
|
+
resp, err := client.RESTPublic(ctx, http.MethodGet, endpoint, "application/vnd.github+json")
|
|
94
|
+
if err != nil {
|
|
95
|
+
return "", err
|
|
96
|
+
}
|
|
97
|
+
var branches []githubBranch
|
|
98
|
+
if err := json.Unmarshal(resp.Body, &branches); err != nil {
|
|
99
|
+
return "", fmt.Errorf("decode GitHub branches: %w", err)
|
|
100
|
+
}
|
|
101
|
+
return renderGitHubBranches(target, branches, resp.Links()), nil
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func readGitHubTags(ctx context.Context, client *GitHubClient, target *GitHubTarget) (string, error) {
|
|
105
|
+
if target.Fragment != "" {
|
|
106
|
+
return "", fmt.Errorf("GitHub tags fragment %q is not a supported native selector", target.Fragment)
|
|
107
|
+
}
|
|
108
|
+
query, err := boundedListQuery(target.Query, []string{"page"})
|
|
109
|
+
if err != nil {
|
|
110
|
+
return "", err
|
|
111
|
+
}
|
|
112
|
+
endpoint := fmt.Sprintf("/repos/%s/%s/tags?%s", url.PathEscape(target.Owner), url.PathEscape(target.Repo), query.Encode())
|
|
113
|
+
resp, err := client.REST(ctx, http.MethodGet, endpoint, "application/vnd.github+json")
|
|
114
|
+
if err != nil {
|
|
115
|
+
return "", err
|
|
116
|
+
}
|
|
117
|
+
var tags []githubTag
|
|
118
|
+
if err := json.Unmarshal(resp.Body, &tags); err != nil {
|
|
119
|
+
return "", fmt.Errorf("decode GitHub tags: %w", err)
|
|
120
|
+
}
|
|
121
|
+
return renderGitHubTags(target, tags, resp.Links()), nil
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func readGitHubReleases(ctx context.Context, client *GitHubClient, target *GitHubTarget) (string, error) {
|
|
125
|
+
if target.Fragment != "" {
|
|
126
|
+
return "", fmt.Errorf("GitHub releases fragment %q is not a supported native selector", target.Fragment)
|
|
127
|
+
}
|
|
128
|
+
query, err := boundedListQuery(target.Query, []string{"page"})
|
|
129
|
+
if err != nil {
|
|
130
|
+
return "", err
|
|
131
|
+
}
|
|
132
|
+
endpoint := fmt.Sprintf("/repos/%s/%s/releases?%s", url.PathEscape(target.Owner), url.PathEscape(target.Repo), query.Encode())
|
|
133
|
+
resp, err := client.REST(ctx, http.MethodGet, endpoint, "application/vnd.github+json")
|
|
134
|
+
if err != nil {
|
|
135
|
+
return "", err
|
|
136
|
+
}
|
|
137
|
+
var releases []githubRelease
|
|
138
|
+
if err := json.Unmarshal(resp.Body, &releases); err != nil {
|
|
139
|
+
return "", fmt.Errorf("decode GitHub releases: %w", err)
|
|
140
|
+
}
|
|
141
|
+
return renderGitHubReleases(target, releases, resp.Links()), nil
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
func readGitHubRelease(ctx context.Context, client *GitHubClient, target *GitHubTarget, latest bool) (string, error) {
|
|
145
|
+
if target.Fragment != "" {
|
|
146
|
+
return "", fmt.Errorf("GitHub release fragment %q is not a supported native selector", target.Fragment)
|
|
147
|
+
}
|
|
148
|
+
if len(target.Query) > 0 {
|
|
149
|
+
return "", fmt.Errorf("GitHub release detail query parameters are not part of the native release contract")
|
|
150
|
+
}
|
|
151
|
+
endpoint := fmt.Sprintf("/repos/%s/%s/releases/latest", url.PathEscape(target.Owner), url.PathEscape(target.Repo))
|
|
152
|
+
if !latest {
|
|
153
|
+
endpoint = fmt.Sprintf("/repos/%s/%s/releases/tags/%s", url.PathEscape(target.Owner), url.PathEscape(target.Repo), url.PathEscape(target.Name))
|
|
154
|
+
}
|
|
155
|
+
resp, err := client.REST(ctx, http.MethodGet, endpoint, "application/vnd.github+json")
|
|
156
|
+
if err != nil {
|
|
157
|
+
return "", err
|
|
158
|
+
}
|
|
159
|
+
var release githubRelease
|
|
160
|
+
if err := json.Unmarshal(resp.Body, &release); err != nil {
|
|
161
|
+
return "", fmt.Errorf("decode GitHub release: %w", err)
|
|
162
|
+
}
|
|
163
|
+
assets, availability, err := fetchGitHubReleaseAssets(ctx, client, target, release.ID)
|
|
164
|
+
if err != nil {
|
|
165
|
+
return "", err
|
|
166
|
+
}
|
|
167
|
+
sortReleaseAssetsByName(assets)
|
|
168
|
+
release.Assets = assets
|
|
169
|
+
return renderGitHubRelease(target, release, availability), nil
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
func fetchGitHubReleaseAssets(ctx context.Context, client *GitHubClient, target *GitHubTarget, releaseID int64) ([]githubReleaseAsset, githubReleaseAssetsAvailability, error) {
|
|
173
|
+
if releaseID <= 0 {
|
|
174
|
+
return nil, githubReleaseAssetsAvailability{}, fmt.Errorf("GitHub release did not report a release ID")
|
|
175
|
+
}
|
|
176
|
+
endpoint := fmt.Sprintf("/repos/%s/%s/releases/%d/assets?per_page=100", url.PathEscape(target.Owner), url.PathEscape(target.Repo), releaseID)
|
|
177
|
+
resp, err := client.REST(ctx, http.MethodGet, endpoint, "application/vnd.github+json")
|
|
178
|
+
if err != nil {
|
|
179
|
+
return nil, githubReleaseAssetsAvailability{}, err
|
|
180
|
+
}
|
|
181
|
+
var assets []githubReleaseAsset
|
|
182
|
+
if err := json.Unmarshal(resp.Body, &assets); err != nil {
|
|
183
|
+
return nil, githubReleaseAssetsAvailability{}, fmt.Errorf("decode GitHub release assets: %w", err)
|
|
184
|
+
}
|
|
185
|
+
return assets, githubReleaseAssetsAvailability{ProviderMore: resp.Links()["next"] != ""}, nil
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
func readGitHubForks(ctx context.Context, client *GitHubClient, target *GitHubTarget) (string, error) {
|
|
189
|
+
if target.Fragment != "" {
|
|
190
|
+
return "", fmt.Errorf("GitHub forks fragment %q is not a supported native selector", target.Fragment)
|
|
191
|
+
}
|
|
192
|
+
query, err := boundedListQuery(target.Query, []string{"page", "sort"})
|
|
193
|
+
if err != nil {
|
|
194
|
+
return "", err
|
|
195
|
+
}
|
|
196
|
+
if sortValue := query.Get("sort"); sortValue != "" && sortValue != "newest" && sortValue != "oldest" && sortValue != "stargazers" && sortValue != "watchers" {
|
|
197
|
+
return "", fmt.Errorf("unsupported GitHub forks sort %q", sortValue)
|
|
198
|
+
}
|
|
199
|
+
endpoint := fmt.Sprintf("/repos/%s/%s/forks?%s", url.PathEscape(target.Owner), url.PathEscape(target.Repo), query.Encode())
|
|
200
|
+
resp, err := client.REST(ctx, http.MethodGet, endpoint, "application/vnd.github+json")
|
|
201
|
+
if err != nil {
|
|
202
|
+
return "", err
|
|
203
|
+
}
|
|
204
|
+
var forks []githubFork
|
|
205
|
+
if err := json.Unmarshal(resp.Body, &forks); err != nil {
|
|
206
|
+
return "", fmt.Errorf("decode GitHub forks: %w", err)
|
|
207
|
+
}
|
|
208
|
+
return renderGitHubForks(target, forks, resp.Links()), nil
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
func readGitHubStargazers(ctx context.Context, client *GitHubClient, target *GitHubTarget) (string, error) {
|
|
212
|
+
if target.Fragment != "" {
|
|
213
|
+
return "", fmt.Errorf("GitHub stargazers fragment %q is not a supported native selector", target.Fragment)
|
|
214
|
+
}
|
|
215
|
+
query, err := boundedListQuery(target.Query, []string{"page"})
|
|
216
|
+
if err != nil {
|
|
217
|
+
return "", err
|
|
218
|
+
}
|
|
219
|
+
endpoint := fmt.Sprintf("/repos/%s/%s/stargazers?%s", url.PathEscape(target.Owner), url.PathEscape(target.Repo), query.Encode())
|
|
220
|
+
resp, err := client.RESTPublic(ctx, http.MethodGet, endpoint, "application/vnd.github.star+json")
|
|
221
|
+
if err != nil {
|
|
222
|
+
return "", err
|
|
223
|
+
}
|
|
224
|
+
var stars []githubStar
|
|
225
|
+
if err := json.Unmarshal(resp.Body, &stars); err != nil {
|
|
226
|
+
return "", fmt.Errorf("decode GitHub stargazers: %w", err)
|
|
227
|
+
}
|
|
228
|
+
return renderGitHubStargazers(target, stars, resp.Links()), nil
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
func readGitHubWatchers(ctx context.Context, client *GitHubClient, target *GitHubTarget) (string, error) {
|
|
232
|
+
if target.Fragment != "" {
|
|
233
|
+
return "", fmt.Errorf("GitHub watchers fragment %q is not a supported native selector", target.Fragment)
|
|
234
|
+
}
|
|
235
|
+
query, err := boundedListQuery(target.Query, []string{"page"})
|
|
236
|
+
if err != nil {
|
|
237
|
+
return "", err
|
|
238
|
+
}
|
|
239
|
+
endpoint := fmt.Sprintf("/repos/%s/%s/subscribers?%s", url.PathEscape(target.Owner), url.PathEscape(target.Repo), query.Encode())
|
|
240
|
+
resp, err := client.RESTPublic(ctx, http.MethodGet, endpoint, "application/vnd.github+json")
|
|
241
|
+
if err != nil {
|
|
242
|
+
return "", err
|
|
243
|
+
}
|
|
244
|
+
var users []githubUser
|
|
245
|
+
if err := json.Unmarshal(resp.Body, &users); err != nil {
|
|
246
|
+
return "", fmt.Errorf("decode GitHub subscribers: %w", err)
|
|
247
|
+
}
|
|
248
|
+
return renderGitHubWatchers(target, users, resp.Links()), nil
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
func boundedListQuery(input url.Values, allowed []string) (url.Values, error) {
|
|
252
|
+
allowedSet := map[string]struct{}{}
|
|
253
|
+
for _, key := range allowed {
|
|
254
|
+
allowedSet[key] = struct{}{}
|
|
255
|
+
}
|
|
256
|
+
for key := range input {
|
|
257
|
+
if _, ok := allowedSet[key]; !ok {
|
|
258
|
+
return nil, fmt.Errorf("GitHub list query parameter %q is not supported by this native view", key)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
query := copySelectedQuery(input, allowed)
|
|
262
|
+
query.Set("per_page", strconv.Itoa(githubPageableListSize))
|
|
263
|
+
if rawPage := query.Get("page"); rawPage != "" {
|
|
264
|
+
page, err := strconv.Atoi(rawPage)
|
|
265
|
+
if err != nil || page <= 0 {
|
|
266
|
+
return nil, fmt.Errorf("invalid GitHub list page %q", rawPage)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return query, nil
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
func renderGitHubBranches(target *GitHubTarget, branches []githubBranch, links GitHubLinkRelations) string {
|
|
273
|
+
limit := len(branches)
|
|
274
|
+
lines := listFrontmatter(target, "branches", len(branches), limit)
|
|
275
|
+
lines = append(lines, "# Branches", "")
|
|
276
|
+
if len(branches) == 0 {
|
|
277
|
+
lines = append(lines, "_No branches returned by GitHub._")
|
|
278
|
+
}
|
|
279
|
+
for _, branch := range branches[:limit] {
|
|
280
|
+
href := fmt.Sprintf("https://github.com/%s/%s/tree/%s", escapePathPreservingSlashes(target.Owner), escapePathPreservingSlashes(target.Repo), escapePathPreservingSlashes(branch.Name))
|
|
281
|
+
name, truncated := githubOverviewInlinePreview(branch.Name, 120)
|
|
282
|
+
if truncated {
|
|
283
|
+
name += "…"
|
|
284
|
+
}
|
|
285
|
+
line := fmt.Sprintf("- [%s](%s)", escapeMarkdownLinkText(name), href)
|
|
286
|
+
meta := []string{}
|
|
287
|
+
if branch.Commit.SHA != "" {
|
|
288
|
+
meta = append(meta, "`"+shortSHA(branch.Commit.SHA)+"`")
|
|
289
|
+
}
|
|
290
|
+
if branch.Protected {
|
|
291
|
+
meta = append(meta, "protected")
|
|
292
|
+
}
|
|
293
|
+
if len(meta) > 0 {
|
|
294
|
+
line += " — " + strings.Join(meta, " · ")
|
|
295
|
+
}
|
|
296
|
+
lines = append(lines, line)
|
|
297
|
+
}
|
|
298
|
+
return appendListNavigation(lines, target, links)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
func renderGitHubTags(target *GitHubTarget, tags []githubTag, links GitHubLinkRelations) string {
|
|
302
|
+
limit := len(tags)
|
|
303
|
+
lines := listFrontmatter(target, "tags", len(tags), limit)
|
|
304
|
+
lines = append(lines, "# Tags", "")
|
|
305
|
+
if len(tags) == 0 {
|
|
306
|
+
lines = append(lines, "_No tags returned by GitHub._")
|
|
307
|
+
}
|
|
308
|
+
for _, tag := range tags[:limit] {
|
|
309
|
+
href := fmt.Sprintf("https://github.com/%s/%s/tree/%s", escapePathPreservingSlashes(target.Owner), escapePathPreservingSlashes(target.Repo), escapePathPreservingSlashes(tag.Name))
|
|
310
|
+
name, truncated := githubOverviewInlinePreview(tag.Name, 120)
|
|
311
|
+
if truncated {
|
|
312
|
+
name += "…"
|
|
313
|
+
}
|
|
314
|
+
line := fmt.Sprintf("- [%s](%s)", escapeMarkdownLinkText(name), href)
|
|
315
|
+
if tag.Commit.SHA != "" {
|
|
316
|
+
line += " — `" + shortSHA(tag.Commit.SHA) + "`"
|
|
317
|
+
}
|
|
318
|
+
lines = append(lines, line)
|
|
319
|
+
}
|
|
320
|
+
return appendListNavigation(lines, target, links)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
func renderGitHubReleases(target *GitHubTarget, releases []githubRelease, links GitHubLinkRelations) string {
|
|
324
|
+
limit := len(releases)
|
|
325
|
+
lines := listFrontmatter(target, "releases", len(releases), limit)
|
|
326
|
+
lines = append(lines, "# Releases", "")
|
|
327
|
+
if len(releases) == 0 {
|
|
328
|
+
lines = append(lines, "_No releases returned by GitHub._")
|
|
329
|
+
}
|
|
330
|
+
for _, release := range releases[:limit] {
|
|
331
|
+
title := release.Name
|
|
332
|
+
if title == "" {
|
|
333
|
+
title = release.TagName
|
|
334
|
+
}
|
|
335
|
+
href := release.HTMLURL
|
|
336
|
+
if href == "" {
|
|
337
|
+
href = fmt.Sprintf("https://github.com/%s/%s/releases/tag/%s", escapePathPreservingSlashes(target.Owner), escapePathPreservingSlashes(target.Repo), escapePathPreservingSlashes(release.TagName))
|
|
338
|
+
}
|
|
339
|
+
titlePreview, truncated := githubOverviewInlinePreview(title, 140)
|
|
340
|
+
if truncated {
|
|
341
|
+
titlePreview += "…"
|
|
342
|
+
}
|
|
343
|
+
line := fmt.Sprintf("- [%s](%s)", escapeMarkdownLinkText(titlePreview), href)
|
|
344
|
+
meta := []string{}
|
|
345
|
+
if release.TagName != "" && release.TagName != title {
|
|
346
|
+
tagPreview, tagTruncated := githubOverviewInlinePreview(release.TagName, 100)
|
|
347
|
+
if tagTruncated {
|
|
348
|
+
tagPreview += "…"
|
|
349
|
+
}
|
|
350
|
+
meta = append(meta, "tag `"+tagPreview+"`")
|
|
351
|
+
}
|
|
352
|
+
if release.Draft {
|
|
353
|
+
meta = append(meta, "draft")
|
|
354
|
+
}
|
|
355
|
+
if release.Prerelease {
|
|
356
|
+
meta = append(meta, "prerelease")
|
|
357
|
+
}
|
|
358
|
+
if release.PublishedAt != "" {
|
|
359
|
+
meta = append(meta, release.PublishedAt)
|
|
360
|
+
}
|
|
361
|
+
if len(meta) > 0 {
|
|
362
|
+
line += " — " + strings.Join(meta, " · ")
|
|
363
|
+
}
|
|
364
|
+
lines = append(lines, line)
|
|
365
|
+
}
|
|
366
|
+
return appendListNavigation(lines, target, links)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
func renderGitHubRelease(target *GitHubTarget, release githubRelease, availability githubReleaseAssetsAvailability) string {
|
|
370
|
+
assetLimit := minInt(16, len(release.Assets))
|
|
371
|
+
notesRunes := 2200
|
|
372
|
+
for {
|
|
373
|
+
out := renderGitHubReleaseWithLimits(target, release, availability, assetLimit, notesRunes)
|
|
374
|
+
if githubOverviewFits(out) {
|
|
375
|
+
return out
|
|
376
|
+
}
|
|
377
|
+
switch {
|
|
378
|
+
case assetLimit > 4:
|
|
379
|
+
assetLimit--
|
|
380
|
+
case notesRunes > 800:
|
|
381
|
+
notesRunes -= 200
|
|
382
|
+
case assetLimit > 1:
|
|
383
|
+
assetLimit--
|
|
384
|
+
default:
|
|
385
|
+
return out
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
func renderGitHubReleaseWithLimits(target *GitHubTarget, release githubRelease, availability githubReleaseAssetsAvailability, assetLimit, notesRunes int) string {
|
|
391
|
+
assetLimit = minInt(assetLimit, len(release.Assets))
|
|
392
|
+
canonicalURL := strings.TrimSpace(release.HTMLURL)
|
|
393
|
+
if canonicalURL == "" && release.TagName != "" {
|
|
394
|
+
canonicalURL = fmt.Sprintf("https://github.com/%s/%s/releases/tag/%s", escapePathPreservingSlashes(target.Owner), escapePathPreservingSlashes(target.Repo), escapePathPreservingSlashes(release.TagName))
|
|
395
|
+
}
|
|
396
|
+
tagPreview, tagTruncated := githubOverviewInlinePreview(release.TagName, 140)
|
|
397
|
+
if tagTruncated {
|
|
398
|
+
tagPreview += "…"
|
|
399
|
+
}
|
|
400
|
+
namePreview, nameTruncated := githubOverviewInlinePreview(release.Name, 180)
|
|
401
|
+
if nameTruncated {
|
|
402
|
+
namePreview += "…"
|
|
403
|
+
}
|
|
404
|
+
lines := []string{
|
|
405
|
+
"---",
|
|
406
|
+
"repository: " + yamlScalar(target.Owner+"/"+target.Repo),
|
|
407
|
+
"tag: " + yamlScalar(tagPreview),
|
|
408
|
+
"name: " + yamlScalar(namePreview),
|
|
409
|
+
fmt.Sprintf("draft: %t", release.Draft),
|
|
410
|
+
fmt.Sprintf("prerelease: %t", release.Prerelease),
|
|
411
|
+
"overview: true",
|
|
412
|
+
fmt.Sprintf("assets_returned: %d", len(release.Assets)),
|
|
413
|
+
fmt.Sprintf("assets_indexed: %d", assetLimit),
|
|
414
|
+
fmt.Sprintf("assets_local_omitted: %d", len(release.Assets)-assetLimit),
|
|
415
|
+
}
|
|
416
|
+
if tagTruncated {
|
|
417
|
+
lines = append(lines, "tag_preview_truncated: true")
|
|
418
|
+
}
|
|
419
|
+
if nameTruncated {
|
|
420
|
+
lines = append(lines, "name_preview_truncated: true")
|
|
421
|
+
}
|
|
422
|
+
if availability.ProviderMore {
|
|
423
|
+
lines = append(lines, "assets_provider_more_available: true")
|
|
424
|
+
}
|
|
425
|
+
if release.Target != "" {
|
|
426
|
+
lines = append(lines, "target: "+yamlScalar(release.Target))
|
|
427
|
+
}
|
|
428
|
+
if release.Author.Login != "" {
|
|
429
|
+
lines = append(lines, "author: "+yamlScalar("@"+release.Author.Login))
|
|
430
|
+
}
|
|
431
|
+
if release.CreatedAt != "" {
|
|
432
|
+
lines = append(lines, "created: "+yamlScalar(release.CreatedAt))
|
|
433
|
+
}
|
|
434
|
+
if release.PublishedAt != "" {
|
|
435
|
+
lines = append(lines, "published: "+yamlScalar(release.PublishedAt))
|
|
436
|
+
}
|
|
437
|
+
if canonicalURL != "" {
|
|
438
|
+
lines = append(lines, "url: "+yamlScalar(canonicalURL))
|
|
439
|
+
}
|
|
440
|
+
lines = append(lines, "---", "")
|
|
441
|
+
title := release.Name
|
|
442
|
+
if title == "" {
|
|
443
|
+
title = release.TagName
|
|
444
|
+
}
|
|
445
|
+
title, titleTruncated := githubOverviewInlinePreview(title, 180)
|
|
446
|
+
if titleTruncated {
|
|
447
|
+
title += "…"
|
|
448
|
+
}
|
|
449
|
+
lines = append(lines, "# "+title, "", "## Release notes", "")
|
|
450
|
+
humanNotes := ""
|
|
451
|
+
if release.Body != nil {
|
|
452
|
+
humanNotes = strings.TrimSpace(stripInvisibleHTMLComments(*release.Body))
|
|
453
|
+
}
|
|
454
|
+
if humanNotes == "" {
|
|
455
|
+
lines = append(lines, "_No release notes._")
|
|
456
|
+
} else {
|
|
457
|
+
preview, truncated := githubOverviewPreview(humanNotes, notesRunes)
|
|
458
|
+
lines = append(lines, preview)
|
|
459
|
+
if truncated {
|
|
460
|
+
lines = append(lines, "", "> Release notes preview locally truncated for this overview.")
|
|
461
|
+
if canonicalURL != "" {
|
|
462
|
+
lines = append(lines, "> Canonical GitHub release page (complete notes in the browser): "+canonicalURL)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
lines = append(lines, "", "## Assets", "")
|
|
467
|
+
if len(release.Assets) == 0 {
|
|
468
|
+
if availability.ProviderMore {
|
|
469
|
+
lines = append(lines, "_No release assets were returned on the provider page fetched for this overview._")
|
|
470
|
+
} else {
|
|
471
|
+
lines = append(lines, "_No release assets._")
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
for _, asset := range release.Assets[:assetLimit] {
|
|
475
|
+
name := asset.Name
|
|
476
|
+
if asset.Label != "" {
|
|
477
|
+
name += " — " + asset.Label
|
|
478
|
+
}
|
|
479
|
+
line := "- **" + name + "**"
|
|
480
|
+
meta := []string{}
|
|
481
|
+
if asset.Size > 0 {
|
|
482
|
+
meta = append(meta, fmt.Sprintf("%d bytes", asset.Size))
|
|
483
|
+
}
|
|
484
|
+
if asset.ContentType != "" {
|
|
485
|
+
meta = append(meta, asset.ContentType)
|
|
486
|
+
}
|
|
487
|
+
if asset.DownloadCount > 0 {
|
|
488
|
+
meta = append(meta, fmt.Sprintf("%d downloads", asset.DownloadCount))
|
|
489
|
+
}
|
|
490
|
+
if len(meta) > 0 {
|
|
491
|
+
line += " — " + strings.Join(meta, " · ")
|
|
492
|
+
}
|
|
493
|
+
if asset.BrowserDownloadURL != "" {
|
|
494
|
+
line += " — " + asset.BrowserDownloadURL
|
|
495
|
+
}
|
|
496
|
+
lines = append(lines, line)
|
|
497
|
+
}
|
|
498
|
+
if note := githubLocalOmissionNote("release assets returned on this provider page", len(release.Assets)-assetLimit); note != "" {
|
|
499
|
+
lines = append(lines, "", note)
|
|
500
|
+
}
|
|
501
|
+
if availability.ProviderMore {
|
|
502
|
+
lines = append(lines, "", "> GitHub has more release assets beyond the provider page fetched for this overview; this provider incompleteness is separate from local overview omission.")
|
|
503
|
+
}
|
|
504
|
+
if canonicalURL != "" {
|
|
505
|
+
lines = append(lines, "", "Canonical release page: "+canonicalURL)
|
|
506
|
+
}
|
|
507
|
+
return strings.TrimSpace(strings.Join(lines, "\n"))
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
func renderGitHubForks(target *GitHubTarget, forks []githubFork, links GitHubLinkRelations) string {
|
|
511
|
+
limit := len(forks)
|
|
512
|
+
lines := listFrontmatter(target, "forks", len(forks), limit)
|
|
513
|
+
lines = append(lines, "# Forks", "")
|
|
514
|
+
if len(forks) == 0 {
|
|
515
|
+
lines = append(lines, "_No forks returned by GitHub._")
|
|
516
|
+
}
|
|
517
|
+
for _, fork := range forks[:limit] {
|
|
518
|
+
name := fork.FullName
|
|
519
|
+
if name == "" {
|
|
520
|
+
name = fork.Owner.Login
|
|
521
|
+
}
|
|
522
|
+
namePreview, truncated := githubOverviewInlinePreview(name, 120)
|
|
523
|
+
if truncated {
|
|
524
|
+
namePreview += "…"
|
|
525
|
+
}
|
|
526
|
+
line := "- [" + escapeMarkdownLinkText(namePreview) + "](" + fork.HTMLURL + ")"
|
|
527
|
+
meta := []string{}
|
|
528
|
+
if fork.StargazersCount > 0 {
|
|
529
|
+
meta = append(meta, fmt.Sprintf("%d stars", fork.StargazersCount))
|
|
530
|
+
}
|
|
531
|
+
if fork.Language != nil && strings.TrimSpace(*fork.Language) != "" {
|
|
532
|
+
meta = append(meta, *fork.Language)
|
|
533
|
+
}
|
|
534
|
+
if fork.Archived {
|
|
535
|
+
meta = append(meta, "archived")
|
|
536
|
+
}
|
|
537
|
+
if fork.UpdatedAt != "" {
|
|
538
|
+
meta = append(meta, "updated "+fork.UpdatedAt)
|
|
539
|
+
}
|
|
540
|
+
if len(meta) > 0 {
|
|
541
|
+
line += " — " + strings.Join(meta, " · ")
|
|
542
|
+
}
|
|
543
|
+
lines = append(lines, line)
|
|
544
|
+
}
|
|
545
|
+
return appendListNavigation(lines, target, links)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
func renderGitHubStargazers(target *GitHubTarget, stars []githubStar, links GitHubLinkRelations) string {
|
|
549
|
+
limit := len(stars)
|
|
550
|
+
lines := listFrontmatter(target, "stargazers", len(stars), limit)
|
|
551
|
+
lines = append(lines, "# Stargazers", "", "_These are users who starred the repository. GitHub's `watchers_count` is also a historical star-count alias; it is not the subscriber count._", "")
|
|
552
|
+
if len(stars) == 0 {
|
|
553
|
+
lines = append(lines, "_No stargazers returned by GitHub._")
|
|
554
|
+
}
|
|
555
|
+
for _, star := range stars[:limit] {
|
|
556
|
+
line := renderGitHubUserIdentity(star.User)
|
|
557
|
+
if star.StarredAt != "" {
|
|
558
|
+
line += " — starred " + star.StarredAt
|
|
559
|
+
}
|
|
560
|
+
lines = append(lines, line)
|
|
561
|
+
}
|
|
562
|
+
return appendListNavigation(lines, target, links)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
func renderGitHubWatchers(target *GitHubTarget, users []githubUser, links GitHubLinkRelations) string {
|
|
566
|
+
limit := len(users)
|
|
567
|
+
lines := listFrontmatter(target, "watchers", len(users), limit)
|
|
568
|
+
lines = append(lines, "# Watchers / subscribers", "", "_This list uses GitHub's subscribers API: these are actual repository watchers/subscribers, not stars._", "")
|
|
569
|
+
if len(users) == 0 {
|
|
570
|
+
lines = append(lines, "_No watchers/subscribers returned by GitHub._")
|
|
571
|
+
}
|
|
572
|
+
for _, user := range users[:limit] {
|
|
573
|
+
lines = append(lines, renderGitHubUserIdentity(user))
|
|
574
|
+
}
|
|
575
|
+
return appendListNavigation(lines, target, links)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
func renderGitHubUserIdentity(user githubUser) string {
|
|
579
|
+
login := user.Login
|
|
580
|
+
if login == "" {
|
|
581
|
+
login = "unknown"
|
|
582
|
+
}
|
|
583
|
+
href := "https://github.com/" + url.PathEscape(login)
|
|
584
|
+
return "- [@" + escapeMarkdownLinkText(login) + "](" + href + ")"
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
func listFrontmatter(target *GitHubTarget, view string, returned int, indexed ...int) []string {
|
|
588
|
+
page := target.Query.Get("page")
|
|
589
|
+
if page == "" {
|
|
590
|
+
page = "1"
|
|
591
|
+
}
|
|
592
|
+
lines := []string{
|
|
593
|
+
"---",
|
|
594
|
+
"repository: " + yamlScalar(target.Owner+"/"+target.Repo),
|
|
595
|
+
"view: " + yamlScalar(view),
|
|
596
|
+
"page: " + page,
|
|
597
|
+
fmt.Sprintf("returned: %d", returned),
|
|
598
|
+
}
|
|
599
|
+
if len(indexed) > 0 {
|
|
600
|
+
shown := minInt(indexed[0], returned)
|
|
601
|
+
lines = append(lines, fmt.Sprintf("indexed: %d", shown), fmt.Sprintf("local_omitted: %d", returned-shown))
|
|
602
|
+
}
|
|
603
|
+
return append(lines, "---", "")
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
func appendListNavigation(lines []string, target *GitHubTarget, links GitHubLinkRelations) string {
|
|
607
|
+
if nav := renderGitHubUIPageNavigation(target, links); len(nav) > 0 {
|
|
608
|
+
lines = append(lines, "", "## Navigation", "")
|
|
609
|
+
lines = append(lines, nav...)
|
|
610
|
+
}
|
|
611
|
+
return strings.TrimSpace(strings.Join(lines, "\n"))
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
func sortReleaseAssetsByName(assets []githubReleaseAsset) {
|
|
615
|
+
sort.SliceStable(assets, func(i, j int) bool {
|
|
616
|
+
if assets[i].Name == assets[j].Name {
|
|
617
|
+
return assets[i].ID < assets[j].ID
|
|
618
|
+
}
|
|
619
|
+
return assets[i].Name < assets[j].Name
|
|
620
|
+
})
|
|
621
|
+
}
|