Files
open-code-review/cmd/opencodereview/arg_errors.go
T
chethanukandKite 47b9d60774 feat(viewer): export a session to a self-contained HTML file (#1167) (#1174)
* feat(viewer): export a session to a self-contained HTML file (#1167)

The only renderer for a review session was the live viewer HTTP server, so
there was no way to archive a review as a CI artifact.

Adds viewer.ExportSession, which drives the same template and the same
LoadSession the HTTP handler does, but renders into an io.Writer with both
/static/ assets inlined. The assets are passed as template.CSS and
template.JS: html/template refuses to interpolate an untyped string into a
<style> or <script>, and would silently emit ZgotmplZ for the stylesheet and
a JSON string literal for the script. session.html gains three gates on a new
Static field, so the served page is unchanged — the field is zero for every
HTTP render — while the exported page inlines the assets and de-links the two
site-absolute breadcrumb anchors, which are dead over file://. style.css
widens nav.breadcrumb a to cover the .crumb span that replaces one of them.

Exposed as `ocr session export [session-id]`, on `session` rather than as a
`viewer --export` flag because `viewer` is cobra.NoArgs and session_cmd.go
already owns session-id resolution and completion. The id is optional and
defaults to the newest session: a successful `ocr review` never prints its
session id, so requiring one would push every CI archive step through
`--format json`. Output goes through the shared --output/-o writer, with
"html" added to isMachineReadable so the page is not run through
stripAnsiWriter, which would eat the ESC bytes in trace-quoted source. The
page is rendered fully into a buffer before the first write, so a failure
part-way through Execute cannot leave a truncated file behind.

Also adds maximumArgs, mirroring the existing minimumArgs, so the optional
positional reports the command's own usage rather than cobra's raw
"accepts at most 1 arg(s), received 2".

Documented in all five locales of cli-reference.md, including the warning
that the artifact embeds reviewed source.

Signed-off-by: ChethanUK <chethanuk@outlook.com>

* refactor(viewer): share the header brand between served and exported pages

Signed-off-by: ChethanUK <chethanuk@outlook.com>

* feat: Apply batched suggestions from code review

Co-authored-by: Kite <254839944+lizhengfeng101@users.noreply.github.com>

---------

Signed-off-by: ChethanUK <chethanuk@outlook.com>
Co-authored-by: Kite <254839944+lizhengfeng101@users.noreply.github.com>
2026-09-19 11:19:10 +08:00

117 lines
3.6 KiB
Go

// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 alibaba/open-code-review Contributors
package main
import (
"errors"
"fmt"
"strings"
"github.com/spf13/cobra"
)
// Positional-argument validators that replace Cobra's raw count message
// ("accepts 2 arg(s), received 1") with an actionable one built from metadata
// the command already declares — the positional signature in Use, plus Example
// and ValidArgs where present — so the guidance cannot drift from the command's
// own help output. Because the root command sets SilenceUsage, everything the
// user sees has to travel in the error itself.
// exactArgs behaves like cobra.ExactArgs(n) but reports a wrong count with the
// command's own usage information.
func exactArgs(n int) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if len(args) == n {
return nil
}
return argCountError(cmd, fmt.Sprintf("requires exactly %d argument(s)", n))
}
}
// minimumArgs behaves like cobra.MinimumNArgs(n) but reports too few arguments
// with the command's own usage information.
func minimumArgs(n int) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if len(args) >= n {
return nil
}
return argCountError(cmd, fmt.Sprintf("requires at least %d argument(s)", n))
}
}
// maximumArgs behaves like cobra.MaximumNArgs(n) but reports too many arguments
// with the command's own usage information.
func maximumArgs(n int) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if len(args) <= n {
return nil
}
return argCountError(cmd, fmt.Sprintf("accepts at most %d argument(s)", n))
}
}
// argCountError assembles the guidance for a wrong argument count. The count the
// user supplied is deliberately not echoed back — it adds nothing they do not
// already know from the line they just typed.
func argCountError(cmd *cobra.Command, requirement string) error {
var b strings.Builder
b.Grow(256)
path := cmd.CommandPath()
fmt.Fprintf(&b, "%q %s", path, requirement)
if sig := positionalSignature(cmd); sig != "" {
fmt.Fprintf(&b, " (%s)", sig)
}
if len(cmd.ValidArgs) > 0 {
b.WriteString("\n\nValid values: ")
b.WriteString(strings.Join(validArgNames(cmd.ValidArgs), ", "))
}
b.WriteString("\n\nUsage:\n ")
b.WriteString(cmd.UseLine())
if example := strings.TrimRight(cmd.Example, "\n"); example != "" {
b.WriteString("\n\nExample:\n")
b.WriteString(example)
}
b.WriteString("\n\nRun '")
b.WriteString(path)
b.WriteString(" --help' for more information.")
return errors.New(b.String())
}
// positionalSignature extracts the positional-argument part of cmd.Use, e.g.
// "<key> <value>" from "set <key> <value>" and "<path...>" from
// "rule [flags] <path...>". Bracketed fields are not placeholders the user
// types, so "[flags]" and inline enumerations such as "[bash|zsh]" are dropped;
// enumerated values are reported from ValidArgs instead.
func positionalSignature(cmd *cobra.Command) string {
fields := strings.Fields(cmd.Use)
if len(fields) < 2 {
return ""
}
parts := make([]string, 0, len(fields)-1)
for _, f := range fields[1:] {
if strings.HasPrefix(f, "<") {
parts = append(parts, f)
}
}
return strings.Join(parts, " ")
}
// validArgNames strips the "name\tdescription" form Cobra allows in ValidArgs,
// keeping only the value the user is expected to type. The split matches the one
// cobra.OnlyValidArgs applies, so the listed values are exactly those accepted.
func validArgNames(valid []string) []string {
names := make([]string, 0, len(valid))
for _, v := range valid {
names = append(names, strings.SplitN(v, "\t", 2)[0])
}
return names
}