Skip to content

Commit ed87cf7

Browse files
committed
Merge branch 'cli:trunk' into first-browse-pull
2 parents 314b8e8 + 75abeb1 commit ed87cf7

33 files changed

+1091
-141
lines changed

.github/CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Run the new binary as:
3636

3737
Run tests with: `go test ./...`
3838

39-
See [project layout documentation](../project-layout.md) for information on where to find specific source files.
39+
See [project layout documentation](../docs/project-layout.md) for information on where to find specific source files.
4040

4141
## Submitting a pull request
4242

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,7 @@ For more information and distro-specific instructions, see the [Linux installati
4949

5050
| Install: | Upgrade: |
5151
| ------------------- | --------------------|
52-
| `winget install gh` | `winget install gh` |
53-
54-
<i>WinGet does not have a specialized `upgrade` command yet, but the `install` command should work for upgrading to a newer version of GitHub CLI.</i>
52+
| `winget install gh` | `winget upgrade gh` |
5553

5654
#### scoop
5755

api/export_pr.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,20 @@ func (pr *PullRequest) ExportData(fields []string) *map[string]interface{} {
8080
case "reviewRequests":
8181
requests := make([]interface{}, 0, len(pr.ReviewRequests.Nodes))
8282
for _, req := range pr.ReviewRequests.Nodes {
83-
if req.RequestedReviewer.TypeName == "" {
84-
continue
83+
r := req.RequestedReviewer
84+
switch r.TypeName {
85+
case "User":
86+
requests = append(requests, map[string]string{
87+
"__typename": r.TypeName,
88+
"login": r.Login,
89+
})
90+
case "Team":
91+
requests = append(requests, map[string]string{
92+
"__typename": r.TypeName,
93+
"name": r.Name,
94+
"slug": r.LoginOrSlug(),
95+
})
8596
}
86-
requests = append(requests, req.RequestedReviewer)
8797
}
8898
data[f] = &requests
8999
default:

api/queries_pr.go

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -150,28 +150,33 @@ type PullRequestFile struct {
150150

151151
type ReviewRequests struct {
152152
Nodes []struct {
153-
RequestedReviewer struct {
154-
TypeName string `json:"__typename"`
155-
Login string `json:"login"`
156-
Name string `json:"name"`
157-
Slug string `json:"slug"`
158-
Organization struct {
159-
Login string `json:"login"`
160-
}
161-
}
153+
RequestedReviewer RequestedReviewer
154+
}
155+
}
156+
157+
type RequestedReviewer struct {
158+
TypeName string `json:"__typename"`
159+
Login string `json:"login"`
160+
Name string `json:"name"`
161+
Slug string `json:"slug"`
162+
Organization struct {
163+
Login string `json:"login"`
164+
} `json:"organization"`
165+
}
166+
167+
func (r RequestedReviewer) LoginOrSlug() string {
168+
if r.TypeName == teamTypeName {
169+
return fmt.Sprintf("%s/%s", r.Organization.Login, r.Slug)
162170
}
171+
return r.Login
163172
}
164173

165174
const teamTypeName = "Team"
166175

167176
func (r ReviewRequests) Logins() []string {
168177
logins := make([]string, len(r.Nodes))
169-
for i, a := range r.Nodes {
170-
if a.RequestedReviewer.TypeName == teamTypeName {
171-
logins[i] = fmt.Sprintf("%s/%s", a.RequestedReviewer.Organization.Login, a.RequestedReviewer.Slug)
172-
} else {
173-
logins[i] = a.RequestedReviewer.Login
174-
}
178+
for i, r := range r.Nodes {
179+
logins[i] = r.RequestedReviewer.LoginOrSlug()
175180
}
176181
return logins
177182
}

cmd/gh/main.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/cli/cli/internal/run"
2222
"github.com/cli/cli/internal/update"
2323
"github.com/cli/cli/pkg/cmd/alias/expand"
24+
"github.com/cli/cli/pkg/cmd/extensions"
2425
"github.com/cli/cli/pkg/cmd/factory"
2526
"github.com/cli/cli/pkg/cmd/root"
2627
"github.com/cli/cli/pkg/cmdutil"
@@ -140,15 +141,27 @@ func mainRun() exitCode {
140141

141142
err = preparedCmd.Run()
142143
if err != nil {
143-
if ee, ok := err.(*exec.ExitError); ok {
144-
return exitCode(ee.ExitCode())
144+
var execError *exec.ExitError
145+
if errors.As(err, &execError) {
146+
return exitCode(execError.ExitCode())
145147
}
146-
147148
fmt.Fprintf(stderr, "failed to run external command: %s", err)
148149
return exitError
149150
}
150151

151152
return exitOK
153+
} else if c, _, err := rootCmd.Traverse(expandedArgs); err == nil && c == rootCmd && len(expandedArgs) > 0 {
154+
extensionManager := extensions.NewManager()
155+
if found, err := extensionManager.Dispatch(expandedArgs, os.Stdin, os.Stdout, os.Stderr); err != nil {
156+
var execError *exec.ExitError
157+
if errors.As(err, &execError) {
158+
return exitCode(execError.ExitCode())
159+
}
160+
fmt.Fprintf(stderr, "failed to run extension: %s", err)
161+
return exitError
162+
} else if found {
163+
return exitOK
164+
}
152165
}
153166
}
154167

@@ -264,7 +277,7 @@ func checkForUpdate(currentVersion string) (*update.ReleaseInfo, error) {
264277
}
265278

266279
repo := updaterEnabled
267-
stateFilePath := filepath.Join(config.ConfigDir(), "state.yml")
280+
stateFilePath := filepath.Join(config.StateDir(), "state.yml")
268281
return update.CheckForUpdate(client, stateFilePath, repo, currentVersion)
269282
}
270283

docs/project-layout.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ and talk through which code gets run in order.
7575
This task might be tricky. Typically, gh commands do things like look up information from the git repository
7676
in the current directory, query the GitHub API, scan the user's `~/.ssh/config` file, clone or fetch git
7777
repositories, etc. Naturally, none of these things should ever happen for real when running tests, unless
78-
you are sure that any filesystem operations are stricly scoped to a location made for and maintained by the
78+
you are sure that any filesystem operations are strictly scoped to a location made for and maintained by the
7979
test itself. To avoid actually running things like making real API requests or shelling out to `git`
8080
commands, we stub them. You should look at how that's done within some existing tests.
8181

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/cli/oauth v0.8.0
1212
github.com/cli/safeexec v1.0.0
1313
github.com/cpuguy83/go-md2man/v2 v2.0.0
14+
github.com/creack/pty v1.1.13
1415
github.com/gabriel-vasile/mimetype v1.1.2
1516
github.com/google/go-cmp v0.5.2
1617
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
6262
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
6363
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
6464
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
65+
github.com/creack/pty v1.1.13 h1:rTPnd/xocYRjutMfqide2zle1u96upp1gm6eUHKi7us=
66+
github.com/creack/pty v1.1.13/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
6567
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
6668
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
6769
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

internal/config/config_file.go

Lines changed: 102 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import (
1616
const (
1717
GH_CONFIG_DIR = "GH_CONFIG_DIR"
1818
XDG_CONFIG_HOME = "XDG_CONFIG_HOME"
19+
XDG_STATE_HOME = "XDG_STATE_HOME"
20+
XDG_DATA_HOME = "XDG_DATA_HOME"
1921
APP_DATA = "AppData"
22+
LOCAL_APP_DATA = "LocalAppData"
2023
)
2124

2225
// Config path precedence
@@ -38,41 +41,128 @@ func ConfigDir() string {
3841
}
3942

4043
// If the path does not exist try migrating config from default paths
41-
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
42-
autoMigrateConfigDir(path)
44+
if !dirExists(path) {
45+
_ = autoMigrateConfigDir(path)
4346
}
4447

4548
return path
4649
}
4750

51+
// State path precedence
52+
// 1. XDG_CONFIG_HOME
53+
// 2. LocalAppData (windows only)
54+
// 3. HOME
55+
func StateDir() string {
56+
var path string
57+
if a := os.Getenv(XDG_STATE_HOME); a != "" {
58+
path = filepath.Join(a, "gh")
59+
} else if b := os.Getenv(LOCAL_APP_DATA); runtime.GOOS == "windows" && b != "" {
60+
path = filepath.Join(b, "GitHub CLI")
61+
} else {
62+
c, _ := os.UserHomeDir()
63+
path = filepath.Join(c, ".local", "state", "gh")
64+
}
65+
66+
// If the path does not exist try migrating state from default paths
67+
if !dirExists(path) {
68+
_ = autoMigrateStateDir(path)
69+
}
70+
71+
return path
72+
}
73+
74+
// Data path precedence
75+
// 1. XDG_DATA_HOME
76+
// 2. LocalAppData (windows only)
77+
// 3. HOME
78+
func DataDir() string {
79+
var path string
80+
if a := os.Getenv(XDG_DATA_HOME); a != "" {
81+
path = filepath.Join(a, "gh")
82+
} else if b := os.Getenv(LOCAL_APP_DATA); runtime.GOOS == "windows" && b != "" {
83+
path = filepath.Join(b, "GitHub CLI")
84+
} else {
85+
c, _ := os.UserHomeDir()
86+
path = filepath.Join(c, ".local", "share", "gh")
87+
}
88+
89+
return path
90+
}
91+
92+
var errSamePath = errors.New("same path")
93+
var errNotExist = errors.New("not exist")
94+
4895
// Check default paths (os.UserHomeDir, and homedir.Dir) for existing configs
4996
// If configs exist then move them to newPath
5097
// TODO: Remove support for homedir.Dir location in v2
51-
func autoMigrateConfigDir(newPath string) {
98+
func autoMigrateConfigDir(newPath string) error {
5299
path, err := os.UserHomeDir()
53100
if oldPath := filepath.Join(path, ".config", "gh"); err == nil && dirExists(oldPath) {
54-
migrateConfigDir(oldPath, newPath)
55-
return
101+
return migrateDir(oldPath, newPath)
56102
}
57103

58104
path, err = homedir.Dir()
59105
if oldPath := filepath.Join(path, ".config", "gh"); err == nil && dirExists(oldPath) {
60-
migrateConfigDir(oldPath, newPath)
106+
return migrateDir(oldPath, newPath)
61107
}
108+
109+
return errNotExist
62110
}
63111

64-
func dirExists(path string) bool {
65-
f, err := os.Stat(path)
66-
return err == nil && f.IsDir()
112+
// Check default paths (os.UserHomeDir, and homedir.Dir) for existing state file (state.yml)
113+
// If state file exist then move it to newPath
114+
// TODO: Remove support for homedir.Dir location in v2
115+
func autoMigrateStateDir(newPath string) error {
116+
path, err := os.UserHomeDir()
117+
if oldPath := filepath.Join(path, ".config", "gh"); err == nil && dirExists(oldPath) {
118+
return migrateFile(oldPath, newPath, "state.yml")
119+
}
120+
121+
path, err = homedir.Dir()
122+
if oldPath := filepath.Join(path, ".config", "gh"); err == nil && dirExists(oldPath) {
123+
return migrateFile(oldPath, newPath, "state.yml")
124+
}
125+
126+
return errNotExist
127+
}
128+
129+
func migrateFile(oldPath, newPath, file string) error {
130+
if oldPath == newPath {
131+
return errSamePath
132+
}
133+
134+
oldFile := filepath.Join(oldPath, file)
135+
newFile := filepath.Join(newPath, file)
136+
137+
if !fileExists(oldFile) {
138+
return errNotExist
139+
}
140+
141+
_ = os.MkdirAll(filepath.Dir(newFile), 0755)
142+
return os.Rename(oldFile, newFile)
67143
}
68144

69-
var migrateConfigDir = func(oldPath, newPath string) {
145+
func migrateDir(oldPath, newPath string) error {
70146
if oldPath == newPath {
71-
return
147+
return errSamePath
148+
}
149+
150+
if !dirExists(oldPath) {
151+
return errNotExist
72152
}
73153

74154
_ = os.MkdirAll(filepath.Dir(newPath), 0755)
75-
_ = os.Rename(oldPath, newPath)
155+
return os.Rename(oldPath, newPath)
156+
}
157+
158+
func dirExists(path string) bool {
159+
f, err := os.Stat(path)
160+
return err == nil && f.IsDir()
161+
}
162+
163+
func fileExists(path string) bool {
164+
f, err := os.Stat(path)
165+
return err == nil && !f.IsDir()
76166
}
77167

78168
func ConfigFile() string {

0 commit comments

Comments
 (0)