66 "net/http"
77 "os"
88 "path/filepath"
9- "strings"
109
1110 "github.com/cli/cli/v2/api"
1211 "github.com/cli/cli/v2/context"
@@ -16,14 +15,13 @@ import (
1615 "github.com/cli/cli/v2/pkg/cmd/extension"
1716 "github.com/cli/cli/v2/pkg/cmdutil"
1817 "github.com/cli/cli/v2/pkg/iostreams"
19- "github.com/cli/safeexec"
2018)
2119
2220func New (appVersion string ) * cmdutil.Factory {
2321 f := & cmdutil.Factory {
24- Config : configFunc (), // No factory dependencies
25- Branch : branchFunc (), // No factory dependencies
26- Executable : executable (), // No factory dependencies
22+ Config : configFunc (), // No factory dependencies
23+ Branch : branchFunc (), // No factory dependencies
24+ Executable : executable ("gh" ), // No factory dependencies
2725
2826 ExtensionManager : extension .NewManager (),
2927 }
@@ -116,24 +114,50 @@ func browserLauncher(f *cmdutil.Factory) string {
116114 return os .Getenv ("BROWSER" )
117115}
118116
119- func executable () string {
120- exe , _ := os .Executable ()
117+ // Finds the location of the executable for the current process as it's found in PATH, respecting symlinks.
118+ // If the process couldn't determine its location, return fallbackName. If the executable wasn't found in
119+ // PATH, return the absolute location to the program.
120+ //
121+ // The idea is that the result of this function is callable in the future and refers to the same
122+ // installation of gh, even across upgrades. This is needed primarily for Homebrew, which installs software
123+ // under a location such as `/usr/local/Cellar/gh/1.13.1/bin/gh` and symlinks it from `/usr/local/bin/gh`.
124+ // When the version is upgraded, Homebrew will often delete older versions, but keep the symlink. Because of
125+ // this, we want to refer to the `gh` binary as `/usr/local/bin/gh` and not as its internal Homebrew
126+ // location.
127+ //
128+ // None of this would be needed if we could just refer to GitHub CLI as `gh`, i.e. without using an absolute
129+ // path. However, for some reason Homebrew does not include `/usr/local/bin` in PATH when it invokes git
130+ // commands to update its taps. If `gh` (no path) is being used as git credential helper, as set up by `gh
131+ // auth login`, running `brew update` will print out authentication errors as git is unable to locate
132+ // Homebrew-installed `gh`.
133+ func executable (fallbackName string ) string {
134+ exe , err := os .Executable ()
135+ if err != nil {
136+ return fallbackName
137+ }
121138
139+ base := filepath .Base (exe )
122140 path := os .Getenv ("PATH" )
123141 for _ , dir := range filepath .SplitList (path ) {
124- if strings .HasSuffix (dir , "gh" ) {
125- if dir == exe {
126- return dir
127- }
128- if symlink , _ := os .Readlink (dir ); symlink == exe {
129- return dir
142+ p , err := filepath .Abs (filepath .Join (dir , base ))
143+ if err != nil {
144+ continue
145+ }
146+ f , err := os .Stat (p )
147+ if err != nil {
148+ continue
149+ }
150+
151+ if p == exe {
152+ return p
153+ } else if f .Mode ()& os .ModeSymlink != 0 {
154+ if t , err := os .Readlink (p ); err == nil && t == exe {
155+ return p
130156 }
131157 }
132158 }
133159
134- gh , _ := safeexec .LookPath ("gh" )
135-
136- return gh
160+ return exe
137161}
138162
139163func configFunc () func () (config.Config , error ) {
0 commit comments