Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add gh.ExecInteractive() #115

Merged
merged 8 commits into from
Apr 5, 2023
Merged

Conversation

stemar94
Copy link
Contributor

For #114

@stemar94
Copy link
Contributor Author

Could also be nice to not wrap the error, if it's a proper exec.ExitError.

@samcoe
Copy link
Contributor

samcoe commented Mar 22, 2023

@stemar94 This is looking good so far. What would be the reason not to wrap an exec.ExitError? Go provides mechanisms to unwrap it if the user wants to.

@samcoe samcoe self-assigned this Mar 22, 2023
@samcoe samcoe self-requested a review March 22, 2023 23:30
Copy link
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this!

gh.go Outdated
func ExecInteractive(args ...string) (err error) {
path, err := path()
if err != nil {
err = fmt.Errorf("could not find gh executable in PATH. error: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that this basically copies the error message from Exec(), but we could maybe improve this error message in both functions.

The original error from exec.LookPath (which safeexec uses under the hood) will be something like:

exec: "gh": executable file not found in $PATH

After being wrapped, the full message will be:

could not find gh executable in PATH. error: exec: "gh": executable file not found in $PATH

I don't think the extra explanation added by wrapping adds any value here. I'd vote to avoid wrapping this error message altogether (and in the Exec() function too).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

gh.go Outdated
}
err = cmd.Run()
if err != nil {
err = fmt.Errorf("failed to run gh. error: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind that the original ExitError is wrapped here. The extra context makes it clear that it was the gh command that failed, and not code from the extension itself. Perhaps we can just make it a lighter wrapper:

Suggested change
err = fmt.Errorf("failed to run gh. error: %w", err)
err = fmt.Errorf("gh: %w", err)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went for fmt.Errorf("gh execution failed: %w", err) for both Execs.

gh.go Outdated
@@ -34,6 +34,16 @@ func Exec(args ...string) (stdOut, stdErr bytes.Buffer, err error) {
return run(path, nil, args...)
}

// Exec gh command interactively with provided arguments.
func ExecInteractive(args ...string) (err error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is meant to spawn a potentially longer-running process, this function could accept a context.Context instance so that the caller has an option to kill the gh process when it needs to.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, also added a test.

@stemar94 stemar94 force-pushed the feat/execInteractive branch from 8917d2a to 80cd12a Compare March 27, 2023 21:13
@stemar94
Copy link
Contributor Author

stemar94 commented Mar 27, 2023

@stemar94 This is looking good so far. What would be the reason not to wrap an exec.ExitError? Go provides mechanisms to unwrap it if the user wants to.

I like for the ExecInteractive to happen transparently, where I like to hand over responsibility to the actual gh.
So it would be nice, if I could easily forward the exit code aswell.

Without unwrapping:

if execErr := gh.ExecInteractive(); execErr != nil {
  if exitErr, ok := execErr.(*exec.ExitError); ok {
    os.Exit(exitErr.ExitCode())
  }
}

With unwrapping:

if err := gh.ExecInteractive(); err != nil {
  if execErr := errors.Unwrap(err); execErr != nil {
    if exiterr, ok := execErr.(*exec.ExitError); ok {
      os.Exit(exiterr.ExitCode())
    }
  }
}

Also what does the one layer of wrapping give us in value?

@stemar94 stemar94 force-pushed the feat/execInteractive branch from 80cd12a to b964df6 Compare March 27, 2023 21:32
@stemar94 stemar94 requested review from mislav and removed request for samcoe March 27, 2023 21:32
@stemar94 stemar94 force-pushed the feat/execInteractive branch from b964df6 to ec48e42 Compare March 27, 2023 21:35
@mislav
Copy link
Contributor

mislav commented Mar 29, 2023

With unwrapping:

if err := gh.ExecInteractive(); err != nil {
  if execErr := errors.Unwrap(err); execErr != nil {
    if exiterr, ok := execErr.(*exec.ExitError); ok {
      os.Exit(exiterr.ExitCode())
    }
  }
}

Go callers that handle specific error types should neither assume that the error value directly returned is a specific type nor manually call Unwrap to get to the desired type. Instead, they should use errors.As() to match a specific type:

if err := gh.ExecInteractive(); err != nil {
  var exitError *exec.ExitError
  if errors.As(err, &exitError) {
    os.Exit(exitError.ExitCode())
  }
}

This way, it doesn't matter whether err is literally a *exec.ExitError or a wrapped *exec.ExitError. It will just work.

Copy link
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good! Feel free to mark your PR as "ready" when you get the test right so we can proceed to the final review. Thank you

gh.go Outdated Show resolved Hide resolved
@mislav mislav marked this pull request as ready for review April 5, 2023 17:26
Copy link
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed some tweaks, including a new ExecContext function (to complement Exec) and added support for the GH_PATH environment variable that the parent gh process can set to inform extensions about the absolute location of gh on disk, in case gh isn't in PATH.

@mislav mislav merged commit 026e976 into cli:trunk Apr 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants