mirror of
https://github.com/restic/restic.git
synced 2026-05-28 04:35:41 -04:00
Merge f1f1d44e05 into 990329013e
This commit is contained in:
commit
a0002752a6
4 changed files with 274 additions and 1 deletions
6
changelog/unreleased/pull-21795
Normal file
6
changelog/unreleased/pull-21795
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
Enhancement: add docs command
|
||||
|
||||
Added a new `docs` command (with user and dev subcommands) to easily open
|
||||
restic's documentation in the default web browser across Linux, macOS, and Windows.
|
||||
|
||||
https://github.com/restic/restic/pull/21795
|
||||
111
cmd/restic/cmd_docs.go
Normal file
111
cmd/restic/cmd_docs.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/restic/restic/internal/global"
|
||||
)
|
||||
|
||||
const (
|
||||
GOOS string = runtime.GOOS
|
||||
ResticDocsURL string = "https://restic.readthedocs.io/en"
|
||||
)
|
||||
|
||||
type execFn func(name string, arg ...string) *exec.Cmd
|
||||
|
||||
var (
|
||||
stdout io.Writer = os.Stdout
|
||||
start execFn = exec.Command
|
||||
versionRegex *regexp.Regexp = regexp.MustCompile(`^(\d+\.\d+\.\d+)`)
|
||||
)
|
||||
|
||||
// newDocsCommand is the `docs` subcommand entry point,
|
||||
// using `restic docs` or `restic docs user` or `restic docs dev`.
|
||||
// It opens the respective documetation in your chosen default browser.
|
||||
func newDocsCommand(globalOptions *global.Options) *cobra.Command {
|
||||
_ = globalOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "docs",
|
||||
Short: "Opens the documentation in the default browser",
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
docsURL := docsURLForVersion(global.Version)
|
||||
openDocs(GOOS, docsURL, "user")
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(&cobra.Command{
|
||||
Use: "user",
|
||||
Short: "Show the user documentation",
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
docsURL := fmt.Sprintf("%s/stable", ResticDocsURL)
|
||||
openDocs(GOOS, docsURL, "user")
|
||||
},
|
||||
})
|
||||
|
||||
cmd.AddCommand(&cobra.Command{
|
||||
Use: "dev",
|
||||
Short: "Show the developer documentation",
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
docsURL := fmt.Sprintf("%s/latest", ResticDocsURL)
|
||||
openDocs(GOOS, docsURL, "developer")
|
||||
},
|
||||
})
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// docsURLForVersion is a function that returns a the URL documentation as a string.
|
||||
// it takes a version string as a "v1.2.3" as an input.
|
||||
func docsURLForVersion(version string) string {
|
||||
// Safe default fallback for empty/unknown versions
|
||||
if version == "" || version == "unknown" {
|
||||
return fmt.Sprintf("%s/stable", ResticDocsURL)
|
||||
}
|
||||
|
||||
// Route development builds / local compiled binaries directly to bleeding edge docs
|
||||
if strings.Contains(version, "dev") || strings.Contains(version, "compiled") {
|
||||
return fmt.Sprintf("%s/latest", ResticDocsURL)
|
||||
}
|
||||
|
||||
// Match strict tag releases (e.g., exact matches like "0.18.1")
|
||||
matches := versionRegex.FindStringSubmatch(version)
|
||||
if len(matches) == 2 {
|
||||
return fmt.Sprintf("%s/v%s", ResticDocsURL, matches[1])
|
||||
}
|
||||
|
||||
// Return the stable docs if all checks fail
|
||||
return fmt.Sprintf("%s/stable", ResticDocsURL)
|
||||
}
|
||||
|
||||
// openDocs is a function that takes in the current operating system platform, documentation url and its type.
|
||||
// It basically opens the documentation in your chosen default browser.
|
||||
func openDocs(GOOS string, url string, docType string) {
|
||||
_, _ = fmt.Fprintf(stdout, "Opening the %s documentation at %s\n", docType, url)
|
||||
|
||||
var cmd *exec.Cmd
|
||||
|
||||
switch GOOS {
|
||||
case "linux":
|
||||
cmd = start("xdg-open", url)
|
||||
case "windows":
|
||||
cmd = start("rundll32", "url.dll,FileProtocolHandler", url)
|
||||
case "darwin":
|
||||
cmd = start("open", url)
|
||||
default:
|
||||
log.Fatalf("Unsupported platform: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatalf("Failed to open browser: %v", err)
|
||||
}
|
||||
}
|
||||
155
cmd/restic/cmd_docs_test.go
Normal file
155
cmd/restic/cmd_docs_test.go
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/restic/restic/internal/global"
|
||||
)
|
||||
|
||||
func TestNewDocsCommand(t *testing.T) {
|
||||
cmd := newDocsCommand(&global.Options{})
|
||||
|
||||
if cmd.Use != "docs" {
|
||||
t.Errorf("expected command Use 'docs' got %q", cmd.Use)
|
||||
}
|
||||
|
||||
subcommands := []struct {
|
||||
name string
|
||||
short string
|
||||
}{
|
||||
{"user", "Show the user documentation"},
|
||||
{"dev", "Show the developer documentation"},
|
||||
}
|
||||
|
||||
for _, sc := range subcommands {
|
||||
found := false
|
||||
for _, sub := range cmd.Commands() {
|
||||
if sub.Name() == sc.name {
|
||||
found = true
|
||||
if sub.Short != sc.short {
|
||||
t.Errorf("expected short description %q for %q, got %q", sc.short, sc.name, sub.Short)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected subcommand %q not found", sc.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsURLForVersion(t *testing.T) {
|
||||
// Dynamically build the expected URLs using the package's base constant
|
||||
stableURL := fmt.Sprintf("%s/stable", ResticDocsURL)
|
||||
latestURL := fmt.Sprintf("%s/latest", ResticDocsURL)
|
||||
tagURL := func(tag string) string {
|
||||
return fmt.Sprintf("%s/v%s", ResticDocsURL, tag)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
version string
|
||||
want string
|
||||
}{
|
||||
// --- 1. Stable Tag Releases ---
|
||||
{name: "Exact Release Tag", version: "0.18.1", want: tagURL("0.18.1")},
|
||||
{name: "Release Tag with Patch", version: "1.23.4", want: tagURL("1.23.4")},
|
||||
|
||||
// --- 2. Development & Bleeding Edge Builds ---
|
||||
{name: "Dev Build with Suffix", version: "0.18.1-dev", want: latestURL},
|
||||
{name: "Manually Compiled Binary", version: "0.18.1 (compiled manually)", want: latestURL},
|
||||
{name: "Pure Dev Keyword", version: "dev", want: latestURL},
|
||||
|
||||
// --- 3. Fallbacks & Unknown States ---
|
||||
{name: "Explicit Unknown Keyword", version: "unknown", want: stableURL},
|
||||
{name: "Empty String Fallback", version: "", want: stableURL},
|
||||
{name: "Malformed Version Fallback", version: "my-custom-version-string", want: stableURL},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := docsURLForVersion(tt.version); got != tt.want {
|
||||
t.Errorf("docsURLForVersion(%q) = %q, want %q", tt.version, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenDocs(t *testing.T) {
|
||||
stableURL := fmt.Sprintf("%s/stable", ResticDocsURL)
|
||||
latestURL := fmt.Sprintf("%s/latest", ResticDocsURL)
|
||||
tagURL := func(tag string) string {
|
||||
return fmt.Sprintf("%s/v%s", ResticDocsURL, tag)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
goos string
|
||||
url string
|
||||
docType string
|
||||
wantBin string
|
||||
wantArg string
|
||||
}{
|
||||
// --- 1. TestOpenDocs on Linux
|
||||
{"Linux version", "linux", tagURL("v0.18.1"), "user", "xdg-open", tagURL("v0.18.1")},
|
||||
{"Linux User", "linux", stableURL, "user", "xdg-open", stableURL},
|
||||
{"Linux Dev", "linux", latestURL, "developer", "xdg-open", latestURL},
|
||||
|
||||
// --- 2. TestOpenDocs on MacOS
|
||||
{"Mac Version", "darwin", tagURL("v0.18.1"), "user", "open", tagURL("v0.18.1")},
|
||||
{"Mac User", "darwin", stableURL, "user", "open", stableURL},
|
||||
{"Mac Dev", "darwin", latestURL, "developer", "open", latestURL},
|
||||
|
||||
// --- 3. TestOpenDocs on Windows
|
||||
{"Windows Version", "windows", tagURL("v0.18.1"), "user", "rundll32", "url.dll,FileProtocolHandler"},
|
||||
{"Windows User", "windows", stableURL, "user", "rundll32", "url.dll,FileProtocolHandler"},
|
||||
{"Windows Dev", "windows", latestURL, "developer", "rundll32", "url.dll,FileProtocolHandler"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
stdout = &buf // Capture console output
|
||||
|
||||
var capturedBin string
|
||||
var capturedArgs []string
|
||||
|
||||
// Mock the execution
|
||||
originalStart := start
|
||||
start = func(name string, arg ...string) *exec.Cmd {
|
||||
capturedBin = name
|
||||
capturedArgs = arg
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
// Cleanup the global state
|
||||
defer func() {
|
||||
stdout = os.Stdout
|
||||
start = originalStart
|
||||
}()
|
||||
|
||||
openDocs(tt.goos, tt.url, tt.docType)
|
||||
|
||||
// Test 1. Verify Command Binary
|
||||
if capturedBin != tt.wantBin {
|
||||
t.Errorf("Binary mismatch: expected %q, got %q", tt.wantBin, capturedBin)
|
||||
}
|
||||
|
||||
// Test 2. Verify Command Arguments
|
||||
argsJoined := strings.Join(capturedArgs, " ")
|
||||
if !strings.Contains(argsJoined, tt.wantArg) {
|
||||
t.Errorf("Args mismatch: expected %q, got %q", tt.wantArg, argsJoined)
|
||||
}
|
||||
|
||||
// Test 3. Verify Console Output Message
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, tt.url) || !strings.Contains(output, tt.docType) {
|
||||
t.Errorf("Console output mismatch. Got: %q", output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -102,6 +102,7 @@ The full documentation can be found at https://restic.readthedocs.io/ .
|
|||
newStatsCommand(globalOptions),
|
||||
newTagCommand(globalOptions),
|
||||
newUnlockCommand(globalOptions),
|
||||
newDocsCommand(globalOptions),
|
||||
newVersionCommand(globalOptions),
|
||||
)
|
||||
|
||||
|
|
@ -118,7 +119,7 @@ The full documentation can be found at https://restic.readthedocs.io/ .
|
|||
// user for authentication).
|
||||
func needsPassword(cmd string) bool {
|
||||
switch cmd {
|
||||
case "cache", "generate", "help", "options", "self-update", "version", "__complete", "__completeNoDesc":
|
||||
case "cache", "docs", "generate", "help", "options", "self-update", "version", "__complete", "__completeNoDesc":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
|
|
|
|||
Loading…
Reference in a new issue