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,505 @@
|
|
|
1
|
+
package app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"net/http"
|
|
8
|
+
"net/url"
|
|
9
|
+
"strconv"
|
|
10
|
+
"strings"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
var githubSearchTypes = map[string]string{
|
|
14
|
+
"repositories": "repositories",
|
|
15
|
+
"issues": "issues",
|
|
16
|
+
"pullrequests": "issues",
|
|
17
|
+
"code": "code",
|
|
18
|
+
"commits": "commits",
|
|
19
|
+
"users": "users",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func isSupportedGitHubSearchURLQuery(query url.Values) bool {
|
|
23
|
+
allowed := map[string]struct{}{"q": {}, "type": {}, "s": {}, "o": {}, "p": {}}
|
|
24
|
+
for key := range query {
|
|
25
|
+
if _, ok := allowed[key]; !ok {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if strings.TrimSpace(query.Get("q")) == "" {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
if _, ok := githubSearchTypes[query.Get("type")]; !ok {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
if order := query.Get("o"); order != "" && order != "asc" && order != "desc" {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
if page := query.Get("p"); page != "" {
|
|
39
|
+
n, err := strconv.Atoi(page)
|
|
40
|
+
if err != nil || n <= 0 {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func isSupportedGitHubProfileURLQuery(query url.Values) bool {
|
|
48
|
+
for key := range query {
|
|
49
|
+
if key != "tab" && key != "page" {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if page := query.Get("page"); page != "" {
|
|
54
|
+
n, err := strconv.Atoi(page)
|
|
55
|
+
if err != nil || n <= 0 {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if tab := query.Get("tab"); tab != "" {
|
|
60
|
+
switch tab {
|
|
61
|
+
case "repositories", "gists", "stars", "followers", "following", "people":
|
|
62
|
+
default:
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type githubSearchEnvelope struct {
|
|
70
|
+
TotalCount int `json:"total_count"`
|
|
71
|
+
IncompleteResults bool `json:"incomplete_results"`
|
|
72
|
+
Items json.RawMessage `json:"items"`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type githubCodeSearchItem struct {
|
|
76
|
+
Name string `json:"name"`
|
|
77
|
+
Path string `json:"path"`
|
|
78
|
+
SHA string `json:"sha"`
|
|
79
|
+
HTMLURL string `json:"html_url"`
|
|
80
|
+
Repository struct {
|
|
81
|
+
FullName string `json:"full_name"`
|
|
82
|
+
} `json:"repository"`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type githubCommitSearchItem struct {
|
|
86
|
+
SHA string `json:"sha"`
|
|
87
|
+
HTMLURL string `json:"html_url"`
|
|
88
|
+
Commit struct {
|
|
89
|
+
Message string `json:"message"`
|
|
90
|
+
Author struct {
|
|
91
|
+
Date string `json:"date"`
|
|
92
|
+
} `json:"author"`
|
|
93
|
+
} `json:"commit"`
|
|
94
|
+
Repository struct {
|
|
95
|
+
FullName string `json:"full_name"`
|
|
96
|
+
} `json:"repository"`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func readGitHubSearch(ctx context.Context, client *GitHubClient, target *GitHubTarget) (string, error) {
|
|
100
|
+
if target == nil || target.Kind != GitHubTargetSearch || !isSupportedGitHubSearchURLQuery(target.Query) {
|
|
101
|
+
return "", fmt.Errorf("GitHub Search URL is not supported by the native reader")
|
|
102
|
+
}
|
|
103
|
+
if target.Fragment != "" {
|
|
104
|
+
return "", fmt.Errorf("GitHub Search fragment %q is not a supported selector", target.Fragment)
|
|
105
|
+
}
|
|
106
|
+
uiType := target.Query.Get("type")
|
|
107
|
+
apiType := githubSearchTypes[uiType]
|
|
108
|
+
q := strings.TrimSpace(target.Query.Get("q"))
|
|
109
|
+
if target.Owner != "" && target.Repo != "" {
|
|
110
|
+
q += " repo:" + target.Owner + "/" + target.Repo
|
|
111
|
+
}
|
|
112
|
+
if uiType == "issues" {
|
|
113
|
+
q += " is:issue"
|
|
114
|
+
} else if uiType == "pullrequests" {
|
|
115
|
+
q += " is:pr"
|
|
116
|
+
}
|
|
117
|
+
provider := url.Values{"q": []string{q}, "per_page": []string{strconv.Itoa(githubPageableListSize)}}
|
|
118
|
+
if sort := target.Query.Get("s"); sort != "" {
|
|
119
|
+
provider.Set("sort", sort)
|
|
120
|
+
}
|
|
121
|
+
if order := target.Query.Get("o"); order != "" {
|
|
122
|
+
provider.Set("order", order)
|
|
123
|
+
}
|
|
124
|
+
if page := target.Query.Get("p"); page != "" {
|
|
125
|
+
provider.Set("page", page)
|
|
126
|
+
}
|
|
127
|
+
endpoint := "/search/" + apiType + "?" + provider.Encode()
|
|
128
|
+
resp, err := client.REST(ctx, http.MethodGet, endpoint, "application/vnd.github+json")
|
|
129
|
+
if err != nil {
|
|
130
|
+
return "", err
|
|
131
|
+
}
|
|
132
|
+
var envelope githubSearchEnvelope
|
|
133
|
+
if err := json.Unmarshal(resp.Body, &envelope); err != nil {
|
|
134
|
+
return "", fmt.Errorf("decode GitHub Search results: %w", err)
|
|
135
|
+
}
|
|
136
|
+
return renderGitHubSearch(target, uiType, envelope, resp.Links())
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func renderGitHubSearch(target *GitHubTarget, searchType string, envelope githubSearchEnvelope, links GitHubLinkRelations) (string, error) {
|
|
140
|
+
page := target.Query.Get("p")
|
|
141
|
+
if page == "" {
|
|
142
|
+
page = "1"
|
|
143
|
+
}
|
|
144
|
+
var rawItems []json.RawMessage
|
|
145
|
+
if len(envelope.Items) > 0 {
|
|
146
|
+
if err := json.Unmarshal(envelope.Items, &rawItems); err != nil {
|
|
147
|
+
return "", fmt.Errorf("decode GitHub Search result count: %w", err)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
returned := len(rawItems)
|
|
151
|
+
indexed := returned
|
|
152
|
+
queryPreview, queryTruncated := githubOverviewInlinePreview(target.Query.Get("q"), 300)
|
|
153
|
+
lines := []string{
|
|
154
|
+
"---",
|
|
155
|
+
"view: " + yamlScalar("search:"+searchType),
|
|
156
|
+
"query: " + yamlScalar(queryPreview),
|
|
157
|
+
"page: " + page,
|
|
158
|
+
fmt.Sprintf("total_count: %d", envelope.TotalCount),
|
|
159
|
+
fmt.Sprintf("incomplete_results: %t", envelope.IncompleteResults),
|
|
160
|
+
fmt.Sprintf("returned: %d", returned),
|
|
161
|
+
fmt.Sprintf("indexed: %d", indexed),
|
|
162
|
+
fmt.Sprintf("local_omitted: %d", returned-indexed),
|
|
163
|
+
}
|
|
164
|
+
if queryTruncated {
|
|
165
|
+
lines = append(lines, "query_preview_truncated: true")
|
|
166
|
+
}
|
|
167
|
+
if target.Owner != "" && target.Repo != "" {
|
|
168
|
+
lines = append(lines, "repository_scope: "+yamlScalar(target.Owner+"/"+target.Repo))
|
|
169
|
+
}
|
|
170
|
+
if envelope.TotalCount > 1000 {
|
|
171
|
+
lines = append(lines, "provider_result_ceiling: 1000")
|
|
172
|
+
}
|
|
173
|
+
lines = append(lines, "---", "", "# GitHub search", "")
|
|
174
|
+
if envelope.IncompleteResults {
|
|
175
|
+
lines = append(lines, "> GitHub marked these search results incomplete.", "")
|
|
176
|
+
}
|
|
177
|
+
if envelope.TotalCount > 1000 {
|
|
178
|
+
lines = append(lines, "> GitHub Search exposes at most 1000 results for a query; this bounded page does not imply access beyond that provider ceiling.", "")
|
|
179
|
+
}
|
|
180
|
+
switch searchType {
|
|
181
|
+
case "repositories":
|
|
182
|
+
var items []githubRepository
|
|
183
|
+
if err := json.Unmarshal(envelope.Items, &items); err != nil {
|
|
184
|
+
return "", fmt.Errorf("decode GitHub repository search items: %w", err)
|
|
185
|
+
}
|
|
186
|
+
for _, item := range items[:minInt(indexed, len(items))] {
|
|
187
|
+
href := item.HTMLURL
|
|
188
|
+
if href == "" {
|
|
189
|
+
href = "https://github.com/" + item.FullName
|
|
190
|
+
}
|
|
191
|
+
meta := []string{fmt.Sprintf("%d stars", item.StargazersCount)}
|
|
192
|
+
if item.Language != "" {
|
|
193
|
+
meta = append(meta, item.Language)
|
|
194
|
+
}
|
|
195
|
+
name, nameTruncated := githubOverviewInlinePreview(item.FullName, 120)
|
|
196
|
+
if nameTruncated {
|
|
197
|
+
name += "…"
|
|
198
|
+
}
|
|
199
|
+
line := "- [" + escapeMarkdownLinkText(name) + "](" + href + ") — " + strings.Join(meta, " · ")
|
|
200
|
+
if item.Description != "" {
|
|
201
|
+
description, truncated := githubOverviewInlinePreview(item.Description, 140)
|
|
202
|
+
if truncated {
|
|
203
|
+
description += "…"
|
|
204
|
+
}
|
|
205
|
+
line += " — " + description
|
|
206
|
+
}
|
|
207
|
+
lines = append(lines, line)
|
|
208
|
+
}
|
|
209
|
+
case "issues", "pullrequests":
|
|
210
|
+
var items []githubIssue
|
|
211
|
+
if err := json.Unmarshal(envelope.Items, &items); err != nil {
|
|
212
|
+
return "", fmt.Errorf("decode GitHub Issue search items: %w", err)
|
|
213
|
+
}
|
|
214
|
+
for _, item := range items[:minInt(indexed, len(items))] {
|
|
215
|
+
meta := []string{item.State}
|
|
216
|
+
if item.User.Login != "" {
|
|
217
|
+
meta = append(meta, "@"+item.User.Login)
|
|
218
|
+
}
|
|
219
|
+
title, truncated := githubOverviewInlinePreview(item.Title, 140)
|
|
220
|
+
if truncated {
|
|
221
|
+
title += "…"
|
|
222
|
+
}
|
|
223
|
+
lines = append(lines, fmt.Sprintf("- [#%d %s](%s) — %s", item.Number, escapeMarkdownLinkText(title), item.HTMLURL, strings.Join(meta, " · ")))
|
|
224
|
+
}
|
|
225
|
+
case "code":
|
|
226
|
+
var items []githubCodeSearchItem
|
|
227
|
+
if err := json.Unmarshal(envelope.Items, &items); err != nil {
|
|
228
|
+
return "", fmt.Errorf("decode GitHub code search items: %w", err)
|
|
229
|
+
}
|
|
230
|
+
for _, item := range items[:minInt(indexed, len(items))] {
|
|
231
|
+
path, truncated := githubOverviewInlinePreview(item.Path, 140)
|
|
232
|
+
if truncated {
|
|
233
|
+
path += "…"
|
|
234
|
+
}
|
|
235
|
+
repo, repoTruncated := githubOverviewInlinePreview(item.Repository.FullName, 120)
|
|
236
|
+
if repoTruncated {
|
|
237
|
+
repo += "…"
|
|
238
|
+
}
|
|
239
|
+
lines = append(lines, fmt.Sprintf("- [%s](%s) — %s · `%s`", escapeMarkdownLinkText(path), item.HTMLURL, repo, shortSHA(item.SHA)))
|
|
240
|
+
}
|
|
241
|
+
case "commits":
|
|
242
|
+
var items []githubCommitSearchItem
|
|
243
|
+
if err := json.Unmarshal(envelope.Items, &items); err != nil {
|
|
244
|
+
return "", fmt.Errorf("decode GitHub commit search items: %w", err)
|
|
245
|
+
}
|
|
246
|
+
for _, item := range items[:minInt(indexed, len(items))] {
|
|
247
|
+
message, truncated := githubOverviewInlinePreview(firstLine(item.Commit.Message), 140)
|
|
248
|
+
if truncated {
|
|
249
|
+
message += "…"
|
|
250
|
+
}
|
|
251
|
+
line := fmt.Sprintf("- [`%s`](%s) %s", shortSHA(item.SHA), item.HTMLURL, message)
|
|
252
|
+
if item.Repository.FullName != "" {
|
|
253
|
+
repo, repoTruncated := githubOverviewInlinePreview(item.Repository.FullName, 120)
|
|
254
|
+
if repoTruncated {
|
|
255
|
+
repo += "…"
|
|
256
|
+
}
|
|
257
|
+
line += " — " + repo
|
|
258
|
+
}
|
|
259
|
+
if item.Commit.Author.Date != "" {
|
|
260
|
+
line += " · " + item.Commit.Author.Date
|
|
261
|
+
}
|
|
262
|
+
lines = append(lines, line)
|
|
263
|
+
}
|
|
264
|
+
case "users":
|
|
265
|
+
var items []githubUser
|
|
266
|
+
if err := json.Unmarshal(envelope.Items, &items); err != nil {
|
|
267
|
+
return "", fmt.Errorf("decode GitHub user search items: %w", err)
|
|
268
|
+
}
|
|
269
|
+
for _, item := range items[:minInt(indexed, len(items))] {
|
|
270
|
+
line := renderGitHubUserIdentity(item)
|
|
271
|
+
if item.Type != "" {
|
|
272
|
+
line += " — " + item.Type
|
|
273
|
+
}
|
|
274
|
+
lines = append(lines, line)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if returned == 0 {
|
|
278
|
+
lines = append(lines, "_No results returned on this page._")
|
|
279
|
+
}
|
|
280
|
+
if nav := renderGitHubSearchNavigation(target, links); len(nav) > 0 {
|
|
281
|
+
lines = append(lines, "", "## Navigation", "")
|
|
282
|
+
lines = append(lines, nav...)
|
|
283
|
+
}
|
|
284
|
+
return strings.TrimSpace(strings.Join(lines, "\n")), nil
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
func renderGitHubSearchNavigation(target *GitHubTarget, links GitHubLinkRelations) []string {
|
|
288
|
+
lines := []string{}
|
|
289
|
+
for _, rel := range []string{"prev", "next"} {
|
|
290
|
+
page, ok := pageFromGitHubLink(links[rel])
|
|
291
|
+
if !ok {
|
|
292
|
+
continue
|
|
293
|
+
}
|
|
294
|
+
parsed, err := url.Parse(target.OriginalURL)
|
|
295
|
+
if err != nil {
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
q := parsed.Query()
|
|
299
|
+
q.Set("p", strconv.Itoa(page))
|
|
300
|
+
parsed.RawQuery = q.Encode()
|
|
301
|
+
label := "Previous"
|
|
302
|
+
if rel == "next" {
|
|
303
|
+
label = "Next"
|
|
304
|
+
}
|
|
305
|
+
lines = append(lines, "- "+label+": "+parsed.String())
|
|
306
|
+
}
|
|
307
|
+
return lines
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
type githubProfile struct {
|
|
311
|
+
Login string `json:"login"`
|
|
312
|
+
Type string `json:"type"`
|
|
313
|
+
Name string `json:"name"`
|
|
314
|
+
Bio string `json:"bio"`
|
|
315
|
+
Blog string `json:"blog"`
|
|
316
|
+
Location string `json:"location"`
|
|
317
|
+
Company string `json:"company"`
|
|
318
|
+
HTMLURL string `json:"html_url"`
|
|
319
|
+
PublicRepos int `json:"public_repos"`
|
|
320
|
+
PublicGists int `json:"public_gists"`
|
|
321
|
+
Followers int `json:"followers"`
|
|
322
|
+
Following int `json:"following"`
|
|
323
|
+
CreatedAt string `json:"created_at"`
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
type githubProfileGist struct {
|
|
327
|
+
ID string `json:"id"`
|
|
328
|
+
HTMLURL string `json:"html_url"`
|
|
329
|
+
Description string `json:"description"`
|
|
330
|
+
Files map[string]struct {
|
|
331
|
+
Filename string `json:"filename"`
|
|
332
|
+
} `json:"files"`
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
func readGitHubProfile(ctx context.Context, client *GitHubClient, target *GitHubTarget) (string, error) {
|
|
336
|
+
if target == nil || target.Kind != GitHubTargetProfile || target.Owner == "" {
|
|
337
|
+
return "", fmt.Errorf("GitHub profile target is invalid")
|
|
338
|
+
}
|
|
339
|
+
if target.Fragment != "" || !isSupportedGitHubProfileURLQuery(target.Query) {
|
|
340
|
+
return "", fmt.Errorf("GitHub profile query/fragment is not supported")
|
|
341
|
+
}
|
|
342
|
+
resp, err := client.REST(ctx, http.MethodGet, "/users/"+url.PathEscape(target.Owner), "application/vnd.github+json")
|
|
343
|
+
if err != nil {
|
|
344
|
+
return "", err
|
|
345
|
+
}
|
|
346
|
+
var profile githubProfile
|
|
347
|
+
if err := json.Unmarshal(resp.Body, &profile); err != nil {
|
|
348
|
+
return "", fmt.Errorf("decode GitHub profile: %w", err)
|
|
349
|
+
}
|
|
350
|
+
if profile.Login == "" {
|
|
351
|
+
profile.Login = target.Owner
|
|
352
|
+
}
|
|
353
|
+
if strings.EqualFold(profile.Type, "Organization") {
|
|
354
|
+
orgResp, err := client.REST(ctx, http.MethodGet, "/orgs/"+url.PathEscape(target.Owner), "application/vnd.github+json")
|
|
355
|
+
if err != nil {
|
|
356
|
+
return "", err
|
|
357
|
+
}
|
|
358
|
+
var org githubProfile
|
|
359
|
+
if err := json.Unmarshal(orgResp.Body, &org); err != nil {
|
|
360
|
+
return "", fmt.Errorf("decode GitHub Organization profile: %w", err)
|
|
361
|
+
}
|
|
362
|
+
org.Type = "Organization"
|
|
363
|
+
if org.Login == "" {
|
|
364
|
+
org.Login = target.Owner
|
|
365
|
+
}
|
|
366
|
+
profile = org
|
|
367
|
+
}
|
|
368
|
+
tab := target.Query.Get("tab")
|
|
369
|
+
if target.Name == "people" {
|
|
370
|
+
tab = "people"
|
|
371
|
+
}
|
|
372
|
+
if tab == "" {
|
|
373
|
+
return renderGitHubProfile(profile), nil
|
|
374
|
+
}
|
|
375
|
+
return readGitHubProfileTab(ctx, client, target, profile, tab)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
func renderGitHubProfile(profile githubProfile) string {
|
|
379
|
+
lines := []string{"---", "login: " + yamlScalar(profile.Login), "type: " + yamlScalar(profile.Type)}
|
|
380
|
+
if profile.Name != "" {
|
|
381
|
+
lines = append(lines, "name: "+yamlScalar(profile.Name))
|
|
382
|
+
}
|
|
383
|
+
lines = append(lines, fmt.Sprintf("public_repositories: %d", profile.PublicRepos))
|
|
384
|
+
if !strings.EqualFold(profile.Type, "Organization") {
|
|
385
|
+
lines = append(lines, fmt.Sprintf("public_gists: %d", profile.PublicGists), fmt.Sprintf("followers: %d", profile.Followers), fmt.Sprintf("following: %d", profile.Following))
|
|
386
|
+
}
|
|
387
|
+
if profile.Location != "" {
|
|
388
|
+
lines = append(lines, "location: "+yamlScalar(profile.Location))
|
|
389
|
+
}
|
|
390
|
+
if profile.CreatedAt != "" {
|
|
391
|
+
lines = append(lines, "created: "+yamlScalar(profile.CreatedAt))
|
|
392
|
+
}
|
|
393
|
+
if profile.HTMLURL != "" {
|
|
394
|
+
lines = append(lines, "url: "+yamlScalar(profile.HTMLURL))
|
|
395
|
+
}
|
|
396
|
+
lines = append(lines, "---", "", "# "+profile.Login, "")
|
|
397
|
+
if profile.Bio != "" {
|
|
398
|
+
lines = append(lines, profile.Bio, "")
|
|
399
|
+
}
|
|
400
|
+
base := "https://github.com/" + url.PathEscape(profile.Login)
|
|
401
|
+
lines = append(lines, "## Useful GitHub URLs", "", "- Repositories: "+base+"?tab=repositories")
|
|
402
|
+
if strings.EqualFold(profile.Type, "Organization") {
|
|
403
|
+
lines = append(lines, "- Public members: https://github.com/orgs/"+url.PathEscape(profile.Login)+"/people")
|
|
404
|
+
} else {
|
|
405
|
+
lines = append(lines, "- Gists: "+base+"?tab=gists", "- Stars: "+base+"?tab=stars", "- Followers: "+base+"?tab=followers", "- Following: "+base+"?tab=following")
|
|
406
|
+
}
|
|
407
|
+
return strings.TrimSpace(strings.Join(lines, "\n"))
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
func readGitHubProfileTab(ctx context.Context, client *GitHubClient, target *GitHubTarget, profile githubProfile, tab string) (string, error) {
|
|
411
|
+
page := target.Query.Get("page")
|
|
412
|
+
provider := url.Values{"per_page": []string{strconv.Itoa(githubPageableListSize)}}
|
|
413
|
+
if page != "" {
|
|
414
|
+
provider.Set("page", page)
|
|
415
|
+
}
|
|
416
|
+
endpoint := ""
|
|
417
|
+
kind := ""
|
|
418
|
+
isOrg := strings.EqualFold(profile.Type, "Organization")
|
|
419
|
+
if isOrg {
|
|
420
|
+
switch tab {
|
|
421
|
+
case "repositories":
|
|
422
|
+
endpoint, kind = "/orgs/"+url.PathEscape(profile.Login)+"/repos", "repositories"
|
|
423
|
+
case "people":
|
|
424
|
+
endpoint, kind = "/orgs/"+url.PathEscape(profile.Login)+"/public_members", "people"
|
|
425
|
+
default:
|
|
426
|
+
return "", fmt.Errorf("GitHub Organization profile tab %q is not supported", tab)
|
|
427
|
+
}
|
|
428
|
+
} else {
|
|
429
|
+
switch tab {
|
|
430
|
+
case "repositories":
|
|
431
|
+
endpoint, kind = "/users/"+url.PathEscape(profile.Login)+"/repos", "repositories"
|
|
432
|
+
case "stars":
|
|
433
|
+
endpoint, kind = "/users/"+url.PathEscape(profile.Login)+"/starred", "repositories"
|
|
434
|
+
case "gists":
|
|
435
|
+
endpoint, kind = "/users/"+url.PathEscape(profile.Login)+"/gists", "gists"
|
|
436
|
+
case "followers":
|
|
437
|
+
endpoint, kind = "/users/"+url.PathEscape(profile.Login)+"/followers", "users"
|
|
438
|
+
case "following":
|
|
439
|
+
endpoint, kind = "/users/"+url.PathEscape(profile.Login)+"/following", "users"
|
|
440
|
+
default:
|
|
441
|
+
return "", fmt.Errorf("GitHub User profile tab %q is not supported", tab)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
resp, err := client.REST(ctx, http.MethodGet, endpoint+"?"+provider.Encode(), "application/vnd.github+json")
|
|
445
|
+
if err != nil {
|
|
446
|
+
return "", err
|
|
447
|
+
}
|
|
448
|
+
if page == "" {
|
|
449
|
+
page = "1"
|
|
450
|
+
}
|
|
451
|
+
rows := []string{}
|
|
452
|
+
returned := 0
|
|
453
|
+
indexed := 0
|
|
454
|
+
switch kind {
|
|
455
|
+
case "repositories":
|
|
456
|
+
var items []githubRepository
|
|
457
|
+
if err := json.Unmarshal(resp.Body, &items); err != nil {
|
|
458
|
+
return "", fmt.Errorf("decode GitHub profile repositories: %w", err)
|
|
459
|
+
}
|
|
460
|
+
returned = len(items)
|
|
461
|
+
indexed = returned
|
|
462
|
+
for _, item := range items[:indexed] {
|
|
463
|
+
name, truncated := githubOverviewInlinePreview(item.FullName, 120)
|
|
464
|
+
if truncated {
|
|
465
|
+
name += "…"
|
|
466
|
+
}
|
|
467
|
+
rows = append(rows, fmt.Sprintf("- [%s](%s) — %d stars", escapeMarkdownLinkText(name), item.HTMLURL, item.StargazersCount))
|
|
468
|
+
}
|
|
469
|
+
case "gists":
|
|
470
|
+
var items []githubProfileGist
|
|
471
|
+
if err := json.Unmarshal(resp.Body, &items); err != nil {
|
|
472
|
+
return "", fmt.Errorf("decode GitHub profile Gists: %w", err)
|
|
473
|
+
}
|
|
474
|
+
returned = len(items)
|
|
475
|
+
indexed = returned
|
|
476
|
+
for _, item := range items[:indexed] {
|
|
477
|
+
line := "- [" + item.ID + "](" + item.HTMLURL + ")"
|
|
478
|
+
if item.Description != "" {
|
|
479
|
+
description, truncated := githubOverviewInlinePreview(item.Description, 140)
|
|
480
|
+
if truncated {
|
|
481
|
+
description += "…"
|
|
482
|
+
}
|
|
483
|
+
line += " — " + description
|
|
484
|
+
}
|
|
485
|
+
rows = append(rows, line)
|
|
486
|
+
}
|
|
487
|
+
case "users", "people":
|
|
488
|
+
var items []githubUser
|
|
489
|
+
if err := json.Unmarshal(resp.Body, &items); err != nil {
|
|
490
|
+
return "", fmt.Errorf("decode GitHub profile users: %w", err)
|
|
491
|
+
}
|
|
492
|
+
returned = len(items)
|
|
493
|
+
indexed = returned
|
|
494
|
+
for _, item := range items[:indexed] {
|
|
495
|
+
rows = append(rows, renderGitHubUserIdentity(item))
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
lines := []string{"---", "login: " + yamlScalar(profile.Login), "type: " + yamlScalar(profile.Type), "view: " + yamlScalar(tab), "page: " + page, fmt.Sprintf("returned: %d", returned), fmt.Sprintf("indexed: %d", indexed), fmt.Sprintf("local_omitted: %d", returned-indexed), "---", "", "# " + profile.Login + " — " + tab, ""}
|
|
499
|
+
lines = append(lines, rows...)
|
|
500
|
+
if nav := renderGitHubUIPageNavigation(target, resp.Links()); len(nav) > 0 {
|
|
501
|
+
lines = append(lines, "", "## Navigation", "")
|
|
502
|
+
lines = append(lines, nav...)
|
|
503
|
+
}
|
|
504
|
+
return strings.TrimSpace(strings.Join(lines, "\n")), nil
|
|
505
|
+
}
|