mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-10-28 07:33:09 +08:00
* 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>
117 lines
3.6 KiB
Go
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
|
|
}
|