From 7cb6ed3cf25dc17f519c96f338704fed85dcee05 Mon Sep 17 00:00:00 2001 From: Turmaxx <88465473+Turmaxx@users.noreply.github.com> Date: Fri, 8 May 2026 14:07:53 +0300 Subject: [PATCH 01/18] added new docs command to cmd/restic/main.go --- cmd/restic/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/restic/main.go b/cmd/restic/main.go index 619eee642..85d91a7b4 100644 --- a/cmd/restic/main.go +++ b/cmd/restic/main.go @@ -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), ) From 98cfe05b7d7ecc5cf780f78bf303b8fb87467f27 Mon Sep 17 00:00:00 2001 From: Turmaxx <88465473+Turmaxx@users.noreply.github.com> Date: Fri, 8 May 2026 14:08:32 +0300 Subject: [PATCH 02/18] added new docs command --- cmd/restic/cmd_docs.go | 78 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 cmd/restic/cmd_docs.go diff --git a/cmd/restic/cmd_docs.go b/cmd/restic/cmd_docs.go new file mode 100644 index 000000000..51ca37eb9 --- /dev/null +++ b/cmd/restic/cmd_docs.go @@ -0,0 +1,78 @@ +package main + +import ( + "fmt" + "io" + "log" + "os" + "os/exec" + "runtime" + + "github.com/restic/restic/internal/global" + "github.com/spf13/cobra" +) + +const ( + ResticDocsURL string = "https://restic.readthedocs.io/en/stable" + ResticDevDocsURL string = "https://restic.readthedocs.io/en/latest" +) + +type execFn func(name string, arg ...string) *exec.Cmd + +var ( + stdout io.Writer = os.Stdout + start execFn = exec.Command +) + +func newDocsCommand(globalOptions *global.Options) *cobra.Command { + + var cmd = &cobra.Command{ + Use: "docs", + Short: "Opens the documentation in the default browser", + Run: func(cmd *cobra.Command, args []string) { + openDocs(ResticDocsURL, "user") + }, + } + + cmd.AddCommand(&cobra.Command{ + Use: "user", + Short: "Show the user documentation", + Run: func(cmd *cobra.Command, args []string) { + openDocs(ResticDocsURL, "user") + }, + }) + + cmd.AddCommand(&cobra.Command{ + Use: "dev", + Short: "Show the development documentation", + Run: func(cmd *cobra.Command, args []string) { + openDocs(ResticDevDocsURL, "developer") + }, + }) + + return cmd +} + +func openDocs(url string, docType string) { + fmt.Fprintf(stdout, "Opening the %s documentation at %s\n", docType, url) + + var cmd *exec.Cmd + + switch runtime.GOOS { + case "linux": + cmd = start("xdg-open", url) + // err = exec.Command("xdg-open", url).Start() + case "windows": + cmd = start("rundll32", "url.dll,FileProtocolHandler", url) + // err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + cmd = start("open", url) + // err = exec.Command("open", url).Start() + default: + log.Fatalf("Unsupported platform: %s", runtime.GOOS) + } + + if err := cmd.Start(); err != nil { + log.Fatalf("Failed to open brower: %v", err) + } +} From 1fd0d81bf5f14e5d5b3cd59ff21b652bc191f3c9 Mon Sep 17 00:00:00 2001 From: Turmaxx <88465473+Turmaxx@users.noreply.github.com> Date: Fri, 8 May 2026 14:09:58 +0300 Subject: [PATCH 03/18] added testing for docs command --- cmd/restic/cmd_docs_test.go | 74 +++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 cmd/restic/cmd_docs_test.go diff --git a/cmd/restic/cmd_docs_test.go b/cmd/restic/cmd_docs_test.go new file mode 100644 index 000000000..62c1652cb --- /dev/null +++ b/cmd/restic/cmd_docs_test.go @@ -0,0 +1,74 @@ +package main + +import ( + "bytes" + "os" + "os/exec" + "strings" + "testing" + + "github.com/restic/restic/internal/global" +) + +func TestNewDocsCommand(t *testing.T) { + opts := &global.Options{} + cmd := newDocsCommand(opts) + + 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 development 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 TestOpenDocs(t *testing.T) { + // Redirect output to capture Printf + var buf bytes.Buffer + stdout = &buf + + // Mock the command execution + originalStart := start + start = func(name string, arg ...string) *exec.Cmd { + // Return a harmless 'echo' command to satisfy .Start() + return exec.Command("echo", arg...) + } + + // Restore original global variables after test + defer func() { + stdout = os.Stdout + start = originalStart + }() + + testURL := "https://example.com" + openDocs(testURL, "user") + output := buf.String() + + // Verify the console output contains the correct metadata + if !strings.Contains(output, "Opening the user documentation") { + t.Errorf("unexpected output message: %q", output) + } + if !strings.Contains(output, testURL) { + t.Errorf("output did not contain the correct URL: %q", output) + } +} From 78be97c8713b1a958bea8c0b4e72dd68f71de3e7 Mon Sep 17 00:00:00 2001 From: Turmaxx <88465473+Turmaxx@users.noreply.github.com> Date: Fri, 8 May 2026 14:32:52 +0300 Subject: [PATCH 04/18] changed dev subcommand Short from development to developer --- cmd/restic/cmd_docs.go | 2 +- cmd/restic/cmd_docs_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/restic/cmd_docs.go b/cmd/restic/cmd_docs.go index 51ca37eb9..0cfee4083 100644 --- a/cmd/restic/cmd_docs.go +++ b/cmd/restic/cmd_docs.go @@ -44,7 +44,7 @@ func newDocsCommand(globalOptions *global.Options) *cobra.Command { cmd.AddCommand(&cobra.Command{ Use: "dev", - Short: "Show the development documentation", + Short: "Show the developer documentation", Run: func(cmd *cobra.Command, args []string) { openDocs(ResticDevDocsURL, "developer") }, diff --git a/cmd/restic/cmd_docs_test.go b/cmd/restic/cmd_docs_test.go index 62c1652cb..e5cc3eece 100644 --- a/cmd/restic/cmd_docs_test.go +++ b/cmd/restic/cmd_docs_test.go @@ -23,7 +23,7 @@ func TestNewDocsCommand(t *testing.T) { short string }{ {"user", "Show the user documentation"}, - {"dev", "Show the development documentation"}, + {"dev", "Show the developer documentation"}, } for _, sc := range subcommands { From 54bc475b1fd58f65fe1f317e6f7b11ce8279b9db Mon Sep 17 00:00:00 2001 From: Turmaxx <88465473+Turmaxx@users.noreply.github.com> Date: Fri, 8 May 2026 14:34:19 +0300 Subject: [PATCH 05/18] removed the last bits of `err`, since I was using this during testing --- cmd/restic/cmd_docs.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cmd/restic/cmd_docs.go b/cmd/restic/cmd_docs.go index 0cfee4083..0a8162a74 100644 --- a/cmd/restic/cmd_docs.go +++ b/cmd/restic/cmd_docs.go @@ -61,13 +61,10 @@ func openDocs(url string, docType string) { switch runtime.GOOS { case "linux": cmd = start("xdg-open", url) - // err = exec.Command("xdg-open", url).Start() case "windows": cmd = start("rundll32", "url.dll,FileProtocolHandler", url) - // err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() case "darwin": cmd = start("open", url) - // err = exec.Command("open", url).Start() default: log.Fatalf("Unsupported platform: %s", runtime.GOOS) } From 45e61f3c84bd7b045c19cc6ad770faf64f4295bf Mon Sep 17 00:00:00 2001 From: Turmaxx <88465473+Turmaxx@users.noreply.github.com> Date: Fri, 8 May 2026 16:38:07 +0300 Subject: [PATCH 06/18] added unreleased changelog for this PR found at pull-21795 --- changelog/unreleased/pull-21795 | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changelog/unreleased/pull-21795 diff --git a/changelog/unreleased/pull-21795 b/changelog/unreleased/pull-21795 new file mode 100644 index 000000000..e9e93a4d5 --- /dev/null +++ b/changelog/unreleased/pull-21795 @@ -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 From 043b97828e93bc47ec335753a5cbefc535176efc Mon Sep 17 00:00:00 2001 From: Turmaxx <88465473+Turmaxx@users.noreply.github.com> Date: Fri, 8 May 2026 16:52:50 +0300 Subject: [PATCH 07/18] fixed some issues with the linting - removed unused parameters in the NewDocsCommand function as well as in the subcommand functions and fmt.Fprintf --- cmd/restic/cmd_docs.go | 13 +++++++------ cmd/restic/cmd_docs_test.go | 5 +---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/cmd/restic/cmd_docs.go b/cmd/restic/cmd_docs.go index 0a8162a74..0367731e7 100644 --- a/cmd/restic/cmd_docs.go +++ b/cmd/restic/cmd_docs.go @@ -8,7 +8,6 @@ import ( "os/exec" "runtime" - "github.com/restic/restic/internal/global" "github.com/spf13/cobra" ) @@ -24,12 +23,12 @@ var ( start execFn = exec.Command ) -func newDocsCommand(globalOptions *global.Options) *cobra.Command { +func newDocsCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "docs", Short: "Opens the documentation in the default browser", - Run: func(cmd *cobra.Command, args []string) { + Run: func(_ *cobra.Command, args []string) { openDocs(ResticDocsURL, "user") }, } @@ -37,7 +36,7 @@ func newDocsCommand(globalOptions *global.Options) *cobra.Command { cmd.AddCommand(&cobra.Command{ Use: "user", Short: "Show the user documentation", - Run: func(cmd *cobra.Command, args []string) { + Run: func(_ *cobra.Command, args []string) { openDocs(ResticDocsURL, "user") }, }) @@ -45,7 +44,7 @@ func newDocsCommand(globalOptions *global.Options) *cobra.Command { cmd.AddCommand(&cobra.Command{ Use: "dev", Short: "Show the developer documentation", - Run: func(cmd *cobra.Command, args []string) { + Run: func(_ *cobra.Command, args []string) { openDocs(ResticDevDocsURL, "developer") }, }) @@ -54,7 +53,9 @@ func newDocsCommand(globalOptions *global.Options) *cobra.Command { } func openDocs(url string, docType string) { - fmt.Fprintf(stdout, "Opening the %s documentation at %s\n", docType, url) + if _, err := fmt.Fprintf(stdout, "Opening the %s documentation at %s\n", docType, url); err != nil { + fmt.Fprintf(stdout, "Cant open the %s documenation", docType) + } var cmd *exec.Cmd diff --git a/cmd/restic/cmd_docs_test.go b/cmd/restic/cmd_docs_test.go index e5cc3eece..61c09888c 100644 --- a/cmd/restic/cmd_docs_test.go +++ b/cmd/restic/cmd_docs_test.go @@ -6,13 +6,10 @@ import ( "os/exec" "strings" "testing" - - "github.com/restic/restic/internal/global" ) func TestNewDocsCommand(t *testing.T) { - opts := &global.Options{} - cmd := newDocsCommand(opts) + cmd := newDocsCommand() if cmd.Use != "docs" { t.Errorf("expected command Use 'docs' got %q", cmd.Use) From f70882647b57c0573daa08700f9c9fe88f4d34cf Mon Sep 17 00:00:00 2001 From: Turmaxx <88465473+Turmaxx@users.noreply.github.com> Date: Fri, 8 May 2026 17:36:40 +0300 Subject: [PATCH 08/18] this fixes the fmt.Fprintf issue much better - the previous one create a logical roundabout which is an error since the linter expects an error to returned. --- cmd/restic/cmd_docs.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cmd/restic/cmd_docs.go b/cmd/restic/cmd_docs.go index 0367731e7..159024652 100644 --- a/cmd/restic/cmd_docs.go +++ b/cmd/restic/cmd_docs.go @@ -53,9 +53,7 @@ func newDocsCommand() *cobra.Command { } func openDocs(url string, docType string) { - if _, err := fmt.Fprintf(stdout, "Opening the %s documentation at %s\n", docType, url); err != nil { - fmt.Fprintf(stdout, "Cant open the %s documenation", docType) - } + _, _ = fmt.Fprintf(stdout, "Opening the %s documentation at %s\n", docType, url) var cmd *exec.Cmd From d558db6955c91f4ef7fa7ff9bb6da36803af744c Mon Sep 17 00:00:00 2001 From: Turmaxx <88465473+Turmaxx@users.noreply.github.com> Date: Fri, 8 May 2026 17:37:08 +0300 Subject: [PATCH 09/18] removed globalOptions from NewDocsCommmand addition in main.go --- cmd/restic/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/restic/main.go b/cmd/restic/main.go index 85d91a7b4..392a062df 100644 --- a/cmd/restic/main.go +++ b/cmd/restic/main.go @@ -102,7 +102,7 @@ The full documentation can be found at https://restic.readthedocs.io/ . newStatsCommand(globalOptions), newTagCommand(globalOptions), newUnlockCommand(globalOptions), - newDocsCommand(globalOptions), + newDocsCommand(), newVersionCommand(globalOptions), ) From ed6d22bde8cdf21d976697ec533b7fbe0cc3b0f4 Mon Sep 17 00:00:00 2001 From: Turmaxx <88465473+Turmaxx@users.noreply.github.com> Date: Fri, 8 May 2026 17:40:51 +0300 Subject: [PATCH 10/18] changed args from main docs command and subcommands to a _ - hopefully this finally fixes the linter --- cmd/restic/cmd_docs.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/restic/cmd_docs.go b/cmd/restic/cmd_docs.go index 159024652..10771e7bd 100644 --- a/cmd/restic/cmd_docs.go +++ b/cmd/restic/cmd_docs.go @@ -28,7 +28,7 @@ func newDocsCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "docs", Short: "Opens the documentation in the default browser", - Run: func(_ *cobra.Command, args []string) { + Run: func(_ *cobra.Command, _ []string) { openDocs(ResticDocsURL, "user") }, } @@ -36,7 +36,7 @@ func newDocsCommand() *cobra.Command { cmd.AddCommand(&cobra.Command{ Use: "user", Short: "Show the user documentation", - Run: func(_ *cobra.Command, args []string) { + Run: func(_ *cobra.Command, _ []string) { openDocs(ResticDocsURL, "user") }, }) @@ -44,7 +44,7 @@ func newDocsCommand() *cobra.Command { cmd.AddCommand(&cobra.Command{ Use: "dev", Short: "Show the developer documentation", - Run: func(_ *cobra.Command, args []string) { + Run: func(_ *cobra.Command, _ []string) { openDocs(ResticDevDocsURL, "developer") }, }) From 9a2dbcee08276e542ef4509974adc1b93a4c441e Mon Sep 17 00:00:00 2001 From: Turmaxx <88465473+Turmaxx@users.noreply.github.com> Date: Fri, 8 May 2026 18:52:23 +0300 Subject: [PATCH 11/18] added GOOS as a runtime constant, updated openDocs by passing GOOS as a parameter this is to ensure platform consistency --- cmd/restic/cmd_docs.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cmd/restic/cmd_docs.go b/cmd/restic/cmd_docs.go index 10771e7bd..9a13ef7e3 100644 --- a/cmd/restic/cmd_docs.go +++ b/cmd/restic/cmd_docs.go @@ -14,6 +14,7 @@ import ( const ( ResticDocsURL string = "https://restic.readthedocs.io/en/stable" ResticDevDocsURL string = "https://restic.readthedocs.io/en/latest" + GOOS string = runtime.GOOS ) type execFn func(name string, arg ...string) *exec.Cmd @@ -29,7 +30,7 @@ func newDocsCommand() *cobra.Command { Use: "docs", Short: "Opens the documentation in the default browser", Run: func(_ *cobra.Command, _ []string) { - openDocs(ResticDocsURL, "user") + openDocs(GOOS, ResticDocsURL, "user") }, } @@ -37,7 +38,7 @@ func newDocsCommand() *cobra.Command { Use: "user", Short: "Show the user documentation", Run: func(_ *cobra.Command, _ []string) { - openDocs(ResticDocsURL, "user") + openDocs(GOOS, ResticDocsURL, "user") }, }) @@ -45,19 +46,19 @@ func newDocsCommand() *cobra.Command { Use: "dev", Short: "Show the developer documentation", Run: func(_ *cobra.Command, _ []string) { - openDocs(ResticDevDocsURL, "developer") + openDocs(GOOS, ResticDevDocsURL, "developer") }, }) return cmd } -func openDocs(url string, docType string) { +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 runtime.GOOS { + switch GOOS { case "linux": cmd = start("xdg-open", url) case "windows": From 6bc438ff7a527ccf18818478f94b71746d4d1a37 Mon Sep 17 00:00:00 2001 From: Turmaxx <88465473+Turmaxx@users.noreply.github.com> Date: Fri, 8 May 2026 18:55:03 +0300 Subject: [PATCH 12/18] rewrote TestOpenDocs in cmd_docs_test.go Included a comprehensive table-driven test suite that mocks system calls to verify correct cross-platform behavior for Linux, macOS and Windows without requiring a browser to run. --- cmd/restic/cmd_docs_test.go | 76 ++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/cmd/restic/cmd_docs_test.go b/cmd/restic/cmd_docs_test.go index 61c09888c..bd62f156c 100644 --- a/cmd/restic/cmd_docs_test.go +++ b/cmd/restic/cmd_docs_test.go @@ -40,32 +40,62 @@ func TestNewDocsCommand(t *testing.T) { } func TestOpenDocs(t *testing.T) { - // Redirect output to capture Printf - var buf bytes.Buffer - stdout = &buf - - // Mock the command execution - originalStart := start - start = func(name string, arg ...string) *exec.Cmd { - // Return a harmless 'echo' command to satisfy .Start() - return exec.Command("echo", arg...) + tests := []struct { + name string + goos string + url string + docType string + wantBin string + wantArg string + }{ + {"Linux User", "linux", ResticDocsURL, "user", "xdg-open", ResticDocsURL}, + {"Linux Dev", "linux", ResticDevDocsURL, "developer", "xdg-open", ResticDevDocsURL}, + {"Mac User", "darwin", ResticDocsURL, "user", "open", ResticDocsURL}, + {"Mac Dev", "darwin", ResticDevDocsURL, "developer", "open", ResticDevDocsURL}, + {"Windows User", "windows", ResticDocsURL, "user", "rundll32", "url.dll,FileProtocolHandler"}, + {"Windows Dev", "windows", ResticDevDocsURL, "developer", "rundll32", "url.dll,FileProtocolHandler"}, } - // Restore original global variables after test - defer func() { - stdout = os.Stdout - start = originalStart - }() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + stdout = &buf // Capture console output - testURL := "https://example.com" - openDocs(testURL, "user") - output := buf.String() + var capturedBin string + var capturedArgs []string - // Verify the console output contains the correct metadata - if !strings.Contains(output, "Opening the user documentation") { - t.Errorf("unexpected output message: %q", output) - } - if !strings.Contains(output, testURL) { - t.Errorf("output did not contain the correct URL: %q", output) + // 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) + } + }) } } From 6212d1cb4f2caf1ce57456d0b17c9510d2c1aeda Mon Sep 17 00:00:00 2001 From: Turmaxx <88465473+Turmaxx@users.noreply.github.com> Date: Mon, 18 May 2026 22:48:57 +0300 Subject: [PATCH 13/18] updated main.go for docs command - added a parameter for the newDocsCommand(globalOptions) - added a docs to neesPassword function's switch statement, it doesn't need a password btw. --- cmd/restic/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/restic/main.go b/cmd/restic/main.go index 392a062df..fed8539c8 100644 --- a/cmd/restic/main.go +++ b/cmd/restic/main.go @@ -102,7 +102,7 @@ The full documentation can be found at https://restic.readthedocs.io/ . newStatsCommand(globalOptions), newTagCommand(globalOptions), newUnlockCommand(globalOptions), - newDocsCommand(), + newDocsCommand(globalOptions), newVersionCommand(globalOptions), ) @@ -119,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 From c39f31a416b88b1441f02a8325c33703f2ab2421 Mon Sep 17 00:00:00 2001 From: Turmaxx <88465473+Turmaxx@users.noreply.github.com> Date: Tue, 19 May 2026 16:18:35 +0300 Subject: [PATCH 14/18] updated cmd_docs.go - restructed cmd_docs.go so that is uses regex to extract the current version of restic the user is running, docsURLForVersion extracts the version. - removed removed redundant strings but instead opted to concatinate strings using a base restic docs url string --- cmd/restic/cmd_docs.go | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/cmd/restic/cmd_docs.go b/cmd/restic/cmd_docs.go index 9a13ef7e3..42ef41707 100644 --- a/cmd/restic/cmd_docs.go +++ b/cmd/restic/cmd_docs.go @@ -6,15 +6,17 @@ import ( "log" "os" "os/exec" + "regexp" "runtime" "github.com/spf13/cobra" + + "github.com/restic/restic/internal/global" ) const ( - ResticDocsURL string = "https://restic.readthedocs.io/en/stable" - ResticDevDocsURL string = "https://restic.readthedocs.io/en/latest" - GOOS string = runtime.GOOS + GOOS string = runtime.GOOS + ResticURL string = "https://restic.readthedocs.io/en" ) type execFn func(name string, arg ...string) *exec.Cmd @@ -24,13 +26,15 @@ var ( start execFn = exec.Command ) -func newDocsCommand() *cobra.Command { +func newDocsCommand(globalOptions *global.Options) *cobra.Command { + _ = globalOptions - var cmd = &cobra.Command{ + cmd := &cobra.Command{ Use: "docs", Short: "Opens the documentation in the default browser", Run: func(_ *cobra.Command, _ []string) { - openDocs(GOOS, ResticDocsURL, "user") + docsURL := docsURLForVersion(global.Version) + openDocs(GOOS, docsURL, "user") }, } @@ -38,7 +42,8 @@ func newDocsCommand() *cobra.Command { Use: "user", Short: "Show the user documentation", Run: func(_ *cobra.Command, _ []string) { - openDocs(GOOS, ResticDocsURL, "user") + docsURL := fmt.Sprintf("%s/stable", ResticURL) + openDocs(GOOS, docsURL, "user") }, }) @@ -46,13 +51,33 @@ func newDocsCommand() *cobra.Command { Use: "dev", Short: "Show the developer documentation", Run: func(_ *cobra.Command, _ []string) { - openDocs(GOOS, ResticDevDocsURL, "developer") + docsURL := fmt.Sprintf("%s/latest", ResticURL) + openDocs(GOOS, docsURL, "developer") }, }) return cmd } +func docsURLForVersion(version string) string { + extractVersion := func(version string) string { + // match a semantic version at the start of the string, e.g. "0.18.1" or + // "0.18.1-dev (compiled manually)" -> capture "0.18.1" + versionRegex := regexp.MustCompile(`^(\d+\.\d+\.\d+)`) + matches := versionRegex.FindStringSubmatch(version) + if len(matches) == 2 { + return matches[1] + } + return "" + } + + if tag := extractVersion(version); tag != "" { + return fmt.Sprintf("%s/v%s", ResticURL, tag) + } + + return ResticURL +} + func openDocs(GOOS string, url string, docType string) { _, _ = fmt.Fprintf(stdout, "Opening the %s documentation at %s\n", docType, url) From 123ea0f7bd79b9cd9ba7c408adf8c90edbac4d5f Mon Sep 17 00:00:00 2001 From: Turmaxx <88465473+Turmaxx@users.noreply.github.com> Date: Tue, 19 May 2026 17:01:27 +0300 Subject: [PATCH 15/18] updated cmd_docs.go again had to change a couple of things, like updating the docsURLForVersion command, and removing the nested function inside since regexMustCompile is a memory sensitive operation. - rewrote docsURLForVersion as well --- cmd/restic/cmd_docs.go | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/cmd/restic/cmd_docs.go b/cmd/restic/cmd_docs.go index 42ef41707..48130d5ed 100644 --- a/cmd/restic/cmd_docs.go +++ b/cmd/restic/cmd_docs.go @@ -8,6 +8,7 @@ import ( "os/exec" "regexp" "runtime" + "strings" "github.com/spf13/cobra" @@ -22,8 +23,9 @@ const ( type execFn func(name string, arg ...string) *exec.Cmd var ( - stdout io.Writer = os.Stdout - start execFn = exec.Command + stdout io.Writer = os.Stdout + start execFn = exec.Command + versionRegex *regexp.Regexp = regexp.MustCompile(`^(\d+\.\d+\.\d+)`) ) func newDocsCommand(globalOptions *global.Options) *cobra.Command { @@ -60,22 +62,23 @@ func newDocsCommand(globalOptions *global.Options) *cobra.Command { } func docsURLForVersion(version string) string { - extractVersion := func(version string) string { - // match a semantic version at the start of the string, e.g. "0.18.1" or - // "0.18.1-dev (compiled manually)" -> capture "0.18.1" - versionRegex := regexp.MustCompile(`^(\d+\.\d+\.\d+)`) - matches := versionRegex.FindStringSubmatch(version) - if len(matches) == 2 { - return matches[1] - } - return "" + // 1. Safe default fallback for empty/unknown versions + if version == "" || version == "unknown" { + return fmt.Sprintf("%s/stable", ResticURL) } - if tag := extractVersion(version); tag != "" { - return fmt.Sprintf("%s/v%s", ResticURL, tag) + // 2. Route development / local compilation environments directly to bleeding edge docs + if strings.Contains(version, "dev") || strings.Contains(version, "compiled") { + return fmt.Sprintf("%s/latest", ResticURL) } - return ResticURL + // 3. 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", ResticURL, matches[1]) + } + + return fmt.Sprintf("%s/stable", ResticURL) } func openDocs(GOOS string, url string, docType string) { From 7fe5544bb1552961c370fa1d26df583728ee7632 Mon Sep 17 00:00:00 2001 From: Turmaxx <88465473+Turmaxx@users.noreply.github.com> Date: Tue, 19 May 2026 17:01:51 +0300 Subject: [PATCH 16/18] updated cmd_docs_test.go to be on par with cmd_docs_test.go - rewrite a changed the tests stuct for TestOpenDocs - added a test for TestDocsURLForVersion --- cmd/restic/cmd_docs_test.go | 97 ++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 7 deletions(-) diff --git a/cmd/restic/cmd_docs_test.go b/cmd/restic/cmd_docs_test.go index bd62f156c..2c61db74a 100644 --- a/cmd/restic/cmd_docs_test.go +++ b/cmd/restic/cmd_docs_test.go @@ -2,14 +2,17 @@ package main import ( "bytes" + "fmt" "os" "os/exec" "strings" "testing" + + "github.com/restic/restic/internal/global" ) func TestNewDocsCommand(t *testing.T) { - cmd := newDocsCommand() + cmd := newDocsCommand(&global.Options{}) if cmd.Use != "docs" { t.Errorf("expected command Use 'docs' got %q", cmd.Use) @@ -39,7 +42,82 @@ func TestNewDocsCommand(t *testing.T) { } } +func TestDocsURLForVersion(t *testing.T) { + // Dynamically build the expected URLs using the package's base constant + stableURL := fmt.Sprintf("%s/stable", ResticURL) + latestURL := fmt.Sprintf("%s/latest", ResticURL) + tagURL := func(tag string) string { + return fmt.Sprintf("%s/v%s", ResticURL, 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", ResticURL) + latestURL := fmt.Sprintf("%s/latest", ResticURL) + tagURL := func(tag string) string { + return fmt.Sprintf("%s/v%s", ResticURL, tag) + } + tests := []struct { name string goos string @@ -48,12 +126,17 @@ func TestOpenDocs(t *testing.T) { wantBin string wantArg string }{ - {"Linux User", "linux", ResticDocsURL, "user", "xdg-open", ResticDocsURL}, - {"Linux Dev", "linux", ResticDevDocsURL, "developer", "xdg-open", ResticDevDocsURL}, - {"Mac User", "darwin", ResticDocsURL, "user", "open", ResticDocsURL}, - {"Mac Dev", "darwin", ResticDevDocsURL, "developer", "open", ResticDevDocsURL}, - {"Windows User", "windows", ResticDocsURL, "user", "rundll32", "url.dll,FileProtocolHandler"}, - {"Windows Dev", "windows", ResticDevDocsURL, "developer", "rundll32", "url.dll,FileProtocolHandler"}, + {"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}, + + {"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}, + + {"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 { From 6ee401a5c9e978912a20caac0fced19e9d514243 Mon Sep 17 00:00:00 2001 From: Turmaxx <88465473+Turmaxx@users.noreply.github.com> Date: Wed, 20 May 2026 20:23:43 +0300 Subject: [PATCH 17/18] updated cmd_docs.go - just moved somethings around. - added comments and better comments I hope. - changed ResticURL to ResticDocsURL --- cmd/restic/cmd_docs.go | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/cmd/restic/cmd_docs.go b/cmd/restic/cmd_docs.go index 48130d5ed..938c4e6a8 100644 --- a/cmd/restic/cmd_docs.go +++ b/cmd/restic/cmd_docs.go @@ -16,8 +16,8 @@ import ( ) const ( - GOOS string = runtime.GOOS - ResticURL string = "https://restic.readthedocs.io/en" + GOOS string = runtime.GOOS + ResticDocsURL string = "https://restic.readthedocs.io/en" ) type execFn func(name string, arg ...string) *exec.Cmd @@ -28,6 +28,9 @@ var ( 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 @@ -44,7 +47,7 @@ func newDocsCommand(globalOptions *global.Options) *cobra.Command { Use: "user", Short: "Show the user documentation", Run: func(_ *cobra.Command, _ []string) { - docsURL := fmt.Sprintf("%s/stable", ResticURL) + docsURL := fmt.Sprintf("%s/stable", ResticDocsURL) openDocs(GOOS, docsURL, "user") }, }) @@ -53,7 +56,7 @@ func newDocsCommand(globalOptions *global.Options) *cobra.Command { Use: "dev", Short: "Show the developer documentation", Run: func(_ *cobra.Command, _ []string) { - docsURL := fmt.Sprintf("%s/latest", ResticURL) + docsURL := fmt.Sprintf("%s/latest", ResticDocsURL) openDocs(GOOS, docsURL, "developer") }, }) @@ -61,26 +64,31 @@ func newDocsCommand(globalOptions *global.Options) *cobra.Command { 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 { - // 1. Safe default fallback for empty/unknown versions + // Safe default fallback for empty/unknown versions if version == "" || version == "unknown" { - return fmt.Sprintf("%s/stable", ResticURL) + return fmt.Sprintf("%s/stable", ResticDocsURL) } - // 2. Route development / local compilation environments directly to bleeding edge docs + // 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", ResticURL) + return fmt.Sprintf("%s/latest", ResticDocsURL) } - // 3. Match strict tag releases (e.g., exact matches like "0.18.1") + // 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", ResticURL, matches[1]) + return fmt.Sprintf("%s/v%s", ResticDocsURL, matches[1]) } - return fmt.Sprintf("%s/stable", ResticURL) + // 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) @@ -98,6 +106,6 @@ func openDocs(GOOS string, url string, docType string) { } if err := cmd.Start(); err != nil { - log.Fatalf("Failed to open brower: %v", err) + log.Fatalf("Failed to open browser: %v", err) } } From f1f1d44e05cc146784e9bf189a6492e32cddb69c Mon Sep 17 00:00:00 2001 From: Turmaxx <88465473+Turmaxx@users.noreply.github.com> Date: Wed, 20 May 2026 20:24:02 +0300 Subject: [PATCH 18/18] updated cmd_docs.test.go to reflect changed made in cmd_docs.go as well. --- cmd/restic/cmd_docs_test.go | 63 ++++++++++--------------------------- 1 file changed, 17 insertions(+), 46 deletions(-) diff --git a/cmd/restic/cmd_docs_test.go b/cmd/restic/cmd_docs_test.go index 2c61db74a..f28b954fc 100644 --- a/cmd/restic/cmd_docs_test.go +++ b/cmd/restic/cmd_docs_test.go @@ -44,10 +44,10 @@ func TestNewDocsCommand(t *testing.T) { func TestDocsURLForVersion(t *testing.T) { // Dynamically build the expected URLs using the package's base constant - stableURL := fmt.Sprintf("%s/stable", ResticURL) - latestURL := fmt.Sprintf("%s/latest", ResticURL) + stableURL := fmt.Sprintf("%s/stable", ResticDocsURL) + latestURL := fmt.Sprintf("%s/latest", ResticDocsURL) tagURL := func(tag string) string { - return fmt.Sprintf("%s/v%s", ResticURL, tag) + return fmt.Sprintf("%s/v%s", ResticDocsURL, tag) } tests := []struct { @@ -56,50 +56,18 @@ func TestDocsURLForVersion(t *testing.T) { 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"), - }, + {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, - }, + {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, - }, + {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 { @@ -112,10 +80,10 @@ func TestDocsURLForVersion(t *testing.T) { } func TestOpenDocs(t *testing.T) { - stableURL := fmt.Sprintf("%s/stable", ResticURL) - latestURL := fmt.Sprintf("%s/latest", ResticURL) + stableURL := fmt.Sprintf("%s/stable", ResticDocsURL) + latestURL := fmt.Sprintf("%s/latest", ResticDocsURL) tagURL := func(tag string) string { - return fmt.Sprintf("%s/v%s", ResticURL, tag) + return fmt.Sprintf("%s/v%s", ResticDocsURL, tag) } tests := []struct { @@ -126,14 +94,17 @@ func TestOpenDocs(t *testing.T) { 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"},