Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
dea446d
"Submit as draft" option for `pr create`
koskeller May 3, 2022
c3a3ce5
Add `exclude-drafts` flag to `gh release list`
ffalor May 9, 2022
8f199ba
status: exclude results from archived repositories
ffalor May 6, 2022
78ae6f8
Merge pull request #5583 from ffalor/gh_status
mislav May 10, 2022
fc4cd12
Ignore FORBIDDEN errors for `gh status`
heaths May 8, 2022
d103ba2
:nail_care: optimize fetches
mislav May 10, 2022
c8baec7
Ensure stable presentation of FORBIDDEN errors
mislav May 10, 2022
f309982
Merge pull request #5588 from heaths/issue5587
mislav May 10, 2022
bd1bf52
add devcontainer (#5592)
joshspicer May 10, 2022
cf92705
pr create: default to "Submit as draft" when `--draft` was used
mislav May 10, 2022
5cb4e4d
Merge pull request #5570 from koskeller/submit-as-draft-option-pr-create
mislav May 10, 2022
d9d3d7b
Fix nil pointer exception in codespace selection
cannist May 10, 2022
bbaf010
repo list: add `--visibility internal` filter (#5564)
hiradp May 10, 2022
0eedef3
auth: respect GH_BROWSER, `browser` config, avoid opening non-HTTP URLs
mislav May 10, 2022
8c5103a
Merge pull request #5595 from ffalor/exclude_drafts
mislav May 10, 2022
1bb0e05
Merge pull request #5604 from cli/auth-browser-fix
mislav May 10, 2022
c26ff6a
Merge pull request #5605 from cannist/cannist/fix-npe
mislav May 10, 2022
51d18d9
repo clone: document the upstream remote (#5466)
dhalbert May 10, 2022
0615377
Merge pull request #5504 from greggroth/gregg/retention-period
greggroth May 10, 2022
a0a9099
Hide retention-period flag (#5607)
greggroth May 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/go/.devcontainer/base.Dockerfile

# VARIANT Defined in devcontainer.json
ARG VARIANT="1.18"
FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT}
23 changes: 20 additions & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
{
"extensions": [
"golang.go"
]
}
"golang.go"
],
"build": {
"dockerfile": "Dockerfile",
"args": {
"VARIANT": "1.18"
}
},
"settings": {
"go.toolsManagement.checkForUpdates": "local",
"go.useLanguageServer": true,
"go.gopath": "/go"
},
"runArgs": [
"--cap-add=SYS_PTRACE",
"--security-opt",
"seccomp=unconfined"
],
"remoteUser": "vscode"
}
34 changes: 26 additions & 8 deletions internal/authflow/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"

Expand All @@ -24,6 +25,7 @@ var (
)

type iconfig interface {
Get(string, string) (string, error)
Set(string, string, string) error
Write() error
WriteHosts() error
Expand All @@ -33,7 +35,16 @@ func AuthFlowWithConfig(cfg iconfig, IO *iostreams.IOStreams, hostname, notice s
// TODO this probably shouldn't live in this package. It should probably be in a new package that
// depends on both iostreams and config.

token, userLogin, err := authFlow(hostname, IO, notice, additionalScopes, isInteractive)
// FIXME: this duplicates `factory.browserLauncher()`
browserLauncher := os.Getenv("GH_BROWSER")
if browserLauncher == "" {
browserLauncher, _ = cfg.Get("", "browser")
}
if browserLauncher == "" {
browserLauncher = os.Getenv("BROWSER")
}

token, userLogin, err := authFlow(hostname, IO, notice, additionalScopes, isInteractive, browserLauncher)
if err != nil {
return "", err
}
Expand All @@ -50,7 +61,7 @@ func AuthFlowWithConfig(cfg iconfig, IO *iostreams.IOStreams, hostname, notice s
return token, cfg.WriteHosts()
}

func authFlow(oauthHost string, IO *iostreams.IOStreams, notice string, additionalScopes []string, isInteractive bool) (string, string, error) {
func authFlow(oauthHost string, IO *iostreams.IOStreams, notice string, additionalScopes []string, isInteractive bool, browserLauncher string) (string, string, error) {
w := IO.ErrOut
cs := IO.ColorScheme()

Expand Down Expand Up @@ -81,19 +92,26 @@ func authFlow(oauthHost string, IO *iostreams.IOStreams, notice string, addition
fmt.Fprintf(w, "%s First copy your one-time code: %s\n", cs.Yellow("!"), cs.Bold(code))
return nil
},
BrowseURL: func(url string) error {
BrowseURL: func(authURL string) error {
if u, err := url.Parse(authURL); err == nil {
if u.Scheme != "http" && u.Scheme != "https" {
return fmt.Errorf("invalid URL: %s", authURL)
}
} else {
return err
}

if !isInteractive {
fmt.Fprintf(w, "%s to continue in your web browser: %s\n", cs.Bold("Open this URL"), url)
fmt.Fprintf(w, "%s to continue in your web browser: %s\n", cs.Bold("Open this URL"), authURL)
return nil
}

fmt.Fprintf(w, "%s to open %s in your browser... ", cs.Bold("Press Enter"), oauthHost)
_ = waitForEnter(IO.In)

// FIXME: read the browser from cmd Factory rather than recreating it
browser := cmdutil.NewBrowser(os.Getenv("BROWSER"), IO.Out, IO.ErrOut)
if err := browser.Browse(url); err != nil {
fmt.Fprintf(w, "%s Failed opening a web browser at %s\n", cs.Red("!"), url)
browser := cmdutil.NewBrowser(browserLauncher, IO.Out, IO.ErrOut)
if err := browser.Browse(authURL); err != nil {
fmt.Fprintf(w, "%s Failed opening a web browser at %s\n", cs.Red("!"), authURL)
fmt.Fprintf(w, " %s\n", err)
fmt.Fprint(w, " Please try entering the URL in your browser manually\n")
}
Expand Down
57 changes: 30 additions & 27 deletions internal/codespaces/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -558,15 +558,16 @@ func (a *API) GetCodespaceRepoSuggestions(ctx context.Context, partialSearch str

// CreateCodespaceParams are the required parameters for provisioning a Codespace.
type CreateCodespaceParams struct {
RepositoryID int
IdleTimeoutMinutes int
Branch string
Machine string
Location string
DevContainerPath string
VSCSTarget string
VSCSTargetURL string
PermissionsOptOut bool
RepositoryID int
IdleTimeoutMinutes int
RetentionPeriodMinutes *int
Branch string
Machine string
Location string
DevContainerPath string
VSCSTarget string
VSCSTargetURL string
PermissionsOptOut bool
}

// CreateCodespace creates a codespace with the given parameters and returns a non-nil error if it
Expand Down Expand Up @@ -607,15 +608,16 @@ func (a *API) CreateCodespace(ctx context.Context, params *CreateCodespaceParams
}

type startCreateRequest struct {
RepositoryID int `json:"repository_id"`
IdleTimeoutMinutes int `json:"idle_timeout_minutes,omitempty"`
Ref string `json:"ref"`
Location string `json:"location"`
Machine string `json:"machine"`
DevContainerPath string `json:"devcontainer_path,omitempty"`
VSCSTarget string `json:"vscs_target,omitempty"`
VSCSTargetURL string `json:"vscs_target_url,omitempty"`
PermissionsOptOut bool `json:"multi_repo_permissions_opt_out"`
RepositoryID int `json:"repository_id"`
IdleTimeoutMinutes int `json:"idle_timeout_minutes,omitempty"`
RetentionPeriodMinutes *int `json:"retention_period_minutes,omitempty"`
Ref string `json:"ref"`
Location string `json:"location"`
Machine string `json:"machine"`
DevContainerPath string `json:"devcontainer_path,omitempty"`
VSCSTarget string `json:"vscs_target,omitempty"`
VSCSTargetURL string `json:"vscs_target_url,omitempty"`
PermissionsOptOut bool `json:"multi_repo_permissions_opt_out"`
}

var errProvisioningInProgress = errors.New("provisioning in progress")
Expand All @@ -639,15 +641,16 @@ func (a *API) startCreate(ctx context.Context, params *CreateCodespaceParams) (*
}

requestBody, err := json.Marshal(startCreateRequest{
RepositoryID: params.RepositoryID,
IdleTimeoutMinutes: params.IdleTimeoutMinutes,
Ref: params.Branch,
Location: params.Location,
Machine: params.Machine,
DevContainerPath: params.DevContainerPath,
VSCSTarget: params.VSCSTarget,
VSCSTargetURL: params.VSCSTargetURL,
PermissionsOptOut: params.PermissionsOptOut,
RepositoryID: params.RepositoryID,
IdleTimeoutMinutes: params.IdleTimeoutMinutes,
RetentionPeriodMinutes: params.RetentionPeriodMinutes,
Ref: params.Branch,
Location: params.Location,
Machine: params.Machine,
DevContainerPath: params.DevContainerPath,
VSCSTarget: params.VSCSTarget,
VSCSTargetURL: params.VSCSTargetURL,
PermissionsOptOut: params.PermissionsOptOut,
})

if err != nil {
Expand Down
65 changes: 65 additions & 0 deletions internal/codespaces/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,71 @@ func createFakeListEndpointServer(t *testing.T, initalTotal int, finalTotal int)
}))
}

func createFakeCreateEndpointServer(t *testing.T, initalTotal int, finalTotal int) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/user/codespaces" {
t.Fatal("Incorrect path")
}

body := r.Body
if body == nil {
t.Fatal("No body")
}
defer body.Close()

var params startCreateRequest
err := json.NewDecoder(body).Decode(&params)
if err != nil {
t.Fatal("error:", err)
}

if params.RepositoryID != 1 {
t.Fatal("Expected RepositoryID to be 1. Got: ", params.RepositoryID)
}

if params.IdleTimeoutMinutes != 10 {
t.Fatal("Expected IdleTimeoutMinutes to be 10. Got: ", params.IdleTimeoutMinutes)
}

if *params.RetentionPeriodMinutes != 0 {
t.Fatal("Expected RetentionPeriodMinutes to be 0. Got: ", *params.RetentionPeriodMinutes)
}

response := Codespace{
Name: "codespace-1",
}

data, _ := json.Marshal(response)
fmt.Fprint(w, string(data))
}))
}

func TestCreateCodespaces(t *testing.T) {
svr := createFakeCreateEndpointServer(t, 200, 200)
defer svr.Close()

api := API{
githubAPI: svr.URL,
client: &http.Client{},
}

ctx := context.TODO()
retentionPeriod := 0
params := &CreateCodespaceParams{
RepositoryID: 1,
IdleTimeoutMinutes: 10,
RetentionPeriodMinutes: &retentionPeriod,
}
codespace, err := api.CreateCodespace(ctx, params)
if err != nil {
t.Fatal(err)
}

if codespace.Name != "codespace-1" {
t.Fatalf("expected codespace-1, got %s", codespace.Name)
}
}

func TestListCodespaces_limited(t *testing.T) {
svr := createFakeListEndpointServer(t, 200, 200)
defer svr.Close()
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/codespace/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func chooseCodespaceFromList(ctx context.Context, codespaces []*api.Codespace) (
codespacesNames[seenCodespace.idx] = fullDisplayNameWithGitStatus

// Update the git status dirty map to reflect the new name.
if cs.hasUnsavedChanges() {
if seenCodespace.cs.hasUnsavedChanges() {
codespacesDirty[fullDisplayNameWithGitStatus] = true
}

Expand Down
23 changes: 14 additions & 9 deletions pkg/cmd/codespace/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type createOptions struct {
permissionsOptOut bool
devContainerPath string
idleTimeout time.Duration
retentionPeriod time.Duration
}

func newCreateCmd(app *App) *cobra.Command {
Expand All @@ -53,6 +54,7 @@ func newCreateCmd(app *App) *cobra.Command {
createCmd.Flags().BoolVarP(&opts.permissionsOptOut, "default-permissions", "", false, "do not prompt to accept additional permissions requested by the codespace")
createCmd.Flags().BoolVarP(&opts.showStatus, "status", "s", false, "show status of post-create command and dotfiles")
createCmd.Flags().DurationVar(&opts.idleTimeout, "idle-timeout", 0, "allowed inactivity before codespace is stopped, e.g. \"10m\", \"1h\"")
// createCmd.Flags().DurationVar(&opts.retentionPeriod, "retention-period", 0, "allowed time after going idle before codespace is automatically deleted (maximum 30 days), e.g. \"1h\", \"72h\"")
createCmd.Flags().StringVar(&opts.devContainerPath, "devcontainer-path", "", "path to the devcontainer.json file to use when creating codespace")

return createCmd
Expand Down Expand Up @@ -176,16 +178,19 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
return errors.New("there are no available machine types for this repository")
}

retentionPeriod := int(opts.retentionPeriod.Minutes())

createParams := &api.CreateCodespaceParams{
RepositoryID: repository.ID,
Branch: branch,
Machine: machine,
Location: userInputs.Location,
VSCSTarget: vscsTarget,
VSCSTargetURL: vscsTargetUrl,
IdleTimeoutMinutes: int(opts.idleTimeout.Minutes()),
DevContainerPath: devContainerPath,
PermissionsOptOut: opts.permissionsOptOut,
RepositoryID: repository.ID,
Branch: branch,
Machine: machine,
Location: userInputs.Location,
VSCSTarget: vscsTarget,
VSCSTargetURL: vscsTargetUrl,
IdleTimeoutMinutes: int(opts.idleTimeout.Minutes()),
RetentionPeriodMinutes: &retentionPeriod,
DevContainerPath: devContainerPath,
PermissionsOptOut: opts.permissionsOptOut,
}

a.StartProgressIndicatorWithLabel("Creating codespace")
Expand Down
14 changes: 9 additions & 5 deletions pkg/cmd/codespace/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ func TestApp_Create(t *testing.T) {
if params.IdleTimeoutMinutes != 30 {
return nil, fmt.Errorf("idle timeout minutes was %v", params.IdleTimeoutMinutes)
}
if *params.RetentionPeriodMinutes != 2880 {
return nil, fmt.Errorf("retention period minutes was %v", params.RetentionPeriodMinutes)
}
return &api.Codespace{
Name: "monalisa-dotfiles-abcd1234",
}, nil
Expand All @@ -64,11 +67,12 @@ func TestApp_Create(t *testing.T) {
},
},
opts: createOptions{
repo: "monalisa/dotfiles",
branch: "",
machine: "GIGA",
showStatus: false,
idleTimeout: 30 * time.Minute,
repo: "monalisa/dotfiles",
branch: "",
machine: "GIGA",
showStatus: false,
idleTimeout: 30 * time.Minute,
retentionPeriod: 48 * time.Hour,
},
wantStdout: "monalisa-dotfiles-abcd1234\n",
},
Expand Down
4 changes: 2 additions & 2 deletions pkg/cmd/issue/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func createRun(opts *CreateOptions) (err error) {
}

allowPreview := !tb.HasMetadata() && utils.ValidURL(openURL)
action, err = prShared.ConfirmSubmission(allowPreview, repo.ViewerCanTriage())
action, err = prShared.ConfirmIssueSubmission(allowPreview, repo.ViewerCanTriage())
if err != nil {
err = fmt.Errorf("unable to confirm: %w", err)
return
Expand All @@ -260,7 +260,7 @@ func createRun(opts *CreateOptions) (err error) {
return
}

action, err = prShared.ConfirmSubmission(!tb.HasMetadata(), false)
action, err = prShared.ConfirmIssueSubmission(!tb.HasMetadata(), false)
if err != nil {
return
}
Expand Down
4 changes: 3 additions & 1 deletion pkg/cmd/issue/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,9 @@ func TestIssueCreate_nonLegacyTemplate(t *testing.T) {

as.StubPrompt("Choose a template").AnswerWith("Submit a request")
as.StubPrompt("Body").AnswerDefault()
as.StubPrompt("What's next?").AnswerWith("Submit")
as.StubPrompt("What's next?").
AssertOptions([]string{"Submit", "Continue in browser", "Cancel"}).
AnswerWith("Submit")

output, err := runCommandWithRootDirOverridden(http, true, `-t hello`, "./fixtures/repoWithNonLegacyIssueTemplates")
if err != nil {
Expand Down
9 changes: 7 additions & 2 deletions pkg/cmd/pr/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ func createRun(opts *CreateOptions) (err error) {

allowPreview := !state.HasMetadata() && utils.ValidURL(openURL)
allowMetadata := ctx.BaseRepo.ViewerCanTriage()
action, err := shared.ConfirmSubmission(allowPreview, allowMetadata)
action, err := shared.ConfirmPRSubmission(allowPreview, allowMetadata, state.Draft)
if err != nil {
return fmt.Errorf("unable to confirm: %w", err)
}
Expand All @@ -329,7 +329,7 @@ func createRun(opts *CreateOptions) (err error) {
return
}

action, err = shared.ConfirmSubmission(!state.HasMetadata(), false)
action, err = shared.ConfirmPRSubmission(!state.HasMetadata(), false, state.Draft)
if err != nil {
return
}
Expand All @@ -350,6 +350,11 @@ func createRun(opts *CreateOptions) (err error) {
return previewPR(*opts, openURL)
}

if action == shared.SubmitDraftAction {
state.Draft = true
return submitPR(*opts, *ctx, *state)
}

if action == shared.SubmitAction {
return submitPR(*opts, *ctx, *state)
}
Expand Down
Loading