From 4ab6d9031022fbcfd59a621ee64600abe13053dd Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 15 Apr 2026 02:43:35 +0300 Subject: [PATCH] all: fix bug report helper not working for large files (fixes #19767) --- cmd/tools/modules/vbugreport/report.v | 174 +++++++++++++++++++++ cmd/tools/modules/vbugreport/report_test.v | 55 +++++++ cmd/tools/vbug.v | 56 ++----- 3 files changed, 245 insertions(+), 40 deletions(-) create mode 100644 cmd/tools/modules/vbugreport/report.v create mode 100644 cmd/tools/modules/vbugreport/report_test.v diff --git a/cmd/tools/modules/vbugreport/report.v b/cmd/tools/modules/vbugreport/report.v new file mode 100644 index 000000000..66b32f48d --- /dev/null +++ b/cmd/tools/modules/vbugreport/report.v @@ -0,0 +1,174 @@ +module vbugreport + +import net.urllib +import os + +const github_bug_issue_form_base_uri = 'https://github.com/vlang/v/issues/new?template=bug-report.yml' +const github_issue_form_url_soft_limit = 7500 + +pub struct BugReport { + description string + reproduction string + expected string + current string + solution string + context string + version string + environment string +} + +struct IssueFormField { + name string + label string + value string +} + +pub enum BugReportDeliveryMode { + github_form + local_report +} + +pub struct BugReportDelivery { +pub: + mode BugReportDeliveryMode + uri string + local_report_path string + local_report_body string +} + +fn field_value_or_default(value string, default_value string) string { + trimmed := value.trim_space() + if trimmed.len == 0 { + return default_value + } + return trimmed +} + +fn fenced_block(language string, value string) string { + content := field_value_or_default(value, 'N/A') + return '```' + language + '\n' + content + '\n```' +} + +fn quoted_command(file_path string, generated_file string, user_args string) string { + mut compile_cmd := './vdbg' + if user_args.len > 0 { + compile_cmd += ' ${user_args}' + } + compile_cmd += ' ${os.quoted_path(file_path)}' + return './v -g -o vdbg cmd/v && ${compile_cmd} && ${os.quoted_path(os.real_path(generated_file))}' +} + +// new_bug_report builds the structured fields used by the GitHub bug report form. +pub fn new_bug_report(file_path string, generated_file string, user_args string, expected_result string, version string, vdoctor_output string, file_content string, build_output string) BugReport { + command := quoted_command(file_path, generated_file, user_args) + source_name := os.file_name(file_path) + return BugReport{ + description: 'Running `${command}` on `${source_name}` produced a compiler error.' + reproduction: '${fenced_block('sh', command)}\n\n${fenced_block('v', file_content)}' + expected: field_value_or_default(expected_result, 'N/A') + current: fenced_block('', build_output) + solution: '_No response_' + context: 'Generated by `v bug` from `${file_path}`.' + version: field_value_or_default(version, 'N/A') + environment: fenced_block('', vdoctor_output) + } +} + +// issue_form_fields must stay aligned with `.github/ISSUE_TEMPLATE/bug-report.yml`. +fn issue_form_fields(report BugReport) []IssueFormField { + return [ + IssueFormField{ + name: 'description' + label: 'Describe the bug' + value: report.description + }, + IssueFormField{ + name: 'reproduction' + label: 'Reproduction Steps' + value: report.reproduction + }, + IssueFormField{ + name: 'expected' + label: 'Expected Behavior' + value: report.expected + }, + IssueFormField{ + name: 'current' + label: 'Current Behavior' + value: report.current + }, + IssueFormField{ + name: 'solution' + label: 'Possible Solution' + value: report.solution + }, + IssueFormField{ + name: 'context' + label: 'Additional Information/Context' + value: report.context + }, + IssueFormField{ + name: 'version' + label: 'V version' + value: report.version + }, + IssueFormField{ + name: 'environment' + label: 'Environment details (OS name and version, etc.)' + value: report.environment + }, + ] +} + +fn github_issue_form_uri(report BugReport) string { + mut query_params := []string{} + for field in issue_form_fields(report) { + query_params << '${field.name}=${urllib.query_escape(field.value)}' + } + return '${github_bug_issue_form_base_uri}&${query_params.join('&')}' +} + +fn bug_report_markdown(report BugReport) string { + mut lines := [ + '# V Bug Report', + '', + 'Generated by `v bug`.', + '', + ] + for field in issue_form_fields(report) { + lines << '### ${field.label}' + lines << '' + lines << field.value + lines << '' + } + return lines.join('\n') +} + +fn bug_report_file_path(file_path string) string { + base_name := os.file_name(file_path) + stem := if base_name.contains('.') { base_name.all_before_last('.') } else { base_name } + file_name := if stem.len == 0 { 'v-bug-report.md' } else { '${stem}.bug-report.md' } + file_dir := os.dir(file_path) + return if file_dir.len == 0 || file_dir == '.' { + file_name + } else { + os.join_path(file_dir, file_name) + } +} + +// prepare_bug_report_delivery chooses between a prefilled issue-form URL and a local markdown fallback. +pub fn prepare_bug_report_delivery(report BugReport, file_path string) BugReportDelivery { + uri := github_issue_form_uri(report) + if uri.len <= github_issue_form_url_soft_limit { + return BugReportDelivery{ + mode: .github_form + uri: uri + } + } + return BugReportDelivery{ + mode: .local_report + uri: github_bug_issue_form_base_uri + local_report_path: bug_report_file_path(file_path) + local_report_body: bug_report_markdown(report) + } +} diff --git a/cmd/tools/modules/vbugreport/report_test.v b/cmd/tools/modules/vbugreport/report_test.v new file mode 100644 index 000000000..c6cd5c8b3 --- /dev/null +++ b/cmd/tools/modules/vbugreport/report_test.v @@ -0,0 +1,55 @@ +module vbugreport + +import os +import net.urllib + +fn sample_bug_report() BugReport { + return BugReport{ + description: 'Running `./vdbg examples/fail.v` produced a compiler error.' + reproduction: '```sh\n./vdbg examples/fail.v\n```\n\n```v\nfn main() {}\n```' + expected: 'An error message.' + current: '```\nbuilder error\n```' + solution: '_No response_' + context: 'Generated by `v bug`.' + version: 'V 0.0.0 deadbeef' + environment: '```\nOS: linux\n```' + } +} + +fn test_github_issue_form_uri_prefills_bug_report_fields() { + report := sample_bug_report() + uri := github_issue_form_uri(report) + assert uri.starts_with(github_bug_issue_form_base_uri + '&') + assert !uri.contains('body=') + + query := urllib.parse_query(uri.all_after('?')) or { panic(err) } + assert (query.get('template') or { panic(err) }) == 'bug-report.yml' + assert (query.get('description') or { panic(err) }) == report.description + assert (query.get('reproduction') or { panic(err) }) == report.reproduction + assert (query.get('expected') or { panic(err) }) == report.expected + assert (query.get('current') or { panic(err) }) == report.current + assert (query.get('solution') or { panic(err) }) == report.solution + assert (query.get('context') or { panic(err) }) == report.context + assert (query.get('version') or { panic(err) }) == report.version + assert (query.get('environment') or { panic(err) }) == report.environment +} + +fn test_prepare_bug_report_delivery_falls_back_to_local_report_for_large_reports() { + report := BugReport{ + description: 'Running `./vdbg examples/fail.v` produced a compiler error.' + reproduction: '```sh\n./vdbg examples/fail.v\n```\n\n```v\nfn main() {}\n```' + expected: 'An error message.' + current: 'X'.repeat(github_issue_form_url_soft_limit) + solution: '_No response_' + context: 'Generated by `v bug`.' + version: 'V 0.0.0 deadbeef' + environment: '```\nOS: linux\n```' + } + delivery := prepare_bug_report_delivery(report, os.join_path('examples', 'foo', 'bar.v')) + assert delivery.mode == .local_report + assert delivery.uri == github_bug_issue_form_base_uri + assert delivery.local_report_path == os.join_path('examples', 'foo', 'bar.bug-report.md') + assert delivery.local_report_body.starts_with('# V Bug Report') + assert delivery.local_report_body.contains('### Current Behavior') + assert delivery.local_report_body.contains(report.current) +} diff --git a/cmd/tools/vbug.v b/cmd/tools/vbug.v index b5024b4cf..3eed5106b 100644 --- a/cmd/tools/vbug.v +++ b/cmd/tools/vbug.v @@ -1,7 +1,7 @@ import os import term import readline -import net.urllib +import vbugreport fn elog(msg string) { eprintln(term.ecolorize(term.gray, msg)) @@ -164,45 +164,21 @@ fn main() { confirm_or_exit('Are you sure you want to continue?') } - // When updating this template, make sure to update `.github/ISSUE_TEMPLATE/bug_report.md` too - raw_body := ' - -
-V version: ${vversion()}, press to see full `v doctor` output - -${vdoctor_output} -
- -**What did you do?** -`./v -g -o vdbg cmd/v && ./vdbg ${user_args} ${file_path} && ${os.real_path(generated_file)}` -{file_content} - -**What did you see?** -``` -${build_output}``` - -**What did you expect to see?** - -${expected_result} - -' - mut encoded_body := urllib.query_escape(raw_body.replace_once('{file_content}', - '```v\n${file_content}\n```')) - mut generated_uri := 'https://github.com/vlang/v/issues/new?labels=Bug&body=${encoded_body}' - if generated_uri.len > 8192 { - // GitHub doesn't support URLs longer than 8192 characters - encoded_body = urllib.query_escape(raw_body.replace_once('{file_content}', - 'See attached file `${file_path}`')) - generated_uri = 'https://github.com/vlang/v/issues/new?labels=Bug&body=${encoded_body}' - elog('> Your file is too big to be submitted.') - elog('> Go to the following URL, and attach your file:') - olog(generated_uri) - } else { - os.open_uri(generated_uri) or { - if is_verbose { - elog(err.str()) - } - olog(generated_uri) + report := vbugreport.new_bug_report(file_path, generated_file, user_args, expected_result, + vversion(), vdoctor_output, file_content, build_output) + delivery := vbugreport.prepare_bug_report_delivery(report, file_path) + if delivery.mode == .local_report { + os.write_file(delivery.local_report_path, delivery.local_report_body) or { + elog('> unable to write the bug report file `${delivery.local_report_path}`: ${err}') + } + elog('> The full bug report is too large to prefill on GitHub.') + elog('> The full report was saved to `${delivery.local_report_path}`.') + elog('> Open the bug form below, then paste the saved markdown sections or attach the file:') + } + os.open_uri(delivery.uri) or { + if is_verbose { + elog(err.str()) } + olog(delivery.uri) } } -- 2.39.5