Repository: jesseduffield/lazygit (language: go)

Issue:
Clicking Donate or Ask Question crashes if the command used by OpenLink is not found
**Describe the bug**

Clicking "Donate" or "Ask Question" crashes the app if the command used to open links does not exist. For example, clicking the Donate button in `lazygit` inside a devcontainer gives this error:

```
*errors.errorString bash: line 1: xdg-open: command not found
```

<details>
<summary>Full stack trace</summary>

```
@simonwhitaker ➜ /workspaces/lazygit (master) $ go run main.go
2023/08/05 16:57:27 An error occurred! Please create an issue at: https://github.com/jesseduffield/lazygit/issues

*errors.errorString bash: line 1: xdg-open: command not found

/workspaces/lazygit/pkg/commands/oscommands/cmd_obj_runner.go:200 (0x97a3bb)
        sanitisedCommandOutput: return outputString, errors.New(outputString)
/workspaces/lazygit/pkg/commands/oscommands/cmd_obj_runner.go:107 (0x97940c)
        (*cmdObjRunner).RunWithOutputAux: output, err := sanitisedCommandOutput(cmdObj.GetCmd().CombinedOutput())
/workspaces/lazygit/pkg/commands/oscommands/cmd_obj_runner.go:45 (0x978a85)
        (*cmdObjRunner).Run: _, err := self.RunWithOutputAux(cmdObj)
/workspaces/lazygit/pkg/commands/oscommands/cmd_obj.go:190 (0x977c4f)
        (*CmdObj).Run: return self.runner.Run(self)
/workspaces/lazygit/pkg/commands/oscommands/os.go:110 (0x97ca57)
        (*OSCommand).OpenLink: return c.Cmd.NewShell(command).Run()
/workspaces/lazygit/pkg/gui/information_panel.go:44 (0xaf6c86)
        (*Gui).handleInfoClick: return gui.os.OpenLink(constants.Links.Donate)
/workspaces/lazygit/pkg/gui/keybindings.go:387 (0xafabcf)
        (*Gui).SetKeybinding.func1: return binding.Handler()
/workspaces/lazygit/pkg/gui/keybindings.go:373 (0xafaafb)
        (*Gui).SetKeybinding.func2: return f()
/workspaces/lazygit/vendor/github.com/jesseduffield/gocui/gui.go:1424 (0x764e59)
        (*Gui).execKeybinding: if err := kb.handler(g, v); err != nil {
/workspaces/lazygit/vendor/github.com/jesseduffield/gocui/gui.go:1392 (0x764cf1)
        (*Gui).execKeybindings: return g.execKeybinding(v, kb)
/workspaces/lazygit/vendor/github.com/jesseduffield/gocui/gui.go:1300 (0x7644f1)
        (*Gui).onKey: if _, err := g.execKeybindings(v, ev); err != nil {
/workspaces/lazygit/vendor/github.com/jesseduffield/gocui/gui.go:798 (0x76288a)
        (*Gui).handleEvent: return g.onKey(ev)
/workspaces/lazygit/vendor/github.com/jesseduffield/gocui/gui.go:752 (0x76240b)
        (*Gui).processEvent: if err := g.handleEvent(&ev); err != nil {
/workspaces/lazygit/vendor/github.com/jesseduffield/gocui/gui.go:739 (0x762125)
        (*Gui).MainLoop: err := g.processEvent()
/workspaces/lazygit/pkg/gui/gui.go:669 (0xaf3345)
        (*Gui).Run: return gui.g.MainLoop()
/workspaces/lazygit/pkg/gui/gui.go:675 (0xaf386c)
        (*Gui).RunAndHandleError.func1: if err := gui.Run(startArgs); err != nil {
/workspaces/lazygit/pkg/utils/utils.go:108 (0x82dea7)
        SafeWithError: err := f()
/workspaces/lazygit/pkg/gui/gui.go:674 (0xaf37b4)
        (*Gui).RunAndHandleError: return utils.SafeWithError(func() error {
/workspaces/lazygit/pkg/app/app.go:263 (0xb1daa9)
        Run: err := app.Gui.RunAndHandleError(startArgs)
/workspaces/lazygit/pkg/app/app.go:48 (0xb1da3e)
        Run: err = app.Run(startArgs)
/workspaces/lazygit/pkg/app/entry_point.go:154 (0xb1fd06)
        Start: Run(appConfig, common, appTypes.NewStartArgs(cliArgs.FilterPath, parsedGitArg, integrationTest))
/workspaces/lazygit/main.go:23 (0xb2159e)
        main: app.Start(ldFlagsBuildInfo, nil)
/usr/local/go/src/runtime/internal/atomic/types.go:194 (0x4397a7)
        (*Uint32).Load: return Load(&u.value)
/usr/local/go/src/runtime/asm_amd64.s:1598 (0x469b41)
        goexit: BYTE    $0x90   // NOP
exit status 1
```
</details>

**To Reproduce**
Steps to reproduce the behavior:

One easy way to reproduce this by using a devcontainer, since the image used by the devcontainer doesn't have the `xdg-open` command.

1. Go to https://github.com/jesseduffield/lazygit/tree/master
2. Click Code > Codespaces and create a codespace
3. Once the codespace is running, run `go run main.go` in the Terminal window, then click on the Donate or Ask Question links.

Alternatively, on any system:

```
# Configure the openLink command to call `exit 42`
echo 'os: { openLink: "exit 42" }' > issue-2882-repro.yml

# Run lazygit using our new, deliberately broken config
lazygit --use-config-file issue-2882-repro.yml
```

Then click on Donate or Ask Question, and lazygit will crash:

```console
$ lazygit --use-config-file issue-2882-repro.yml
2023/08/05 18:16:57 An error occurred! Please create an issue at: https://github.com/jesseduffield/lazygit/issues

*exec.ExitError exit status 42
```

**Expected behavior**

An error message is displayed

**Screenshots**

<img width="1624" alt="Screenshot 2023-08-05 at 5 57 12 pm" src="https://github.com/jesseduffield/lazygit/assets/116432/670cda81-181d-4a7d-b799-64202730b7b7">

**Version info:**

```console
$ go run main.go --version
commit=, build date=, build source=unknown, version=unversioned, os=linux, arch=amd64, git version=2.41.0
$ git rev-parse HEAD
7847203d6e8e89d4f5dc52ac167a219b0bd69d5f
```

**Additional context**
Add any other context about the problem here.

**Note:** please try updating to the latest version or [manually building](https://github.com/jesseduffield/lazygit/#manual) the latest `master` to see if the issue still occurs.

<!--
If you want to try and debug this issue yourself, you can run `lazygit --debug` in one terminal panel and `lazygit --logs` in another to view the logs.
-->



Discussion / hints:

Fix patch:
diff --git a/pkg/gui/information_panel.go b/pkg/gui/information_panel.go
index 5de212b4d07..00867fb9254 100644
--- a/pkg/gui/information_panel.go
+++ b/pkg/gui/information_panel.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/jesseduffield/lazygit/pkg/constants"
 	"github.com/jesseduffield/lazygit/pkg/gui/style"
+	"github.com/jesseduffield/lazygit/pkg/utils"
 	"github.com/mattn/go-runewidth"
 )
 
@@ -39,11 +40,28 @@ func (gui *Gui) handleInfoClick() error {
 		return activeMode.Reset()
 	}
 
+	var title, url string
+
 	// if we're not in an active mode we show the donate button
 	if cx <= runewidth.StringWidth(gui.c.Tr.Donate) {
-		return gui.os.OpenLink(constants.Links.Donate)
+		url = constants.Links.Donate
+		title = gui.c.Tr.Donate
 	} else if cx <= runewidth.StringWidth(gui.c.Tr.Donate)+1+runewidth.StringWidth(gui.c.Tr.AskQuestion) {
-		return gui.os.OpenLink(constants.Links.Discussions)
+		url = constants.Links.Discussions
+		title = gui.c.Tr.AskQuestion
+	}
+	err := gui.os.OpenLink(url)
+	if err != nil {
+		// Opening the link via the OS failed for some reason. (For example, this
+		// can happen if the `os.openLink` config key references a command that
+		// doesn't exist, or that errors when called.)
+		//
+		// In that case, rather than crash the app, fall back to simply showing a
+		// dialog asking the user to visit the URL.
+		placeholders := map[string]string{"url": url}
+		message := utils.ResolvePlaceholderString(gui.c.Tr.PleaseGoToURL, placeholders)
+		return gui.c.Alert(title, message)
 	}
+
 	return nil
 }
diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go
index 6dee0046766..4fb86573fa0 100644
--- a/pkg/i18n/english.go
+++ b/pkg/i18n/english.go
@@ -585,6 +585,7 @@ type TranslationSet struct {
 	MarkAsBaseCommit                    string
 	MarkAsBaseCommitTooltip             string
 	MarkedCommitMarker                  string
+	PleaseGoToURL                       string
 	Actions                             Actions
 	Bisect                              Bisect
 	Log                                 Log
@@ -1347,6 +1348,7 @@ func EnglishTranslationSet() TranslationSet {
 		MarkAsBaseCommit:                    "Mark commit as base commit for rebase",
 		MarkAsBaseCommitTooltip:             "Select a base commit for the next rebase; this will effectively perform a 'git rebase --onto'.",
 		MarkedCommitMarker:                  "↑↑↑ Will rebase from here ↑↑↑",
+		PleaseGoToURL:                       "Please go to {{.url}}",
 		Actions: Actions{
 			// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
 			CheckoutCommit:                    "Checkout commit",


Test patch:
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index e3ef9bebc07..8c9527bec7c 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -233,6 +233,7 @@ var tests = []*components.IntegrationTest{
 	ui.Accordion,
 	ui.DoublePopup,
 	ui.EmptyMenu,
+	ui.OpenLinkFailure,
 	ui.SwitchTabFromMenu,
 	undo.UndoCheckoutAndDrop,
 	undo.UndoDrop,
diff --git a/pkg/integration/tests/ui/open_link_failure.go b/pkg/integration/tests/ui/open_link_failure.go
new file mode 100644
index 00000000000..c7a223555fc
--- /dev/null
+++ b/pkg/integration/tests/ui/open_link_failure.go
@@ -0,0 +1,24 @@
+package ui
+
+import (
+	"github.com/jesseduffield/lazygit/pkg/config"
+	. "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var OpenLinkFailure = NewIntegrationTest(NewIntegrationTestArgs{
+	Description:  "When opening links via the OS fails, show a dialog instead.",
+	ExtraCmdArgs: []string{},
+	Skip:         false,
+	SetupConfig: func(config *config.AppConfig) {
+		config.UserConfig.OS.OpenLink = "exit 42"
+	},
+	SetupRepo: func(shell *Shell) {},
+	Run: func(t *TestDriver, keys config.KeybindingConfig) {
+		t.Views().Information().Click(0, 0)
+
+		t.ExpectPopup().Confirmation().
+			Title(Equals("Donate")).
+			Content(Equals("Please go to https://github.com/sponsors/jesseduffield")).
+			Confirm()
+	},
+})