|
| 1 | +module main |
| 2 | + |
| 3 | +import os |
| 4 | +import cli |
| 5 | +import net.http |
| 6 | +import net.urllib |
| 7 | +import json |
| 8 | +import rand |
| 9 | +import term |
| 10 | + |
| 11 | +const search_endpoint = 'https://api.github.com/search/issues' |
| 12 | +const search_query = 'repo:vlang/v is:issue is:open -label:"Status: Confirmed"' |
| 13 | +const per_page = 100 |
| 14 | +const max_search_results = 1000 |
| 15 | + |
| 16 | +struct SearchResponse { |
| 17 | + total_count int |
| 18 | + items []Issue |
| 19 | +} |
| 20 | + |
| 21 | +struct Issue { |
| 22 | + html_url string |
| 23 | +} |
| 24 | + |
| 25 | +fn main() { |
| 26 | + // the 0th arg is /path/to/vquest, the 1st is `quest`; the args after that are the subcommands |
| 27 | + mut args := []string{} |
| 28 | + args << os.args[0] |
| 29 | + args << os.args#[2..] |
| 30 | + mut app := cli.Command{ |
| 31 | + name: 'quest' |
| 32 | + description: 'A tool to help make V better for everyone, by spending some time each day, on random tasks/missions like:\n * documenting public APIs\n * issue confirmation reviewing and triage\n * testing' |
| 33 | + execute: cli.print_help_for_command |
| 34 | + defaults: struct { |
| 35 | + man: false |
| 36 | + } |
| 37 | + commands: [ |
| 38 | + cli.Command{ |
| 39 | + name: 'document' |
| 40 | + description: 'Print a random missing doc entry from the V standard library.' |
| 41 | + execute: run_document |
| 42 | + }, |
| 43 | + cli.Command{ |
| 44 | + name: 'confirm' |
| 45 | + description: 'Open a random unconfirmed vlang/v issue in your browser.' |
| 46 | + flags: [ |
| 47 | + cli.Flag{ |
| 48 | + flag: .bool |
| 49 | + name: 'print-only' |
| 50 | + abbrev: 'p' |
| 51 | + description: 'Print the issue URL without opening a browser.' |
| 52 | + }, |
| 53 | + ] |
| 54 | + execute: run_confirm |
| 55 | + }, |
| 56 | + ] |
| 57 | + } |
| 58 | + app.setup() |
| 59 | + if args.len <= 1 { |
| 60 | + if rcmd := rand.element(app.commands) { |
| 61 | + rcmd.execute(rcmd)! |
| 62 | + return |
| 63 | + } |
| 64 | + } |
| 65 | + app.parse(args) |
| 66 | +} |
| 67 | + |
| 68 | +fn run_confirm(cmd cli.Command) ! { |
| 69 | + print_only := cmd.flags.get_bool('print-only') or { false } |
| 70 | + total := fetch_total_count()! |
| 71 | + max_pages := total_to_max_pages(total) |
| 72 | + if max_pages == 0 { |
| 73 | + return error('no unconfirmed issues found') |
| 74 | + } |
| 75 | + page := (rand.intn(max_pages) or { 0 }) + 1 |
| 76 | + eprintln(term.colorize(term.gray, 'Found: ${total} still unconfirmed issues. Fetching issue from page: ${page} ...')) |
| 77 | + issue := fetch_issue_from_page(page)! |
| 78 | + if print_only { |
| 79 | + println(issue.html_url) |
| 80 | + return |
| 81 | + } |
| 82 | + os.open_uri(issue.html_url)! |
| 83 | + println(term.colorize(term.green, 'Help us by confirming and triaging this issue:')) |
| 84 | + println(issue.html_url) |
| 85 | +} |
| 86 | + |
| 87 | +fn run_document(cmd cli.Command) ! { |
| 88 | + res := os.execute('v missdoc --exclude vlib/v --exclude /linux_bare/ --exclude /wasm_bare/ @vlib') |
| 89 | + if res.exit_code != 0 { |
| 90 | + return error('v missdoc failed: ${res.output}') |
| 91 | + } |
| 92 | + lines := res.output.split_into_lines().filter(it.trim_space() != '') |
| 93 | + if lines.len == 0 { |
| 94 | + return error('no missing doc entries found') |
| 95 | + } |
| 96 | + idx := rand.intn(lines.len) or { 0 } |
| 97 | + eprintln(term.colorize(term.green, 'Help us document this public API:')) |
| 98 | + println(term.colorize(term.bold, lines[idx])) |
| 99 | +} |
| 100 | + |
| 101 | +fn fetch_total_count() !int { |
| 102 | + url := build_search_url(1, 1) |
| 103 | + body := api_get(url)! |
| 104 | + resp := json.decode(SearchResponse, body)! |
| 105 | + return resp.total_count |
| 106 | +} |
| 107 | + |
| 108 | +fn fetch_issue_from_page(page int) !Issue { |
| 109 | + url := build_search_url(page, per_page) |
| 110 | + body := api_get(url)! |
| 111 | + resp := json.decode(SearchResponse, body)! |
| 112 | + if resp.items.len == 0 { |
| 113 | + return error('no issues returned for page ${page}') |
| 114 | + } |
| 115 | + idx := rand.intn(resp.items.len) or { 0 } |
| 116 | + return resp.items[idx] |
| 117 | +} |
| 118 | + |
| 119 | +fn build_search_url(page int, per_page int) string { |
| 120 | + mut values := urllib.new_values() |
| 121 | + values.add('q', search_query) |
| 122 | + values.add('per_page', per_page.str()) |
| 123 | + values.add('page', page.str()) |
| 124 | + return '${search_endpoint}?${values.encode()}' |
| 125 | +} |
| 126 | + |
| 127 | +fn api_get(url string) !string { |
| 128 | + resp := http.fetch( |
| 129 | + url: url |
| 130 | + method: .get |
| 131 | + header: http.new_header_from_map({ |
| 132 | + http.CommonHeader.accept: 'application/vnd.github+json' |
| 133 | + http.CommonHeader.user_agent: 'v quest' |
| 134 | + }) |
| 135 | + )! |
| 136 | + if resp.status_code != 200 { |
| 137 | + return error('GitHub API error ${resp.status_code}: ${resp.body}') |
| 138 | + } |
| 139 | + return resp.body |
| 140 | +} |
| 141 | + |
| 142 | +fn total_to_max_pages(total int) int { |
| 143 | + if total <= 0 { |
| 144 | + return 0 |
| 145 | + } |
| 146 | + max_pages := (total + per_page - 1) / per_page |
| 147 | + limit := max_search_results / per_page |
| 148 | + return if max_pages < limit { max_pages } else { limit } |
| 149 | +} |
0 commit comments