Merge branch 'trunk' into n1lesh-remove-scope
This commit is contained in:
commit
4ec6ece95a
144 changed files with 16519 additions and 1597 deletions
5
.github/workflows/deployment.yml
vendored
5
.github/workflows/deployment.yml
vendored
|
|
@ -218,7 +218,7 @@ jobs:
|
|||
repository: github/cli.github.com
|
||||
path: site
|
||||
fetch-depth: 0
|
||||
ssh-key: ${{ secrets.SITE_SSH_KEY }}
|
||||
token: ${{ secrets.SITE_DEPLOY_PAT }}
|
||||
- name: Update site man pages
|
||||
env:
|
||||
GIT_COMMITTER_NAME: cli automation
|
||||
|
|
@ -341,5 +341,6 @@ jobs:
|
|||
with:
|
||||
formula-name: gh
|
||||
tag-name: ${{ inputs.tag_name }}
|
||||
push-to: cli/homebrew-core
|
||||
env:
|
||||
COMMITTER_TOKEN: ${{ secrets.UPLOAD_GITHUB_TOKEN }}
|
||||
COMMITTER_TOKEN: ${{ secrets.HOMEBREW_PR_PAT }}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ For more information, see [Linux & BSD installation](./docs/install_linux.md).
|
|||
| `winget install --id GitHub.cli` | `winget upgrade --id GitHub.cli` |
|
||||
|
||||
> **Note**
|
||||
> The Windows installer modifies your PATH. When using Windows Terminal, you will need to **open a new window** for the changes to take affect. (Simply opening a new tab will _not_ be sufficient.)
|
||||
> The Windows installer modifies your PATH. When using Windows Terminal, you will need to **open a new window** for the changes to take effect. (Simply opening a new tab will _not_ be sufficient.)
|
||||
|
||||
#### scoop
|
||||
|
||||
|
|
|
|||
|
|
@ -2,42 +2,414 @@ package api
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPullRequest_ChecksStatus(t *testing.T) {
|
||||
pr := PullRequest{}
|
||||
func TestChecksStatus_NoCheckRunsOrStatusContexts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
payload := `
|
||||
{ "statusCheckRollup": { "nodes": [] } }
|
||||
`
|
||||
var pr PullRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(payload), &pr))
|
||||
|
||||
expectedChecksStatus := PullRequestChecksStatus{
|
||||
Pending: 0,
|
||||
Failing: 0,
|
||||
Passing: 0,
|
||||
Total: 0,
|
||||
}
|
||||
require.Equal(t, expectedChecksStatus, pr.ChecksStatus())
|
||||
}
|
||||
|
||||
func TestChecksStatus_SummarisingCheckRuns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
payload string
|
||||
expectedChecksStatus PullRequestChecksStatus
|
||||
}{
|
||||
{
|
||||
name: "QUEUED is treated as Pending",
|
||||
payload: singleCheckRunWithStatus("QUEUED"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "IN_PROGRESS is treated as Pending",
|
||||
payload: singleCheckRunWithStatus("IN_PROGRESS"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "WAITING is treated as Pending",
|
||||
payload: singleCheckRunWithStatus("WAITING"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "PENDING is treated as Pending",
|
||||
payload: singleCheckRunWithStatus("PENDING"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "REQUESTED is treated as Pending",
|
||||
payload: singleCheckRunWithStatus("REQUESTED"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED with no conclusion is treated as Pending",
|
||||
payload: singleCheckRunWithStatus("COMPLETED"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / STARTUP_FAILURE is treated as Pending",
|
||||
payload: singleCompletedCheckRunWithConclusion("STARTUP_FAILURE"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / STALE is treated as Pending",
|
||||
payload: singleCompletedCheckRunWithConclusion("STALE"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / SUCCESS is treated as Passing",
|
||||
payload: singleCompletedCheckRunWithConclusion("SUCCESS"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Passing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / NEUTRAL is treated as Passing",
|
||||
payload: singleCompletedCheckRunWithConclusion("NEUTRAL"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Passing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / SKIPPED is treated as Passing",
|
||||
payload: singleCompletedCheckRunWithConclusion("SKIPPED"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Passing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / ACTION_REQUIRED is treated as Failing",
|
||||
payload: singleCompletedCheckRunWithConclusion("ACTION_REQUIRED"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / TIMED_OUT is treated as Failing",
|
||||
payload: singleCompletedCheckRunWithConclusion("TIMED_OUT"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / CANCELLED is treated as Failing",
|
||||
payload: singleCompletedCheckRunWithConclusion("CANCELLED"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / CANCELLED is treated as Failing",
|
||||
payload: singleCompletedCheckRunWithConclusion("CANCELLED"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "COMPLETED / FAILURE is treated as Failing",
|
||||
payload: singleCompletedCheckRunWithConclusion("FAILURE"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "Unrecognized Status are treated as Pending",
|
||||
payload: singleCheckRunWithStatus("AnUnrecognizedStatusJustForThisTest"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "Unrecognized Conclusions are treated as Pending",
|
||||
payload: singleCompletedCheckRunWithConclusion("AnUnrecognizedConclusionJustForThisTest"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var pr PullRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(tt.payload), &pr))
|
||||
|
||||
require.Equal(t, tt.expectedChecksStatus, pr.ChecksStatus())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChecksStatus_SummarisingStatusContexts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
payload string
|
||||
expectedChecksStatus PullRequestChecksStatus
|
||||
}{
|
||||
{
|
||||
name: "EXPECTED is treated as Pending",
|
||||
payload: singleStatusContextWithState("EXPECTED"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "PENDING is treated as Pending",
|
||||
payload: singleStatusContextWithState("PENDING"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "SUCCESS is treated as Passing",
|
||||
payload: singleStatusContextWithState("SUCCESS"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Passing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "ERROR is treated as Failing",
|
||||
payload: singleStatusContextWithState("ERROR"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "FAILURE is treated as Failing",
|
||||
payload: singleStatusContextWithState("FAILURE"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Failing: 1, Total: 1},
|
||||
},
|
||||
{
|
||||
name: "Unrecognized States are treated as Pending",
|
||||
payload: singleStatusContextWithState("AnUnrecognizedStateJustForThisTest"),
|
||||
expectedChecksStatus: PullRequestChecksStatus{Pending: 1, Total: 1},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var pr PullRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(tt.payload), &pr))
|
||||
|
||||
require.Equal(t, tt.expectedChecksStatus, pr.ChecksStatus())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChecksStatus_SummarisingCheckRunsAndStatusContexts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// This might look a bit intimidating, but we're just inserting three nodes
|
||||
// into the rollup, two completed check run nodes and one status context node.
|
||||
payload := fmt.Sprintf(`
|
||||
{ "statusCheckRollup": { "nodes": [{ "commit": {
|
||||
"statusCheckRollup": {
|
||||
"contexts": {
|
||||
"nodes": [
|
||||
{ "state": "SUCCESS" },
|
||||
{ "state": "PENDING" },
|
||||
{ "state": "FAILURE" },
|
||||
{ "status": "IN_PROGRESS",
|
||||
"conclusion": null },
|
||||
{ "status": "COMPLETED",
|
||||
"conclusion": "SUCCESS" },
|
||||
{ "status": "COMPLETED",
|
||||
"conclusion": "FAILURE" },
|
||||
{ "status": "COMPLETED",
|
||||
"conclusion": "ACTION_REQUIRED" },
|
||||
{ "status": "COMPLETED",
|
||||
"conclusion": "STALE" }
|
||||
%s,
|
||||
%s,
|
||||
%s
|
||||
]
|
||||
}
|
||||
}
|
||||
} }] } }
|
||||
`,
|
||||
completedCheckRunNode("SUCCESS"),
|
||||
statusContextNode("PENDING"),
|
||||
completedCheckRunNode("FAILURE"),
|
||||
)
|
||||
|
||||
var pr PullRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(payload), &pr))
|
||||
|
||||
expectedChecksStatus := PullRequestChecksStatus{
|
||||
Pending: 1,
|
||||
Failing: 1,
|
||||
Passing: 1,
|
||||
Total: 3,
|
||||
}
|
||||
require.Equal(t, expectedChecksStatus, pr.ChecksStatus())
|
||||
}
|
||||
|
||||
func TestChecksStatus_SummarisingCheckRunAndStatusContextCountsByState(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
payload := `
|
||||
{ "statusCheckRollup": { "nodes": [{ "commit": {
|
||||
"statusCheckRollup": {
|
||||
"contexts": {
|
||||
"checkRunCount": 14,
|
||||
"checkRunCountsByState": [
|
||||
{
|
||||
"state": "ACTION_REQUIRED",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "CANCELLED",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "COMPLETED",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "FAILURE",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "IN_PROGRESS",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "NEUTRAL",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "PENDING",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "QUEUED",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "SKIPPED",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "STALE",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "STARTUP_FAILURE",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "SUCCESS",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "TIMED_OUT",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "WAITING",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "AnUnrecognizedStateJustForThisTest",
|
||||
"count": 1
|
||||
}
|
||||
],
|
||||
"statusContextCount": 6,
|
||||
"statusContextCountsByState": [
|
||||
{
|
||||
"state": "EXPECTED",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "ERROR",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "FAILURE",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "PENDING",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "SUCCESS",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "AnUnrecognizedStateJustForThisTest",
|
||||
"count": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
} }] } }
|
||||
`
|
||||
err := json.Unmarshal([]byte(payload), &pr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
checks := pr.ChecksStatus()
|
||||
assert.Equal(t, 8, checks.Total)
|
||||
assert.Equal(t, 3, checks.Pending)
|
||||
assert.Equal(t, 3, checks.Failing)
|
||||
assert.Equal(t, 2, checks.Passing)
|
||||
var pr PullRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(payload), &pr))
|
||||
|
||||
expectedChecksStatus := PullRequestChecksStatus{
|
||||
Pending: 11,
|
||||
Failing: 6,
|
||||
Passing: 4,
|
||||
Total: 20,
|
||||
}
|
||||
require.Equal(t, expectedChecksStatus, pr.ChecksStatus())
|
||||
}
|
||||
|
||||
// Note that it would be incorrect to provide a status of COMPLETED here
|
||||
// as the conclusion is always set to null. If you want a COMPLETED status,
|
||||
// use `singleCompletedCheckRunWithConclusion`.
|
||||
func singleCheckRunWithStatus(status string) string {
|
||||
return fmt.Sprintf(`
|
||||
{ "statusCheckRollup": { "nodes": [{ "commit": {
|
||||
"statusCheckRollup": {
|
||||
"contexts": {
|
||||
"nodes": [
|
||||
{
|
||||
"__typename": "CheckRun",
|
||||
"status": "%s",
|
||||
"conclusion": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
} }] } }
|
||||
`, status)
|
||||
}
|
||||
|
||||
func singleCompletedCheckRunWithConclusion(conclusion string) string {
|
||||
return fmt.Sprintf(`
|
||||
{ "statusCheckRollup": { "nodes": [{ "commit": {
|
||||
"statusCheckRollup": {
|
||||
"contexts": {
|
||||
"nodes": [
|
||||
{
|
||||
"__typename": "CheckRun",
|
||||
"status": "COMPLETED",
|
||||
"conclusion": "%s"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
} }] } }
|
||||
`, conclusion)
|
||||
}
|
||||
|
||||
func singleStatusContextWithState(state string) string {
|
||||
return fmt.Sprintf(`
|
||||
{ "statusCheckRollup": { "nodes": [{ "commit": {
|
||||
"statusCheckRollup": {
|
||||
"contexts": {
|
||||
"nodes": [
|
||||
{
|
||||
"__typename": "StatusContext",
|
||||
"state": "%s"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
} }] } }
|
||||
`, state)
|
||||
}
|
||||
|
||||
func completedCheckRunNode(conclusion string) string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"__typename": "CheckRun",
|
||||
"status": "COMPLETED",
|
||||
"conclusion": "%s"
|
||||
}`, conclusion)
|
||||
}
|
||||
|
||||
func statusContextNode(state string) string {
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"__typename": "StatusContext",
|
||||
"state": "%s"
|
||||
}`, state)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,7 +97,84 @@ type CommitStatusCheckRollup struct {
|
|||
Contexts CheckContexts
|
||||
}
|
||||
|
||||
// https://docs.github.com/en/graphql/reference/enums#checkrunstate
|
||||
type CheckRunState string
|
||||
|
||||
const (
|
||||
CheckRunStateActionRequired CheckRunState = "ACTION_REQUIRED"
|
||||
CheckRunStateCancelled CheckRunState = "CANCELLED"
|
||||
CheckRunStateCompleted CheckRunState = "COMPLETED"
|
||||
CheckRunStateFailure CheckRunState = "FAILURE"
|
||||
CheckRunStateInProgress CheckRunState = "IN_PROGRESS"
|
||||
CheckRunStateNeutral CheckRunState = "NEUTRAL"
|
||||
CheckRunStatePending CheckRunState = "PENDING"
|
||||
CheckRunStateQueued CheckRunState = "QUEUED"
|
||||
CheckRunStateSkipped CheckRunState = "SKIPPED"
|
||||
CheckRunStateStale CheckRunState = "STALE"
|
||||
CheckRunStateStartupFailure CheckRunState = "STARTUP_FAILURE"
|
||||
CheckRunStateSuccess CheckRunState = "SUCCESS"
|
||||
CheckRunStateTimedOut CheckRunState = "TIMED_OUT"
|
||||
CheckRunStateWaiting CheckRunState = "WAITING"
|
||||
)
|
||||
|
||||
type CheckRunCountByState struct {
|
||||
State CheckRunState
|
||||
Count int
|
||||
}
|
||||
|
||||
// https://docs.github.com/en/graphql/reference/enums#statusstate
|
||||
type StatusState string
|
||||
|
||||
const (
|
||||
StatusStateError StatusState = "ERROR"
|
||||
StatusStateExpected StatusState = "EXPECTED"
|
||||
StatusStateFailure StatusState = "FAILURE"
|
||||
StatusStatePending StatusState = "PENDING"
|
||||
StatusStateSuccess StatusState = "SUCCESS"
|
||||
)
|
||||
|
||||
type StatusContextCountByState struct {
|
||||
State StatusState
|
||||
Count int
|
||||
}
|
||||
|
||||
// https://docs.github.com/en/graphql/reference/enums#checkstatusstate
|
||||
type CheckStatusState string
|
||||
|
||||
const (
|
||||
CheckStatusStateCompleted CheckStatusState = "COMPLETED"
|
||||
CheckStatusStateInProgress CheckStatusState = "IN_PROGRESS"
|
||||
CheckStatusStatePending CheckStatusState = "PENDING"
|
||||
CheckStatusStateQueued CheckStatusState = "QUEUED"
|
||||
CheckStatusStateRequested CheckStatusState = "REQUESTED"
|
||||
CheckStatusStateWaiting CheckStatusState = "WAITING"
|
||||
)
|
||||
|
||||
// https://docs.github.com/en/graphql/reference/enums#checkconclusionstate
|
||||
type CheckConclusionState string
|
||||
|
||||
const (
|
||||
CheckConclusionStateActionRequired CheckConclusionState = "ACTION_REQUIRED"
|
||||
CheckConclusionStateCancelled CheckConclusionState = "CANCELLED"
|
||||
CheckConclusionStateFailure CheckConclusionState = "FAILURE"
|
||||
CheckConclusionStateNeutral CheckConclusionState = "NEUTRAL"
|
||||
CheckConclusionStateSkipped CheckConclusionState = "SKIPPED"
|
||||
CheckConclusionStateStale CheckConclusionState = "STALE"
|
||||
CheckConclusionStateStartupFailure CheckConclusionState = "STARTUP_FAILURE"
|
||||
CheckConclusionStateSuccess CheckConclusionState = "SUCCESS"
|
||||
CheckConclusionStateTimedOut CheckConclusionState = "TIMED_OUT"
|
||||
)
|
||||
|
||||
type CheckContexts struct {
|
||||
// These fields are available on newer versions of the GraphQL API
|
||||
// to support summary counts by state
|
||||
CheckRunCount int
|
||||
CheckRunCountsByState []CheckRunCountByState
|
||||
StatusContextCount int
|
||||
StatusContextCountsByState []StatusContextCountByState
|
||||
|
||||
// These are available on older versions and provide more details
|
||||
// required for checks
|
||||
Nodes []CheckContext
|
||||
PageInfo struct {
|
||||
HasNextPage bool
|
||||
|
|
@ -119,18 +196,18 @@ type CheckContext struct {
|
|||
// QUEUED IN_PROGRESS COMPLETED WAITING PENDING REQUESTED
|
||||
Status string `json:"status"`
|
||||
// ACTION_REQUIRED TIMED_OUT CANCELLED FAILURE SUCCESS NEUTRAL SKIPPED STARTUP_FAILURE STALE
|
||||
Conclusion string `json:"conclusion"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
CompletedAt time.Time `json:"completedAt"`
|
||||
DetailsURL string `json:"detailsUrl"`
|
||||
Conclusion CheckConclusionState `json:"conclusion"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
CompletedAt time.Time `json:"completedAt"`
|
||||
DetailsURL string `json:"detailsUrl"`
|
||||
|
||||
/* StatusContext fields */
|
||||
|
||||
Context string `json:"context"`
|
||||
// EXPECTED ERROR FAILURE PENDING SUCCESS
|
||||
State string `json:"state"`
|
||||
TargetURL string `json:"targetUrl"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
State StatusState `json:"state"`
|
||||
TargetURL string `json:"targetUrl"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type PRRepository struct {
|
||||
|
|
@ -261,33 +338,136 @@ type PullRequestChecksStatus struct {
|
|||
Total int
|
||||
}
|
||||
|
||||
func (pr *PullRequest) ChecksStatus() (summary PullRequestChecksStatus) {
|
||||
func (pr *PullRequest) ChecksStatus() PullRequestChecksStatus {
|
||||
var summary PullRequestChecksStatus
|
||||
|
||||
if len(pr.StatusCheckRollup.Nodes) == 0 {
|
||||
return
|
||||
return summary
|
||||
}
|
||||
commit := pr.StatusCheckRollup.Nodes[0].Commit
|
||||
for _, c := range commit.StatusCheckRollup.Contexts.Nodes {
|
||||
state := c.State // StatusContext
|
||||
if state == "" {
|
||||
// CheckRun
|
||||
if c.Status == "COMPLETED" {
|
||||
state = c.Conclusion
|
||||
} else {
|
||||
state = c.Status
|
||||
|
||||
contexts := pr.StatusCheckRollup.Nodes[0].Commit.StatusCheckRollup.Contexts
|
||||
|
||||
// If this commit has counts by state then we can summarise check status from those
|
||||
if len(contexts.CheckRunCountsByState) != 0 && len(contexts.StatusContextCountsByState) != 0 {
|
||||
summary.Total = contexts.CheckRunCount + contexts.StatusContextCount
|
||||
for _, countByState := range contexts.CheckRunCountsByState {
|
||||
switch parseCheckStatusFromCheckRunState(countByState.State) {
|
||||
case passing:
|
||||
summary.Passing += countByState.Count
|
||||
case failing:
|
||||
summary.Failing += countByState.Count
|
||||
default:
|
||||
summary.Pending += countByState.Count
|
||||
}
|
||||
}
|
||||
switch state {
|
||||
case "SUCCESS", "NEUTRAL", "SKIPPED":
|
||||
summary.Passing++
|
||||
case "ERROR", "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED":
|
||||
summary.Failing++
|
||||
default: // "EXPECTED", "REQUESTED", "WAITING", "QUEUED", "PENDING", "IN_PROGRESS", "STALE"
|
||||
summary.Pending++
|
||||
|
||||
for _, countByState := range contexts.StatusContextCountsByState {
|
||||
switch parseCheckStatusFromStatusState(countByState.State) {
|
||||
case passing:
|
||||
summary.Passing += countByState.Count
|
||||
case failing:
|
||||
summary.Failing += countByState.Count
|
||||
default:
|
||||
summary.Pending += countByState.Count
|
||||
}
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
// If we don't have the counts by state, then we'll need to summarise by looking at the more detailed contexts
|
||||
for _, c := range contexts.Nodes {
|
||||
// Nodes are a discriminated union of CheckRun or StatusContext and we can match on
|
||||
// the TypeName to narrow the type.
|
||||
if c.TypeName == "CheckRun" {
|
||||
// https://docs.github.com/en/graphql/reference/enums#checkstatusstate
|
||||
// If the status is completed then we can check the conclusion field
|
||||
if c.Status == "COMPLETED" {
|
||||
switch parseCheckStatusFromCheckConclusionState(c.Conclusion) {
|
||||
case passing:
|
||||
summary.Passing++
|
||||
case failing:
|
||||
summary.Failing++
|
||||
default:
|
||||
summary.Pending++
|
||||
}
|
||||
// otherwise we're in some form of pending state:
|
||||
// "COMPLETED", "IN_PROGRESS", "PENDING", "QUEUED", "REQUESTED", "WAITING" or otherwise unknown
|
||||
} else {
|
||||
summary.Pending++
|
||||
}
|
||||
|
||||
} else { // c.TypeName == StatusContext
|
||||
switch parseCheckStatusFromStatusState(c.State) {
|
||||
case passing:
|
||||
summary.Passing++
|
||||
case failing:
|
||||
summary.Failing++
|
||||
default:
|
||||
summary.Pending++
|
||||
}
|
||||
}
|
||||
summary.Total++
|
||||
}
|
||||
|
||||
return
|
||||
return summary
|
||||
}
|
||||
|
||||
type checkStatus int
|
||||
|
||||
const (
|
||||
passing checkStatus = iota
|
||||
failing
|
||||
pending
|
||||
)
|
||||
|
||||
func parseCheckStatusFromStatusState(state StatusState) checkStatus {
|
||||
switch state {
|
||||
case StatusStateSuccess:
|
||||
return passing
|
||||
case StatusStateFailure, StatusStateError:
|
||||
return failing
|
||||
case StatusStateExpected, StatusStatePending:
|
||||
return pending
|
||||
// Currently, we treat anything unknown as pending, which includes any future unknown
|
||||
// states we might get back from the API. It might be interesting to do some work to add an additional
|
||||
// unknown state.
|
||||
default:
|
||||
return pending
|
||||
}
|
||||
}
|
||||
|
||||
func parseCheckStatusFromCheckRunState(state CheckRunState) checkStatus {
|
||||
switch state {
|
||||
case CheckRunStateNeutral, CheckRunStateSkipped, CheckRunStateSuccess:
|
||||
return passing
|
||||
case CheckRunStateActionRequired, CheckRunStateCancelled, CheckRunStateFailure, CheckRunStateTimedOut:
|
||||
return failing
|
||||
case CheckRunStateCompleted, CheckRunStateInProgress, CheckRunStatePending, CheckRunStateQueued,
|
||||
CheckRunStateStale, CheckRunStateStartupFailure, CheckRunStateWaiting:
|
||||
return pending
|
||||
// Currently, we treat anything unknown as pending, which includes any future unknown
|
||||
// states we might get back from the API. It might be interesting to do some work to add an additional
|
||||
// unknown state.
|
||||
default:
|
||||
return pending
|
||||
}
|
||||
}
|
||||
|
||||
func parseCheckStatusFromCheckConclusionState(state CheckConclusionState) checkStatus {
|
||||
switch state {
|
||||
case CheckConclusionStateNeutral, CheckConclusionStateSkipped, CheckConclusionStateSuccess:
|
||||
return passing
|
||||
case CheckConclusionStateActionRequired, CheckConclusionStateCancelled, CheckConclusionStateFailure, CheckConclusionStateTimedOut:
|
||||
return failing
|
||||
case CheckConclusionStateStale, CheckConclusionStateStartupFailure:
|
||||
return pending
|
||||
// Currently, we treat anything unknown as pending, which includes any future unknown
|
||||
// states we might get back from the API. It might be interesting to do some work to add an additional
|
||||
// unknown state.
|
||||
default:
|
||||
return pending
|
||||
}
|
||||
}
|
||||
|
||||
func (pr *PullRequest) DisplayableReviews() PullRequestReviews {
|
||||
|
|
|
|||
|
|
@ -143,7 +143,31 @@ var autoMergeRequest = shortenQuery(`
|
|||
}
|
||||
`)
|
||||
|
||||
func StatusCheckRollupGraphQL(after string) string {
|
||||
func StatusCheckRollupGraphQLWithCountByState() string {
|
||||
return shortenQuery(`
|
||||
statusCheckRollup: commits(last: 1) {
|
||||
nodes {
|
||||
commit {
|
||||
statusCheckRollup {
|
||||
contexts {
|
||||
checkRunCount,
|
||||
checkRunCountsByState {
|
||||
state,
|
||||
count
|
||||
},
|
||||
statusContextCount,
|
||||
statusContextCountsByState {
|
||||
state,
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`)
|
||||
}
|
||||
|
||||
func StatusCheckRollupGraphQLWithoutCountByState(after string) string {
|
||||
var afterClause string
|
||||
if after != "" {
|
||||
afterClause = ",after:" + after
|
||||
|
|
@ -320,7 +344,9 @@ func IssueGraphQL(fields []string) string {
|
|||
case "requiresStrictStatusChecks": // pseudo-field
|
||||
q = append(q, `baseRef{branchProtectionRule{requiresStrictStatusChecks}}`)
|
||||
case "statusCheckRollup":
|
||||
q = append(q, StatusCheckRollupGraphQL(""))
|
||||
q = append(q, StatusCheckRollupGraphQLWithoutCountByState(""))
|
||||
case "statusCheckRollupWithCountByState": // pseudo-field
|
||||
q = append(q, StatusCheckRollupGraphQLWithCountByState())
|
||||
default:
|
||||
q = append(q, field)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,17 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/docs"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/root"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/extensions"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
|
@ -41,9 +45,13 @@ func run(args []string) error {
|
|||
}
|
||||
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
rootCmd := root.NewCmdRoot(&cmdutil.Factory{
|
||||
rootCmd, _ := root.NewCmdRoot(&cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
Browser: &browser{},
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewFromString(""), nil
|
||||
},
|
||||
ExtensionManager: &em{},
|
||||
}, "", "")
|
||||
rootCmd.InitDefaultHelpCmd()
|
||||
|
||||
|
|
@ -79,8 +87,42 @@ func linkHandler(name string) string {
|
|||
return fmt.Sprintf("./%s", strings.TrimSuffix(name, ".md"))
|
||||
}
|
||||
|
||||
// Implements browser.Browser interface.
|
||||
type browser struct{}
|
||||
|
||||
func (b *browser) Browse(url string) error {
|
||||
func (b *browser) Browse(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Implements extensions.ExtensionManager interface.
|
||||
type em struct{}
|
||||
|
||||
func (e *em) List() []extensions.Extension {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *em) Install(_ ghrepo.Interface, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *em) InstallLocal(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *em) Upgrade(_ string, _ bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *em) Remove(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *em) Dispatch(_ []string, _ io.Reader, _, _ io.Writer) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (e *em) Create(_ string, _ extensions.ExtTemplateType) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *em) EnableDryRunMode() {}
|
||||
|
|
|
|||
158
cmd/gh/main.go
158
cmd/gh/main.go
|
|
@ -14,16 +14,10 @@ import (
|
|||
|
||||
surveyCore "github.com/AlecAivazis/survey/v2/core"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/build"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/internal/update"
|
||||
"github.com/cli/cli/v2/pkg/cmd/alias/expand"
|
||||
"github.com/cli/cli/v2/pkg/cmd/factory"
|
||||
"github.com/cli/cli/v2/pkg/cmd/root"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
|
|
@ -80,9 +74,6 @@ func mainRun() exitCode {
|
|||
surveyCore.TemplateFuncsWithColor["color"] = func(style string) string {
|
||||
switch style {
|
||||
case "white":
|
||||
if cmdFactory.IOStreams.ColorSupport256() {
|
||||
return fmt.Sprintf("\x1b[%d;5;%dm", 38, 242)
|
||||
}
|
||||
return ansi.ColorCode("default")
|
||||
default:
|
||||
return ansi.ColorCode(style)
|
||||
|
|
@ -96,11 +87,9 @@ func mainRun() exitCode {
|
|||
cobra.MousetrapHelpText = ""
|
||||
}
|
||||
|
||||
rootCmd := root.NewCmdRoot(cmdFactory, buildVersion, buildDate)
|
||||
|
||||
cfg, err := cmdFactory.Config()
|
||||
rootCmd, err := root.NewCmdRoot(cmdFactory, buildVersion, buildDate)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "failed to read configuration: %s\n", err)
|
||||
fmt.Fprintf(stderr, "failed to create root command: %s\n", err)
|
||||
return exitError
|
||||
}
|
||||
|
||||
|
|
@ -109,110 +98,10 @@ func mainRun() exitCode {
|
|||
expandedArgs = os.Args[1:]
|
||||
}
|
||||
|
||||
// translate `gh help <command>` to `gh <command> --help` for extensions
|
||||
if len(expandedArgs) == 2 && expandedArgs[0] == "help" && !hasCommand(rootCmd, expandedArgs[1:]) {
|
||||
expandedArgs = []string{expandedArgs[1], "--help"}
|
||||
}
|
||||
|
||||
if !hasCommand(rootCmd, expandedArgs) {
|
||||
originalArgs := expandedArgs
|
||||
isShell := false
|
||||
|
||||
argsForExpansion := append([]string{"gh"}, expandedArgs...)
|
||||
expandedArgs, isShell, err = expand.ExpandAlias(cfg, argsForExpansion, nil)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "failed to process aliases: %s\n", err)
|
||||
return exitError
|
||||
}
|
||||
|
||||
if hasDebug {
|
||||
fmt.Fprintf(stderr, "%v -> %v\n", originalArgs, expandedArgs)
|
||||
}
|
||||
|
||||
if isShell {
|
||||
exe, err := safeexec.LookPath(expandedArgs[0])
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "failed to run external command: %s", err)
|
||||
return exitError
|
||||
}
|
||||
|
||||
externalCmd := exec.Command(exe, expandedArgs[1:]...)
|
||||
externalCmd.Stderr = os.Stderr
|
||||
externalCmd.Stdout = os.Stdout
|
||||
externalCmd.Stdin = os.Stdin
|
||||
preparedCmd := run.PrepareCmd(externalCmd)
|
||||
|
||||
err = preparedCmd.Run()
|
||||
if err != nil {
|
||||
var execError *exec.ExitError
|
||||
if errors.As(err, &execError) {
|
||||
return exitCode(execError.ExitCode())
|
||||
}
|
||||
fmt.Fprintf(stderr, "failed to run external command: %s\n", err)
|
||||
return exitError
|
||||
}
|
||||
|
||||
return exitOK
|
||||
} else if len(expandedArgs) > 0 && !hasCommand(rootCmd, expandedArgs) {
|
||||
extensionManager := cmdFactory.ExtensionManager
|
||||
if found, err := extensionManager.Dispatch(expandedArgs, os.Stdin, os.Stdout, os.Stderr); err != nil {
|
||||
var execError *exec.ExitError
|
||||
if errors.As(err, &execError) {
|
||||
return exitCode(execError.ExitCode())
|
||||
}
|
||||
fmt.Fprintf(stderr, "failed to run extension: %s\n", err)
|
||||
return exitError
|
||||
} else if found {
|
||||
return exitOK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// provide completions for aliases and extensions
|
||||
rootCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
var results []string
|
||||
aliases := cfg.Aliases()
|
||||
for aliasName, aliasValue := range aliases.All() {
|
||||
if strings.HasPrefix(aliasName, toComplete) {
|
||||
var s string
|
||||
if strings.HasPrefix(aliasValue, "!") {
|
||||
s = fmt.Sprintf("%s\tShell alias", aliasName)
|
||||
} else {
|
||||
aliasValue = text.Truncate(80, aliasValue)
|
||||
s = fmt.Sprintf("%s\tAlias for %s", aliasName, aliasValue)
|
||||
}
|
||||
results = append(results, s)
|
||||
}
|
||||
}
|
||||
for _, ext := range cmdFactory.ExtensionManager.List() {
|
||||
if strings.HasPrefix(ext.Name(), toComplete) {
|
||||
var s string
|
||||
if ext.IsLocal() {
|
||||
s = fmt.Sprintf("%s\tLocal extension gh-%s", ext.Name(), ext.Name())
|
||||
} else {
|
||||
path := ext.URL()
|
||||
if u, err := git.ParseURL(ext.URL()); err == nil {
|
||||
if r, err := ghrepo.FromURL(u); err == nil {
|
||||
path = ghrepo.FullName(r)
|
||||
}
|
||||
}
|
||||
s = fmt.Sprintf("%s\tExtension %s", ext.Name(), path)
|
||||
}
|
||||
results = append(results, s)
|
||||
}
|
||||
}
|
||||
return results, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
authError := errors.New("authError")
|
||||
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
// require that the user is authenticated before running most commands
|
||||
if cmdutil.IsAuthCheckEnabled(cmd) && !cmdutil.CheckAuth(cfg) {
|
||||
fmt.Fprint(stderr, authHelp())
|
||||
return authError
|
||||
}
|
||||
|
||||
return nil
|
||||
// translate `gh help <command>` to `gh <command> --help` for extensions.
|
||||
if len(expandedArgs) >= 2 && expandedArgs[0] == "help" && isExtensionCommand(rootCmd, expandedArgs[1:]) {
|
||||
expandedArgs = expandedArgs[1:]
|
||||
expandedArgs = append(expandedArgs, "--help")
|
||||
}
|
||||
|
||||
rootCmd.SetArgs(expandedArgs)
|
||||
|
|
@ -220,6 +109,8 @@ func mainRun() exitCode {
|
|||
if cmd, err := rootCmd.ExecuteContextC(ctx); err != nil {
|
||||
var pagerPipeError *iostreams.ErrClosedPagerPipe
|
||||
var noResultsError cmdutil.NoResultsError
|
||||
var execError *exec.ExitError
|
||||
var authError *root.AuthError
|
||||
if err == cmdutil.SilentError {
|
||||
return exitError
|
||||
} else if cmdutil.IsUserCancellation(err) {
|
||||
|
|
@ -228,7 +119,7 @@ func mainRun() exitCode {
|
|||
fmt.Fprint(stderr, "\n")
|
||||
}
|
||||
return exitCancel
|
||||
} else if errors.Is(err, authError) {
|
||||
} else if errors.As(err, &authError) {
|
||||
return exitAuth
|
||||
} else if errors.As(err, &pagerPipeError) {
|
||||
// ignore the error raised when piping to a closed pager
|
||||
|
|
@ -239,6 +130,8 @@ func mainRun() exitCode {
|
|||
}
|
||||
// no results is not a command failure
|
||||
return exitOK
|
||||
} else if errors.As(err, &execError) {
|
||||
return exitCode(execError.ExitCode())
|
||||
}
|
||||
|
||||
printError(stderr, err, cmd, hasDebug)
|
||||
|
|
@ -287,10 +180,10 @@ func mainRun() exitCode {
|
|||
return exitOK
|
||||
}
|
||||
|
||||
// hasCommand returns true if args resolve to a built-in command
|
||||
func hasCommand(rootCmd *cobra.Command, args []string) bool {
|
||||
c, _, err := rootCmd.Traverse(args)
|
||||
return err == nil && c != rootCmd
|
||||
// isExtensionCommand returns true if args resolve to an extension command.
|
||||
func isExtensionCommand(rootCmd *cobra.Command, args []string) bool {
|
||||
c, _, err := rootCmd.Find(args)
|
||||
return err == nil && c != nil && c.GroupID == "extension"
|
||||
}
|
||||
|
||||
func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) {
|
||||
|
|
@ -315,27 +208,6 @@ func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) {
|
|||
}
|
||||
}
|
||||
|
||||
func authHelp() string {
|
||||
if os.Getenv("GITHUB_ACTIONS") == "true" {
|
||||
return heredoc.Doc(`
|
||||
gh: To use GitHub CLI in a GitHub Actions workflow, set the GH_TOKEN environment variable. Example:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
`)
|
||||
}
|
||||
|
||||
if os.Getenv("CI") != "" {
|
||||
return heredoc.Doc(`
|
||||
gh: To use GitHub CLI in automation, set the GH_TOKEN environment variable.
|
||||
`)
|
||||
}
|
||||
|
||||
return heredoc.Doc(`
|
||||
To get started with GitHub CLI, please run: gh auth login
|
||||
Alternatively, populate the GH_TOKEN environment variable with a GitHub API authentication token.
|
||||
`)
|
||||
}
|
||||
|
||||
func shouldCheckForUpdate() bool {
|
||||
if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@ To be considered triaged, `enhancement` issues require at least one of the follo
|
|||
- `needs-investigation`: work that requires a mystery be solved by the core team before it can move forward
|
||||
- `needs-user-input`: we need more information from our users before the task can move forward
|
||||
|
||||
To be considered triaged, `bug` issues require a severity label: one of `p1`, `p2`, or `p3`
|
||||
To be considered triaged, `bug` issues require a severity label: one of `p1`, `p2`, or `p3`, which are defined as follows:
|
||||
- `p1`: Affects a large population and inhibits work
|
||||
- `p2`: Affects more than a few users but doesn't prevent core functions
|
||||
- `p3`: Affects a small number of users or is largely cosmetic
|
||||
|
||||
## Expectations for community pull requests
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,19 @@ type Client struct {
|
|||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (c *Client) Copy() *Client {
|
||||
return &Client{
|
||||
GhPath: c.GhPath,
|
||||
RepoDir: c.RepoDir,
|
||||
GitPath: c.GitPath,
|
||||
Stderr: c.Stderr,
|
||||
Stdin: c.Stdin,
|
||||
Stdout: c.Stdout,
|
||||
|
||||
commandContext: c.commandContext,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Command(ctx context.Context, args ...string) (*Command, error) {
|
||||
if c.RepoDir != "" {
|
||||
args = append([]string{"-C", c.RepoDir}, args...)
|
||||
|
|
@ -408,6 +421,44 @@ func (c *Client) revParse(ctx context.Context, args ...string) ([]byte, error) {
|
|||
return cmd.Output()
|
||||
}
|
||||
|
||||
func (c *Client) IsLocalGitRepo(ctx context.Context) (bool, error) {
|
||||
_, err := c.GitDir(ctx)
|
||||
if err != nil {
|
||||
var execError errWithExitCode
|
||||
if errors.As(err, &execError) && execError.ExitCode() == 128 {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *Client) UnsetRemoteResolution(ctx context.Context, name string) error {
|
||||
args := []string{"config", "--unset", fmt.Sprintf("remote.%s.gh-resolved", name)}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = cmd.Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) SetRemoteBranches(ctx context.Context, remote string, refspec string) error {
|
||||
args := []string{"remote", "set-branches", remote, refspec}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = cmd.Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Below are commands that make network calls and need authentication credentials supplied from gh.
|
||||
|
||||
func (c *Client) Fetch(ctx context.Context, remote string, refspec string, mods ...CommandModifier) error {
|
||||
|
|
@ -513,31 +564,6 @@ func (c *Client) AddRemote(ctx context.Context, name, urlStr string, trackingBra
|
|||
return remote, nil
|
||||
}
|
||||
|
||||
func (c *Client) IsLocalGitRepo(ctx context.Context) (bool, error) {
|
||||
_, err := c.GitDir(ctx)
|
||||
if err != nil {
|
||||
var execError errWithExitCode
|
||||
if errors.As(err, &execError) && execError.ExitCode() == 128 {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *Client) UnsetRemoteResolution(ctx context.Context, name string) error {
|
||||
args := []string{"config", "--unset", fmt.Sprintf("remote.%s.gh-resolved", name)}
|
||||
cmd, err := c.Command(ctx, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = cmd.Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveGitPath() (string, error) {
|
||||
path, err := safeexec.LookPath("git")
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -834,6 +834,84 @@ func TestClientPathFromRoot(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClientUnsetRemoteResolution(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cmdExitStatus int
|
||||
cmdStdout string
|
||||
cmdStderr string
|
||||
wantCmdArgs string
|
||||
wantErrorMsg string
|
||||
}{
|
||||
{
|
||||
name: "unset remote resolution",
|
||||
wantCmdArgs: `path/to/git config --unset remote.origin.gh-resolved`,
|
||||
},
|
||||
{
|
||||
name: "git error",
|
||||
cmdExitStatus: 1,
|
||||
cmdStderr: "git error message",
|
||||
wantCmdArgs: `path/to/git config --unset remote.origin.gh-resolved`,
|
||||
wantErrorMsg: "failed to run git: git error message",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
|
||||
client := Client{
|
||||
GitPath: "path/to/git",
|
||||
commandContext: cmdCtx,
|
||||
}
|
||||
err := client.UnsetRemoteResolution(context.Background(), "origin")
|
||||
assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
|
||||
if tt.wantErrorMsg == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tt.wantErrorMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientSetRemoteBranches(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cmdExitStatus int
|
||||
cmdStdout string
|
||||
cmdStderr string
|
||||
wantCmdArgs string
|
||||
wantErrorMsg string
|
||||
}{
|
||||
{
|
||||
name: "set remote branches",
|
||||
wantCmdArgs: `path/to/git remote set-branches origin trunk`,
|
||||
},
|
||||
{
|
||||
name: "git error",
|
||||
cmdExitStatus: 1,
|
||||
cmdStderr: "git error message",
|
||||
wantCmdArgs: `path/to/git remote set-branches origin trunk`,
|
||||
wantErrorMsg: "failed to run git: git error message",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr)
|
||||
client := Client{
|
||||
GitPath: "path/to/git",
|
||||
commandContext: cmdCtx,
|
||||
}
|
||||
err := client.SetRemoteBranches(context.Background(), "origin", "trunk")
|
||||
assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " "))
|
||||
if tt.wantErrorMsg == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tt.wantErrorMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientFetch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
34
go.mod
34
go.mod
|
|
@ -3,13 +3,13 @@ module github.com/cli/cli/v2
|
|||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/MakeNowJust/heredoc v1.0.0
|
||||
github.com/briandowns/spinner v1.18.1
|
||||
github.com/cenkalti/backoff/v4 v4.2.1
|
||||
github.com/charmbracelet/glamour v0.5.1-0.20220727184942-e70ff2d969da
|
||||
github.com/charmbracelet/lipgloss v0.5.0
|
||||
github.com/cli/go-gh/v2 v2.0.0
|
||||
github.com/cli/go-gh/v2 v2.0.1
|
||||
github.com/cli/oauth v1.0.1
|
||||
github.com/cli/safeexec v1.0.1
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2
|
||||
|
|
@ -25,23 +25,24 @@ require (
|
|||
github.com/joho/godotenv v1.5.1
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/mattn/go-colorable v0.1.13
|
||||
github.com/mattn/go-isatty v0.0.18
|
||||
github.com/mattn/go-isatty v0.0.19
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
|
||||
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38
|
||||
github.com/opentracing/opentracing-go v1.1.0
|
||||
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d
|
||||
github.com/shurcooL/githubv4 v0.0.0-20220520033151-0b4e3294ff00
|
||||
github.com/shurcooL/githubv4 v0.0.0-20230424031643-6cea62ecd5a9
|
||||
github.com/sourcegraph/jsonrpc2 v0.1.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.7.5
|
||||
github.com/zalando/go-keyring v0.2.2
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/zalando/go-keyring v0.2.3
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/term v0.6.0
|
||||
golang.org/x/text v0.8.0
|
||||
golang.org/x/term v0.7.0
|
||||
golang.org/x/text v0.9.0
|
||||
google.golang.org/grpc v1.49.0
|
||||
google.golang.org/protobuf v1.27.1
|
||||
gopkg.in/h2non/gock.v1 v1.1.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
|
|
@ -49,9 +50,9 @@ require (
|
|||
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||
github.com/alessio/shellescape v1.4.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/cli/browser v1.1.0 // indirect
|
||||
github.com/cli/browser v1.2.0 // indirect
|
||||
github.com/cli/shurcooL-graphql v0.0.3 // indirect
|
||||
github.com/danieljoos/wincred v1.1.2 // indirect
|
||||
github.com/danieljoos/wincred v1.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/fatih/color v1.7.0 // indirect
|
||||
|
|
@ -59,10 +60,11 @@ require (
|
|||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/itchyny/gojq v0.12.8 // indirect
|
||||
github.com/itchyny/timefmt-go v0.1.3 // indirect
|
||||
github.com/itchyny/gojq v0.12.13 // indirect
|
||||
github.com/itchyny/timefmt-go v0.1.5 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.20 // indirect
|
||||
|
|
@ -70,16 +72,16 @@ require (
|
|||
github.com/muesli/termenv v0.12.0 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 // indirect
|
||||
github.com/stretchr/objx v0.4.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect
|
||||
github.com/yuin/goldmark v1.4.13 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.1 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/net v0.9.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
)
|
||||
|
|
|
|||
74
go.sum
74
go.sum
|
|
@ -31,8 +31,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
|||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
|
|
@ -58,12 +58,12 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
|
|||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q=
|
||||
github.com/cli/browser v1.1.0 h1:xOZBfkfY9L9vMBgqb1YwRirGu6QFaQ5dP/vXt5ENSOY=
|
||||
github.com/cli/browser v1.1.0/go.mod h1:HKMQAt9t12kov91Mn7RfZxyJQQgWgyS/3SZswlZ5iTI=
|
||||
github.com/cli/browser v1.2.0 h1:yvU7e9qf97kZqGFX6n2zJPHsmSObY9ske+iCvKelvXg=
|
||||
github.com/cli/browser v1.2.0/go.mod h1:xFFnXLVcAyW9ni0cuo6NnrbCP75JxJ0RO7VtCBiH/oI=
|
||||
github.com/cli/crypto v0.0.0-20210929142629-6be313f59b03 h1:3f4uHLfWx4/WlnMPXGai03eoWAI+oGHJwr+5OXfxCr8=
|
||||
github.com/cli/crypto v0.0.0-20210929142629-6be313f59b03/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
github.com/cli/go-gh/v2 v2.0.0 h1:JAgQY7VNHletsO0Eqr+/PzF7fF5QEjhY2t2+Tev3vmk=
|
||||
github.com/cli/go-gh/v2 v2.0.0/go.mod h1:2/ox3Dnc8wDBT5bnTAH1aKGy6Qt1ztlFBe10EufnvoA=
|
||||
github.com/cli/go-gh/v2 v2.0.1 h1:W4L7C5xT9CwkcqTsUzBhFhRKGpek9oRtdDLku2Hku+E=
|
||||
github.com/cli/go-gh/v2 v2.0.1/go.mod h1:zWab1jRnJ0Ug8qRjsZHFk/Oq51ZWuhSxRL2FDUDgQWk=
|
||||
github.com/cli/oauth v1.0.1 h1:pXnTFl/qUegXHK531Dv0LNjW4mLx626eS42gnzfXJPA=
|
||||
github.com/cli/oauth v1.0.1/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4=
|
||||
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
|
||||
|
|
@ -78,8 +78,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
|
|||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
|
||||
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
|
||||
github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE=
|
||||
github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
|
@ -138,7 +138,6 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
|
|
@ -162,6 +161,7 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
|||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
|
|
@ -175,10 +175,10 @@ github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4Dvx
|
|||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/itchyny/gojq v0.12.8 h1:Zxcwq8w4IeR8JJYEtoG2MWJZUv0RGY6QqJcO1cqV8+A=
|
||||
github.com/itchyny/gojq v0.12.8/go.mod h1:gE2kZ9fVRU0+JAksaTzjIlgnCa2akU+a1V0WXgJQN5c=
|
||||
github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921iRkU=
|
||||
github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A=
|
||||
github.com/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU=
|
||||
github.com/itchyny/gojq v0.12.13/go.mod h1:JzwzAqenfhrPUuwbmEz3nu3JQmFLlQTQMUcOdnu/Sf4=
|
||||
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
|
||||
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
|
|
@ -199,8 +199,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
|||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
|
|
@ -224,6 +224,8 @@ github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc
|
|||
github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A=
|
||||
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38 h1:0FrBxrkJ0hVembTb/e4EU5Ml6vLcOusAqymmYISg5Uo=
|
||||
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
|
||||
|
|
@ -235,13 +237,13 @@ github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d h1:jKIUJdMcIVGOSHi6LSqJ
|
|||
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d/go.mod h1:YX2wUZOcJGOIycErz2s9KvDaP0jnWwRCirQMPLPpQ+Y=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
|
||||
github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/githubv4 v0.0.0-20220520033151-0b4e3294ff00 h1:fiFvD4lT0aWjuuAb64LlZ/67v87m+Kc9Qsu5cMFNK0w=
|
||||
github.com/shurcooL/githubv4 v0.0.0-20220520033151-0b4e3294ff00/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
|
||||
github.com/shurcooL/githubv4 v0.0.0-20230424031643-6cea62ecd5a9 h1:nCBaIs5/R0HFP5+aPW/SzFUF8z0oKuCXmuDmHWaxzjY=
|
||||
github.com/shurcooL/githubv4 v0.0.0-20230424031643-6cea62ecd5a9/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
|
||||
github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29 h1:B1PEwpArrNp4dkQrfxh/abbBAOZBVp0ds+fBEOUOqOc=
|
||||
github.com/shurcooL/graphql v0.0.0-20220606043923-3cf50f8a0a29/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
|
||||
github.com/sourcegraph/jsonrpc2 v0.1.0 h1:ohJHjZ+PcaLxDUjqk2NC3tIGsVa5bXThe1ZheSXOjuk=
|
||||
|
|
@ -251,14 +253,16 @@ github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUq
|
|||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q=
|
||||
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8=
|
||||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
|
@ -271,8 +275,8 @@ github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
|
|||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
|
||||
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
|
||||
github.com/zalando/go-keyring v0.2.2 h1:f0xmpYiSrHtSNAVgwip93Cg8tuF45HJM6rHq/A5RI/4=
|
||||
github.com/zalando/go-keyring v0.2.2/go.mod h1:sI3evg9Wvpw3+n4SqplGSJUMwtDeROfD4nsFz4z9PG0=
|
||||
github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms=
|
||||
github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
|
|
@ -341,8 +345,8 @@ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
|
@ -389,27 +393,25 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
|
@ -417,9 +419,10 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
|
@ -557,6 +560,7 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
|
|||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package build
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
|
|
@ -16,4 +17,11 @@ func init() {
|
|||
Version = info.Main.Version
|
||||
}
|
||||
}
|
||||
|
||||
// Signal the tcell library to skip its expensive `init` block. This saves 30-40ms in startup
|
||||
// time for the gh process. The downside is that some Unicode glyphs from user-generated
|
||||
// content might cause mis-alignment in tcell-enabled views.
|
||||
//
|
||||
// https://github.com/gdamore/tcell/commit/2f889d79bd61b1fd2f43372529975a65b792a7ae
|
||||
_ = os.Setenv("TCELL_MINIMIZE", "1")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,6 +192,15 @@ type Codespace struct {
|
|||
PendingOperationDisabledReason string `json:"pending_operation_disabled_reason"`
|
||||
IdleTimeoutNotice string `json:"idle_timeout_notice"`
|
||||
WebURL string `json:"web_url"`
|
||||
DevContainerPath string `json:"devcontainer_path"`
|
||||
Prebuild bool `json:"prebuild"`
|
||||
Location string `json:"location"`
|
||||
IdleTimeoutMinutes int `json:"idle_timeout_minutes"`
|
||||
RetentionPeriodMinutes int `json:"retention_period_minutes"`
|
||||
RetentionExpiresAt string `json:"retention_expires_at"`
|
||||
RecentFolders []string `json:"recent_folders"`
|
||||
BillableOwner User `json:"billable_owner"`
|
||||
EnvironmentId string `json:"environment_id"`
|
||||
}
|
||||
|
||||
type CodespaceGitStatus struct {
|
||||
|
|
@ -230,8 +239,8 @@ type CodespaceConnection struct {
|
|||
HostPublicKeys []string `json:"hostPublicKeys"`
|
||||
}
|
||||
|
||||
// CodespaceFields is the list of exportable fields for a codespace.
|
||||
var CodespaceFields = []string{
|
||||
// ListCodespaceFields is the list of exportable fields for a codespace when using the `gh cs list` command.
|
||||
var ListCodespaceFields = []string{
|
||||
"displayName",
|
||||
"name",
|
||||
"owner",
|
||||
|
|
@ -244,6 +253,30 @@ var CodespaceFields = []string{
|
|||
"vscsTarget",
|
||||
}
|
||||
|
||||
// ViewCodespaceFields is the list of exportable fields for a codespace when using the `gh cs view` command.
|
||||
var ViewCodespaceFields = []string{
|
||||
"name",
|
||||
"displayName",
|
||||
"state",
|
||||
"owner",
|
||||
"billableOwner",
|
||||
"location",
|
||||
"repository",
|
||||
"gitStatus",
|
||||
"devcontainerPath",
|
||||
"machineName",
|
||||
"machineDisplayName",
|
||||
"prebuild",
|
||||
"createdAt",
|
||||
"lastUsedAt",
|
||||
"idleTimeoutMinutes",
|
||||
"retentionPeriodDays",
|
||||
"retentionExpiresAt",
|
||||
"recentFolders",
|
||||
"vscsTarget",
|
||||
"environmentId",
|
||||
}
|
||||
|
||||
func (c *Codespace) ExportData(fields []string) map[string]interface{} {
|
||||
v := reflect.ValueOf(c).Elem()
|
||||
data := map[string]interface{}{}
|
||||
|
|
@ -256,11 +289,17 @@ func (c *Codespace) ExportData(fields []string) map[string]interface{} {
|
|||
data[f] = c.Repository.FullName
|
||||
case "machineName":
|
||||
data[f] = c.Machine.Name
|
||||
case "machineDisplayName":
|
||||
data[f] = c.Machine.DisplayName
|
||||
case "retentionPeriodDays":
|
||||
data[f] = c.RetentionPeriodMinutes / 1440
|
||||
case "gitStatus":
|
||||
data[f] = map[string]interface{}{
|
||||
"ref": c.GitStatus.Ref,
|
||||
"hasUnpushedChanges": c.GitStatus.HasUnpushedChanges,
|
||||
"hasUncommittedChanges": c.GitStatus.HasUncommittedChanges,
|
||||
"ahead": c.GitStatus.Ahead,
|
||||
"behind": c.GitStatus.Behind,
|
||||
}
|
||||
case "vscsTarget":
|
||||
if c.VSCSTarget != "" && c.VSCSTarget != VSCSTargetProduction {
|
||||
|
|
|
|||
|
|
@ -22,30 +22,25 @@ var allIssueFeatures = IssueFeatures{
|
|||
}
|
||||
|
||||
type PullRequestFeatures struct {
|
||||
ReviewDecision bool
|
||||
StatusCheckRollup bool
|
||||
BranchProtectionRule bool
|
||||
MergeQueue bool
|
||||
MergeQueue bool
|
||||
// CheckRunAndStatusContextCounts indicates whether the API supports
|
||||
// the checkRunCount, checkRunCountsByState, statusContextCount and stausContextCountsByState
|
||||
// fields on the StatusCheckRollupContextConnection
|
||||
CheckRunAndStatusContextCounts bool
|
||||
}
|
||||
|
||||
var allPullRequestFeatures = PullRequestFeatures{
|
||||
ReviewDecision: true,
|
||||
StatusCheckRollup: true,
|
||||
BranchProtectionRule: true,
|
||||
MergeQueue: true,
|
||||
MergeQueue: true,
|
||||
CheckRunAndStatusContextCounts: true,
|
||||
}
|
||||
|
||||
type RepositoryFeatures struct {
|
||||
IssueTemplateMutation bool
|
||||
IssueTemplateQuery bool
|
||||
PullRequestTemplateQuery bool
|
||||
VisibilityField bool
|
||||
AutoMerge bool
|
||||
}
|
||||
|
||||
var allRepositoryFeatures = RepositoryFeatures{
|
||||
IssueTemplateMutation: true,
|
||||
IssueTemplateQuery: true,
|
||||
PullRequestTemplateQuery: true,
|
||||
VisibilityField: true,
|
||||
AutoMerge: true,
|
||||
|
|
@ -103,32 +98,40 @@ func (d *detector) PullRequestFeatures() (PullRequestFeatures, error) {
|
|||
// return allPullRequestFeatures, nil
|
||||
// }
|
||||
|
||||
features := PullRequestFeatures{
|
||||
ReviewDecision: true,
|
||||
StatusCheckRollup: true,
|
||||
BranchProtectionRule: true,
|
||||
}
|
||||
|
||||
var featureDetection struct {
|
||||
var pullRequestFeatureDetection struct {
|
||||
PullRequest struct {
|
||||
Fields []struct {
|
||||
Name string
|
||||
} `graphql:"fields(includeDeprecated: true)"`
|
||||
} `graphql:"PullRequest: __type(name: \"PullRequest\")"`
|
||||
StatusCheckRollupContextConnection struct {
|
||||
Fields []struct {
|
||||
Name string
|
||||
} `graphql:"fields(includeDeprecated: true)"`
|
||||
} `graphql:"StatusCheckRollupContextConnection: __type(name: \"StatusCheckRollupContextConnection\")"`
|
||||
}
|
||||
|
||||
gql := api.NewClientFromHTTP(d.httpClient)
|
||||
err := gql.Query(d.host, "PullRequest_fields", &featureDetection, nil)
|
||||
if err != nil {
|
||||
return features, err
|
||||
if err := gql.Query(d.host, "PullRequest_fields", &pullRequestFeatureDetection, nil); err != nil {
|
||||
return PullRequestFeatures{}, err
|
||||
}
|
||||
|
||||
for _, field := range featureDetection.PullRequest.Fields {
|
||||
features := PullRequestFeatures{}
|
||||
|
||||
for _, field := range pullRequestFeatureDetection.PullRequest.Fields {
|
||||
if field.Name == "isInMergeQueue" {
|
||||
features.MergeQueue = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, field := range pullRequestFeatureDetection.StatusCheckRollupContextConnection.Fields {
|
||||
// We only check for checkRunCount here but it, checkRunCountsByState, statusContextCount and statusContextCountsByState
|
||||
// were all introduced in the same version of the API.
|
||||
if field.Name == "checkRunCount" {
|
||||
features.CheckRunAndStatusContextCounts = true
|
||||
}
|
||||
}
|
||||
|
||||
return features, nil
|
||||
}
|
||||
|
||||
|
|
@ -137,10 +140,7 @@ func (d *detector) RepositoryFeatures() (RepositoryFeatures, error) {
|
|||
return allRepositoryFeatures, nil
|
||||
}
|
||||
|
||||
features := RepositoryFeatures{
|
||||
IssueTemplateQuery: true,
|
||||
IssueTemplateMutation: true,
|
||||
}
|
||||
features := RepositoryFeatures{}
|
||||
|
||||
var featureDetection struct {
|
||||
Repository struct {
|
||||
|
|
|
|||
|
|
@ -82,21 +82,32 @@ func TestPullRequestFeatures(t *testing.T) {
|
|||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "github.com",
|
||||
name: "github.com with merge queue and status check counts by state",
|
||||
hostname: "github.com",
|
||||
queryResponse: map[string]string{
|
||||
`query PullRequest_fields\b`: heredoc.Doc(`
|
||||
{ "data": { "PullRequest": { "fields": [
|
||||
{"name": "isInMergeQueue"},
|
||||
{"name": "isMergeQueueEnabled"}
|
||||
] } } }
|
||||
`),
|
||||
{
|
||||
"data": {
|
||||
"PullRequest": {
|
||||
"fields": [
|
||||
{"name": "isInMergeQueue"},
|
||||
{"name": "isMergeQueueEnabled"}
|
||||
]
|
||||
},
|
||||
"StatusCheckRollupContextConnection": {
|
||||
"fields": [
|
||||
{"name": "checkRunCount"},
|
||||
{"name": "checkRunCountsByState"},
|
||||
{"name": "statusContextCount"},
|
||||
{"name": "statusContextCountsByState"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
wantFeatures: PullRequestFeatures{
|
||||
ReviewDecision: true,
|
||||
StatusCheckRollup: true,
|
||||
BranchProtectionRule: true,
|
||||
MergeQueue: true,
|
||||
MergeQueue: true,
|
||||
CheckRunAndStatusContextCounts: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
@ -105,34 +116,77 @@ func TestPullRequestFeatures(t *testing.T) {
|
|||
hostname: "github.com",
|
||||
queryResponse: map[string]string{
|
||||
`query PullRequest_fields\b`: heredoc.Doc(`
|
||||
{ "data": { "PullRequest": { "fields": [
|
||||
] } } }
|
||||
`),
|
||||
{
|
||||
"data": {
|
||||
"PullRequest": {
|
||||
"fields": []
|
||||
},
|
||||
"StatusCheckRollupContextConnection": {
|
||||
"fields": [
|
||||
{"name": "checkRunCount"},
|
||||
{"name": "checkRunCountsByState"},
|
||||
{"name": "statusContextCount"},
|
||||
{"name": "statusContextCountsByState"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
wantFeatures: PullRequestFeatures{
|
||||
ReviewDecision: true,
|
||||
StatusCheckRollup: true,
|
||||
BranchProtectionRule: true,
|
||||
MergeQueue: false,
|
||||
MergeQueue: false,
|
||||
CheckRunAndStatusContextCounts: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "GHE",
|
||||
name: "GHE with merge queue and status check counts by state",
|
||||
hostname: "git.my.org",
|
||||
queryResponse: map[string]string{
|
||||
`query PullRequest_fields\b`: heredoc.Doc(`
|
||||
{ "data": { "PullRequest": { "fields": [
|
||||
{"name": "isInMergeQueue"},
|
||||
{"name": "isMergeQueueEnabled"}
|
||||
] } } }
|
||||
`),
|
||||
{
|
||||
"data": {
|
||||
"PullRequest": {
|
||||
"fields": [
|
||||
{"name": "isInMergeQueue"},
|
||||
{"name": "isMergeQueueEnabled"}
|
||||
]
|
||||
},
|
||||
"StatusCheckRollupContextConnection": {
|
||||
"fields": [
|
||||
{"name": "checkRunCount"},
|
||||
{"name": "checkRunCountsByState"},
|
||||
{"name": "statusContextCount"},
|
||||
{"name": "statusContextCountsByState"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
wantFeatures: PullRequestFeatures{
|
||||
ReviewDecision: true,
|
||||
StatusCheckRollup: true,
|
||||
BranchProtectionRule: true,
|
||||
MergeQueue: true,
|
||||
MergeQueue: true,
|
||||
CheckRunAndStatusContextCounts: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "GHE without merge queue and status check counts by state",
|
||||
hostname: "git.my.org",
|
||||
queryResponse: map[string]string{
|
||||
`query PullRequest_fields\b`: heredoc.Doc(`
|
||||
{
|
||||
"data": {
|
||||
"PullRequest": {
|
||||
"fields": []
|
||||
},
|
||||
"StatusCheckRollupContextConnection": {
|
||||
"fields": []
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
wantFeatures: PullRequestFeatures{
|
||||
MergeQueue: false,
|
||||
CheckRunAndStatusContextCounts: false,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
@ -169,8 +223,6 @@ func TestRepositoryFeatures(t *testing.T) {
|
|||
name: "github.com",
|
||||
hostname: "github.com",
|
||||
wantFeatures: RepositoryFeatures{
|
||||
IssueTemplateMutation: true,
|
||||
IssueTemplateQuery: true,
|
||||
PullRequestTemplateQuery: true,
|
||||
VisibilityField: true,
|
||||
AutoMerge: true,
|
||||
|
|
@ -184,8 +236,6 @@ func TestRepositoryFeatures(t *testing.T) {
|
|||
`query Repository_fields\b`: `{"data": {}}`,
|
||||
},
|
||||
wantFeatures: RepositoryFeatures{
|
||||
IssueTemplateMutation: true,
|
||||
IssueTemplateQuery: true,
|
||||
PullRequestTemplateQuery: false,
|
||||
},
|
||||
wantErr: false,
|
||||
|
|
@ -201,8 +251,6 @@ func TestRepositoryFeatures(t *testing.T) {
|
|||
`),
|
||||
},
|
||||
wantFeatures: RepositoryFeatures{
|
||||
IssueTemplateMutation: true,
|
||||
IssueTemplateQuery: true,
|
||||
PullRequestTemplateQuery: true,
|
||||
},
|
||||
wantErr: false,
|
||||
|
|
@ -218,9 +266,7 @@ func TestRepositoryFeatures(t *testing.T) {
|
|||
`),
|
||||
},
|
||||
wantFeatures: RepositoryFeatures{
|
||||
IssueTemplateMutation: true,
|
||||
IssueTemplateQuery: true,
|
||||
VisibilityField: true,
|
||||
VisibilityField: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
@ -235,9 +281,7 @@ func TestRepositoryFeatures(t *testing.T) {
|
|||
`),
|
||||
},
|
||||
wantFeatures: RepositoryFeatures{
|
||||
IssueTemplateMutation: true,
|
||||
IssueTemplateQuery: true,
|
||||
AutoMerge: true,
|
||||
AutoMerge: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import (
|
|||
//go:generate moq -rm -out prompter_mock.go . Prompter
|
||||
type Prompter interface {
|
||||
Select(string, string, []string) (int, error)
|
||||
MultiSelect(string, string, []string) ([]string, error)
|
||||
MultiSelect(string, []string, []string) ([]int, error)
|
||||
Input(string, string) (string, error)
|
||||
InputHostname() (string, error)
|
||||
Password(string) (string, error)
|
||||
|
|
@ -86,7 +86,7 @@ func (p *surveyPrompter) Select(message, defaultValue string, options []string)
|
|||
return
|
||||
}
|
||||
|
||||
func (p *surveyPrompter) MultiSelect(message, defaultValue string, options []string) (result []string, err error) {
|
||||
func (p *surveyPrompter) MultiSelect(message string, defaultValues, options []string) (result []int, err error) {
|
||||
q := &survey.MultiSelect{
|
||||
Message: message,
|
||||
Options: options,
|
||||
|
|
@ -94,8 +94,17 @@ func (p *surveyPrompter) MultiSelect(message, defaultValue string, options []str
|
|||
Filter: LatinMatchingFilter,
|
||||
}
|
||||
|
||||
if defaultValue != "" {
|
||||
q.Default = defaultValue
|
||||
if len(defaultValues) > 0 {
|
||||
// TODO I don't actually know that this is needed, just being extra cautious
|
||||
validatedDefault := []string{}
|
||||
for _, x := range defaultValues {
|
||||
for _, y := range options {
|
||||
if x == y {
|
||||
validatedDefault = append(validatedDefault, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
q.Default = validatedDefault
|
||||
}
|
||||
|
||||
err = p.ask(q, &result)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ var _ Prompter = &PrompterMock{}
|
|||
// MarkdownEditorFunc: func(s1 string, s2 string, b bool) (string, error) {
|
||||
// panic("mock out the MarkdownEditor method")
|
||||
// },
|
||||
// MultiSelectFunc: func(s1 string, s2 string, strings []string) ([]string, error) {
|
||||
// MultiSelectFunc: func(s string, strings1 []string, strings2 []string) ([]int, error) {
|
||||
// panic("mock out the MultiSelect method")
|
||||
// },
|
||||
// PasswordFunc: func(s string) (string, error) {
|
||||
|
|
@ -70,7 +70,7 @@ type PrompterMock struct {
|
|||
MarkdownEditorFunc func(s1 string, s2 string, b bool) (string, error)
|
||||
|
||||
// MultiSelectFunc mocks the MultiSelect method.
|
||||
MultiSelectFunc func(s1 string, s2 string, strings []string) ([]string, error)
|
||||
MultiSelectFunc func(s string, strings1 []string, strings2 []string) ([]int, error)
|
||||
|
||||
// PasswordFunc mocks the Password method.
|
||||
PasswordFunc func(s string) (string, error)
|
||||
|
|
@ -116,12 +116,12 @@ type PrompterMock struct {
|
|||
}
|
||||
// MultiSelect holds details about calls to the MultiSelect method.
|
||||
MultiSelect []struct {
|
||||
// S1 is the s1 argument value.
|
||||
S1 string
|
||||
// S2 is the s2 argument value.
|
||||
S2 string
|
||||
// Strings is the strings argument value.
|
||||
Strings []string
|
||||
// S is the s argument value.
|
||||
S string
|
||||
// Strings1 is the strings1 argument value.
|
||||
Strings1 []string
|
||||
// Strings2 is the strings2 argument value.
|
||||
Strings2 []string
|
||||
}
|
||||
// Password holds details about calls to the Password method.
|
||||
Password []struct {
|
||||
|
|
@ -348,23 +348,23 @@ func (mock *PrompterMock) MarkdownEditorCalls() []struct {
|
|||
}
|
||||
|
||||
// MultiSelect calls MultiSelectFunc.
|
||||
func (mock *PrompterMock) MultiSelect(s1 string, s2 string, strings []string) ([]string, error) {
|
||||
func (mock *PrompterMock) MultiSelect(s string, strings1 []string, strings2 []string) ([]int, error) {
|
||||
if mock.MultiSelectFunc == nil {
|
||||
panic("PrompterMock.MultiSelectFunc: method is nil but Prompter.MultiSelect was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
S1 string
|
||||
S2 string
|
||||
Strings []string
|
||||
S string
|
||||
Strings1 []string
|
||||
Strings2 []string
|
||||
}{
|
||||
S1: s1,
|
||||
S2: s2,
|
||||
Strings: strings,
|
||||
S: s,
|
||||
Strings1: strings1,
|
||||
Strings2: strings2,
|
||||
}
|
||||
mock.lockMultiSelect.Lock()
|
||||
mock.calls.MultiSelect = append(mock.calls.MultiSelect, callInfo)
|
||||
mock.lockMultiSelect.Unlock()
|
||||
return mock.MultiSelectFunc(s1, s2, strings)
|
||||
return mock.MultiSelectFunc(s, strings1, strings2)
|
||||
}
|
||||
|
||||
// MultiSelectCalls gets all the calls that were made to MultiSelect.
|
||||
|
|
@ -372,14 +372,14 @@ func (mock *PrompterMock) MultiSelect(s1 string, s2 string, strings []string) ([
|
|||
//
|
||||
// len(mockedPrompter.MultiSelectCalls())
|
||||
func (mock *PrompterMock) MultiSelectCalls() []struct {
|
||||
S1 string
|
||||
S2 string
|
||||
Strings []string
|
||||
S string
|
||||
Strings1 []string
|
||||
Strings2 []string
|
||||
} {
|
||||
var calls []struct {
|
||||
S1 string
|
||||
S2 string
|
||||
Strings []string
|
||||
S string
|
||||
Strings1 []string
|
||||
Strings2 []string
|
||||
}
|
||||
mock.lockMultiSelect.RLock()
|
||||
calls = mock.calls.MultiSelect
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ type ConfirmStub struct {
|
|||
type MultiSelectStub struct {
|
||||
Prompt string
|
||||
ExpectedOpts []string
|
||||
Fn func(string, string, []string) ([]string, error)
|
||||
Fn func(string, []string, []string) ([]int, error)
|
||||
}
|
||||
|
||||
type InputHostnameStub struct {
|
||||
|
|
@ -84,8 +84,6 @@ type MockPrompter struct {
|
|||
ConfirmDeletionStubs []ConfirmDeletionStub
|
||||
}
|
||||
|
||||
// TODO thread safety
|
||||
|
||||
func NewMockPrompter(t *testing.T) *MockPrompter {
|
||||
m := &MockPrompter{
|
||||
t: t,
|
||||
|
|
@ -120,17 +118,17 @@ func NewMockPrompter(t *testing.T) *MockPrompter {
|
|||
return s.Fn(p, d, opts)
|
||||
}
|
||||
|
||||
m.MultiSelectFunc = func(p, d string, opts []string) ([]string, error) {
|
||||
m.MultiSelectFunc = func(p string, d, opts []string) ([]int, error) {
|
||||
var s MultiSelectStub
|
||||
if len(m.SelectStubs) > 0 {
|
||||
if len(m.MultiSelectStubs) > 0 {
|
||||
s = m.MultiSelectStubs[0]
|
||||
m.SelectStubs = m.SelectStubs[1:len(m.SelectStubs)]
|
||||
m.MultiSelectStubs = m.MultiSelectStubs[1:len(m.MultiSelectStubs)]
|
||||
} else {
|
||||
return []string{}, NoSuchPromptErr(p)
|
||||
return []int{}, NoSuchPromptErr(p)
|
||||
}
|
||||
|
||||
if s.Prompt != p {
|
||||
return []string{}, NoSuchPromptErr(p)
|
||||
return []int{}, NoSuchPromptErr(p)
|
||||
}
|
||||
|
||||
AssertOptions(m.t, s.ExpectedOpts, opts)
|
||||
|
|
@ -240,7 +238,7 @@ func (m *MockPrompter) RegisterSelect(prompt string, opts []string, stub func(_,
|
|||
Fn: stub})
|
||||
}
|
||||
|
||||
func (m *MockPrompter) RegisterMultiSelect(prompt string, opts []string, stub func(_, _ string, _ []string) ([]string, error)) {
|
||||
func (m *MockPrompter) RegisterMultiSelect(prompt string, d, opts []string, stub func(_ string, _, _ []string) ([]int, error)) {
|
||||
m.MultiSelectStubs = append(m.MultiSelectStubs, MultiSelectStub{
|
||||
Prompt: prompt,
|
||||
ExpectedOpts: opts,
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ func actionsExplainer(cs *iostreams.ColorScheme) string {
|
|||
To see more help, run 'gh help run <subcommand>'
|
||||
|
||||
%s
|
||||
gh workflow list: List all the workflow files in your repository
|
||||
gh workflow list: List workflow files in your repository
|
||||
gh workflow view: View details for a workflow file
|
||||
gh workflow enable: Enable a workflow file
|
||||
gh workflow disable: Disable a workflow file
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
package expand
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/pkg/findsh"
|
||||
"github.com/google/shlex"
|
||||
)
|
||||
|
||||
// ExpandAlias processes argv to see if it should be rewritten according to a user's aliases. The
|
||||
// second return value indicates whether the alias should be executed in a new shell process instead
|
||||
// of running gh itself.
|
||||
func ExpandAlias(cfg config.Config, args []string, findShFunc func() (string, error)) (expanded []string, isShell bool, err error) {
|
||||
if len(args) < 2 {
|
||||
// the command is lacking a subcommand
|
||||
return
|
||||
}
|
||||
expanded = args[1:]
|
||||
|
||||
aliases := cfg.Aliases()
|
||||
|
||||
expansion, getErr := aliases.Get(args[1])
|
||||
if getErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(expansion, "!") {
|
||||
isShell = true
|
||||
if findShFunc == nil {
|
||||
findShFunc = findSh
|
||||
}
|
||||
shPath, shErr := findShFunc()
|
||||
if shErr != nil {
|
||||
err = shErr
|
||||
return
|
||||
}
|
||||
|
||||
expanded = []string{shPath, "-c", expansion[1:]}
|
||||
|
||||
if len(args[2:]) > 0 {
|
||||
expanded = append(expanded, "--")
|
||||
expanded = append(expanded, args[2:]...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
extraArgs := []string{}
|
||||
for i, a := range args[2:] {
|
||||
if !strings.Contains(expansion, "$") {
|
||||
extraArgs = append(extraArgs, a)
|
||||
} else {
|
||||
expansion = strings.ReplaceAll(expansion, fmt.Sprintf("$%d", i+1), a)
|
||||
}
|
||||
}
|
||||
lingeringRE := regexp.MustCompile(`\$\d`)
|
||||
if lingeringRE.MatchString(expansion) {
|
||||
err = fmt.Errorf("not enough arguments for alias: %s", expansion)
|
||||
return
|
||||
}
|
||||
|
||||
var newArgs []string
|
||||
newArgs, err = shlex.Split(expansion)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
expanded = append(newArgs, extraArgs...)
|
||||
return
|
||||
}
|
||||
|
||||
func findSh() (string, error) {
|
||||
shPath, err := findsh.Find()
|
||||
if err != nil {
|
||||
if errors.Is(err, exec.ErrNotFound) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return "", errors.New("unable to locate sh to execute the shell alias with. The sh.exe interpreter is typically distributed with Git for Windows.")
|
||||
}
|
||||
return "", errors.New("unable to locate sh to execute shell alias with")
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return shPath, nil
|
||||
}
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
package expand
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
)
|
||||
|
||||
func TestExpandAlias(t *testing.T) {
|
||||
findShFunc := func() (string, error) {
|
||||
return "/usr/bin/sh", nil
|
||||
}
|
||||
|
||||
cfg := config.NewFromString(heredoc.Doc(`
|
||||
aliases:
|
||||
co: pr checkout
|
||||
il: issue list --author="$1" --label="$2"
|
||||
ia: issue list --author="$1" --assignee="$1"
|
||||
`))
|
||||
|
||||
type args struct {
|
||||
config config.Config
|
||||
argv []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantExpanded []string
|
||||
wantIsShell bool
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{},
|
||||
},
|
||||
wantExpanded: []string(nil),
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "too few arguments",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh"},
|
||||
},
|
||||
wantExpanded: []string(nil),
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "no expansion",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "pr", "status"},
|
||||
},
|
||||
wantExpanded: []string{"pr", "status"},
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "simple expansion",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "co"},
|
||||
},
|
||||
wantExpanded: []string{"pr", "checkout"},
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "adding arguments after expansion",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "co", "123"},
|
||||
},
|
||||
wantExpanded: []string{"pr", "checkout", "123"},
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "not enough arguments for expansion",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "il"},
|
||||
},
|
||||
wantExpanded: []string{},
|
||||
wantIsShell: false,
|
||||
wantErr: errors.New(`not enough arguments for alias: issue list --author="$1" --label="$2"`),
|
||||
},
|
||||
{
|
||||
name: "not enough arguments for expansion 2",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "il", "vilmibm"},
|
||||
},
|
||||
wantExpanded: []string{},
|
||||
wantIsShell: false,
|
||||
wantErr: errors.New(`not enough arguments for alias: issue list --author="vilmibm" --label="$2"`),
|
||||
},
|
||||
{
|
||||
name: "satisfy expansion arguments",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "il", "vilmibm", "help wanted"},
|
||||
},
|
||||
wantExpanded: []string{"issue", "list", "--author=vilmibm", "--label=help wanted"},
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "mixed positional and non-positional arguments",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "il", "vilmibm", "epic", "-R", "monalisa/testing"},
|
||||
},
|
||||
wantExpanded: []string{"issue", "list", "--author=vilmibm", "--label=epic", "-R", "monalisa/testing"},
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "dollar in expansion",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "ia", "$coolmoney$"},
|
||||
},
|
||||
wantExpanded: []string{"issue", "list", "--author=$coolmoney$", "--assignee=$coolmoney$"},
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotExpanded, gotIsShell, err := ExpandAlias(tt.args.config, tt.args.argv, findShFunc)
|
||||
if tt.wantErr != nil {
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if tt.wantErr.Error() != err.Error() {
|
||||
t.Fatalf("expected error %q, got %q", tt.wantErr, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(gotExpanded, tt.wantExpanded) {
|
||||
t.Errorf("ExpandAlias() gotExpanded = %v, want %v", gotExpanded, tt.wantExpanded)
|
||||
}
|
||||
if gotIsShell != tt.wantIsShell {
|
||||
t.Errorf("ExpandAlias() gotIsShell = %v, want %v", gotIsShell, tt.wantIsShell)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// cfg := `---
|
||||
// aliases:
|
||||
// co: pr checkout
|
||||
// il: issue list --author="$1" --label="$2"
|
||||
// ia: issue list --author="$1" --assignee="$1"
|
||||
// `
|
||||
// initBlankContext(cfg, "OWNER/REPO", "trunk")
|
||||
// for _, c := range []struct {
|
||||
// Args string
|
||||
// ExpectedArgs []string
|
||||
// Err string
|
||||
// }{
|
||||
// {"gh co", []string{"pr", "checkout"}, ""},
|
||||
// {"gh il", nil, `not enough arguments for alias: issue list --author="$1" --label="$2"`},
|
||||
// {"gh il vilmibm", nil, `not enough arguments for alias: issue list --author="vilmibm" --label="$2"`},
|
||||
// {"gh co 123", []string{"pr", "checkout", "123"}, ""},
|
||||
// {"gh il vilmibm epic", []string{"issue", "list", `--author=vilmibm`, `--label=epic`}, ""},
|
||||
// {"gh ia vilmibm", []string{"issue", "list", `--author=vilmibm`, `--assignee=vilmibm`}, ""},
|
||||
// {"gh ia $coolmoney$", []string{"issue", "list", `--author=$coolmoney$`, `--assignee=$coolmoney$`}, ""},
|
||||
// {"gh pr status", []string{"pr", "status"}, ""},
|
||||
// {"gh il vilmibm epic -R vilmibm/testing", []string{"issue", "list", "--author=vilmibm", "--label=epic", "-R", "vilmibm/testing"}, ""},
|
||||
// {"gh dne", []string{"dne"}, ""},
|
||||
// {"gh", []string{}, ""},
|
||||
// {"", []string{}, ""},
|
||||
// } {
|
||||
|
|
@ -21,7 +21,8 @@ type ImportOptions struct {
|
|||
Filename string
|
||||
OverwriteExisting bool
|
||||
|
||||
existingCommand func(string) bool
|
||||
validAliasName func(string) bool
|
||||
validAliasExpansion func(string) bool
|
||||
}
|
||||
|
||||
func NewCmdImport(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Command {
|
||||
|
|
@ -74,7 +75,8 @@ func NewCmdImport(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Co
|
|||
opts.Filename = args[0]
|
||||
}
|
||||
|
||||
opts.existingCommand = shared.ExistingCommandFunc(f, cmd)
|
||||
opts.validAliasName = shared.ValidAliasNameFunc(cmd)
|
||||
opts.validAliasExpansion = shared.ValidAliasExpansionFunc(cmd)
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
|
|
@ -120,9 +122,9 @@ func importRun(opts *ImportOptions) error {
|
|||
var msg strings.Builder
|
||||
|
||||
for _, alias := range getSortedKeys(aliasMap) {
|
||||
if opts.existingCommand(alias) {
|
||||
if !opts.validAliasName(alias) {
|
||||
msg.WriteString(
|
||||
fmt.Sprintf("%s Could not import alias %s: already a gh command\n",
|
||||
fmt.Sprintf("%s Could not import alias %s: already a gh command, extension, or alias\n",
|
||||
cs.FailureIcon(),
|
||||
cs.Bold(alias),
|
||||
),
|
||||
|
|
@ -133,9 +135,9 @@ func importRun(opts *ImportOptions) error {
|
|||
|
||||
expansion := aliasMap[alias]
|
||||
|
||||
if !(strings.HasPrefix(expansion, "!") || opts.existingCommand(expansion)) {
|
||||
if !opts.validAliasExpansion(expansion) {
|
||||
msg.WriteString(
|
||||
fmt.Sprintf("%s Could not import alias %s: expansion does not correspond to a gh command\n",
|
||||
fmt.Sprintf("%s Could not import alias %s: expansion does not correspond to a gh command, extension, or alias\n",
|
||||
cs.FailureIcon(),
|
||||
cs.Bold(alias),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import (
|
|||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/pkg/cmd/alias/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/extensions"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -257,9 +256,9 @@ func TestImportRun(t *testing.T) {
|
|||
wantStderr: strings.Join(
|
||||
[]string{
|
||||
importFileMsg,
|
||||
"X Could not import alias api: already a gh command",
|
||||
"X Could not import alias issue: already a gh command",
|
||||
"X Could not import alias pr: already a gh command\n\n",
|
||||
"X Could not import alias api: already a gh command, extension, or alias",
|
||||
"X Could not import alias issue: already a gh command, extension, or alias",
|
||||
"X Could not import alias pr: already a gh command, extension, or alias\n\n",
|
||||
},
|
||||
"\n",
|
||||
),
|
||||
|
|
@ -277,8 +276,8 @@ func TestImportRun(t *testing.T) {
|
|||
wantStderr: strings.Join(
|
||||
[]string{
|
||||
importFileMsg,
|
||||
"X Could not import alias alias1: expansion does not correspond to a gh command",
|
||||
"X Could not import alias alias2: expansion does not correspond to a gh command\n\n",
|
||||
"X Could not import alias alias1: expansion does not correspond to a gh command, extension, or alias",
|
||||
"X Could not import alias alias2: expansion does not correspond to a gh command, extension, or alias\n\n",
|
||||
},
|
||||
"\n",
|
||||
),
|
||||
|
|
@ -304,16 +303,6 @@ func TestImportRun(t *testing.T) {
|
|||
return cfg, nil
|
||||
}
|
||||
|
||||
// Create fake command factory for testing.
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
ExtensionManager: &extensions.ExtensionManagerMock{
|
||||
ListFunc: func() []extensions.Extension {
|
||||
return []extensions.Extension{}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create fake command structure for testing.
|
||||
rootCmd := &cobra.Command{}
|
||||
prCmd := &cobra.Command{Use: "pr"}
|
||||
|
|
@ -327,7 +316,8 @@ func TestImportRun(t *testing.T) {
|
|||
apiCmd.AddCommand(&cobra.Command{Use: "graphql"})
|
||||
rootCmd.AddCommand(apiCmd)
|
||||
|
||||
tt.opts.existingCommand = shared.ExistingCommandFunc(f, rootCmd)
|
||||
tt.opts.validAliasName = shared.ValidAliasNameFunc(rootCmd)
|
||||
tt.opts.validAliasExpansion = shared.ValidAliasExpansionFunc(rootCmd)
|
||||
|
||||
if tt.stdin != "" {
|
||||
stdin.WriteString(tt.stdin)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ type SetOptions struct {
|
|||
Expansion string
|
||||
IsShell bool
|
||||
|
||||
existingCommand func(string) bool
|
||||
validAliasName func(string) bool
|
||||
validAliasExpansion func(string) bool
|
||||
}
|
||||
|
||||
func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command {
|
||||
|
|
@ -59,6 +60,9 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
$ gh alias set homework 'issue list --assignee @me'
|
||||
$ gh homework
|
||||
|
||||
$ gh alias set 'issue mine' 'issue list --mention @me'
|
||||
$ gh issue mine
|
||||
|
||||
$ gh alias set epicsBy 'issue list --author="$1" --label="epic"'
|
||||
$ gh epicsBy vilmibm #=> gh issue list --author="vilmibm" --label="epic"
|
||||
|
||||
|
|
@ -70,7 +74,8 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
opts.Name = args[0]
|
||||
opts.Expansion = args[1]
|
||||
|
||||
opts.existingCommand = shared.ExistingCommandFunc(f, cmd)
|
||||
opts.validAliasName = shared.ValidAliasNameFunc(cmd)
|
||||
opts.validAliasExpansion = shared.ValidAliasExpansionFunc(cmd)
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
|
|
@ -104,18 +109,16 @@ func setRun(opts *SetOptions) error {
|
|||
fmt.Fprintf(opts.IO.ErrOut, "- Adding alias for %s: %s\n", cs.Bold(opts.Name), cs.Bold(expansion))
|
||||
}
|
||||
|
||||
isShell := opts.IsShell
|
||||
if isShell && !strings.HasPrefix(expansion, "!") {
|
||||
if opts.IsShell && !strings.HasPrefix(expansion, "!") {
|
||||
expansion = "!" + expansion
|
||||
}
|
||||
isShell = strings.HasPrefix(expansion, "!")
|
||||
|
||||
if opts.existingCommand(opts.Name) {
|
||||
return fmt.Errorf("could not create alias: %q is already a gh command", opts.Name)
|
||||
if !opts.validAliasName(opts.Name) {
|
||||
return fmt.Errorf("could not create alias: %q is already a gh command, extension, or alias", opts.Name)
|
||||
}
|
||||
|
||||
if !isShell && !opts.existingCommand(expansion) {
|
||||
return fmt.Errorf("could not create alias: %s does not correspond to a gh command", expansion)
|
||||
if !opts.validAliasExpansion(expansion) {
|
||||
return fmt.Errorf("could not create alias: %s does not correspond to a gh command, extension, or alias", expansion)
|
||||
}
|
||||
|
||||
successMsg := fmt.Sprintf("%s Added alias.", cs.SuccessIcon())
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ func TestAliasSet_gh_command(t *testing.T) {
|
|||
cfg := config.NewFromString(``)
|
||||
|
||||
_, err := runCommand(cfg, true, "pr 'pr status'", "")
|
||||
assert.EqualError(t, err, `could not create alias: "pr" is already a gh command`)
|
||||
assert.EqualError(t, err, `could not create alias: "pr" is already a gh command, extension, or alias`)
|
||||
}
|
||||
|
||||
func TestAliasSet_empty_aliases(t *testing.T) {
|
||||
|
|
@ -231,7 +231,7 @@ func TestAliasSet_invalid_command(t *testing.T) {
|
|||
cfg := config.NewFromString(``)
|
||||
|
||||
_, err := runCommand(cfg, true, "co 'pe checkout'", "")
|
||||
assert.EqualError(t, err, "could not create alias: pe checkout does not correspond to a gh command")
|
||||
assert.EqualError(t, err, "could not create alias: pe checkout does not correspond to a gh command, extension, or alias")
|
||||
}
|
||||
|
||||
func TestShellAlias_flag(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"strings"
|
||||
|
||||
"github.com/google/shlex"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ExistingCommandFunc returns a function that will check if the given string
|
||||
// corresponds to an existing command.
|
||||
func ExistingCommandFunc(f *cmdutil.Factory, cmd *cobra.Command) func(string) bool {
|
||||
// ValidAliasNameFunc returns a function that will check if the given string
|
||||
// is a valid alias name. A name is valid if:
|
||||
// - it does not shadow an existing command,
|
||||
// - it is not nested under a command that is runnable,
|
||||
// - it is not nested under a command that does not exist.
|
||||
func ValidAliasNameFunc(cmd *cobra.Command) func(string) bool {
|
||||
return func(args string) bool {
|
||||
split, err := shlex.Split(args)
|
||||
if err != nil || len(split) == 0 {
|
||||
|
|
@ -16,17 +20,32 @@ func ExistingCommandFunc(f *cmdutil.Factory, cmd *cobra.Command) func(string) bo
|
|||
}
|
||||
|
||||
rootCmd := cmd.Root()
|
||||
cmd, _, err = rootCmd.Traverse(split)
|
||||
if err == nil && cmd != rootCmd {
|
||||
foundCmd, foundArgs, _ := rootCmd.Find(split)
|
||||
if foundCmd != nil && !foundCmd.Runnable() && len(foundArgs) == 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, ext := range f.ExtensionManager.List() {
|
||||
if ext.Name() == split[0] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ValidAliasExpansionFunc returns a function that will check if the given string
|
||||
// is a valid alias expansion. An expansion is valid if:
|
||||
// - it is a shell expansion,
|
||||
// - it is a non-shell expansion that corresponds to an existing command, extension, or alias.
|
||||
func ValidAliasExpansionFunc(cmd *cobra.Command) func(string) bool {
|
||||
return func(expansion string) bool {
|
||||
if strings.HasPrefix(expansion, "!") {
|
||||
return true
|
||||
}
|
||||
|
||||
split, err := shlex.Split(expansion)
|
||||
if err != nil || len(split) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
rootCmd := cmd.Root()
|
||||
cmd, _, _ = rootCmd.Find(split)
|
||||
return cmd != rootCmd
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,22 +3,11 @@ package shared
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/extensions"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExistingCommandFunc(t *testing.T) {
|
||||
// Create fake command factory for testing.
|
||||
factory := &cmdutil.Factory{
|
||||
ExtensionManager: &extensions.ExtensionManagerMock{
|
||||
ListFunc: func() []extensions.Extension {
|
||||
return []extensions.Extension{}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestValidAliasNameFunc(t *testing.T) {
|
||||
// Create fake command structure for testing.
|
||||
issueCmd := &cobra.Command{Use: "issue"}
|
||||
prCmd := &cobra.Command{Use: "pr"}
|
||||
|
|
@ -28,13 +17,38 @@ func TestExistingCommandFunc(t *testing.T) {
|
|||
cmd.AddCommand(prCmd)
|
||||
cmd.AddCommand(issueCmd)
|
||||
|
||||
f := ExistingCommandFunc(factory, cmd)
|
||||
f := ValidAliasNameFunc(cmd)
|
||||
|
||||
assert.True(t, f("pr"))
|
||||
assert.True(t, f("pr checkout"))
|
||||
assert.True(t, f("issue"))
|
||||
assert.False(t, f("pr"))
|
||||
assert.False(t, f("pr checkout"))
|
||||
assert.False(t, f("issue"))
|
||||
assert.False(t, f("repo list"))
|
||||
|
||||
assert.True(t, f("ps"))
|
||||
assert.True(t, f("checkout"))
|
||||
assert.True(t, f("issue erase"))
|
||||
assert.True(t, f("pr erase"))
|
||||
assert.True(t, f("pr checkout branch"))
|
||||
}
|
||||
|
||||
func TestValidAliasExpansionFunc(t *testing.T) {
|
||||
// Create fake command structure for testing.
|
||||
issueCmd := &cobra.Command{Use: "issue"}
|
||||
prCmd := &cobra.Command{Use: "pr"}
|
||||
prCmd.AddCommand(&cobra.Command{Use: "checkout"})
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.AddCommand(prCmd)
|
||||
cmd.AddCommand(issueCmd)
|
||||
|
||||
f := ValidAliasExpansionFunc(cmd)
|
||||
|
||||
assert.False(t, f("ps"))
|
||||
assert.False(t, f("checkout"))
|
||||
assert.False(t, f("repo list"))
|
||||
|
||||
assert.True(t, f("!git branch --show-current"))
|
||||
assert.True(t, f("pr"))
|
||||
assert.True(t, f("pr checkout"))
|
||||
assert.True(t, f("issue"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -321,6 +321,7 @@ func apiRun(opts *ApiOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
isFirstPage := true
|
||||
hasNextPage := true
|
||||
for hasNextPage {
|
||||
resp, err := httpRequest(httpClient, host, method, requestPath, requestBody, requestHeaders)
|
||||
|
|
@ -328,10 +329,16 @@ func apiRun(opts *ApiOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
endCursor, err := processResponse(resp, opts, bodyWriter, headersWriter, tmpl)
|
||||
if !isGraphQL {
|
||||
requestPath, hasNextPage = findNextPage(resp)
|
||||
requestBody = nil // prevent repeating GET parameters
|
||||
}
|
||||
|
||||
endCursor, err := processResponse(resp, opts, bodyWriter, headersWriter, tmpl, isFirstPage, !hasNextPage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isFirstPage = false
|
||||
|
||||
if !opts.Paginate {
|
||||
break
|
||||
|
|
@ -342,9 +349,6 @@ func apiRun(opts *ApiOptions) error {
|
|||
if hasNextPage {
|
||||
params["endCursor"] = endCursor
|
||||
}
|
||||
} else {
|
||||
requestPath, hasNextPage = findNextPage(resp)
|
||||
requestBody = nil // prevent repeating GET parameters
|
||||
}
|
||||
|
||||
if hasNextPage && opts.ShowResponseHeaders {
|
||||
|
|
@ -355,7 +359,7 @@ func apiRun(opts *ApiOptions) error {
|
|||
return tmpl.Flush()
|
||||
}
|
||||
|
||||
func processResponse(resp *http.Response, opts *ApiOptions, bodyWriter, headersWriter io.Writer, template *template.Template) (endCursor string, err error) {
|
||||
func processResponse(resp *http.Response, opts *ApiOptions, bodyWriter, headersWriter io.Writer, template *template.Template, isFirstPage, isLastPage bool) (endCursor string, err error) {
|
||||
if opts.ShowResponseHeaders {
|
||||
fmt.Fprintln(headersWriter, resp.Proto, resp.Status)
|
||||
printHeaders(headersWriter, resp.Header, opts.IO.ColorEnabled())
|
||||
|
|
@ -403,6 +407,13 @@ func processResponse(resp *http.Response, opts *ApiOptions, bodyWriter, headersW
|
|||
} else if isJSON && opts.IO.ColorEnabled() {
|
||||
err = jsoncolor.Write(bodyWriter, responseBody, " ")
|
||||
} else {
|
||||
if isJSON && opts.Paginate && !isGraphQLPaginate && !opts.ShowResponseHeaders {
|
||||
responseBody = &paginatedArrayReader{
|
||||
Reader: responseBody,
|
||||
isFirstPage: isFirstPage,
|
||||
isLastPage: isLastPage,
|
||||
}
|
||||
}
|
||||
_, err = io.Copy(bodyWriter, responseBody)
|
||||
}
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -668,6 +668,78 @@ func Test_apiRun_paginationREST(t *testing.T) {
|
|||
assert.Equal(t, "https://api.github.com/repositories/1227/issues?page=3", responses[2].Request.URL.String())
|
||||
}
|
||||
|
||||
func Test_apiRun_arrayPaginationREST(t *testing.T) {
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
ios.SetStdoutTTY(false)
|
||||
|
||||
requestCount := 0
|
||||
responses := []*http.Response{
|
||||
{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`[{"item":1},{"item":2}]`)),
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"application/json"},
|
||||
"Link": []string{`<https://api.github.com/repositories/1227/issues?page=2>; rel="next", <https://api.github.com/repositories/1227/issues?page=4>; rel="last"`},
|
||||
},
|
||||
},
|
||||
{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`[{"item":3},{"item":4}]`)),
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"application/json"},
|
||||
"Link": []string{`<https://api.github.com/repositories/1227/issues?page=3>; rel="next", <https://api.github.com/repositories/1227/issues?page=4>; rel="last"`},
|
||||
},
|
||||
},
|
||||
{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`[{"item":5}]`)),
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"application/json"},
|
||||
"Link": []string{`<https://api.github.com/repositories/1227/issues?page=4>; rel="next", <https://api.github.com/repositories/1227/issues?page=4>; rel="last"`},
|
||||
},
|
||||
},
|
||||
{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`[]`)),
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"application/json"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
options := ApiOptions{
|
||||
IO: ios,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
var tr roundTripper = func(req *http.Request) (*http.Response, error) {
|
||||
resp := responses[requestCount]
|
||||
resp.Request = req
|
||||
requestCount++
|
||||
return resp, nil
|
||||
}
|
||||
return &http.Client{Transport: tr}, nil
|
||||
},
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
|
||||
RequestMethod: "GET",
|
||||
RequestMethodPassed: true,
|
||||
RequestPath: "issues",
|
||||
Paginate: true,
|
||||
RawFields: []string{"per_page=50", "page=1"},
|
||||
}
|
||||
|
||||
err := apiRun(&options)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `[{"item":1},{"item":2},{"item":3},{"item":4},{"item":5} ]`, stdout.String(), "stdout")
|
||||
assert.Equal(t, "", stderr.String(), "stderr")
|
||||
|
||||
assert.Equal(t, "https://api.github.com/issues?page=1&per_page=50", responses[0].Request.URL.String())
|
||||
assert.Equal(t, "https://api.github.com/repositories/1227/issues?page=2", responses[1].Request.URL.String())
|
||||
assert.Equal(t, "https://api.github.com/repositories/1227/issues?page=3", responses[2].Request.URL.String())
|
||||
}
|
||||
|
||||
func Test_apiRun_paginationGraphQL(t *testing.T) {
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
|
||||
|
|
@ -1236,7 +1308,7 @@ func Test_processResponse_template(t *testing.T) {
|
|||
tmpl := template.New(ios.Out, ios.TerminalWidth(), ios.ColorEnabled())
|
||||
err := tmpl.Parse(opts.Template)
|
||||
require.NoError(t, err)
|
||||
_, err = processResponse(&resp, &opts, ios.Out, io.Discard, tmpl)
|
||||
_, err = processResponse(&resp, &opts, ios.Out, io.Discard, tmpl, true, true)
|
||||
require.NoError(t, err)
|
||||
err = tmpl.Flush()
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ func httpRequest(client *http.Client, hostname string, method string, p string,
|
|||
return nil, fmt.Errorf("unrecognized parameters type: %v", params)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, requestURL, body)
|
||||
req, err := http.NewRequest(strings.ToUpper(method), requestURL, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -103,21 +103,8 @@ func addQuery(path string, params map[string]interface{}) string {
|
|||
}
|
||||
|
||||
query := url.Values{}
|
||||
for key, value := range params {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
query.Add(key, v)
|
||||
case []byte:
|
||||
query.Add(key, string(v))
|
||||
case nil:
|
||||
query.Add(key, "")
|
||||
case int:
|
||||
query.Add(key, fmt.Sprintf("%d", v))
|
||||
case bool:
|
||||
query.Add(key, fmt.Sprintf("%v", v))
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown type %v", v))
|
||||
}
|
||||
if err := addQueryParam(query, "", params); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sep := "?"
|
||||
|
|
@ -126,3 +113,34 @@ func addQuery(path string, params map[string]interface{}) string {
|
|||
}
|
||||
return path + sep + query.Encode()
|
||||
}
|
||||
|
||||
func addQueryParam(query url.Values, key string, value interface{}) error {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
query.Add(key, v)
|
||||
case []byte:
|
||||
query.Add(key, string(v))
|
||||
case nil:
|
||||
query.Add(key, "")
|
||||
case int:
|
||||
query.Add(key, fmt.Sprintf("%d", v))
|
||||
case bool:
|
||||
query.Add(key, fmt.Sprintf("%v", v))
|
||||
case map[string]interface{}:
|
||||
for subkey, value := range v {
|
||||
// support for nested subkeys can be added here if that is ever necessary
|
||||
if err := addQueryParam(query, subkey, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for _, entry := range v {
|
||||
if err := addQueryParam(query, key+"[]", entry); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown type %v", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,6 +129,24 @@ func Test_httpRequest(t *testing.T) {
|
|||
headers: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "lowercase HTTP method",
|
||||
args: args{
|
||||
client: &httpClient,
|
||||
host: "github.com",
|
||||
method: "get",
|
||||
p: "repos/octocat/spoon-knife",
|
||||
params: nil,
|
||||
headers: []string{},
|
||||
},
|
||||
wantErr: false,
|
||||
want: expects{
|
||||
method: "GET",
|
||||
u: "https://api.github.com/repos/octocat/spoon-knife",
|
||||
body: "",
|
||||
headers: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GET with leading slash",
|
||||
args: args{
|
||||
|
|
@ -322,6 +340,14 @@ func Test_addQuery(t *testing.T) {
|
|||
},
|
||||
want: "?a=hello",
|
||||
},
|
||||
{
|
||||
name: "array",
|
||||
args: args{
|
||||
path: "",
|
||||
params: map[string]interface{}{"a": []interface{}{"hello", "world"}},
|
||||
},
|
||||
want: "?a%5B%5D=hello&a%5B%5D=world",
|
||||
},
|
||||
{
|
||||
name: "append",
|
||||
args: args{
|
||||
|
|
|
|||
|
|
@ -106,3 +106,43 @@ func addPerPage(p string, perPage int, params map[string]interface{}) string {
|
|||
|
||||
return fmt.Sprintf("%s%sper_page=%d", p, sep, perPage)
|
||||
}
|
||||
|
||||
// paginatedArrayReader wraps a Reader to omit the opening and/or the closing square bracket of a
|
||||
// JSON array in order to apply pagination context between multiple API requests.
|
||||
type paginatedArrayReader struct {
|
||||
io.Reader
|
||||
isFirstPage bool
|
||||
isLastPage bool
|
||||
|
||||
isSubsequentRead bool
|
||||
cachedByte byte
|
||||
}
|
||||
|
||||
func (r *paginatedArrayReader) Read(p []byte) (int, error) {
|
||||
var n int
|
||||
var err error
|
||||
if r.cachedByte != 0 && len(p) > 0 {
|
||||
p[0] = r.cachedByte
|
||||
n, err = r.Reader.Read(p[1:])
|
||||
n += 1
|
||||
r.cachedByte = 0
|
||||
} else {
|
||||
n, err = r.Reader.Read(p)
|
||||
}
|
||||
if !r.isSubsequentRead && !r.isFirstPage && n > 0 && p[0] == '[' {
|
||||
if n > 1 && p[1] == ']' {
|
||||
// empty array case
|
||||
p[0] = ' '
|
||||
} else {
|
||||
// avoid starting a new array and continue with a comma instead
|
||||
p[0] = ','
|
||||
}
|
||||
}
|
||||
if !r.isLastPage && n > 0 && p[n-1] == ']' {
|
||||
// avoid closing off an array in case we determine we are at EOF
|
||||
r.cachedByte = p[n-1]
|
||||
n -= 1
|
||||
}
|
||||
r.isSubsequentRead = true
|
||||
return n, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ func newListCmd(app *App) *cobra.Command {
|
|||
|
||||
listCmd.Flags().StringVarP(&opts.orgName, "org", "o", "", "The `login` handle of the organization to list codespaces for (admin-only)")
|
||||
listCmd.Flags().StringVarP(&opts.userName, "user", "u", "", "The `username` to list codespaces for (used with --org)")
|
||||
cmdutil.AddJSONFlags(listCmd, &exporter, api.CodespaceFields)
|
||||
cmdutil.AddJSONFlags(listCmd, &exporter, api.ListCodespaceFields)
|
||||
|
||||
listCmd.Flags().BoolVarP(&opts.useWeb, "web", "w", false, "List codespaces in the web browser, cannot be used with --user or --org")
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ func NewRootCmd(app *App) *cobra.Command {
|
|||
root.AddCommand(newDeleteCmd(app))
|
||||
root.AddCommand(newJupyterCmd(app))
|
||||
root.AddCommand(newListCmd(app))
|
||||
root.AddCommand(newViewCmd(app))
|
||||
root.AddCommand(newLogsCmd(app))
|
||||
root.AddCommand(newPortsCmd(app))
|
||||
root.AddCommand(newSSHCmd(app))
|
||||
|
|
|
|||
133
pkg/cmd/codespace/view.go
Normal file
133
pkg/cmd/codespace/view.go
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
package codespace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/codespaces/api"
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
minutesInDay = 1440
|
||||
)
|
||||
|
||||
type viewOptions struct {
|
||||
selector *CodespaceSelector
|
||||
exporter cmdutil.Exporter
|
||||
}
|
||||
|
||||
func newViewCmd(app *App) *cobra.Command {
|
||||
opts := &viewOptions{}
|
||||
|
||||
viewCmd := &cobra.Command{
|
||||
Use: "view",
|
||||
Short: "View details about a codespace",
|
||||
Example: heredoc.Doc(`
|
||||
# select a codespace from a list of all codespaces you own
|
||||
$ gh cs view
|
||||
|
||||
# view the details of a specific codespace
|
||||
$ gh cs view -c codespace-name-12345
|
||||
|
||||
# view the list of all available fields for a codespace
|
||||
$ gh cs view --json
|
||||
|
||||
# view specific fields for a codespace
|
||||
$ gh cs view --json displayName,machineDisplayName,state
|
||||
`),
|
||||
Args: noArgsConstraint,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return app.ViewCodespace(cmd.Context(), opts)
|
||||
},
|
||||
}
|
||||
opts.selector = AddCodespaceSelector(viewCmd, app.apiClient)
|
||||
cmdutil.AddJSONFlags(viewCmd, &opts.exporter, api.ViewCodespaceFields)
|
||||
|
||||
return viewCmd
|
||||
}
|
||||
|
||||
func (a *App) ViewCodespace(ctx context.Context, opts *viewOptions) error {
|
||||
// If we are in a codespace and a codespace name wasn't provided, show the details for the codespace we are connected to
|
||||
if (os.Getenv("CODESPACES") == "true") && opts.selector.codespaceName == "" {
|
||||
codespaceName := os.Getenv("CODESPACE_NAME")
|
||||
opts.selector.codespaceName = codespaceName
|
||||
}
|
||||
|
||||
selectedCodespace, err := opts.selector.Select(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.io.StartPager(); err != nil {
|
||||
a.errLogger.Printf("error starting pager: %v", err)
|
||||
}
|
||||
defer a.io.StopPager()
|
||||
|
||||
if opts.exporter != nil {
|
||||
return opts.exporter.Write(a.io, selectedCodespace)
|
||||
}
|
||||
|
||||
tp := tableprinter.New(a.io)
|
||||
c := codespace{selectedCodespace}
|
||||
formattedName := formatNameForVSCSTarget(c.Name, c.VSCSTarget)
|
||||
|
||||
// Create an array of fields to display in the table with their values
|
||||
fields := []struct {
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{"Name", formattedName},
|
||||
{"State", c.State},
|
||||
{"Repository", c.Repository.FullName},
|
||||
{"Git Status", formatGitStatus(c)},
|
||||
{"Devcontainer Path", c.DevContainerPath},
|
||||
{"Machine Display Name", c.Machine.DisplayName},
|
||||
{"Idle Timeout", fmt.Sprintf("%d minutes", c.IdleTimeoutMinutes)},
|
||||
{"Created At", c.CreatedAt},
|
||||
{"Retention Period", formatRetentionPeriodDays(c)},
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
// Don't display the field if it is empty and we are printing to a TTY
|
||||
if !a.io.IsStdoutTTY() || field.value != "" {
|
||||
tp.AddField(field.name)
|
||||
tp.AddField(field.value)
|
||||
tp.EndRow()
|
||||
}
|
||||
}
|
||||
|
||||
err = tp.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatGitStatus(codespace codespace) string {
|
||||
branchWithGitStatus := codespace.branchWithGitStatus()
|
||||
|
||||
// Format the commits ahead/behind with proper pluralization
|
||||
commitsAhead := text.Pluralize(codespace.GitStatus.Ahead, "commit")
|
||||
commitsBehind := text.Pluralize(codespace.GitStatus.Behind, "commit")
|
||||
|
||||
return fmt.Sprintf("%s - %s ahead, %s behind", branchWithGitStatus, commitsAhead, commitsBehind)
|
||||
}
|
||||
|
||||
func formatRetentionPeriodDays(codespace codespace) string {
|
||||
days := codespace.RetentionPeriodMinutes / minutesInDay
|
||||
// Don't display the retention period if it is 0 days
|
||||
if days == 0 {
|
||||
return ""
|
||||
} else if days == 1 {
|
||||
return "1 day"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d days", days)
|
||||
}
|
||||
131
pkg/cmd/codespace/view_test.go
Normal file
131
pkg/cmd/codespace/view_test.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package codespace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/codespaces/api"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
)
|
||||
|
||||
func Test_NewCmdView(t *testing.T) {
|
||||
tests := []struct {
|
||||
tName string
|
||||
codespaceName string
|
||||
opts *viewOptions
|
||||
cliArgs []string
|
||||
wantErr bool
|
||||
wantStdout string
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
tName: "selector throws because no terminal found",
|
||||
opts: &viewOptions{},
|
||||
wantErr: true,
|
||||
errMsg: "choosing codespace: error getting answers: no terminal",
|
||||
},
|
||||
{
|
||||
tName: "command fails because provided codespace doesn't exist",
|
||||
codespaceName: "i-dont-exist",
|
||||
opts: &viewOptions{},
|
||||
wantErr: true,
|
||||
errMsg: "getting full codespace details: codespace not found",
|
||||
},
|
||||
{
|
||||
tName: "command succeeds because codespace exists (no details)",
|
||||
codespaceName: "monalisa-cli-cli-abcdef",
|
||||
opts: &viewOptions{},
|
||||
wantErr: false,
|
||||
wantStdout: "Name\tmonalisa-cli-cli-abcdef\nState\t\nRepository\t\nGit Status\t - 0 commits ahead, 0 commits behind\nDevcontainer Path\t\nMachine Display Name\t\nIdle Timeout\t0 minutes\nCreated At\t\nRetention Period\t\n",
|
||||
},
|
||||
{
|
||||
tName: "command succeeds because codespace exists (with details)",
|
||||
codespaceName: "monalisa-cli-cli-hijklm",
|
||||
opts: &viewOptions{},
|
||||
wantErr: false,
|
||||
wantStdout: "Name\tmonalisa-cli-cli-hijklm\nState\tAvailable\nRepository\tcli/cli\nGit Status\tmain* - 1 commit ahead, 2 commits behind\nDevcontainer Path\t.devcontainer/devcontainer.json\nMachine Display Name\tTest Display Name\nIdle Timeout\t30 minutes\nCreated At\t\nRetention Period\t1 day\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.tName, func(t *testing.T) {
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
a := &App{
|
||||
apiClient: testViewApiMock(),
|
||||
io: ios,
|
||||
}
|
||||
selector := &CodespaceSelector{api: a.apiClient, codespaceName: tt.codespaceName}
|
||||
tt.opts.selector = selector
|
||||
|
||||
var err error
|
||||
if tt.cliArgs == nil {
|
||||
if tt.opts.selector == nil {
|
||||
t.Fatalf("selector must be set in opts if cliArgs are not provided")
|
||||
}
|
||||
|
||||
err = a.ViewCodespace(context.Background(), tt.opts)
|
||||
} else {
|
||||
cmd := newViewCmd(a)
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SilenceErrors = true
|
||||
cmd.SetOut(ios.ErrOut)
|
||||
cmd.SetErr(ios.ErrOut)
|
||||
cmd.SetArgs(tt.cliArgs)
|
||||
_, err = cmd.ExecuteC()
|
||||
}
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("Edit() expected error, got nil")
|
||||
} else if err.Error() != tt.errMsg {
|
||||
t.Errorf("Edit() error = %q, want %q", err, tt.errMsg)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("Edit() expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if out := stdout.String(); out != tt.wantStdout {
|
||||
t.Errorf("stdout = %q, want %q", out, tt.wantStdout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testViewApiMock() *apiClientMock {
|
||||
codespaceWithNoDetails := &api.Codespace{
|
||||
Name: "monalisa-cli-cli-abcdef",
|
||||
}
|
||||
codespaceWithDetails := &api.Codespace{
|
||||
Name: "monalisa-cli-cli-hijklm",
|
||||
GitStatus: api.CodespaceGitStatus{
|
||||
Ahead: 1,
|
||||
Behind: 2,
|
||||
Ref: "main",
|
||||
HasUnpushedChanges: true,
|
||||
HasUncommittedChanges: true,
|
||||
},
|
||||
IdleTimeoutMinutes: 30,
|
||||
RetentionPeriodMinutes: 1440,
|
||||
State: "Available",
|
||||
Repository: api.Repository{FullName: "cli/cli"},
|
||||
DevContainerPath: ".devcontainer/devcontainer.json",
|
||||
Machine: api.CodespaceMachine{
|
||||
DisplayName: "Test Display Name",
|
||||
},
|
||||
}
|
||||
return &apiClientMock{
|
||||
GetCodespaceFunc: func(_ context.Context, name string, _ bool) (*api.Codespace, error) {
|
||||
if name == codespaceWithDetails.Name {
|
||||
return codespaceWithDetails, nil
|
||||
} else if name == codespaceWithNoDetails.Name {
|
||||
return codespaceWithNoDetails, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("codespace not found")
|
||||
},
|
||||
ListCodespacesFunc: func(ctx context.Context, opts api.ListCodespacesOptions) ([]*api.Codespace, error) {
|
||||
return []*api.Codespace{codespaceWithNoDetails, codespaceWithDetails}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -84,43 +84,43 @@ func Test_getExtensionRepos(t *testing.T) {
|
|||
|
||||
reg.Register(
|
||||
httpmock.QueryMatcher("GET", "search/repositories", values),
|
||||
httpmock.JSONResponse(search.RepositoriesResult{
|
||||
IncompleteResults: false,
|
||||
Items: []search.Repository{
|
||||
{
|
||||
FullName: "vilmibm/gh-screensaver",
|
||||
Name: "gh-screensaver",
|
||||
Description: "terminal animations",
|
||||
Owner: search.User{
|
||||
Login: "vilmibm",
|
||||
httpmock.JSONResponse(map[string]interface{}{
|
||||
"incomplete_results": false,
|
||||
"total_count": 4,
|
||||
"items": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "gh-screensaver",
|
||||
"full_name": "vilmibm/gh-screensaver",
|
||||
"description": "terminal animations",
|
||||
"owner": map[string]interface{}{
|
||||
"login": "vilmibm",
|
||||
},
|
||||
},
|
||||
{
|
||||
FullName: "cli/gh-cool",
|
||||
Name: "gh-cool",
|
||||
Description: "it's just cool ok",
|
||||
Owner: search.User{
|
||||
Login: "cli",
|
||||
map[string]interface{}{
|
||||
"name": "gh-cool",
|
||||
"full_name": "cli/gh-cool",
|
||||
"description": "it's just cool ok",
|
||||
"owner": map[string]interface{}{
|
||||
"login": "cli",
|
||||
},
|
||||
},
|
||||
{
|
||||
FullName: "samcoe/gh-triage",
|
||||
Name: "gh-triage",
|
||||
Description: "helps with triage",
|
||||
Owner: search.User{
|
||||
Login: "samcoe",
|
||||
map[string]interface{}{
|
||||
"name": "gh-triage",
|
||||
"full_name": "samcoe/gh-triage",
|
||||
"description": "helps with triage",
|
||||
"owner": map[string]interface{}{
|
||||
"login": "samcoe",
|
||||
},
|
||||
},
|
||||
{
|
||||
FullName: "github/gh-gei",
|
||||
Name: "gh-gei",
|
||||
Description: "something something enterprise",
|
||||
Owner: search.User{
|
||||
Login: "github",
|
||||
map[string]interface{}{
|
||||
"name": "gh-gei",
|
||||
"full_name": "github/gh-gei",
|
||||
"description": "something something enterprise",
|
||||
"owner": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
},
|
||||
},
|
||||
Total: 4,
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -643,10 +643,8 @@ func checkValidExtension(rootCmd *cobra.Command, m extensions.ExtensionManager,
|
|||
}
|
||||
|
||||
commandName := strings.TrimPrefix(extName, "gh-")
|
||||
if c, _, err := rootCmd.Traverse([]string{commandName}); err != nil {
|
||||
return nil, err
|
||||
} else if c != rootCmd {
|
||||
return nil, fmt.Errorf("%q matches the name of a built-in command", commandName)
|
||||
if c, _, _ := rootCmd.Find([]string{commandName}); c != rootCmd && c.GroupID != "extension" {
|
||||
return nil, fmt.Errorf("%q matches the name of a built-in command or alias", commandName)
|
||||
}
|
||||
|
||||
for _, ext := range m.List() {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/extensions"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/search"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
|
@ -74,7 +73,7 @@ func TestNewCmdExtension(t *testing.T) {
|
|||
}
|
||||
reg.Register(
|
||||
httpmock.QueryMatcher("GET", "search/repositories", values),
|
||||
httpmock.JSONResponse(searchResults()),
|
||||
httpmock.JSONResponse(searchResults(4)),
|
||||
)
|
||||
},
|
||||
isTTY: true,
|
||||
|
|
@ -111,7 +110,7 @@ func TestNewCmdExtension(t *testing.T) {
|
|||
}
|
||||
reg.Register(
|
||||
httpmock.QueryMatcher("GET", "search/repositories", values),
|
||||
httpmock.JSONResponse(searchResults()),
|
||||
httpmock.JSONResponse(searchResults(4)),
|
||||
)
|
||||
},
|
||||
wantStdout: "installed\tvilmibm/gh-screensaver\tterminal animations\n\tcli/gh-cool\tit's just cool ok\n\tsamcoe/gh-triage\thelps with triage\ninstalled\tgithub/gh-gei\tsomething something enterprise\n",
|
||||
|
|
@ -145,9 +144,7 @@ func TestNewCmdExtension(t *testing.T) {
|
|||
"per_page": []string{"30"},
|
||||
"q": []string{"screen topic:gh-extension"},
|
||||
}
|
||||
results := searchResults()
|
||||
results.Total = 1
|
||||
results.Items = []search.Repository{results.Items[0]}
|
||||
results := searchResults(1)
|
||||
reg.Register(
|
||||
httpmock.QueryMatcher("GET", "search/repositories", values),
|
||||
httpmock.JSONResponse(results),
|
||||
|
|
@ -175,9 +172,7 @@ func TestNewCmdExtension(t *testing.T) {
|
|||
"per_page": []string{"1"},
|
||||
"q": []string{"topic:gh-extension"},
|
||||
}
|
||||
results := searchResults()
|
||||
results.Total = 1
|
||||
results.Items = []search.Repository{results.Items[0]}
|
||||
results := searchResults(1)
|
||||
reg.Register(
|
||||
httpmock.QueryMatcher("GET", "search/repositories", values),
|
||||
httpmock.JSONResponse(results),
|
||||
|
|
@ -203,9 +198,7 @@ func TestNewCmdExtension(t *testing.T) {
|
|||
"per_page": []string{"30"},
|
||||
"q": []string{"license:GPLv3 topic:gh-extension user:jillvalentine"},
|
||||
}
|
||||
results := searchResults()
|
||||
results.Total = 1
|
||||
results.Items = []search.Repository{results.Items[0]}
|
||||
results := searchResults(1)
|
||||
reg.Register(
|
||||
httpmock.QueryMatcher("GET", "search/repositories", values),
|
||||
httpmock.JSONResponse(results),
|
||||
|
|
@ -974,7 +967,7 @@ func Test_checkValidExtension(t *testing.T) {
|
|||
manager: m,
|
||||
extName: "gh-auth",
|
||||
},
|
||||
wantError: "\"auth\" matches the name of a built-in command",
|
||||
wantError: "\"auth\" matches the name of a built-in command or alias",
|
||||
},
|
||||
{
|
||||
name: "clashes with an installed extension",
|
||||
|
|
@ -998,43 +991,48 @@ func Test_checkValidExtension(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func searchResults() search.RepositoriesResult {
|
||||
return search.RepositoriesResult{
|
||||
IncompleteResults: false,
|
||||
Items: []search.Repository{
|
||||
{
|
||||
FullName: "vilmibm/gh-screensaver",
|
||||
Name: "gh-screensaver",
|
||||
Description: "terminal animations",
|
||||
Owner: search.User{
|
||||
Login: "vilmibm",
|
||||
func searchResults(numResults int) interface{} {
|
||||
result := map[string]interface{}{
|
||||
"incomplete_results": false,
|
||||
"total_count": 4,
|
||||
"items": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "gh-screensaver",
|
||||
"full_name": "vilmibm/gh-screensaver",
|
||||
"description": "terminal animations",
|
||||
"owner": map[string]interface{}{
|
||||
"login": "vilmibm",
|
||||
},
|
||||
},
|
||||
{
|
||||
FullName: "cli/gh-cool",
|
||||
Name: "gh-cool",
|
||||
Description: "it's just cool ok",
|
||||
Owner: search.User{
|
||||
Login: "cli",
|
||||
map[string]interface{}{
|
||||
"name": "gh-cool",
|
||||
"full_name": "cli/gh-cool",
|
||||
"description": "it's just cool ok",
|
||||
"owner": map[string]interface{}{
|
||||
"login": "cli",
|
||||
},
|
||||
},
|
||||
{
|
||||
FullName: "samcoe/gh-triage",
|
||||
Name: "gh-triage",
|
||||
Description: "helps with triage",
|
||||
Owner: search.User{
|
||||
Login: "samcoe",
|
||||
map[string]interface{}{
|
||||
"name": "gh-triage",
|
||||
"full_name": "samcoe/gh-triage",
|
||||
"description": "helps with triage",
|
||||
"owner": map[string]interface{}{
|
||||
"login": "samcoe",
|
||||
},
|
||||
},
|
||||
{
|
||||
FullName: "github/gh-gei",
|
||||
Name: "gh-gei",
|
||||
Description: "something something enterprise",
|
||||
Owner: search.User{
|
||||
Login: "github",
|
||||
map[string]interface{}{
|
||||
"name": "gh-gei",
|
||||
"full_name": "github/gh-gei",
|
||||
"description": "something something enterprise",
|
||||
"owner": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
},
|
||||
},
|
||||
Total: 4,
|
||||
}
|
||||
if len(result["items"].([]interface{})) > numResults {
|
||||
fewerItems := result["items"].([]interface{})[0:numResults]
|
||||
result["items"] = fewerItems
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const manifestName = "manifest.yml"
|
||||
|
|
@ -12,16 +20,22 @@ type ExtensionKind int
|
|||
const (
|
||||
GitKind ExtensionKind = iota
|
||||
BinaryKind
|
||||
LocalKind
|
||||
)
|
||||
|
||||
type Extension struct {
|
||||
path string
|
||||
path string
|
||||
kind ExtensionKind
|
||||
gitClient gitClient
|
||||
httpClient *http.Client
|
||||
|
||||
mu sync.RWMutex
|
||||
|
||||
// These fields get resolved dynamically:
|
||||
url string
|
||||
isLocal bool
|
||||
isPinned bool
|
||||
isPinned *bool
|
||||
currentVersion string
|
||||
latestVersion string
|
||||
kind ExtensionKind
|
||||
}
|
||||
|
||||
func (e *Extension) Name() string {
|
||||
|
|
@ -32,32 +46,157 @@ func (e *Extension) Path() string {
|
|||
return e.path
|
||||
}
|
||||
|
||||
func (e *Extension) URL() string {
|
||||
return e.url
|
||||
}
|
||||
|
||||
func (e *Extension) IsLocal() bool {
|
||||
return e.isLocal
|
||||
}
|
||||
|
||||
func (e *Extension) CurrentVersion() string {
|
||||
return e.currentVersion
|
||||
}
|
||||
|
||||
func (e *Extension) IsPinned() bool {
|
||||
return e.isPinned
|
||||
}
|
||||
|
||||
func (e *Extension) UpdateAvailable() bool {
|
||||
if e.isLocal ||
|
||||
e.currentVersion == "" ||
|
||||
e.latestVersion == "" ||
|
||||
e.currentVersion == e.latestVersion {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return e.kind == LocalKind
|
||||
}
|
||||
|
||||
func (e *Extension) IsBinary() bool {
|
||||
return e.kind == BinaryKind
|
||||
}
|
||||
|
||||
func (e *Extension) URL() string {
|
||||
e.mu.RLock()
|
||||
if e.url != "" {
|
||||
defer e.mu.RUnlock()
|
||||
return e.url
|
||||
}
|
||||
e.mu.RUnlock()
|
||||
|
||||
var url string
|
||||
switch e.kind {
|
||||
case LocalKind:
|
||||
case BinaryKind:
|
||||
if manifest, err := e.loadManifest(); err == nil {
|
||||
repo := ghrepo.NewWithHost(manifest.Owner, manifest.Name, manifest.Host)
|
||||
url = ghrepo.GenerateRepoURL(repo, "")
|
||||
}
|
||||
case GitKind:
|
||||
if remoteURL, err := e.gitClient.Config("remote.origin.url"); err == nil {
|
||||
url = strings.TrimSpace(string(remoteURL))
|
||||
}
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
e.url = url
|
||||
e.mu.Unlock()
|
||||
|
||||
return e.url
|
||||
}
|
||||
|
||||
func (e *Extension) CurrentVersion() string {
|
||||
e.mu.RLock()
|
||||
if e.currentVersion != "" {
|
||||
defer e.mu.RUnlock()
|
||||
return e.currentVersion
|
||||
}
|
||||
e.mu.RUnlock()
|
||||
|
||||
var currentVersion string
|
||||
switch e.kind {
|
||||
case LocalKind:
|
||||
case BinaryKind:
|
||||
if manifest, err := e.loadManifest(); err == nil {
|
||||
currentVersion = manifest.Tag
|
||||
}
|
||||
case GitKind:
|
||||
if sha, err := e.gitClient.CommandOutput([]string{"rev-parse", "HEAD"}); err == nil {
|
||||
currentVersion = string(bytes.TrimSpace(sha))
|
||||
}
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
e.currentVersion = currentVersion
|
||||
e.mu.Unlock()
|
||||
|
||||
return e.currentVersion
|
||||
}
|
||||
|
||||
func (e *Extension) LatestVersion() string {
|
||||
e.mu.RLock()
|
||||
if e.latestVersion != "" {
|
||||
defer e.mu.RUnlock()
|
||||
return e.latestVersion
|
||||
}
|
||||
e.mu.RUnlock()
|
||||
|
||||
var latestVersion string
|
||||
switch e.kind {
|
||||
case LocalKind:
|
||||
case BinaryKind:
|
||||
repo, err := ghrepo.FromFullName(e.URL())
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
release, err := fetchLatestRelease(e.httpClient, repo)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
latestVersion = release.Tag
|
||||
case GitKind:
|
||||
if lsRemote, err := e.gitClient.CommandOutput([]string{"ls-remote", "origin", "HEAD"}); err == nil {
|
||||
latestVersion = string(bytes.SplitN(lsRemote, []byte("\t"), 2)[0])
|
||||
}
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
e.latestVersion = latestVersion
|
||||
e.mu.Unlock()
|
||||
|
||||
return e.latestVersion
|
||||
}
|
||||
|
||||
func (e *Extension) IsPinned() bool {
|
||||
e.mu.RLock()
|
||||
if e.isPinned != nil {
|
||||
defer e.mu.RUnlock()
|
||||
return *e.isPinned
|
||||
}
|
||||
e.mu.RUnlock()
|
||||
|
||||
var isPinned bool
|
||||
switch e.kind {
|
||||
case LocalKind:
|
||||
case BinaryKind:
|
||||
if manifest, err := e.loadManifest(); err == nil {
|
||||
isPinned = manifest.IsPinned
|
||||
}
|
||||
case GitKind:
|
||||
pinPath := filepath.Join(e.Path(), fmt.Sprintf(".pin-%s", e.CurrentVersion()))
|
||||
if _, err := os.Stat(pinPath); err == nil {
|
||||
isPinned = true
|
||||
} else {
|
||||
isPinned = false
|
||||
}
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
e.isPinned = &isPinned
|
||||
e.mu.Unlock()
|
||||
|
||||
return *e.isPinned
|
||||
}
|
||||
|
||||
func (e *Extension) UpdateAvailable() bool {
|
||||
if e.IsLocal() ||
|
||||
e.CurrentVersion() == "" ||
|
||||
e.LatestVersion() == "" ||
|
||||
e.CurrentVersion() == e.LatestVersion() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (e *Extension) loadManifest() (binManifest, error) {
|
||||
var bm binManifest
|
||||
dir, _ := filepath.Split(e.Path())
|
||||
manifestPath := filepath.Join(dir, manifestName)
|
||||
manifest, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return bm, fmt.Errorf("could not open %s for reading: %w", manifestPath, err)
|
||||
}
|
||||
err = yaml.Unmarshal(manifest, &bm)
|
||||
if err != nil {
|
||||
return bm, fmt.Errorf("could not parse %s: %w", manifestPath, err)
|
||||
}
|
||||
return bm, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
func TestUpdateAvailable_IsLocal(t *testing.T) {
|
||||
e := &Extension{
|
||||
isLocal: true,
|
||||
kind: LocalKind,
|
||||
}
|
||||
|
||||
assert.False(t, e.UpdateAvailable())
|
||||
|
|
@ -16,7 +16,7 @@ func TestUpdateAvailable_IsLocal(t *testing.T) {
|
|||
|
||||
func TestUpdateAvailable_NoCurrentVersion(t *testing.T) {
|
||||
e := &Extension{
|
||||
isLocal: false,
|
||||
kind: LocalKind,
|
||||
}
|
||||
|
||||
assert.False(t, e.UpdateAvailable())
|
||||
|
|
@ -24,7 +24,7 @@ func TestUpdateAvailable_NoCurrentVersion(t *testing.T) {
|
|||
|
||||
func TestUpdateAvailable_NoLatestVersion(t *testing.T) {
|
||||
e := &Extension{
|
||||
isLocal: false,
|
||||
kind: BinaryKind,
|
||||
currentVersion: "1.0.0",
|
||||
}
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ func TestUpdateAvailable_NoLatestVersion(t *testing.T) {
|
|||
|
||||
func TestUpdateAvailable_CurrentVersionIsLatestVersion(t *testing.T) {
|
||||
e := &Extension{
|
||||
isLocal: false,
|
||||
kind: BinaryKind,
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: "1.0.0",
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ func TestUpdateAvailable_CurrentVersionIsLatestVersion(t *testing.T) {
|
|||
|
||||
func TestUpdateAvailable(t *testing.T) {
|
||||
e := &Extension{
|
||||
isLocal: false,
|
||||
kind: BinaryKind,
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: "1.1.0",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,16 +46,9 @@ func (g *gitExecuter) Fetch(remote string, refspec string) error {
|
|||
}
|
||||
|
||||
func (g *gitExecuter) ForRepo(repoDir string) gitClient {
|
||||
return &gitExecuter{
|
||||
client: &git.Client{
|
||||
GhPath: g.client.GhPath,
|
||||
RepoDir: repoDir,
|
||||
GitPath: g.client.GitPath,
|
||||
Stderr: g.client.Stderr,
|
||||
Stdin: g.client.Stdin,
|
||||
Stdout: g.client.Stdout,
|
||||
},
|
||||
}
|
||||
gc := g.client.Copy()
|
||||
gc.RepoDir = repoDir
|
||||
return &gitExecuter{client: gc}
|
||||
}
|
||||
|
||||
func (g *gitExecuter) Pull(remote, branch string) error {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
|
@ -83,7 +81,7 @@ func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Wri
|
|||
forwardArgs := args[1:]
|
||||
|
||||
exts, _ := m.list(false)
|
||||
var ext Extension
|
||||
var ext *Extension
|
||||
for _, e := range exts {
|
||||
if e.Name() == extName {
|
||||
ext = e
|
||||
|
|
@ -121,39 +119,53 @@ func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Wri
|
|||
func (m *Manager) List() []extensions.Extension {
|
||||
exts, _ := m.list(false)
|
||||
r := make([]extensions.Extension, len(exts))
|
||||
for i, v := range exts {
|
||||
val := v
|
||||
r[i] = &val
|
||||
for i, ext := range exts {
|
||||
r[i] = ext
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (m *Manager) list(includeMetadata bool) ([]Extension, error) {
|
||||
func (m *Manager) list(includeMetadata bool) ([]*Extension, error) {
|
||||
dir := m.installDir()
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []Extension
|
||||
results := make([]*Extension, 0, len(entries))
|
||||
for _, f := range entries {
|
||||
if !strings.HasPrefix(f.Name(), "gh-") {
|
||||
continue
|
||||
}
|
||||
var ext Extension
|
||||
var err error
|
||||
if f.IsDir() {
|
||||
ext, err = m.parseExtensionDir(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if _, err := os.Stat(filepath.Join(dir, f.Name(), manifestName)); err == nil {
|
||||
results = append(results, &Extension{
|
||||
path: filepath.Join(dir, f.Name(), f.Name()),
|
||||
kind: BinaryKind,
|
||||
httpClient: m.client,
|
||||
})
|
||||
} else {
|
||||
results = append(results, &Extension{
|
||||
path: filepath.Join(dir, f.Name(), f.Name()),
|
||||
kind: GitKind,
|
||||
gitClient: m.gitClient.ForRepo(filepath.Join(dir, f.Name())),
|
||||
})
|
||||
}
|
||||
results = append(results, ext)
|
||||
} else if isSymlink(f.Type()) {
|
||||
results = append(results, &Extension{
|
||||
path: filepath.Join(dir, f.Name(), f.Name()),
|
||||
kind: LocalKind,
|
||||
})
|
||||
} else {
|
||||
ext, err = m.parseExtensionFile(f)
|
||||
// the contents of a regular file point to a local extension on disk
|
||||
p, err := readPathFromFile(filepath.Join(dir, f.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, ext)
|
||||
results = append(results, &Extension{
|
||||
path: filepath.Join(p, f.Name()),
|
||||
kind: LocalKind,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -164,145 +176,16 @@ func (m *Manager) list(includeMetadata bool) ([]Extension, error) {
|
|||
return results, nil
|
||||
}
|
||||
|
||||
func (m *Manager) parseExtensionFile(fi fs.DirEntry) (Extension, error) {
|
||||
ext := Extension{isLocal: true}
|
||||
id := m.installDir()
|
||||
exePath := filepath.Join(id, fi.Name(), fi.Name())
|
||||
if !isSymlink(fi.Type()) {
|
||||
// if this is a regular file, its contents is the local directory of the extension
|
||||
p, err := readPathFromFile(filepath.Join(id, fi.Name()))
|
||||
if err != nil {
|
||||
return ext, err
|
||||
}
|
||||
exePath = filepath.Join(p, fi.Name())
|
||||
}
|
||||
ext.path = exePath
|
||||
return ext, nil
|
||||
}
|
||||
|
||||
func (m *Manager) parseExtensionDir(fi fs.DirEntry) (Extension, error) {
|
||||
id := m.installDir()
|
||||
if _, err := os.Stat(filepath.Join(id, fi.Name(), manifestName)); err == nil {
|
||||
return m.parseBinaryExtensionDir(fi)
|
||||
}
|
||||
|
||||
return m.parseGitExtensionDir(fi)
|
||||
}
|
||||
|
||||
func (m *Manager) parseBinaryExtensionDir(fi fs.DirEntry) (Extension, error) {
|
||||
id := m.installDir()
|
||||
exePath := filepath.Join(id, fi.Name(), fi.Name())
|
||||
ext := Extension{path: exePath, kind: BinaryKind}
|
||||
manifestPath := filepath.Join(id, fi.Name(), manifestName)
|
||||
manifest, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return ext, fmt.Errorf("could not open %s for reading: %w", manifestPath, err)
|
||||
}
|
||||
var bm binManifest
|
||||
err = yaml.Unmarshal(manifest, &bm)
|
||||
if err != nil {
|
||||
return ext, fmt.Errorf("could not parse %s: %w", manifestPath, err)
|
||||
}
|
||||
repo := ghrepo.NewWithHost(bm.Owner, bm.Name, bm.Host)
|
||||
remoteURL := ghrepo.GenerateRepoURL(repo, "")
|
||||
ext.url = remoteURL
|
||||
ext.currentVersion = bm.Tag
|
||||
ext.isPinned = bm.IsPinned
|
||||
return ext, nil
|
||||
}
|
||||
|
||||
func (m *Manager) parseGitExtensionDir(fi fs.DirEntry) (Extension, error) {
|
||||
id := m.installDir()
|
||||
exePath := filepath.Join(id, fi.Name(), fi.Name())
|
||||
remoteUrl := m.getRemoteUrl(fi.Name())
|
||||
currentVersion := m.getCurrentVersion(fi.Name())
|
||||
|
||||
var isPinned bool
|
||||
pinPath := filepath.Join(id, fi.Name(), fmt.Sprintf(".pin-%s", currentVersion))
|
||||
if _, err := os.Stat(pinPath); err == nil {
|
||||
isPinned = true
|
||||
}
|
||||
|
||||
return Extension{
|
||||
path: exePath,
|
||||
url: remoteUrl,
|
||||
isLocal: false,
|
||||
currentVersion: currentVersion,
|
||||
kind: GitKind,
|
||||
isPinned: isPinned,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getCurrentVersion determines the current version for non-local git extensions.
|
||||
func (m *Manager) getCurrentVersion(extension string) string {
|
||||
dir := filepath.Join(m.installDir(), extension)
|
||||
scopedClient := m.gitClient.ForRepo(dir)
|
||||
localSha, err := scopedClient.CommandOutput([]string{"rev-parse", "HEAD"})
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(bytes.TrimSpace(localSha))
|
||||
}
|
||||
|
||||
// getRemoteUrl determines the remote URL for non-local git extensions.
|
||||
func (m *Manager) getRemoteUrl(extension string) string {
|
||||
dir := filepath.Join(m.installDir(), extension)
|
||||
scopedClient := m.gitClient.ForRepo(dir)
|
||||
url, err := scopedClient.Config("remote.origin.url")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(url))
|
||||
}
|
||||
|
||||
func (m *Manager) populateLatestVersions(exts []Extension) {
|
||||
size := len(exts)
|
||||
type result struct {
|
||||
index int
|
||||
version string
|
||||
}
|
||||
ch := make(chan result, size)
|
||||
func (m *Manager) populateLatestVersions(exts []*Extension) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(size)
|
||||
for idx, ext := range exts {
|
||||
go func(i int, e Extension) {
|
||||
for _, ext := range exts {
|
||||
wg.Add(1)
|
||||
go func(e *Extension) {
|
||||
defer wg.Done()
|
||||
version, _ := m.getLatestVersion(e)
|
||||
ch <- result{index: i, version: version}
|
||||
}(idx, ext)
|
||||
e.LatestVersion()
|
||||
}(ext)
|
||||
}
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
for r := range ch {
|
||||
ext := &exts[r.index]
|
||||
ext.latestVersion = r.version
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) getLatestVersion(ext Extension) (string, error) {
|
||||
if ext.isLocal {
|
||||
return "", localExtensionUpgradeError
|
||||
}
|
||||
if ext.IsBinary() {
|
||||
repo, err := ghrepo.FromFullName(ext.url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
r, err := fetchLatestRelease(m.client, repo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.Tag, nil
|
||||
} else {
|
||||
extDir := filepath.Dir(ext.path)
|
||||
scopedClient := m.gitClient.ForRepo(extDir)
|
||||
lsRemote, err := scopedClient.CommandOutput([]string{"ls-remote", "origin", "HEAD"})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
remoteSha := bytes.SplitN(lsRemote, []byte("\t"), 2)[0]
|
||||
return string(remoteSha), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) InstallLocal(dir string) error {
|
||||
|
|
@ -521,22 +404,23 @@ func (m *Manager) Upgrade(name string, force bool) error {
|
|||
if f.Name() != name {
|
||||
continue
|
||||
}
|
||||
var err error
|
||||
// For single extensions manually retrieve latest version since we forgo
|
||||
// doing it during list.
|
||||
f.latestVersion, err = m.getLatestVersion(f)
|
||||
if err != nil {
|
||||
return err
|
||||
if f.IsLocal() {
|
||||
return localExtensionUpgradeError
|
||||
}
|
||||
return m.upgradeExtensions([]Extension{f}, force)
|
||||
// For single extensions manually retrieve latest version since we forgo doing it during list.
|
||||
if latestVersion := f.LatestVersion(); latestVersion == "" {
|
||||
return fmt.Errorf("unable to retrieve latest version for extension %q", name)
|
||||
}
|
||||
return m.upgradeExtensions([]*Extension{f}, force)
|
||||
}
|
||||
return fmt.Errorf("no extension matched %q", name)
|
||||
}
|
||||
|
||||
func (m *Manager) upgradeExtensions(exts []Extension, force bool) error {
|
||||
func (m *Manager) upgradeExtensions(exts []*Extension, force bool) error {
|
||||
var failed bool
|
||||
for _, f := range exts {
|
||||
fmt.Fprintf(m.io.Out, "[%s]: ", f.Name())
|
||||
currentVersion := displayExtensionVersion(f, f.CurrentVersion())
|
||||
err := m.upgradeExtension(f, force)
|
||||
if err != nil {
|
||||
if !errors.Is(err, localExtensionUpgradeError) &&
|
||||
|
|
@ -547,8 +431,7 @@ func (m *Manager) upgradeExtensions(exts []Extension, force bool) error {
|
|||
fmt.Fprintf(m.io.Out, "%s\n", err)
|
||||
continue
|
||||
}
|
||||
currentVersion := displayExtensionVersion(&f, f.currentVersion)
|
||||
latestVersion := displayExtensionVersion(&f, f.latestVersion)
|
||||
latestVersion := displayExtensionVersion(f, f.LatestVersion())
|
||||
if m.dryRunMode {
|
||||
fmt.Fprintf(m.io.Out, "would have upgraded from %s to %s\n", currentVersion, latestVersion)
|
||||
} else {
|
||||
|
|
@ -561,8 +444,8 @@ func (m *Manager) upgradeExtensions(exts []Extension, force bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) upgradeExtension(ext Extension, force bool) error {
|
||||
if ext.isLocal {
|
||||
func (m *Manager) upgradeExtension(ext *Extension, force bool) error {
|
||||
if ext.IsLocal() {
|
||||
return localExtensionUpgradeError
|
||||
}
|
||||
if !force && ext.IsPinned() {
|
||||
|
|
@ -592,7 +475,7 @@ func (m *Manager) upgradeExtension(ext Extension, force bool) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) upgradeGitExtension(ext Extension, force bool) error {
|
||||
func (m *Manager) upgradeGitExtension(ext *Extension, force bool) error {
|
||||
if m.dryRunMode {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -611,10 +494,10 @@ func (m *Manager) upgradeGitExtension(ext Extension, force bool) error {
|
|||
return scopedClient.Pull("", "")
|
||||
}
|
||||
|
||||
func (m *Manager) upgradeBinExtension(ext Extension) error {
|
||||
repo, err := ghrepo.FromFullName(ext.url)
|
||||
func (m *Manager) upgradeBinExtension(ext *Extension) error {
|
||||
repo, err := ghrepo.FromFullName(ext.URL())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse URL %s: %w", ext.url, err)
|
||||
return fmt.Errorf("failed to parse URL %s: %w", ext.URL(), err)
|
||||
}
|
||||
return m.installBin(repo, "")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,12 +80,8 @@ func TestManager_List(t *testing.T) {
|
|||
dirOne := filepath.Join(tempDir, "extensions", "gh-hello")
|
||||
dirTwo := filepath.Join(tempDir, "extensions", "gh-two")
|
||||
gc, gcOne, gcTwo := &mockGitClient{}, &mockGitClient{}, &mockGitClient{}
|
||||
gc.On("ForRepo", dirOne).Return(gcOne).Twice()
|
||||
gc.On("ForRepo", dirTwo).Return(gcTwo).Twice()
|
||||
gcOne.On("Config", "remote.origin.url").Return("", nil).Once()
|
||||
gcTwo.On("Config", "remote.origin.url").Return("", nil).Once()
|
||||
gcOne.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
|
||||
gcTwo.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
|
||||
gc.On("ForRepo", dirOne).Return(gcOne).Once()
|
||||
gc.On("ForRepo", dirTwo).Return(gcTwo).Once()
|
||||
|
||||
m := newTestManager(tempDir, nil, gc, nil)
|
||||
exts := m.List()
|
||||
|
|
@ -145,9 +141,7 @@ func TestManager_Dispatch(t *testing.T) {
|
|||
assert.NoError(t, stubExtension(extPath))
|
||||
|
||||
gc, gcOne := &mockGitClient{}, &mockGitClient{}
|
||||
gc.On("ForRepo", extDir).Return(gcOne).Twice()
|
||||
gcOne.On("Config", "remote.origin.url").Return("", nil).Once()
|
||||
gcOne.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
|
||||
gc.On("ForRepo", extDir).Return(gcOne).Once()
|
||||
|
||||
m := newTestManager(tempDir, nil, gc, nil)
|
||||
|
||||
|
|
@ -223,9 +217,7 @@ func TestManager_Upgrade_NoMatchingExtension(t *testing.T) {
|
|||
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-hello", "gh-hello")))
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
gc, gcOne := &mockGitClient{}, &mockGitClient{}
|
||||
gc.On("ForRepo", extDir).Return(gcOne).Twice()
|
||||
gcOne.On("Config", "remote.origin.url").Return("", nil).Once()
|
||||
gcOne.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
|
||||
gc.On("ForRepo", extDir).Return(gcOne).Once()
|
||||
m := newTestManager(tempDir, nil, gc, ios)
|
||||
err := m.Upgrade("invalid", false)
|
||||
assert.EqualError(t, err, `no extension matched "invalid"`)
|
||||
|
|
@ -244,12 +236,8 @@ func TestManager_UpgradeExtensions(t *testing.T) {
|
|||
assert.NoError(t, stubLocalExtension(tempDir, filepath.Join(tempDir, "extensions", "gh-local", "gh-local")))
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
gc, gcOne, gcTwo := &mockGitClient{}, &mockGitClient{}, &mockGitClient{}
|
||||
gc.On("ForRepo", dirOne).Return(gcOne).Times(4)
|
||||
gc.On("ForRepo", dirTwo).Return(gcTwo).Times(4)
|
||||
gcOne.On("Config", "remote.origin.url").Return("", nil).Once()
|
||||
gcTwo.On("Config", "remote.origin.url").Return("", nil).Once()
|
||||
gcOne.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
|
||||
gcTwo.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
|
||||
gc.On("ForRepo", dirOne).Return(gcOne).Times(3)
|
||||
gc.On("ForRepo", dirTwo).Return(gcTwo).Times(3)
|
||||
gcOne.On("Remotes").Return(nil, nil).Once()
|
||||
gcTwo.On("Remotes").Return(nil, nil).Once()
|
||||
gcOne.On("Pull", "", "").Return(nil).Once()
|
||||
|
|
@ -286,12 +274,8 @@ func TestManager_UpgradeExtensions_DryRun(t *testing.T) {
|
|||
assert.NoError(t, stubLocalExtension(tempDir, filepath.Join(tempDir, "extensions", "gh-local", "gh-local")))
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
gc, gcOne, gcTwo := &mockGitClient{}, &mockGitClient{}, &mockGitClient{}
|
||||
gc.On("ForRepo", dirOne).Return(gcOne).Times(3)
|
||||
gc.On("ForRepo", dirTwo).Return(gcTwo).Times(3)
|
||||
gcOne.On("Config", "remote.origin.url").Return("", nil).Once()
|
||||
gcTwo.On("Config", "remote.origin.url").Return("", nil).Once()
|
||||
gcOne.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
|
||||
gcTwo.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
|
||||
gc.On("ForRepo", dirOne).Return(gcOne).Twice()
|
||||
gc.On("ForRepo", dirTwo).Return(gcTwo).Twice()
|
||||
gcOne.On("Remotes").Return(nil, nil).Once()
|
||||
gcTwo.On("Remotes").Return(nil, nil).Once()
|
||||
m := newTestManager(tempDir, nil, gc, ios)
|
||||
|
|
@ -355,9 +339,7 @@ func TestManager_UpgradeExtension_GitExtension(t *testing.T) {
|
|||
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote")))
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
gc, gcOne := &mockGitClient{}, &mockGitClient{}
|
||||
gc.On("ForRepo", extensionDir).Return(gcOne).Times(4)
|
||||
gcOne.On("Config", "remote.origin.url").Return("", nil).Once()
|
||||
gcOne.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
|
||||
gc.On("ForRepo", extensionDir).Return(gcOne).Times(3)
|
||||
gcOne.On("Remotes").Return(nil, nil).Once()
|
||||
gcOne.On("Pull", "", "").Return(nil).Once()
|
||||
m := newTestManager(tempDir, nil, gc, ios)
|
||||
|
|
@ -381,9 +363,7 @@ func TestManager_UpgradeExtension_GitExtension_DryRun(t *testing.T) {
|
|||
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote")))
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
gc, gcOne := &mockGitClient{}, &mockGitClient{}
|
||||
gc.On("ForRepo", extDir).Return(gcOne).Times(3)
|
||||
gcOne.On("Config", "remote.origin.url").Return("", nil).Once()
|
||||
gcOne.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
|
||||
gc.On("ForRepo", extDir).Return(gcOne).Twice()
|
||||
gcOne.On("Remotes").Return(nil, nil).Once()
|
||||
m := newTestManager(tempDir, nil, gc, ios)
|
||||
m.EnableDryRunMode()
|
||||
|
|
@ -407,9 +387,7 @@ func TestManager_UpgradeExtension_GitExtension_Force(t *testing.T) {
|
|||
assert.NoError(t, stubExtension(filepath.Join(tempDir, "extensions", "gh-remote", "gh-remote")))
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
gc, gcOne := &mockGitClient{}, &mockGitClient{}
|
||||
gc.On("ForRepo", extensionDir).Return(gcOne).Times(4)
|
||||
gcOne.On("Config", "remote.origin.url").Return("", nil).Once()
|
||||
gcOne.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
|
||||
gc.On("ForRepo", extensionDir).Return(gcOne).Times(3)
|
||||
gcOne.On("Remotes").Return(nil, nil).Once()
|
||||
gcOne.On("Fetch", "origin", "HEAD").Return(nil).Once()
|
||||
gcOne.On("CommandOutput", []string{"reset", "--hard", "origin/HEAD"}).Return("", nil).Once()
|
||||
|
|
@ -721,9 +699,7 @@ func TestManager_UpgradeExtension_GitExtension_Pinned(t *testing.T) {
|
|||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
gc, gcOne := &mockGitClient{}, &mockGitClient{}
|
||||
gc.On("ForRepo", extDir).Return(gcOne).Twice()
|
||||
gcOne.On("Config", "remote.origin.url").Return("", nil).Once()
|
||||
gcOne.On("CommandOutput", []string{"rev-parse", "HEAD"}).Return("", nil).Once()
|
||||
gc.On("ForRepo", extDir).Return(gcOne).Once()
|
||||
|
||||
m := newTestManager(tempDir, nil, gc, ios)
|
||||
|
||||
|
|
@ -732,7 +708,8 @@ func TestManager_UpgradeExtension_GitExtension_Pinned(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(exts))
|
||||
ext := exts[0]
|
||||
ext.isPinned = true
|
||||
pinnedTrue := true
|
||||
ext.isPinned = &pinnedTrue
|
||||
ext.latestVersion = "new version"
|
||||
|
||||
err = m.upgradeExtension(ext, false)
|
||||
|
|
|
|||
|
|
@ -12,21 +12,23 @@ import (
|
|||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/prompt"
|
||||
"github.com/cli/cli/v2/pkg/surveyext"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var editNextOptions = []string{"Edit another file", "Submit", "Cancel"}
|
||||
|
||||
type EditOptions struct {
|
||||
IO *iostreams.IOStreams
|
||||
HttpClient func() (*http.Client, error)
|
||||
Config func() (config.Config, error)
|
||||
Prompter prompter.Prompter
|
||||
|
||||
Edit func(string, string, string, *iostreams.IOStreams) (string, error)
|
||||
|
||||
|
|
@ -42,6 +44,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
|
|||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Config: f.Config,
|
||||
Prompter: f.Prompter,
|
||||
Edit: func(editorCmd, filename, defaultContent string, io *iostreams.IOStreams) (string, error) {
|
||||
return surveyext.Edit(
|
||||
editorCmd,
|
||||
|
|
@ -55,16 +58,15 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
|
|||
Use: "edit {<id> | <url>} [<filename>]",
|
||||
Short: "Edit one of your gists",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return cmdutil.FlagErrorf("cannot edit: gist argument required")
|
||||
}
|
||||
if len(args) > 2 {
|
||||
return cmdutil.FlagErrorf("too many arguments")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
opts.Selector = args[0]
|
||||
if len(args) > 0 {
|
||||
opts.Selector = args[0]
|
||||
}
|
||||
if len(args) > 1 {
|
||||
opts.SourceFile = args[1]
|
||||
}
|
||||
|
|
@ -85,7 +87,33 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
|
|||
}
|
||||
|
||||
func editRun(opts *EditOptions) error {
|
||||
client, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := opts.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
host, _ := cfg.Authentication().DefaultHost()
|
||||
|
||||
gistID := opts.Selector
|
||||
if gistID == "" {
|
||||
cs := opts.IO.ColorScheme()
|
||||
if gistID == "" {
|
||||
gistID, err = shared.PromptGists(opts.Prompter, client, host, cs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gistID == "" {
|
||||
fmt.Fprintln(opts.IO.Out, "No gists found.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(gistID, "/") {
|
||||
id, err := shared.GistIDFromURL(gistID)
|
||||
|
|
@ -95,20 +123,8 @@ func editRun(opts *EditOptions) error {
|
|||
gistID = id
|
||||
}
|
||||
|
||||
client, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiClient := api.NewClientFromHTTP(client)
|
||||
|
||||
cfg, err := opts.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
host, _ := cfg.Authentication().DefaultHost()
|
||||
|
||||
gist, err := shared.GetGist(client, host, gistID)
|
||||
if err != nil {
|
||||
if errors.Is(err, shared.NotFoundErr) {
|
||||
|
|
@ -189,15 +205,11 @@ func editRun(opts *EditOptions) error {
|
|||
if !opts.IO.CanPrompt() {
|
||||
return errors.New("unsure what file to edit; either specify --filename or run interactively")
|
||||
}
|
||||
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
|
||||
err = prompt.SurveyAskOne(&survey.Select{
|
||||
Message: "Edit which file?",
|
||||
Options: candidates,
|
||||
}, &filename)
|
||||
|
||||
result, err := opts.Prompter.Select("Edit which file?", "", candidates)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
filename = candidates[result]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -249,20 +261,11 @@ func editRun(opts *EditOptions) error {
|
|||
}
|
||||
|
||||
choice := ""
|
||||
|
||||
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
|
||||
err = prompt.SurveyAskOne(&survey.Select{
|
||||
Message: "What next?",
|
||||
Options: []string{
|
||||
"Edit another file",
|
||||
"Submit",
|
||||
"Cancel",
|
||||
},
|
||||
}, &choice)
|
||||
|
||||
result, err := opts.Prompter.Select("What next?", "", editNextOptions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
choice = editNextOptions[result]
|
||||
|
||||
stop := false
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/prompt"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -115,15 +115,15 @@ func Test_editRun(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *EditOptions
|
||||
gist *shared.Gist
|
||||
httpStubs func(*httpmock.Registry)
|
||||
askStubs func(*prompt.AskStubber)
|
||||
nontty bool
|
||||
stdin string
|
||||
wantErr string
|
||||
wantParams map[string]interface{}
|
||||
name string
|
||||
opts *EditOptions
|
||||
gist *shared.Gist
|
||||
httpStubs func(*httpmock.Registry)
|
||||
prompterStubs func(*prompter.MockPrompter)
|
||||
nontty bool
|
||||
stdin string
|
||||
wantErr string
|
||||
wantParams map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "no such gist",
|
||||
|
|
@ -161,9 +161,17 @@ func Test_editRun(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "multiple files, submit",
|
||||
askStubs: func(as *prompt.AskStubber) {
|
||||
as.StubPrompt("Edit which file?").AnswerWith("unix.md")
|
||||
as.StubPrompt("What next?").AnswerWith("Submit")
|
||||
prompterStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterSelect("Edit which file?",
|
||||
[]string{"cicada.txt", "unix.md"},
|
||||
func(_, _ string, opts []string) (int, error) {
|
||||
return prompter.IndexFor(opts, "unix.md")
|
||||
})
|
||||
pm.RegisterSelect("What next?",
|
||||
editNextOptions,
|
||||
func(_, _ string, opts []string) (int, error) {
|
||||
return prompter.IndexFor(opts, "Submit")
|
||||
})
|
||||
},
|
||||
gist: &shared.Gist{
|
||||
ID: "1234",
|
||||
|
|
@ -206,9 +214,17 @@ func Test_editRun(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "multiple files, cancel",
|
||||
askStubs: func(as *prompt.AskStubber) {
|
||||
as.StubPrompt("Edit which file?").AnswerWith("unix.md")
|
||||
as.StubPrompt("What next?").AnswerWith("Cancel")
|
||||
prompterStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterSelect("Edit which file?",
|
||||
[]string{"cicada.txt", "unix.md"},
|
||||
func(_, _ string, opts []string) (int, error) {
|
||||
return prompter.IndexFor(opts, "unix.md")
|
||||
})
|
||||
pm.RegisterSelect("What next?",
|
||||
editNextOptions,
|
||||
func(_, _ string, opts []string) (int, error) {
|
||||
return prompter.IndexFor(opts, "Cancel")
|
||||
})
|
||||
},
|
||||
wantErr: "CancelError",
|
||||
gist: &shared.Gist{
|
||||
|
|
@ -486,11 +502,11 @@ func Test_editRun(t *testing.T) {
|
|||
}
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
|
||||
as := prompt.NewAskStubber(t)
|
||||
if tt.askStubs != nil {
|
||||
tt.askStubs(as)
|
||||
pm := prompter.NewMockPrompter(t)
|
||||
if tt.prompterStubs != nil {
|
||||
tt.prompterStubs(pm)
|
||||
}
|
||||
tt.opts.Prompter = pm
|
||||
|
||||
err := editRun(tt.opts)
|
||||
reg.Verify(t)
|
||||
|
|
|
|||
|
|
@ -5,10 +5,14 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/shurcooL/githubv4"
|
||||
)
|
||||
|
|
@ -171,3 +175,48 @@ func IsBinaryContents(contents []byte) bool {
|
|||
}
|
||||
return isBinary
|
||||
}
|
||||
|
||||
func PromptGists(prompter prompter.Prompter, client *http.Client, host string, cs *iostreams.ColorScheme) (gistID string, err error) {
|
||||
gists, err := ListGists(client, host, 10, "all")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(gists) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var opts []string
|
||||
var gistIDs = make([]string, len(gists))
|
||||
|
||||
for i, gist := range gists {
|
||||
gistIDs[i] = gist.ID
|
||||
description := ""
|
||||
gistName := ""
|
||||
|
||||
if gist.Description != "" {
|
||||
description = gist.Description
|
||||
}
|
||||
|
||||
filenames := make([]string, 0, len(gist.Files))
|
||||
for fn := range gist.Files {
|
||||
filenames = append(filenames, fn)
|
||||
}
|
||||
sort.Strings(filenames)
|
||||
gistName = filenames[0]
|
||||
|
||||
gistTime := text.FuzzyAgo(time.Now(), gist.UpdatedAt)
|
||||
// TODO: support dynamic maxWidth
|
||||
description = text.Truncate(100, text.RemoveExcessiveWhitespace(description))
|
||||
opt := fmt.Sprintf("%s %s %s", cs.Bold(gistName), description, cs.Gray(gistTime))
|
||||
opts = append(opts, opt)
|
||||
}
|
||||
|
||||
result, err := prompter.Select("Select a gist", "", opts)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return gistIDs[result], nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
@ -85,3 +91,104 @@ func TestIsBinaryContents(t *testing.T) {
|
|||
assert.Equal(t, tt.want, IsBinaryContents(tt.fileContent))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptGists(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prompterStubs func(pm *prompter.MockPrompter)
|
||||
response string
|
||||
wantOut string
|
||||
gist *Gist
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "multiple files, select first gist",
|
||||
prompterStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterSelect("Select a gist",
|
||||
[]string{"cool.txt about 6 hours ago", "gistfile0.txt about 6 hours ago"},
|
||||
func(_, _ string, opts []string) (int, error) {
|
||||
return prompter.IndexFor(opts, "cool.txt about 6 hours ago")
|
||||
})
|
||||
},
|
||||
response: `{ "data": { "viewer": { "gists": { "nodes": [
|
||||
{
|
||||
"name": "gistid1",
|
||||
"files": [{ "name": "cool.txt" }],
|
||||
"description": "",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
},
|
||||
{
|
||||
"name": "gistid2",
|
||||
"files": [{ "name": "gistfile0.txt" }],
|
||||
"description": "",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
}
|
||||
] } } } }`,
|
||||
wantOut: "gistid1",
|
||||
},
|
||||
{
|
||||
name: "multiple files, select second gist",
|
||||
prompterStubs: func(pm *prompter.MockPrompter) {
|
||||
pm.RegisterSelect("Select a gist",
|
||||
[]string{"cool.txt about 6 hours ago", "gistfile0.txt about 6 hours ago"},
|
||||
func(_, _ string, opts []string) (int, error) {
|
||||
return prompter.IndexFor(opts, "gistfile0.txt about 6 hours ago")
|
||||
})
|
||||
},
|
||||
response: `{ "data": { "viewer": { "gists": { "nodes": [
|
||||
{
|
||||
"name": "gistid1",
|
||||
"files": [{ "name": "cool.txt" }],
|
||||
"description": "",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
},
|
||||
{
|
||||
"name": "gistid2",
|
||||
"files": [{ "name": "gistfile0.txt" }],
|
||||
"description": "",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
}
|
||||
] } } } }`,
|
||||
wantOut: "gistid2",
|
||||
},
|
||||
{
|
||||
name: "no files",
|
||||
response: `{ "data": { "viewer": { "gists": { "nodes": [] } } } }`,
|
||||
wantOut: "",
|
||||
},
|
||||
}
|
||||
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
for _, tt := range tests {
|
||||
reg := &httpmock.Registry{}
|
||||
|
||||
const query = `query GistList\b`
|
||||
sixHours, _ := time.ParseDuration("6h")
|
||||
sixHoursAgo := time.Now().Add(-sixHours)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(query),
|
||||
httpmock.StringResponse(fmt.Sprintf(
|
||||
tt.response,
|
||||
sixHoursAgo.Format(time.RFC3339),
|
||||
)),
|
||||
)
|
||||
client := &http.Client{Transport: reg}
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockPrompter := prompter.NewMockPrompter(t)
|
||||
if tt.prompterStubs != nil {
|
||||
tt.prompterStubs(mockPrompter)
|
||||
}
|
||||
|
||||
gistID, err := PromptGists(mockPrompter, client, "github.com", ios.ColorScheme())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantOut, gistID)
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,17 +5,15 @@ import (
|
|||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghinstance"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/markdown"
|
||||
"github.com/cli/cli/v2/pkg/prompt"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -28,6 +26,7 @@ type ViewOptions struct {
|
|||
Config func() (config.Config, error)
|
||||
HttpClient func() (*http.Client, error)
|
||||
Browser browser
|
||||
Prompter prompter.Prompter
|
||||
|
||||
Selector string
|
||||
Filename string
|
||||
|
|
@ -42,6 +41,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
Config: f.Config,
|
||||
HttpClient: f.HttpClient,
|
||||
Browser: f.Browser,
|
||||
Prompter: f.Prompter,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
|
@ -89,7 +89,7 @@ func viewRun(opts *ViewOptions) error {
|
|||
|
||||
cs := opts.IO.ColorScheme()
|
||||
if gistID == "" {
|
||||
gistID, err = promptGists(client, hostname, cs)
|
||||
gistID, err = shared.PromptGists(opts.Prompter, client, hostname, cs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -204,55 +204,3 @@ func viewRun(opts *ViewOptions) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func promptGists(client *http.Client, host string, cs *iostreams.ColorScheme) (gistID string, err error) {
|
||||
gists, err := shared.ListGists(client, host, 10, "all")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(gists) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var opts []string
|
||||
var result int
|
||||
var gistIDs = make([]string, len(gists))
|
||||
|
||||
for i, gist := range gists {
|
||||
gistIDs[i] = gist.ID
|
||||
description := ""
|
||||
gistName := ""
|
||||
|
||||
if gist.Description != "" {
|
||||
description = gist.Description
|
||||
}
|
||||
|
||||
filenames := make([]string, 0, len(gist.Files))
|
||||
for fn := range gist.Files {
|
||||
filenames = append(filenames, fn)
|
||||
}
|
||||
sort.Strings(filenames)
|
||||
gistName = filenames[0]
|
||||
|
||||
gistTime := text.FuzzyAgo(time.Now(), gist.UpdatedAt)
|
||||
// TODO: support dynamic maxWidth
|
||||
description = text.Truncate(100, text.RemoveExcessiveWhitespace(description))
|
||||
opt := fmt.Sprintf("%s %s %s", cs.Bold(gistName), description, cs.Gray(gistTime))
|
||||
opts = append(opts, opt)
|
||||
}
|
||||
|
||||
questions := &survey.Select{
|
||||
Message: "Select a gist",
|
||||
Options: opts,
|
||||
}
|
||||
|
||||
//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
|
||||
err = prompt.SurveyAskOne(questions, &result)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return gistIDs[result], nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/prompt"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
|
@ -336,6 +336,10 @@ func Test_viewRun(t *testing.T) {
|
|||
httpmock.JSONResponse(tt.gist))
|
||||
}
|
||||
|
||||
if tt.opts == nil {
|
||||
tt.opts = &ViewOptions{}
|
||||
}
|
||||
|
||||
if tt.mockGistList {
|
||||
sixHours, _ := time.ParseDuration("6h")
|
||||
sixHoursAgo := time.Now().Add(-sixHours)
|
||||
|
|
@ -355,13 +359,11 @@ func Test_viewRun(t *testing.T) {
|
|||
)),
|
||||
)
|
||||
|
||||
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
|
||||
as := prompt.NewAskStubber(t)
|
||||
as.StubPrompt("Select a gist").AnswerDefault()
|
||||
}
|
||||
|
||||
if tt.opts == nil {
|
||||
tt.opts = &ViewOptions{}
|
||||
pm := prompter.NewMockPrompter(t)
|
||||
pm.RegisterSelect("Select a gist", []string{"cool.txt about 6 hours ago"}, func(_, _ string, opts []string) (int, error) {
|
||||
return 0, nil
|
||||
})
|
||||
tt.opts.Prompter = pm
|
||||
}
|
||||
|
||||
tt.opts.HttpClient = func() (*http.Client, error) {
|
||||
|
|
@ -389,97 +391,3 @@ func Test_viewRun(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_promptGists(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
askStubs func(as *prompt.AskStubber)
|
||||
response string
|
||||
wantOut string
|
||||
gist *shared.Gist
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "multiple files, select first gist",
|
||||
askStubs: func(as *prompt.AskStubber) {
|
||||
as.StubPrompt("Select a gist").AnswerWith("cool.txt about 6 hours ago")
|
||||
},
|
||||
response: `{ "data": { "viewer": { "gists": { "nodes": [
|
||||
{
|
||||
"name": "gistid1",
|
||||
"files": [{ "name": "cool.txt" }],
|
||||
"description": "",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
},
|
||||
{
|
||||
"name": "gistid2",
|
||||
"files": [{ "name": "gistfile0.txt" }],
|
||||
"description": "",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
}
|
||||
] } } } }`,
|
||||
wantOut: "gistid1",
|
||||
},
|
||||
{
|
||||
name: "multiple files, select second gist",
|
||||
askStubs: func(as *prompt.AskStubber) {
|
||||
as.StubPrompt("Select a gist").AnswerWith("gistfile0.txt about 6 hours ago")
|
||||
},
|
||||
response: `{ "data": { "viewer": { "gists": { "nodes": [
|
||||
{
|
||||
"name": "gistid1",
|
||||
"files": [{ "name": "cool.txt" }],
|
||||
"description": "",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
},
|
||||
{
|
||||
"name": "gistid2",
|
||||
"files": [{ "name": "gistfile0.txt" }],
|
||||
"description": "",
|
||||
"updatedAt": "%[1]v",
|
||||
"isPublic": true
|
||||
}
|
||||
] } } } }`,
|
||||
wantOut: "gistid2",
|
||||
},
|
||||
{
|
||||
name: "no files",
|
||||
response: `{ "data": { "viewer": { "gists": { "nodes": [] } } } }`,
|
||||
wantOut: "",
|
||||
},
|
||||
}
|
||||
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
for _, tt := range tests {
|
||||
reg := &httpmock.Registry{}
|
||||
|
||||
const query = `query GistList\b`
|
||||
sixHours, _ := time.ParseDuration("6h")
|
||||
sixHoursAgo := time.Now().Add(-sixHours)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(query),
|
||||
httpmock.StringResponse(fmt.Sprintf(
|
||||
tt.response,
|
||||
sixHoursAgo.Format(time.RFC3339),
|
||||
)),
|
||||
)
|
||||
client := &http.Client{Transport: reg}
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
|
||||
as := prompt.NewAskStubber(t)
|
||||
if tt.askStubs != nil {
|
||||
tt.askStubs(as)
|
||||
}
|
||||
|
||||
gistID, err := promptGists(client, "github.com", ios.ColorScheme())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantOut, gistID)
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import (
|
|||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
||||
prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
|
|
@ -143,7 +142,7 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
milestones = []string{opts.Milestone}
|
||||
}
|
||||
|
||||
meReplacer := shared.NewMeReplacer(apiClient, baseRepo.RepoHost())
|
||||
meReplacer := prShared.NewMeReplacer(apiClient, baseRepo.RepoHost())
|
||||
assignees, err := meReplacer.ReplaceSlice(opts.Assignees)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -167,7 +166,7 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
tpl := shared.NewTemplateManager(httpClient, baseRepo, opts.Prompter, opts.RootDirOverride, !opts.HasRepoOverride, false)
|
||||
tpl := prShared.NewTemplateManager(httpClient, baseRepo, opts.Prompter, opts.RootDirOverride, !opts.HasRepoOverride, false)
|
||||
|
||||
if opts.WebMode {
|
||||
var openURL string
|
||||
|
|
@ -222,7 +221,7 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
templateContent := ""
|
||||
|
||||
if opts.RecoverFile == "" {
|
||||
var template shared.Template
|
||||
var template prShared.Template
|
||||
|
||||
if opts.Template != "" {
|
||||
template, err = tpl.Select(opts.Template)
|
||||
|
|
@ -325,7 +324,7 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func generatePreviewURL(apiClient *api.Client, baseRepo ghrepo.Interface, tb shared.IssueMetadataState) (string, error) {
|
||||
func generatePreviewURL(apiClient *api.Client, baseRepo ghrepo.Interface, tb prShared.IssueMetadataState) (string, error) {
|
||||
openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new")
|
||||
return prShared.WithPrAndIssueQueryParams(apiClient, baseRepo, openURL, tb)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import (
|
|||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
issueShared "github.com/cli/cli/v2/pkg/cmd/issue/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
||||
prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
|
|
@ -133,7 +132,7 @@ func listRun(opts *ListOptions) error {
|
|||
}
|
||||
|
||||
issueState := strings.ToLower(opts.State)
|
||||
if issueState == "open" && shared.QueryHasStateClause(opts.Search) {
|
||||
if issueState == "open" && prShared.QueryHasStateClause(opts.Search) {
|
||||
issueState = ""
|
||||
}
|
||||
|
||||
|
|
@ -228,7 +227,7 @@ func issueList(client *http.Client, repo ghrepo.Interface, filters prShared.Filt
|
|||
}
|
||||
|
||||
var err error
|
||||
meReplacer := shared.NewMeReplacer(apiClient, repo.RepoHost())
|
||||
meReplacer := prShared.NewMeReplacer(apiClient, repo.RepoHost())
|
||||
filters.Assignee, err = meReplacer.Replace(filters.Assignee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -285,10 +285,8 @@ func TestIssuesFromArgsWithFields(t *testing.T) {
|
|||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
for i, issue := range issues {
|
||||
if issue.Number != tt.wantIssues[i] {
|
||||
t.Errorf("want issue #%d, got #%d", tt.wantIssues[i], issue.Number)
|
||||
}
|
||||
for i := range issues {
|
||||
assert.Contains(t, tt.wantIssues, issues[i].Number)
|
||||
}
|
||||
if repo != nil {
|
||||
repoURL := ghrepo.GenerateRepoURL(repo, "")
|
||||
|
|
|
|||
|
|
@ -30,10 +30,10 @@ func aggregateChecks(checkContexts []api.CheckContext, requiredChecks bool) (che
|
|||
continue
|
||||
}
|
||||
|
||||
state := c.State
|
||||
state := string(c.State)
|
||||
if state == "" {
|
||||
if c.Status == "COMPLETED" {
|
||||
state = c.Conclusion
|
||||
state = string(c.Conclusion)
|
||||
} else {
|
||||
state = c.Status
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -129,6 +130,12 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
|
|||
|
||||
opts.TitleProvided = cmd.Flags().Changed("title")
|
||||
opts.RepoOverride, _ = cmd.Flags().GetString("repo")
|
||||
// Workaround: Due to the way this command is implemented, we need to manually check GH_REPO.
|
||||
// Commands should use the standard BaseRepoOverride functionality to handle this behavior instead.
|
||||
if opts.RepoOverride == "" {
|
||||
opts.RepoOverride = os.Getenv("GH_REPO")
|
||||
}
|
||||
|
||||
noMaintainerEdit, _ := cmd.Flags().GetBool("no-maintainer-edit")
|
||||
opts.MaintainerCanModify = !noMaintainerEdit
|
||||
|
||||
|
|
@ -318,8 +325,6 @@ func createRun(opts *CreateOptions) (err error) {
|
|||
|
||||
if template != nil {
|
||||
templateContent = string(template.Body())
|
||||
} else {
|
||||
templateContent = string(tpl.LegacyBody())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -465,7 +465,7 @@ func preloadPrChecks(client *http.Client, repo ghrepo.Interface, pr *api.PullReq
|
|||
%s
|
||||
}
|
||||
}
|
||||
}`, api.StatusCheckRollupGraphQL("$endCursor"))
|
||||
}`, api.StatusCheckRollupGraphQLWithoutCountByState("$endCursor"))
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"id": pr.ID,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/prompter"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/prompt"
|
||||
)
|
||||
|
|
@ -223,6 +224,7 @@ func MetadataSurvey(io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher
|
|||
Message: "Reviewers",
|
||||
Options: reviewers,
|
||||
Default: state.Reviewers,
|
||||
Filter: prompter.LatinMatchingFilter,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
|
|
@ -237,6 +239,7 @@ func MetadataSurvey(io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher
|
|||
Message: "Assignees",
|
||||
Options: assignees,
|
||||
Default: state.Assignees,
|
||||
Filter: prompter.LatinMatchingFilter,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
|
|
@ -251,6 +254,7 @@ func MetadataSurvey(io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher
|
|||
Message: "Labels",
|
||||
Options: labels,
|
||||
Default: state.Labels,
|
||||
Filter: prompter.LatinMatchingFilter,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
|
|
@ -265,6 +269,7 @@ func MetadataSurvey(io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher
|
|||
Message: "Projects",
|
||||
Options: projects,
|
||||
Default: state.Projects,
|
||||
Filter: prompter.LatinMatchingFilter,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
"contexts": {
|
||||
"nodes": [
|
||||
{
|
||||
"__typeName": "StatusContext",
|
||||
"state": "SUCCESS"
|
||||
}
|
||||
]
|
||||
|
|
@ -80,6 +81,7 @@
|
|||
"contexts": {
|
||||
"nodes": [
|
||||
{
|
||||
"__typeName": "CheckRun",
|
||||
"status": "IN_PROGRESS",
|
||||
"conclusion": ""
|
||||
}
|
||||
|
|
@ -109,13 +111,16 @@
|
|||
"contexts": {
|
||||
"nodes": [
|
||||
{
|
||||
"__typeName": "CheckRun",
|
||||
"status": "IN_PROGRESS",
|
||||
"conclusion": ""
|
||||
},
|
||||
{
|
||||
"__typeName": "StatusContext",
|
||||
"state": "FAILURE"
|
||||
},
|
||||
{
|
||||
"__typeName": "CheckRun",
|
||||
"status": "COMPLETED",
|
||||
"conclusion": "NEUTRAL"
|
||||
}
|
||||
|
|
@ -144,6 +149,7 @@
|
|||
"contexts": {
|
||||
"nodes": [
|
||||
{
|
||||
"__typeName": "StatusContext",
|
||||
"state": "SUCCESS"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
464
pkg/cmd/pr/status/fixtures/prStatusChecksWithStatesByCount.json
Normal file
464
pkg/cmd/pr/status/fixtures/prStatusChecksWithStatesByCount.json
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
{
|
||||
"data": {
|
||||
"repository": {
|
||||
"pullRequests": {
|
||||
"totalCount": 0,
|
||||
"edges": []
|
||||
}
|
||||
},
|
||||
"viewerCreated": {
|
||||
"totalCount": 4,
|
||||
"edges": [
|
||||
{
|
||||
"node": {
|
||||
"number": 8,
|
||||
"title": "Strawberries are not actually berries",
|
||||
"state": "OPEN",
|
||||
"url": "https://github.com/cli/cli/pull/8",
|
||||
"headRefName": "strawberries",
|
||||
"mergeable": "UNKNOWN",
|
||||
"reviewDecision": "CHANGES_REQUESTED",
|
||||
"statusCheckRollup": {
|
||||
"nodes": [
|
||||
{
|
||||
"commit": {
|
||||
"statusCheckRollup": {
|
||||
"contexts": {
|
||||
"checkRunCount": 0,
|
||||
"checkRunCountsByState": [
|
||||
{
|
||||
"state": "ACTION_REQUIRED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "CANCELLED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "COMPLETED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "FAILURE",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "IN_PROGRESS",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "NEUTRAL",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "PENDING",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "QUEUED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "SKIPPED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "STALE",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "STARTUP_FAILURE",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "SUCCESS",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "TIMED_OUT",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "WAITING",
|
||||
"count": 0
|
||||
}
|
||||
],
|
||||
"statusContextCount": 1,
|
||||
"statusContextCountsByState": [
|
||||
{
|
||||
"state": "EXPECTED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "ERROR",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "FAILURE",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "PENDING",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "SUCCESS",
|
||||
"count": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 7,
|
||||
"title": "Bananas are berries",
|
||||
"state": "OPEN",
|
||||
"url": "https://github.com/cli/cli/pull/7",
|
||||
"headRefName": "banananana",
|
||||
"reviewDecision": "APPROVED",
|
||||
"baseRef": {
|
||||
"branchProtectionRule": {
|
||||
"requiredApprovingReviewCount": 0
|
||||
}
|
||||
},
|
||||
"latestReviews": {
|
||||
"nodes": [
|
||||
{
|
||||
"author": {
|
||||
"login": "bob"
|
||||
},
|
||||
"state": "APPROVED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "stella"
|
||||
},
|
||||
"state": "CHANGES_REQUESTED"
|
||||
},
|
||||
{
|
||||
"author": {
|
||||
"login": "alice"
|
||||
},
|
||||
"state": "APPROVED"
|
||||
}
|
||||
]
|
||||
},
|
||||
"statusCheckRollup": {
|
||||
"nodes": [
|
||||
{
|
||||
"commit": {
|
||||
"statusCheckRollup": {
|
||||
"contexts": {
|
||||
"checkRunCount": 1,
|
||||
"checkRunCountsByState": [
|
||||
{
|
||||
"state": "ACTION_REQUIRED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "CANCELLED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "COMPLETED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "FAILURE",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "IN_PROGRESS",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "NEUTRAL",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "PENDING",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "QUEUED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "SKIPPED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "STALE",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "STARTUP_FAILURE",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "SUCCESS",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "TIMED_OUT",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "WAITING",
|
||||
"count": 0
|
||||
}
|
||||
],
|
||||
"statusContextCount": 1,
|
||||
"statusContextCountsByState": [
|
||||
{
|
||||
"state": "EXPECTED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "ERROR",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "FAILURE",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "PENDING",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "SUCCESS",
|
||||
"count": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 6,
|
||||
"title": "Avocado is probably not a berry",
|
||||
"state": "OPEN",
|
||||
"url": "https://github.com/cli/cli/pull/6",
|
||||
"headRefName": "avo",
|
||||
"mergeable": "MERGEABLE",
|
||||
"reviewDecision": "REVIEW_REQUIRED",
|
||||
"statusCheckRollup": {
|
||||
"nodes": [
|
||||
{
|
||||
"commit": {
|
||||
"statusCheckRollup": {
|
||||
"contexts": {
|
||||
"checkRunCount": 2,
|
||||
"checkRunCountsByState": [
|
||||
{
|
||||
"state": "ACTION_REQUIRED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "CANCELLED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "COMPLETED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "FAILURE",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "IN_PROGRESS",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "NEUTRAL",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "PENDING",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "QUEUED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "SKIPPED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "STALE",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "STARTUP_FAILURE",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "SUCCESS",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "TIMED_OUT",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "WAITING",
|
||||
"count": 0
|
||||
}
|
||||
],
|
||||
"statusContextCount": 1,
|
||||
"statusContextCountsByState": [
|
||||
{
|
||||
"state": "EXPECTED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "ERROR",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "FAILURE",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"state": "PENDING",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "SUCCESS",
|
||||
"count": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"node": {
|
||||
"number": 5,
|
||||
"title": "Why can't berries get along?",
|
||||
"state": "OPEN",
|
||||
"url": "https://github.com/cli/cli/pull/5",
|
||||
"headRefName": "strawberries",
|
||||
"mergeable": "CONFLICTING",
|
||||
"statusCheckRollup": {
|
||||
"nodes": [
|
||||
{
|
||||
"commit": {
|
||||
"statusCheckRollup": {
|
||||
"contexts": {
|
||||
"checkRunCount": 0,
|
||||
"checkRunCountsByState": [
|
||||
{
|
||||
"state": "ACTION_REQUIRED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "CANCELLED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "COMPLETED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "FAILURE",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "IN_PROGRESS",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "NEUTRAL",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "PENDING",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "QUEUED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "SKIPPED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "STALE",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "STARTUP_FAILURE",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "SUCCESS",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "TIMED_OUT",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "WAITING",
|
||||
"count": 0
|
||||
}
|
||||
],
|
||||
"statusContextCount": 1,
|
||||
"statusContextCountsByState": [
|
||||
{
|
||||
"state": "EXPECTED",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "ERROR",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "FAILURE",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "PENDING",
|
||||
"count": 0
|
||||
},
|
||||
{
|
||||
"state": "SUCCESS",
|
||||
"count": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"reviewRequested": {
|
||||
"totalCount": 0,
|
||||
"edges": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@ type requestOptions struct {
|
|||
Username string
|
||||
Fields []string
|
||||
ConflictStatus bool
|
||||
|
||||
CheckRunAndStatusContextCountsSupported bool
|
||||
}
|
||||
|
||||
type pullRequestsPayload struct {
|
||||
|
|
@ -57,7 +59,7 @@ func pullRequestStatus(httpClient *http.Client, repo ghrepo.Interface, options r
|
|||
fragments = fmt.Sprintf("fragment pr on PullRequest{%s}fragment prWithReviews on PullRequest{...pr}", gr)
|
||||
} else {
|
||||
var err error
|
||||
fragments, err = pullRequestFragment(repo.RepoHost(), options.ConflictStatus)
|
||||
fragments, err = pullRequestFragment(repo.RepoHost(), options.ConflictStatus, options.CheckRunAndStatusContextCountsSupported)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -146,8 +148,7 @@ func pullRequestStatus(httpClient *http.Client, repo ghrepo.Interface, options r
|
|||
}
|
||||
|
||||
var resp response
|
||||
err := apiClient.GraphQL(repo.RepoHost(), query, variables, &resp)
|
||||
if err != nil {
|
||||
if err := apiClient.GraphQL(repo.RepoHost(), query, variables, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -187,16 +188,23 @@ func pullRequestStatus(httpClient *http.Client, repo ghrepo.Interface, options r
|
|||
return &payload, nil
|
||||
}
|
||||
|
||||
func pullRequestFragment(hostname string, conflictStatus bool) (string, error) {
|
||||
func pullRequestFragment(hostname string, conflictStatus bool, statusCheckRollupWithCountByState bool) (string, error) {
|
||||
fields := []string{
|
||||
"number", "title", "state", "url", "isDraft", "isCrossRepository",
|
||||
"headRefName", "headRepositoryOwner", "mergeStateStatus",
|
||||
"statusCheckRollup", "requiresStrictStatusChecks", "autoMergeRequest",
|
||||
"requiresStrictStatusChecks", "autoMergeRequest",
|
||||
}
|
||||
|
||||
if conflictStatus {
|
||||
fields = append(fields, "mergeable")
|
||||
}
|
||||
|
||||
if statusCheckRollupWithCountByState {
|
||||
fields = append(fields, "statusCheckRollupWithCountByState")
|
||||
} else {
|
||||
fields = append(fields, "statusCheckRollup")
|
||||
}
|
||||
|
||||
reviewFields := []string{"reviewDecision", "latestReviews"}
|
||||
fragments := fmt.Sprintf(`
|
||||
fragment pr on PullRequest {%s}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ import (
|
|||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/v2/api"
|
||||
ghContext "github.com/cli/cli/v2/context"
|
||||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
fd "github.com/cli/cli/v2/internal/featuredetection"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmd/pr/shared"
|
||||
|
|
@ -33,6 +35,8 @@ type StatusOptions struct {
|
|||
HasRepoOverride bool
|
||||
Exporter cmdutil.Exporter
|
||||
ConflictStatus bool
|
||||
|
||||
Detector fd.Detector
|
||||
}
|
||||
|
||||
func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Command {
|
||||
|
|
@ -105,6 +109,16 @@ func statusRun(opts *StatusOptions) error {
|
|||
options.Fields = opts.Exporter.Fields()
|
||||
}
|
||||
|
||||
if opts.Detector == nil {
|
||||
cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
|
||||
opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost())
|
||||
}
|
||||
prFeatures, err := opts.Detector.PullRequestFeatures()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.CheckRunAndStatusContextCountsSupported = prFeatures.CheckRunAndStatusContextCounts
|
||||
|
||||
prPayload, err := pullRequestStatus(httpClient, baseRepo, options)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/cli/cli/v2/context"
|
||||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
fd "github.com/cli/cli/v2/internal/featuredetection"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
|
|
@ -22,6 +23,10 @@ import (
|
|||
)
|
||||
|
||||
func runCommand(rt http.RoundTripper, branch string, isTTY bool, cli string) (*test.CmdOut, error) {
|
||||
return runCommandWithDetector(rt, branch, isTTY, cli, &fd.DisabledDetectorMock{})
|
||||
}
|
||||
|
||||
func runCommandWithDetector(rt http.RoundTripper, branch string, isTTY bool, cli string, detector fd.Detector) (*test.CmdOut, error) {
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
ios.SetStdoutTTY(isTTY)
|
||||
ios.SetStdinTTY(isTTY)
|
||||
|
|
@ -55,7 +60,12 @@ func runCommand(rt http.RoundTripper, branch string, isTTY bool, cli string) (*t
|
|||
GitClient: &git.Client{GitPath: "some/path/git"},
|
||||
}
|
||||
|
||||
cmd := NewCmdStatus(factory, nil)
|
||||
withProvidedDetector := func(opts *StatusOptions) error {
|
||||
opts.Detector = detector
|
||||
return statusRun(opts)
|
||||
}
|
||||
|
||||
cmd := NewCmdStatus(factory, withProvidedDetector)
|
||||
cmd.PersistentFlags().StringP("repo", "R", "", "")
|
||||
|
||||
argv, err := shlex.Split(cli)
|
||||
|
|
@ -106,7 +116,8 @@ func TestPRStatus(t *testing.T) {
|
|||
func TestPRStatus_reviewsAndChecks(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(httpmock.GraphQL(`query PullRequestStatus\b`), httpmock.FileResponse("./fixtures/prStatusChecks.json"))
|
||||
// status,conclusion matches the old StatusContextRollup query
|
||||
http.Register(httpmock.GraphQL(`status,conclusion`), httpmock.FileResponse("./fixtures/prStatusChecks.json"))
|
||||
|
||||
output, err := runCommand(http, "blueberries", true, "")
|
||||
if err != nil {
|
||||
|
|
@ -127,6 +138,31 @@ func TestPRStatus_reviewsAndChecks(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPRStatus_reviewsAndChecksWithStatesByCount(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
// checkRunCount,checkRunCountsByState matches the new StatusContextRollup query
|
||||
http.Register(httpmock.GraphQL(`checkRunCount,checkRunCountsByState`), httpmock.FileResponse("./fixtures/prStatusChecksWithStatesByCount.json"))
|
||||
|
||||
output, err := runCommandWithDetector(http, "blueberries", true, "", &fd.EnabledDetectorMock{})
|
||||
if err != nil {
|
||||
t.Errorf("error running command `pr status`: %v", err)
|
||||
}
|
||||
|
||||
expected := []string{
|
||||
"✓ Checks passing + Changes requested ! Merge conflict status unknown",
|
||||
"- Checks pending ✓ 2 Approved",
|
||||
"× 1/3 checks failing - Review required ✓ No merge conflicts",
|
||||
"✓ Checks passing × Merge conflicts",
|
||||
}
|
||||
|
||||
for _, line := range expected {
|
||||
if !strings.Contains(output.String(), line) {
|
||||
t.Errorf("output did not contain %q: %q", line, output.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRStatus_currentBranch_showTheMostRecentPR(t *testing.T) {
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
"contexts": {
|
||||
"nodes": [
|
||||
{
|
||||
"__typename": "CheckRun",
|
||||
"conclusion": "FAILURE",
|
||||
"status": "COMPLETED",
|
||||
"name": "cool tests",
|
||||
|
|
@ -54,6 +55,7 @@
|
|||
"detailsUrl": "sweet link"
|
||||
},
|
||||
{
|
||||
"__typename": "CheckRun",
|
||||
"conclusion": "FAILURE",
|
||||
"status": "COMPLETED",
|
||||
"name": "sad tests",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
"contexts": {
|
||||
"nodes": [
|
||||
{
|
||||
"__typename": "CheckRun",
|
||||
"conclusion": "SUCCESS",
|
||||
"status": "COMPLETED",
|
||||
"name": "cool tests",
|
||||
|
|
@ -54,6 +55,7 @@
|
|||
"detailsUrl": "sweet link"
|
||||
},
|
||||
{
|
||||
"__typename": "CheckRun",
|
||||
"conclusion": "SUCCESS",
|
||||
"status": "COMPLETED",
|
||||
"name": "rad tests",
|
||||
|
|
@ -62,6 +64,7 @@
|
|||
"detailsUrl": "sweet link"
|
||||
},
|
||||
{
|
||||
"__typename": "CheckRun",
|
||||
"conclusion": "SUCCESS",
|
||||
"status": "COMPLETED",
|
||||
"name": "awesome tests",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
"contexts": {
|
||||
"nodes": [
|
||||
{
|
||||
"__typename": "CheckRun",
|
||||
"conclusion": "SUCCESS",
|
||||
"status": "COMPLETED",
|
||||
"name": "cool tests",
|
||||
|
|
@ -54,6 +55,7 @@
|
|||
"detailsUrl": "sweet link"
|
||||
},
|
||||
{
|
||||
"__typename": "CheckRun",
|
||||
"conclusion": "FAILURE",
|
||||
"status": "COMPLETED",
|
||||
"name": "sad tests",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
"contexts": {
|
||||
"nodes": [
|
||||
{
|
||||
"__typename": "CheckRun",
|
||||
"conclusion": "",
|
||||
"status": "WAITING",
|
||||
"name": "cool tests",
|
||||
|
|
@ -54,6 +55,7 @@
|
|||
"detailsUrl": "sweet link"
|
||||
},
|
||||
{
|
||||
"__typename": "CheckRun",
|
||||
"conclusion": "SUCCESS",
|
||||
"status": "COMPLETED",
|
||||
"name": "sad tests",
|
||||
|
|
|
|||
148
pkg/cmd/project/close/close.go
Normal file
148
pkg/cmd/project/close/close.go
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
package close
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type closeOpts struct {
|
||||
number int32
|
||||
owner string
|
||||
reopen bool
|
||||
projectID string
|
||||
format string
|
||||
}
|
||||
|
||||
type closeConfig struct {
|
||||
io *iostreams.IOStreams
|
||||
client *queries.Client
|
||||
opts closeOpts
|
||||
}
|
||||
|
||||
// the close command relies on the updateProjectV2 mutation
|
||||
type updateProjectMutation struct {
|
||||
UpdateProjectV2 struct {
|
||||
ProjectV2 queries.Project `graphql:"projectV2"`
|
||||
} `graphql:"updateProjectV2(input:$input)"`
|
||||
}
|
||||
|
||||
func NewCmdClose(f *cmdutil.Factory, runF func(config closeConfig) error) *cobra.Command {
|
||||
opts := closeOpts{}
|
||||
closeCmd := &cobra.Command{
|
||||
Short: "Close a project",
|
||||
Use: "close [<number>]",
|
||||
Example: heredoc.Doc(`
|
||||
# close project "1" owned by monalisa
|
||||
gh project close 1 --owner monalisa
|
||||
|
||||
# reopen closed project "1" owned by github
|
||||
gh project close 1 --owner github --undo
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := client.New(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
num, err := strconv.ParseInt(args[0], 10, 32)
|
||||
if err != nil {
|
||||
return cmdutil.FlagErrorf("invalid number: %v", args[0])
|
||||
}
|
||||
opts.number = int32(num)
|
||||
}
|
||||
|
||||
config := closeConfig{
|
||||
io: f.IOStreams,
|
||||
client: client,
|
||||
opts: opts,
|
||||
}
|
||||
|
||||
// allow testing of the command without actually running it
|
||||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
return runClose(config)
|
||||
},
|
||||
}
|
||||
|
||||
closeCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner. Use \"@me\" for the current user.")
|
||||
closeCmd.Flags().BoolVar(&opts.reopen, "undo", false, "Reopen a closed project")
|
||||
closeCmd.Flags().StringVar(&opts.format, "format", "", "Output format, must be 'json'")
|
||||
|
||||
return closeCmd
|
||||
}
|
||||
|
||||
func runClose(config closeConfig) error {
|
||||
canPrompt := config.io.CanPrompt()
|
||||
owner, err := config.client.NewOwner(canPrompt, config.opts.owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := config.client.NewProject(canPrompt, owner, config.opts.number, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.opts.projectID = project.ID
|
||||
|
||||
query, variables := closeArgs(config)
|
||||
|
||||
err = config.client.Mutate("CloseProjectV2", query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printJSON(config, *project)
|
||||
}
|
||||
|
||||
return printResults(config, query.UpdateProjectV2.ProjectV2)
|
||||
}
|
||||
|
||||
func closeArgs(config closeConfig) (*updateProjectMutation, map[string]interface{}) {
|
||||
closed := !config.opts.reopen
|
||||
return &updateProjectMutation{}, map[string]interface{}{
|
||||
"input": githubv4.UpdateProjectV2Input{
|
||||
ProjectID: githubv4.ID(config.opts.projectID),
|
||||
Closed: githubv4.NewBoolean(githubv4.Boolean(closed)),
|
||||
},
|
||||
"firstItems": githubv4.Int(0),
|
||||
"afterItems": (*githubv4.String)(nil),
|
||||
"firstFields": githubv4.Int(0),
|
||||
"afterFields": (*githubv4.String)(nil),
|
||||
}
|
||||
}
|
||||
|
||||
func printResults(config closeConfig, project queries.Project) error {
|
||||
if !config.io.IsStdoutTTY() {
|
||||
return nil
|
||||
}
|
||||
var action string
|
||||
if config.opts.reopen {
|
||||
action = "Reopened"
|
||||
} else {
|
||||
action = "Closed"
|
||||
}
|
||||
_, err := fmt.Fprintf(config.io.Out, "%s project %s\n", action, project.URL)
|
||||
return err
|
||||
}
|
||||
|
||||
func printJSON(config closeConfig, project queries.Project) error {
|
||||
b, err := format.JSONProject(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = config.io.Out.Write(b)
|
||||
return err
|
||||
}
|
||||
457
pkg/cmd/project/close/close_test.go
Normal file
457
pkg/cmd/project/close/close_test.go
Normal file
|
|
@ -0,0 +1,457 @@
|
|||
package close
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestNewCmdClose(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants closeOpts
|
||||
wantsErr bool
|
||||
wantsErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "not-a-number",
|
||||
cli: "x",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "invalid number: x",
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
cli: "123",
|
||||
wants: closeOpts{
|
||||
number: 123,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "owner",
|
||||
cli: "--owner monalisa",
|
||||
wants: closeOpts{
|
||||
owner: "monalisa",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reopen",
|
||||
cli: "--undo",
|
||||
wants: closeOpts{
|
||||
reopen: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
cli: "--format json",
|
||||
wants: closeOpts{
|
||||
format: "json",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts closeOpts
|
||||
cmd := NewCmdClose(f, func(config closeConfig) error {
|
||||
gotOpts = config.opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.wantsErrMsg, err.Error())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.number, gotOpts.number)
|
||||
assert.Equal(t, tt.wants.owner, gotOpts.owner)
|
||||
assert.Equal(t, tt.wants.format, gotOpts.format)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunClose_User(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get user project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"projectV2": map[string]string{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// close project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CloseProjectV2.*"variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"projectId":"an ID","closed":true}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"updateProjectV2": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"title": "a title",
|
||||
"url": "http://a-url.com",
|
||||
"owner": map[string]string{
|
||||
"login": "monalisa",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
|
||||
config := closeConfig{
|
||||
io: ios,
|
||||
opts: closeOpts{
|
||||
number: 1,
|
||||
owner: "monalisa",
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runClose(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Closed project http://a-url.com\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunClose_Org(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get org ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"user"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get org project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query OrgProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"projectV2": map[string]string{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// close project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CloseProjectV2.*"variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"projectId":"an ID","closed":true}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"updateProjectV2": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"title": "a title",
|
||||
"url": "http://a-url.com",
|
||||
"owner": map[string]string{
|
||||
"login": "monalisa",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
config := closeConfig{
|
||||
io: ios,
|
||||
opts: closeOpts{
|
||||
number: 1,
|
||||
owner: "github",
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runClose(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", stdout.String())
|
||||
}
|
||||
|
||||
func TestRunClose_Me(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get viewer project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"projectV2": map[string]string{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// close project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CloseProjectV2.*"variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"projectId":"an ID","closed":true}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"updateProjectV2": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"title": "a title",
|
||||
"url": "http://a-url.com",
|
||||
"owner": map[string]string{
|
||||
"login": "me",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
config := closeConfig{
|
||||
io: ios,
|
||||
opts: closeOpts{
|
||||
number: 1,
|
||||
owner: "@me",
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runClose(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", stdout.String())
|
||||
}
|
||||
|
||||
func TestRunClose_Reopen(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get user project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"projectV2": map[string]string{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// close project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CloseProjectV2.*"variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"projectId":"an ID","closed":false}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"updateProjectV2": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"title": "a title",
|
||||
"url": "http://a-url.com",
|
||||
"owner": map[string]string{
|
||||
"login": "monalisa",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
|
||||
config := closeConfig{
|
||||
io: ios,
|
||||
opts: closeOpts{
|
||||
number: 1,
|
||||
owner: "monalisa",
|
||||
reopen: true,
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runClose(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Reopened project http://a-url.com\n",
|
||||
stdout.String())
|
||||
}
|
||||
153
pkg/cmd/project/copy/copy.go
Normal file
153
pkg/cmd/project/copy/copy.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
package copy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type copyOpts struct {
|
||||
includeDraftIssues bool
|
||||
number int32
|
||||
ownerID string
|
||||
projectID string
|
||||
sourceOwner string
|
||||
targetOwner string
|
||||
title string
|
||||
format string
|
||||
}
|
||||
|
||||
type copyConfig struct {
|
||||
io *iostreams.IOStreams
|
||||
client *queries.Client
|
||||
opts copyOpts
|
||||
}
|
||||
|
||||
type copyProjectMutation struct {
|
||||
CopyProjectV2 struct {
|
||||
ProjectV2 queries.Project `graphql:"projectV2"`
|
||||
} `graphql:"copyProjectV2(input:$input)"`
|
||||
}
|
||||
|
||||
func NewCmdCopy(f *cmdutil.Factory, runF func(config copyConfig) error) *cobra.Command {
|
||||
opts := copyOpts{}
|
||||
copyCmd := &cobra.Command{
|
||||
Short: "Copy a project",
|
||||
Use: "copy [<number>]",
|
||||
Example: heredoc.Doc(`
|
||||
# copy project "1" owned by monalisa to github
|
||||
gh project copy 1 --source-owner monalisa --target-owner github --title "a new project"
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := client.New(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
num, err := strconv.ParseInt(args[0], 10, 32)
|
||||
if err != nil {
|
||||
return cmdutil.FlagErrorf("invalid number: %v", args[0])
|
||||
}
|
||||
opts.number = int32(num)
|
||||
}
|
||||
|
||||
config := copyConfig{
|
||||
io: f.IOStreams,
|
||||
client: client,
|
||||
opts: opts,
|
||||
}
|
||||
|
||||
// allow testing of the command without actually running it
|
||||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
return runCopy(config)
|
||||
},
|
||||
}
|
||||
|
||||
copyCmd.Flags().StringVar(&opts.sourceOwner, "source-owner", "", "Login of the source owner. Use \"@me\" for the current user.")
|
||||
copyCmd.Flags().StringVar(&opts.targetOwner, "target-owner", "", "Login of the target owner. Use \"@me\" for the current user.")
|
||||
copyCmd.Flags().StringVar(&opts.title, "title", "", "Title for the new project")
|
||||
copyCmd.Flags().BoolVar(&opts.includeDraftIssues, "drafts", false, "Include draft issues when copying")
|
||||
cmdutil.StringEnumFlag(copyCmd, &opts.format, "format", "", "", []string{"json"}, "Output format")
|
||||
|
||||
_ = copyCmd.MarkFlagRequired("title")
|
||||
|
||||
return copyCmd
|
||||
}
|
||||
|
||||
func runCopy(config copyConfig) error {
|
||||
canPrompt := config.io.CanPrompt()
|
||||
sourceOwner, err := config.client.NewOwner(canPrompt, config.opts.sourceOwner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetOwner, err := config.client.NewOwner(canPrompt, config.opts.targetOwner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := config.client.NewProject(canPrompt, sourceOwner, config.opts.number, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config.opts.projectID = project.ID
|
||||
config.opts.ownerID = targetOwner.ID
|
||||
|
||||
query, variables := copyArgs(config)
|
||||
|
||||
err = config.client.Mutate("CopyProjectV2", query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printJSON(config, query.CopyProjectV2.ProjectV2)
|
||||
}
|
||||
|
||||
return printResults(config, query.CopyProjectV2.ProjectV2)
|
||||
}
|
||||
|
||||
func copyArgs(config copyConfig) (*copyProjectMutation, map[string]interface{}) {
|
||||
return ©ProjectMutation{}, map[string]interface{}{
|
||||
"input": githubv4.CopyProjectV2Input{
|
||||
OwnerID: githubv4.ID(config.opts.ownerID),
|
||||
ProjectID: githubv4.ID(config.opts.projectID),
|
||||
Title: githubv4.String(config.opts.title),
|
||||
IncludeDraftIssues: githubv4.NewBoolean(githubv4.Boolean(config.opts.includeDraftIssues)),
|
||||
},
|
||||
"firstItems": githubv4.Int(0),
|
||||
"afterItems": (*githubv4.String)(nil),
|
||||
"firstFields": githubv4.Int(0),
|
||||
"afterFields": (*githubv4.String)(nil),
|
||||
}
|
||||
}
|
||||
|
||||
func printResults(config copyConfig, project queries.Project) error {
|
||||
if !config.io.IsStdoutTTY() {
|
||||
return nil
|
||||
}
|
||||
_, err := fmt.Fprintf(config.io.Out, "Copied project to %s\n", project.URL)
|
||||
return err
|
||||
}
|
||||
|
||||
func printJSON(config copyConfig, project queries.Project) error {
|
||||
b, err := format.JSONProject(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = config.io.Out.Write(b)
|
||||
return err
|
||||
}
|
||||
462
pkg/cmd/project/copy/copy_test.go
Normal file
462
pkg/cmd/project/copy/copy_test.go
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
package copy
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestNewCmdCopy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants copyOpts
|
||||
wantsErr bool
|
||||
wantsErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "not-a-number",
|
||||
cli: "x --title t",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "invalid number: x",
|
||||
},
|
||||
{
|
||||
name: "title",
|
||||
cli: "--title t",
|
||||
wants: copyOpts{
|
||||
title: "t",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
cli: "123 --title t",
|
||||
wants: copyOpts{
|
||||
number: 123,
|
||||
title: "t",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "source-owner",
|
||||
cli: "--source-owner monalisa --title t",
|
||||
wants: copyOpts{
|
||||
sourceOwner: "monalisa",
|
||||
title: "t",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "target-owner",
|
||||
cli: "--target-owner monalisa --title t",
|
||||
wants: copyOpts{
|
||||
targetOwner: "monalisa",
|
||||
title: "t",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "drafts",
|
||||
cli: "--drafts --title t",
|
||||
wants: copyOpts{
|
||||
includeDraftIssues: true,
|
||||
title: "t",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
cli: "--format json --title t",
|
||||
wants: copyOpts{
|
||||
format: "json",
|
||||
title: "t",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts copyOpts
|
||||
cmd := NewCmdCopy(f, func(config copyConfig) error {
|
||||
gotOpts = config.opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.wantsErrMsg, err.Error())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.number, gotOpts.number)
|
||||
assert.Equal(t, tt.wants.sourceOwner, gotOpts.sourceOwner)
|
||||
assert.Equal(t, tt.wants.targetOwner, gotOpts.targetOwner)
|
||||
assert.Equal(t, tt.wants.title, gotOpts.title)
|
||||
assert.Equal(t, tt.wants.includeDraftIssues, gotOpts.includeDraftIssues)
|
||||
assert.Equal(t, tt.wants.format, gotOpts.format)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCopy_User(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get user project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"projectV2": map[string]string{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get source user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]string{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
"login": "monalisa",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get target user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]string{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
"login": "monalisa",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Copy project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CopyProjectV2.*","variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"projectId":"an ID","ownerId":"an ID","title":"a title","includeDraftIssues":false}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"copyProjectV2": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"title": "a title",
|
||||
"url": "http://a-url.com",
|
||||
"owner": map[string]string{
|
||||
"login": "monalisa",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(false)
|
||||
|
||||
config := copyConfig{
|
||||
io: ios,
|
||||
opts: copyOpts{
|
||||
title: "a title",
|
||||
sourceOwner: "monalisa",
|
||||
targetOwner: "monalisa",
|
||||
number: 1,
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runCopy(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", stdout.String())
|
||||
}
|
||||
|
||||
func TestRunCopy_Org(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get org project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query OrgProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"projectV2": map[string]string{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
// get source org ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]string{
|
||||
"login": "github",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
"login": "github",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"user"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get target source org ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]string{
|
||||
"login": "github",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
"login": "github",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"user"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Copy project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CopyProjectV2.*","variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"projectId":"an ID","ownerId":"an ID","title":"a title","includeDraftIssues":false}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"copyProjectV2": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"title": "a title",
|
||||
"url": "http://a-url.com",
|
||||
"owner": map[string]string{
|
||||
"login": "monalisa",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(false)
|
||||
|
||||
config := copyConfig{
|
||||
io: ios,
|
||||
opts: copyOpts{
|
||||
title: "a title",
|
||||
sourceOwner: "github",
|
||||
targetOwner: "github",
|
||||
number: 1,
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runCopy(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", stdout.String())
|
||||
}
|
||||
|
||||
func TestRunCopy_Me(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get viewer project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"projectV2": map[string]string{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get source viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
"login": "me",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get target viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
"login": "me",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Copy project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CopyProjectV2.*","variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"projectId":"an ID","ownerId":"an ID","title":"a title","includeDraftIssues":false}}}`).Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"copyProjectV2": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"title": "a title",
|
||||
"url": "http://a-url.com",
|
||||
"owner": map[string]string{
|
||||
"login": "me",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
|
||||
config := copyConfig{
|
||||
io: ios,
|
||||
opts: copyOpts{
|
||||
title: "a title",
|
||||
sourceOwner: "@me",
|
||||
targetOwner: "@me",
|
||||
number: 1,
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runCopy(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Copied project to http://a-url.com\n",
|
||||
stdout.String())
|
||||
}
|
||||
125
pkg/cmd/project/create/create.go
Normal file
125
pkg/cmd/project/create/create.go
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
package create
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type createOpts struct {
|
||||
title string
|
||||
owner string
|
||||
ownerID string
|
||||
format string
|
||||
}
|
||||
|
||||
type createConfig struct {
|
||||
client *queries.Client
|
||||
opts createOpts
|
||||
io *iostreams.IOStreams
|
||||
}
|
||||
|
||||
type createProjectMutation struct {
|
||||
CreateProjectV2 struct {
|
||||
ProjectV2 queries.Project `graphql:"projectV2"`
|
||||
} `graphql:"createProjectV2(input:$input)"`
|
||||
}
|
||||
|
||||
func NewCmdCreate(f *cmdutil.Factory, runF func(config createConfig) error) *cobra.Command {
|
||||
opts := createOpts{}
|
||||
createCmd := &cobra.Command{
|
||||
Short: "Create a project",
|
||||
Use: "create",
|
||||
Example: heredoc.Doc(`
|
||||
# create a new project owned by login monalisa
|
||||
gh project create --owner monalisa --title "a new project"
|
||||
`),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := client.New(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := createConfig{
|
||||
client: client,
|
||||
opts: opts,
|
||||
io: f.IOStreams,
|
||||
}
|
||||
|
||||
// allow testing of the command without actually running it
|
||||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
return runCreate(config)
|
||||
},
|
||||
}
|
||||
|
||||
createCmd.Flags().StringVar(&opts.title, "title", "", "Title for the project")
|
||||
createCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner. Use \"@me\" for the current user.")
|
||||
cmdutil.StringEnumFlag(createCmd, &opts.format, "format", "", "", []string{"json"}, "Output format")
|
||||
|
||||
_ = createCmd.MarkFlagRequired("title")
|
||||
|
||||
return createCmd
|
||||
}
|
||||
|
||||
func runCreate(config createConfig) error {
|
||||
canPrompt := config.io.CanPrompt()
|
||||
owner, err := config.client.NewOwner(canPrompt, config.opts.owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config.opts.ownerID = owner.ID
|
||||
query, variables := createArgs(config)
|
||||
|
||||
err = config.client.Mutate("CreateProjectV2", query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printJSON(config, query.CreateProjectV2.ProjectV2)
|
||||
}
|
||||
|
||||
return printResults(config, query.CreateProjectV2.ProjectV2)
|
||||
}
|
||||
|
||||
func createArgs(config createConfig) (*createProjectMutation, map[string]interface{}) {
|
||||
return &createProjectMutation{}, map[string]interface{}{
|
||||
"input": githubv4.CreateProjectV2Input{
|
||||
OwnerID: githubv4.ID(config.opts.ownerID),
|
||||
Title: githubv4.String(config.opts.title),
|
||||
},
|
||||
"firstItems": githubv4.Int(0),
|
||||
"afterItems": (*githubv4.String)(nil),
|
||||
"firstFields": githubv4.Int(0),
|
||||
"afterFields": (*githubv4.String)(nil),
|
||||
}
|
||||
}
|
||||
|
||||
func printResults(config createConfig, project queries.Project) error {
|
||||
if !config.io.IsStdoutTTY() {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := fmt.Fprintf(config.io.Out, "Created project '%s'\n%s\n", project.Title, project.URL)
|
||||
return err
|
||||
}
|
||||
|
||||
func printJSON(config createConfig, project queries.Project) error {
|
||||
b, err := format.JSONProject(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = config.io.Out.Write(b)
|
||||
return err
|
||||
}
|
||||
278
pkg/cmd/project/create/create_test.go
Normal file
278
pkg/cmd/project/create/create_test.go
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
package create
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestNewCmdCreate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants createOpts
|
||||
wantsErr bool
|
||||
wantsErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "title",
|
||||
cli: "--title t",
|
||||
wants: createOpts{
|
||||
title: "t",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "owner",
|
||||
cli: "--title t --owner monalisa",
|
||||
wants: createOpts{
|
||||
owner: "monalisa",
|
||||
title: "t",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
cli: "--title t --format json",
|
||||
wants: createOpts{
|
||||
format: "json",
|
||||
title: "t",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts createOpts
|
||||
cmd := NewCmdCreate(f, func(config createConfig) error {
|
||||
gotOpts = config.opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.wantsErrMsg, err.Error())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.title, gotOpts.title)
|
||||
assert.Equal(t, tt.wants.owner, gotOpts.owner)
|
||||
assert.Equal(t, tt.wants.format, gotOpts.format)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCreate_User(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]string{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
"login": "monalisa",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// create project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CreateProjectV2.*"variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"ownerId":"an ID","title":"a title"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"createProjectV2": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"title": "a title",
|
||||
"url": "http://a-url.com",
|
||||
"owner": map[string]string{
|
||||
"login": "monalisa",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := createConfig{
|
||||
opts: createOpts{
|
||||
title: "a title",
|
||||
owner: "monalisa",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runCreate(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Created project 'a title'\nhttp://a-url.com\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunCreate_Org(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get org ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]string{
|
||||
"login": "github",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
"login": "github",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"user"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// create project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CreateProjectV2.*"variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"ownerId":"an ID","title":"a title"}}}`).Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"createProjectV2": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"title": "a title",
|
||||
"url": "http://a-url.com",
|
||||
"owner": map[string]string{
|
||||
"login": "monalisa",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := createConfig{
|
||||
opts: createOpts{
|
||||
title: "a title",
|
||||
owner: "github",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runCreate(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Created project 'a title'\nhttp://a-url.com\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunCreate_Me(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
"login": "me",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// create project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CreateProjectV2.*"variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"ownerId":"an ID","title":"a title"}}}`).Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"createProjectV2": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"title": "a title",
|
||||
"url": "http://a-url.com",
|
||||
"owner": map[string]string{
|
||||
"login": "me",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := createConfig{
|
||||
opts: createOpts{
|
||||
title: "a title",
|
||||
owner: "@me",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runCreate(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Created project 'a title'\nhttp://a-url.com\n",
|
||||
stdout.String())
|
||||
}
|
||||
136
pkg/cmd/project/delete/delete.go
Normal file
136
pkg/cmd/project/delete/delete.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type deleteOpts struct {
|
||||
owner string
|
||||
number int32
|
||||
projectID string
|
||||
format string
|
||||
}
|
||||
|
||||
type deleteConfig struct {
|
||||
client *queries.Client
|
||||
opts deleteOpts
|
||||
io *iostreams.IOStreams
|
||||
}
|
||||
|
||||
type deleteProjectMutation struct {
|
||||
DeleteProject struct {
|
||||
Project queries.Project `graphql:"projectV2"`
|
||||
} `graphql:"deleteProjectV2(input:$input)"`
|
||||
}
|
||||
|
||||
func NewCmdDelete(f *cmdutil.Factory, runF func(config deleteConfig) error) *cobra.Command {
|
||||
opts := deleteOpts{}
|
||||
deleteCmd := &cobra.Command{
|
||||
Short: "Delete a project",
|
||||
Use: "delete [<number>]",
|
||||
Example: heredoc.Doc(`
|
||||
# delete the current user's project "1"
|
||||
gh project delete 1 --owner "@me"
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := client.New(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
num, err := strconv.ParseInt(args[0], 10, 32)
|
||||
if err != nil {
|
||||
return cmdutil.FlagErrorf("invalid number: %v", args[0])
|
||||
}
|
||||
opts.number = int32(num)
|
||||
}
|
||||
|
||||
config := deleteConfig{
|
||||
client: client,
|
||||
opts: opts,
|
||||
io: f.IOStreams,
|
||||
}
|
||||
|
||||
// allow testing of the command without actually running it
|
||||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
return runDelete(config)
|
||||
},
|
||||
}
|
||||
|
||||
deleteCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner. Use \"@me\" for the current user.")
|
||||
cmdutil.StringEnumFlag(deleteCmd, &opts.format, "format", "", "", []string{"json"}, "Output format")
|
||||
|
||||
return deleteCmd
|
||||
}
|
||||
|
||||
func runDelete(config deleteConfig) error {
|
||||
canPrompt := config.io.CanPrompt()
|
||||
owner, err := config.client.NewOwner(canPrompt, config.opts.owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := config.client.NewProject(canPrompt, owner, config.opts.number, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.opts.projectID = project.ID
|
||||
|
||||
query, variables := deleteItemArgs(config)
|
||||
err = config.client.Mutate("DeleteProject", query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printJSON(config, *project)
|
||||
}
|
||||
|
||||
return printResults(config)
|
||||
|
||||
}
|
||||
|
||||
func deleteItemArgs(config deleteConfig) (*deleteProjectMutation, map[string]interface{}) {
|
||||
return &deleteProjectMutation{}, map[string]interface{}{
|
||||
"input": githubv4.DeleteProjectV2Input{
|
||||
ProjectID: githubv4.ID(config.opts.projectID),
|
||||
},
|
||||
"firstItems": githubv4.Int(0),
|
||||
"afterItems": (*githubv4.String)(nil),
|
||||
"firstFields": githubv4.Int(0),
|
||||
"afterFields": (*githubv4.String)(nil),
|
||||
}
|
||||
}
|
||||
|
||||
func printResults(config deleteConfig) error {
|
||||
if !config.io.IsStdoutTTY() {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := fmt.Fprintf(config.io.Out, "Deleted project\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func printJSON(config deleteConfig, project queries.Project) error {
|
||||
b, err := format.JSONProject(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = config.io.Out.Write(b)
|
||||
return err
|
||||
}
|
||||
348
pkg/cmd/project/delete/delete_test.go
Normal file
348
pkg/cmd/project/delete/delete_test.go
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
package delete
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestNewCmdDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants deleteOpts
|
||||
wantsErr bool
|
||||
wantsErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "not-a-number",
|
||||
cli: "x",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "invalid number: x",
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
cli: "123",
|
||||
wants: deleteOpts{
|
||||
number: 123,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "owner",
|
||||
cli: "--owner monalisa",
|
||||
wants: deleteOpts{
|
||||
owner: "monalisa",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
cli: "--format json",
|
||||
wants: deleteOpts{
|
||||
format: "json",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts deleteOpts
|
||||
cmd := NewCmdDelete(f, func(config deleteConfig) error {
|
||||
gotOpts = config.opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.wantsErrMsg, err.Error())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.number, gotOpts.number)
|
||||
assert.Equal(t, tt.wants.owner, gotOpts.owner)
|
||||
assert.Equal(t, tt.wants.format, gotOpts.format)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunDelete_User(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// delete project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation DeleteProject.*","variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"projectId":"an ID"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"deleteProjectV2": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "project ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := deleteConfig{
|
||||
opts: deleteOpts{
|
||||
owner: "monalisa",
|
||||
number: 1,
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runDelete(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Deleted project\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunDelete_Org(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get org ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"user"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query OrgProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// delete project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation DeleteProject.*","variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"projectId":"an ID"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"deleteProjectV2": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "project ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := deleteConfig{
|
||||
opts: deleteOpts{
|
||||
owner: "github",
|
||||
number: 1,
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runDelete(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Deleted project\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunDelete_Me(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// delete project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation DeleteProject.*","variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"projectId":"an ID"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"deleteProjectV2": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "project ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := deleteConfig{
|
||||
opts: deleteOpts{
|
||||
owner: "@me",
|
||||
number: 1,
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runDelete(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Deleted project\n",
|
||||
stdout.String())
|
||||
}
|
||||
165
pkg/cmd/project/edit/edit.go
Normal file
165
pkg/cmd/project/edit/edit.go
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
package edit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type editOpts struct {
|
||||
number int32
|
||||
owner string
|
||||
title string
|
||||
readme string
|
||||
visibility string
|
||||
shortDescription string
|
||||
projectID string
|
||||
format string
|
||||
}
|
||||
|
||||
type editConfig struct {
|
||||
client *queries.Client
|
||||
opts editOpts
|
||||
io *iostreams.IOStreams
|
||||
}
|
||||
|
||||
type updateProjectMutation struct {
|
||||
UpdateProjectV2 struct {
|
||||
ProjectV2 queries.Project `graphql:"projectV2"`
|
||||
} `graphql:"updateProjectV2(input:$input)"`
|
||||
}
|
||||
|
||||
const projectVisibilityPublic = "PUBLIC"
|
||||
const projectVisibilityPrivate = "PRIVATE"
|
||||
|
||||
func NewCmdEdit(f *cmdutil.Factory, runF func(config editConfig) error) *cobra.Command {
|
||||
opts := editOpts{}
|
||||
editCmd := &cobra.Command{
|
||||
Short: "Edit a project",
|
||||
Use: "edit [<number>]",
|
||||
Example: heredoc.Doc(`
|
||||
# edit the title of monalisa's project "1"
|
||||
gh project edit 1 --owner monalisa --title "New title"
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := client.New(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
num, err := strconv.ParseInt(args[0], 10, 32)
|
||||
if err != nil {
|
||||
return cmdutil.FlagErrorf("invalid number: %v", args[0])
|
||||
}
|
||||
opts.number = int32(num)
|
||||
}
|
||||
|
||||
config := editConfig{
|
||||
client: client,
|
||||
opts: opts,
|
||||
io: f.IOStreams,
|
||||
}
|
||||
|
||||
if config.opts.title == "" && config.opts.shortDescription == "" && config.opts.readme == "" && config.opts.visibility == "" {
|
||||
return fmt.Errorf("no fields to edit")
|
||||
}
|
||||
// allow testing of the command without actually running it
|
||||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
return runEdit(config)
|
||||
},
|
||||
}
|
||||
|
||||
editCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner. Use \"@me\" for the current user.")
|
||||
cmdutil.StringEnumFlag(editCmd, &opts.visibility, "visibility", "", "", []string{projectVisibilityPublic, projectVisibilityPrivate}, "Change project visibility")
|
||||
editCmd.Flags().StringVar(&opts.title, "title", "", "New title for the project")
|
||||
editCmd.Flags().StringVar(&opts.readme, "readme", "", "New readme for the project")
|
||||
editCmd.Flags().StringVarP(&opts.shortDescription, "description", "d", "", "New description of the project")
|
||||
cmdutil.StringEnumFlag(editCmd, &opts.format, "format", "", "", []string{"json"}, "Output format")
|
||||
|
||||
return editCmd
|
||||
}
|
||||
|
||||
func runEdit(config editConfig) error {
|
||||
canPrompt := config.io.CanPrompt()
|
||||
owner, err := config.client.NewOwner(canPrompt, config.opts.owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := config.client.NewProject(canPrompt, owner, config.opts.number, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.opts.projectID = project.ID
|
||||
|
||||
query, variables := editArgs(config)
|
||||
err = config.client.Mutate("UpdateProjectV2", query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printJSON(config, *project)
|
||||
}
|
||||
|
||||
return printResults(config, query.UpdateProjectV2.ProjectV2)
|
||||
}
|
||||
|
||||
func editArgs(config editConfig) (*updateProjectMutation, map[string]interface{}) {
|
||||
variables := githubv4.UpdateProjectV2Input{ProjectID: githubv4.ID(config.opts.projectID)}
|
||||
if config.opts.title != "" {
|
||||
variables.Title = githubv4.NewString(githubv4.String(config.opts.title))
|
||||
}
|
||||
if config.opts.shortDescription != "" {
|
||||
variables.ShortDescription = githubv4.NewString(githubv4.String(config.opts.shortDescription))
|
||||
}
|
||||
if config.opts.readme != "" {
|
||||
variables.Readme = githubv4.NewString(githubv4.String(config.opts.readme))
|
||||
}
|
||||
if config.opts.visibility != "" {
|
||||
if config.opts.visibility == projectVisibilityPublic {
|
||||
variables.Public = githubv4.NewBoolean(githubv4.Boolean(true))
|
||||
} else if config.opts.visibility == projectVisibilityPrivate {
|
||||
variables.Public = githubv4.NewBoolean(githubv4.Boolean(false))
|
||||
}
|
||||
}
|
||||
|
||||
return &updateProjectMutation{}, map[string]interface{}{
|
||||
"input": variables,
|
||||
"firstItems": githubv4.Int(0),
|
||||
"afterItems": (*githubv4.String)(nil),
|
||||
"firstFields": githubv4.Int(0),
|
||||
"afterFields": (*githubv4.String)(nil),
|
||||
}
|
||||
}
|
||||
|
||||
func printResults(config editConfig, project queries.Project) error {
|
||||
if !config.io.IsStdoutTTY() {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := fmt.Fprintf(config.io.Out, "Updated project %s\n", project.URL)
|
||||
return err
|
||||
}
|
||||
|
||||
func printJSON(config editConfig, project queries.Project) error {
|
||||
b, err := format.JSONProject(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = config.io.Out.Write(b)
|
||||
return err
|
||||
}
|
||||
512
pkg/cmd/project/edit/edit_test.go
Normal file
512
pkg/cmd/project/edit/edit_test.go
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
package edit
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestNewCmdEdit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants editOpts
|
||||
wantsErr bool
|
||||
wantsErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "not-a-number",
|
||||
cli: "x",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "invalid number: x",
|
||||
},
|
||||
{
|
||||
name: "visibility-error",
|
||||
cli: "--visibility v",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "invalid argument \"v\" for \"--visibility\" flag: valid values are {PUBLIC|PRIVATE}",
|
||||
},
|
||||
{
|
||||
name: "no-args",
|
||||
cli: "",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "no fields to edit",
|
||||
},
|
||||
{
|
||||
name: "title",
|
||||
cli: "--title t",
|
||||
wants: editOpts{
|
||||
title: "t",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
cli: "123 --title t",
|
||||
wants: editOpts{
|
||||
number: 123,
|
||||
title: "t",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "owner",
|
||||
cli: "--owner monalisa --title t",
|
||||
wants: editOpts{
|
||||
owner: "monalisa",
|
||||
title: "t",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "readme",
|
||||
cli: "--readme r",
|
||||
wants: editOpts{
|
||||
readme: "r",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
cli: "--description d",
|
||||
wants: editOpts{
|
||||
shortDescription: "d",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "visibility",
|
||||
cli: "--visibility PUBLIC",
|
||||
wants: editOpts{
|
||||
visibility: "PUBLIC",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
cli: "--format json --title t",
|
||||
wants: editOpts{
|
||||
format: "json",
|
||||
title: "t",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts editOpts
|
||||
cmd := NewCmdEdit(f, func(config editConfig) error {
|
||||
gotOpts = config.opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.wantsErrMsg, err.Error())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.number, gotOpts.number)
|
||||
assert.Equal(t, tt.wants.owner, gotOpts.owner)
|
||||
assert.Equal(t, tt.wants.visibility, gotOpts.visibility)
|
||||
assert.Equal(t, tt.wants.title, gotOpts.title)
|
||||
assert.Equal(t, tt.wants.readme, gotOpts.readme)
|
||||
assert.Equal(t, tt.wants.shortDescription, gotOpts.shortDescription)
|
||||
assert.Equal(t, tt.wants.format, gotOpts.format)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunUpdate_User(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get user project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"projectV2": map[string]string{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// edit project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation UpdateProjectV2.*"variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"projectId":"an ID","title":"a new title","shortDescription":"a new description","readme":"a new readme","public":true}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"updateProjectV2": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"title": "a title",
|
||||
"url": "http://a-url.com",
|
||||
"owner": map[string]string{
|
||||
"login": "monalisa",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := editConfig{
|
||||
opts: editOpts{
|
||||
number: 1,
|
||||
owner: "monalisa",
|
||||
title: "a new title",
|
||||
shortDescription: "a new description",
|
||||
visibility: "PUBLIC",
|
||||
readme: "a new readme",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runEdit(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Updated project http://a-url.com\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunUpdate_Org(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get org ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"user"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get org project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query OrgProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"projectV2": map[string]string{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// edit project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation UpdateProjectV2.*"variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"projectId":"an ID","title":"a new title","shortDescription":"a new description","readme":"a new readme","public":true}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"updateProjectV2": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"title": "a title",
|
||||
"url": "http://a-url.com",
|
||||
"owner": map[string]string{
|
||||
"login": "monalisa",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := editConfig{
|
||||
opts: editOpts{
|
||||
number: 1,
|
||||
owner: "github",
|
||||
title: "a new title",
|
||||
shortDescription: "a new description",
|
||||
visibility: "PUBLIC",
|
||||
readme: "a new readme",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runEdit(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Updated project http://a-url.com\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunUpdate_Me(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get viewer project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"projectV2": map[string]string{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// edit project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation UpdateProjectV2.*"variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"projectId":"an ID","title":"a new title","shortDescription":"a new description","readme":"a new readme","public":false}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"updateProjectV2": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"title": "a title",
|
||||
"url": "http://a-url.com",
|
||||
"owner": map[string]string{
|
||||
"login": "me",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := editConfig{
|
||||
opts: editOpts{
|
||||
number: 1,
|
||||
owner: "@me",
|
||||
title: "a new title",
|
||||
shortDescription: "a new description",
|
||||
visibility: "PRIVATE",
|
||||
readme: "a new readme",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runEdit(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Updated project http://a-url.com\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunUpdate_OmitParams(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get user project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"projectV2": map[string]string{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Update project
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation UpdateProjectV2.*"variables":{"afterFields":null,"afterItems":null,"firstFields":0,"firstItems":0,"input":{"projectId":"an ID","title":"another title"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"updateProjectV2": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"title": "a title",
|
||||
"url": "http://a-url.com",
|
||||
"owner": map[string]string{
|
||||
"login": "monalisa",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := editConfig{
|
||||
opts: editOpts{
|
||||
number: 1,
|
||||
owner: "monalisa",
|
||||
title: "another title",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runEdit(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Updated project http://a-url.com\n",
|
||||
stdout.String())
|
||||
}
|
||||
163
pkg/cmd/project/field-create/field_create.go
Normal file
163
pkg/cmd/project/field-create/field_create.go
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
package fieldcreate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type createFieldOpts struct {
|
||||
name string
|
||||
dataType string
|
||||
owner string
|
||||
singleSelectOptions []string
|
||||
number int32
|
||||
projectID string
|
||||
format string
|
||||
}
|
||||
|
||||
type createFieldConfig struct {
|
||||
client *queries.Client
|
||||
opts createFieldOpts
|
||||
io *iostreams.IOStreams
|
||||
}
|
||||
|
||||
type createProjectV2FieldMutation struct {
|
||||
CreateProjectV2Field struct {
|
||||
Field queries.ProjectField `graphql:"projectV2Field"`
|
||||
} `graphql:"createProjectV2Field(input:$input)"`
|
||||
}
|
||||
|
||||
func NewCmdCreateField(f *cmdutil.Factory, runF func(config createFieldConfig) error) *cobra.Command {
|
||||
opts := createFieldOpts{}
|
||||
createFieldCmd := &cobra.Command{
|
||||
Short: "Create a field in a project",
|
||||
Use: "field-create [<number>]",
|
||||
Example: heredoc.Doc(`
|
||||
# create a field in the current user's project "1"
|
||||
gh project field-create 1 --owner "@me" --name "new field" --data-type "text"
|
||||
|
||||
# create a field with three options to select from for owner monalisa
|
||||
gh project field-create 1 --owner monalisa --name "new field" --data-type "SINGLE_SELECT" --single-select-options "one,two,three"
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := client.New(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
num, err := strconv.ParseInt(args[0], 10, 32)
|
||||
if err != nil {
|
||||
return cmdutil.FlagErrorf("invalid number: %v", args[0])
|
||||
}
|
||||
opts.number = int32(num)
|
||||
}
|
||||
|
||||
config := createFieldConfig{
|
||||
client: client,
|
||||
opts: opts,
|
||||
io: f.IOStreams,
|
||||
}
|
||||
|
||||
if config.opts.dataType == "SINGLE_SELECT" && len(config.opts.singleSelectOptions) == 0 {
|
||||
return fmt.Errorf("passing `--single-select-options` is required for SINGLE_SELECT data type")
|
||||
}
|
||||
|
||||
// allow testing of the command without actually running it
|
||||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
return runCreateField(config)
|
||||
},
|
||||
}
|
||||
|
||||
createFieldCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner. Use \"@me\" for the current user.")
|
||||
createFieldCmd.Flags().StringVar(&opts.name, "name", "", "Name of the new field")
|
||||
cmdutil.StringEnumFlag(createFieldCmd, &opts.dataType, "data-type", "", "", []string{"TEXT", "SINGLE_SELECT", "DATE", "NUMBER"}, "DataType of the new field.")
|
||||
createFieldCmd.Flags().StringSliceVar(&opts.singleSelectOptions, "single-select-options", []string{}, "Options for SINGLE_SELECT data type")
|
||||
cmdutil.StringEnumFlag(createFieldCmd, &opts.format, "format", "", "", []string{"json"}, "Output format")
|
||||
|
||||
_ = createFieldCmd.MarkFlagRequired("name")
|
||||
_ = createFieldCmd.MarkFlagRequired("data-type")
|
||||
|
||||
return createFieldCmd
|
||||
}
|
||||
|
||||
func runCreateField(config createFieldConfig) error {
|
||||
canPrompt := config.io.CanPrompt()
|
||||
owner, err := config.client.NewOwner(canPrompt, config.opts.owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := config.client.NewProject(canPrompt, owner, config.opts.number, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.opts.projectID = project.ID
|
||||
|
||||
query, variables := createFieldArgs(config)
|
||||
|
||||
err = config.client.Mutate("CreateField", query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printJSON(config, query.CreateProjectV2Field.Field)
|
||||
}
|
||||
|
||||
return printResults(config, query.CreateProjectV2Field.Field)
|
||||
}
|
||||
|
||||
func createFieldArgs(config createFieldConfig) (*createProjectV2FieldMutation, map[string]interface{}) {
|
||||
input := githubv4.CreateProjectV2FieldInput{
|
||||
ProjectID: githubv4.ID(config.opts.projectID),
|
||||
DataType: githubv4.ProjectV2CustomFieldType(config.opts.dataType),
|
||||
Name: githubv4.String(config.opts.name),
|
||||
}
|
||||
|
||||
if len(config.opts.singleSelectOptions) != 0 {
|
||||
opts := make([]githubv4.ProjectV2SingleSelectFieldOptionInput, 0)
|
||||
for _, opt := range config.opts.singleSelectOptions {
|
||||
opts = append(opts, githubv4.ProjectV2SingleSelectFieldOptionInput{
|
||||
Name: githubv4.String(opt),
|
||||
Color: githubv4.ProjectV2SingleSelectFieldOptionColor("GRAY"),
|
||||
})
|
||||
}
|
||||
input.SingleSelectOptions = &opts
|
||||
}
|
||||
|
||||
return &createProjectV2FieldMutation{}, map[string]interface{}{
|
||||
"input": input,
|
||||
}
|
||||
}
|
||||
|
||||
func printResults(config createFieldConfig, field queries.ProjectField) error {
|
||||
if !config.io.IsStdoutTTY() {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := fmt.Fprintf(config.io.Out, "Created field\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func printJSON(config createFieldConfig, field queries.ProjectField) error {
|
||||
b, err := format.JSONProjectField(field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = config.io.Out.Write(b)
|
||||
return err
|
||||
}
|
||||
632
pkg/cmd/project/field-create/field_create_test.go
Normal file
632
pkg/cmd/project/field-create/field_create_test.go
Normal file
|
|
@ -0,0 +1,632 @@
|
|||
package fieldcreate
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestNewCmdCreateField(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants createFieldOpts
|
||||
wantsErr bool
|
||||
wantsErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "missing-name-and-data-type",
|
||||
cli: "",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "required flag(s) \"data-type\", \"name\" not set",
|
||||
},
|
||||
{
|
||||
name: "not-a-number",
|
||||
cli: "x --name n --data-type TEXT",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "invalid number: x",
|
||||
},
|
||||
{
|
||||
name: "single-select-no-options",
|
||||
cli: "123 --name n --data-type SINGLE_SELECT",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "passing `--single-select-options` is required for SINGLE_SELECT data type",
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
cli: "123 --name n --data-type TEXT",
|
||||
wants: createFieldOpts{
|
||||
number: 123,
|
||||
name: "n",
|
||||
dataType: "TEXT",
|
||||
singleSelectOptions: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "owner",
|
||||
cli: "--owner monalisa --name n --data-type TEXT",
|
||||
wants: createFieldOpts{
|
||||
owner: "monalisa",
|
||||
name: "n",
|
||||
dataType: "TEXT",
|
||||
singleSelectOptions: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single-select-options",
|
||||
cli: "--name n --data-type TEXT --single-select-options a,b",
|
||||
wants: createFieldOpts{
|
||||
singleSelectOptions: []string{"a", "b"},
|
||||
name: "n",
|
||||
dataType: "TEXT",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
cli: "--format json --name n --data-type TEXT ",
|
||||
wants: createFieldOpts{
|
||||
format: "json",
|
||||
name: "n",
|
||||
dataType: "TEXT",
|
||||
singleSelectOptions: []string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts createFieldOpts
|
||||
cmd := NewCmdCreateField(f, func(config createFieldConfig) error {
|
||||
gotOpts = config.opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.wantsErrMsg, err.Error())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.number, gotOpts.number)
|
||||
assert.Equal(t, tt.wants.owner, gotOpts.owner)
|
||||
assert.Equal(t, tt.wants.name, gotOpts.name)
|
||||
assert.Equal(t, tt.wants.dataType, gotOpts.dataType)
|
||||
assert.Equal(t, tt.wants.singleSelectOptions, gotOpts.singleSelectOptions)
|
||||
assert.Equal(t, tt.wants.format, gotOpts.format)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCreateField_User(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// create Field
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CreateField.*","variables":{"input":{"projectId":"an ID","dataType":"TEXT","name":"a name"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"createProjectV2Field": map[string]interface{}{
|
||||
"projectV2Field": map[string]interface{}{
|
||||
"id": "Field ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := createFieldConfig{
|
||||
opts: createFieldOpts{
|
||||
name: "a name",
|
||||
owner: "monalisa",
|
||||
number: 1,
|
||||
dataType: "TEXT",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runCreateField(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Created field\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunCreateField_Org(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get org ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"user"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query OrgProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// create Field
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CreateField.*","variables":{"input":{"projectId":"an ID","dataType":"TEXT","name":"a name"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"createProjectV2Field": map[string]interface{}{
|
||||
"projectV2Field": map[string]interface{}{
|
||||
"id": "Field ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := createFieldConfig{
|
||||
opts: createFieldOpts{
|
||||
name: "a name",
|
||||
owner: "github",
|
||||
number: 1,
|
||||
dataType: "TEXT",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runCreateField(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Created field\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunCreateField_Me(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// create Field
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CreateField.*","variables":{"input":{"projectId":"an ID","dataType":"TEXT","name":"a name"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"createProjectV2Field": map[string]interface{}{
|
||||
"projectV2Field": map[string]interface{}{
|
||||
"id": "Field ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := createFieldConfig{
|
||||
opts: createFieldOpts{
|
||||
owner: "@me",
|
||||
number: 1,
|
||||
name: "a name",
|
||||
dataType: "TEXT",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runCreateField(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Created field\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunCreateField_TEXT(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// create Field
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CreateField.*","variables":{"input":{"projectId":"an ID","dataType":"TEXT","name":"a name"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"createProjectV2Field": map[string]interface{}{
|
||||
"projectV2Field": map[string]interface{}{
|
||||
"id": "Field ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := createFieldConfig{
|
||||
opts: createFieldOpts{
|
||||
owner: "@me",
|
||||
number: 1,
|
||||
name: "a name",
|
||||
dataType: "TEXT",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runCreateField(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Created field\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunCreateField_DATE(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// create Field
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CreateField.*","variables":{"input":{"projectId":"an ID","dataType":"DATE","name":"a name"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"createProjectV2Field": map[string]interface{}{
|
||||
"projectV2Field": map[string]interface{}{
|
||||
"id": "Field ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := createFieldConfig{
|
||||
opts: createFieldOpts{
|
||||
owner: "@me",
|
||||
number: 1,
|
||||
name: "a name",
|
||||
dataType: "DATE",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runCreateField(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Created field\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunCreateField_NUMBER(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// create Field
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CreateField.*","variables":{"input":{"projectId":"an ID","dataType":"NUMBER","name":"a name"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"createProjectV2Field": map[string]interface{}{
|
||||
"projectV2Field": map[string]interface{}{
|
||||
"id": "Field ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := createFieldConfig{
|
||||
opts: createFieldOpts{
|
||||
owner: "@me",
|
||||
number: 1,
|
||||
name: "a name",
|
||||
dataType: "NUMBER",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runCreateField(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Created field\n",
|
||||
stdout.String())
|
||||
}
|
||||
105
pkg/cmd/project/field-delete/field_delete.go
Normal file
105
pkg/cmd/project/field-delete/field_delete.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package fielddelete
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type deleteFieldOpts struct {
|
||||
fieldID string
|
||||
format string
|
||||
}
|
||||
|
||||
type deleteFieldConfig struct {
|
||||
client *queries.Client
|
||||
opts deleteFieldOpts
|
||||
io *iostreams.IOStreams
|
||||
}
|
||||
|
||||
type deleteProjectV2FieldMutation struct {
|
||||
DeleteProjectV2Field struct {
|
||||
Field queries.ProjectField `graphql:"projectV2Field"`
|
||||
} `graphql:"deleteProjectV2Field(input:$input)"`
|
||||
}
|
||||
|
||||
func NewCmdDeleteField(f *cmdutil.Factory, runF func(config deleteFieldConfig) error) *cobra.Command {
|
||||
opts := deleteFieldOpts{}
|
||||
deleteFieldCmd := &cobra.Command{
|
||||
Short: "Delete a field in a project",
|
||||
Use: "field-delete",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := client.New(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := deleteFieldConfig{
|
||||
client: client,
|
||||
opts: opts,
|
||||
io: f.IOStreams,
|
||||
}
|
||||
|
||||
// allow testing of the command without actually running it
|
||||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
return runDeleteField(config)
|
||||
},
|
||||
}
|
||||
|
||||
deleteFieldCmd.Flags().StringVar(&opts.fieldID, "id", "", "ID of the field to delete")
|
||||
cmdutil.StringEnumFlag(deleteFieldCmd, &opts.format, "format", "", "", []string{"json"}, "Output format")
|
||||
|
||||
_ = deleteFieldCmd.MarkFlagRequired("id")
|
||||
|
||||
return deleteFieldCmd
|
||||
}
|
||||
|
||||
func runDeleteField(config deleteFieldConfig) error {
|
||||
query, variables := deleteFieldArgs(config)
|
||||
|
||||
err := config.client.Mutate("DeleteField", query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printJSON(config, query.DeleteProjectV2Field.Field)
|
||||
}
|
||||
|
||||
return printResults(config, query.DeleteProjectV2Field.Field)
|
||||
}
|
||||
|
||||
func deleteFieldArgs(config deleteFieldConfig) (*deleteProjectV2FieldMutation, map[string]interface{}) {
|
||||
return &deleteProjectV2FieldMutation{}, map[string]interface{}{
|
||||
"input": githubv4.DeleteProjectV2FieldInput{
|
||||
FieldID: githubv4.ID(config.opts.fieldID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func printResults(config deleteFieldConfig, field queries.ProjectField) error {
|
||||
if !config.io.IsStdoutTTY() {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := fmt.Fprintf(config.io.Out, "Deleted field\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func printJSON(config deleteFieldConfig, field queries.ProjectField) error {
|
||||
b, err := format.JSONProjectField(field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = config.io.Out.Write(b)
|
||||
return err
|
||||
}
|
||||
117
pkg/cmd/project/field-delete/field_delete_test.go
Normal file
117
pkg/cmd/project/field-delete/field_delete_test.go
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
package fielddelete
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestNewCmdDeleteField(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants deleteFieldOpts
|
||||
wantsErr bool
|
||||
wantsErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "no id",
|
||||
cli: "",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "required flag(s) \"id\" not set",
|
||||
},
|
||||
{
|
||||
name: "id",
|
||||
cli: "--id 123",
|
||||
wants: deleteFieldOpts{
|
||||
fieldID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
cli: "--id 123 --format json",
|
||||
wants: deleteFieldOpts{
|
||||
format: "json",
|
||||
fieldID: "123",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts deleteFieldOpts
|
||||
cmd := NewCmdDeleteField(f, func(config deleteFieldConfig) error {
|
||||
gotOpts = config.opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.wantsErrMsg, err.Error())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.fieldID, gotOpts.fieldID)
|
||||
assert.Equal(t, tt.wants.format, gotOpts.format)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunDeleteField(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// delete Field
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation DeleteField.*","variables":{"input":{"fieldId":"an ID"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"deleteProjectV2Field": map[string]interface{}{
|
||||
"projectV2Field": map[string]interface{}{
|
||||
"id": "Field ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := deleteFieldConfig{
|
||||
opts: deleteFieldOpts{
|
||||
fieldID: "an ID",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runDeleteField(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Deleted field\n",
|
||||
stdout.String())
|
||||
}
|
||||
132
pkg/cmd/project/field-list/field_list.go
Normal file
132
pkg/cmd/project/field-list/field_list.go
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
package fieldlist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type listOpts struct {
|
||||
limit int
|
||||
owner string
|
||||
number int32
|
||||
format string
|
||||
}
|
||||
|
||||
type listConfig struct {
|
||||
io *iostreams.IOStreams
|
||||
tp *tableprinter.TablePrinter
|
||||
client *queries.Client
|
||||
opts listOpts
|
||||
}
|
||||
|
||||
func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.Command {
|
||||
opts := listOpts{}
|
||||
listCmd := &cobra.Command{
|
||||
Short: "List the fields in a project",
|
||||
Use: "field-list number",
|
||||
Example: heredoc.Doc(`
|
||||
# list fields in the current user's project "1"
|
||||
gh project field-list 1 --owner "@me"
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := client.New(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
num, err := strconv.ParseInt(args[0], 10, 32)
|
||||
if err != nil {
|
||||
return cmdutil.FlagErrorf("invalid number: %v", args[0])
|
||||
}
|
||||
opts.number = int32(num)
|
||||
}
|
||||
|
||||
t := tableprinter.New(f.IOStreams)
|
||||
config := listConfig{
|
||||
io: f.IOStreams,
|
||||
tp: t,
|
||||
client: client,
|
||||
opts: opts,
|
||||
}
|
||||
|
||||
// allow testing of the command without actually running it
|
||||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
return runList(config)
|
||||
},
|
||||
}
|
||||
|
||||
listCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner. Use \"@me\" for the current user.")
|
||||
cmdutil.StringEnumFlag(listCmd, &opts.format, "format", "", "", []string{"json"}, "Output format")
|
||||
listCmd.Flags().IntVarP(&opts.limit, "limit", "L", queries.LimitDefault, "Maximum number of fields to fetch")
|
||||
|
||||
return listCmd
|
||||
}
|
||||
|
||||
func runList(config listConfig) error {
|
||||
canPrompt := config.io.CanPrompt()
|
||||
owner, err := config.client.NewOwner(canPrompt, config.opts.owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// no need to fetch the project if we already have the number
|
||||
if config.opts.number == 0 {
|
||||
canPrompt := config.io.CanPrompt()
|
||||
project, err := config.client.NewProject(canPrompt, owner, config.opts.number, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.opts.number = project.Number
|
||||
}
|
||||
|
||||
project, err := config.client.ProjectFields(owner, config.opts.number, config.opts.limit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printJSON(config, project)
|
||||
}
|
||||
|
||||
return printResults(config, project.Fields.Nodes, owner.Login)
|
||||
}
|
||||
|
||||
func printResults(config listConfig, fields []queries.ProjectField, login string) error {
|
||||
if len(fields) == 0 {
|
||||
return cmdutil.NewNoResultsError(fmt.Sprintf("Project %d for owner %s has no fields", config.opts.number, login))
|
||||
}
|
||||
|
||||
config.tp.HeaderRow("Name", "Data type", "ID")
|
||||
|
||||
for _, f := range fields {
|
||||
config.tp.AddField(f.Name())
|
||||
config.tp.AddField(f.Type())
|
||||
config.tp.AddField(f.ID(), tableprinter.WithTruncate(nil))
|
||||
config.tp.EndRow()
|
||||
}
|
||||
|
||||
return config.tp.Render()
|
||||
}
|
||||
|
||||
func printJSON(config listConfig, project *queries.Project) error {
|
||||
b, err := format.JSONProjectFields(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = config.io.Out.Write(b)
|
||||
return err
|
||||
}
|
||||
425
pkg/cmd/project/field-list/field_list_test.go
Normal file
425
pkg/cmd/project/field-list/field_list_test.go
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
package fieldlist
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestNewCmdList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants listOpts
|
||||
wantsErr bool
|
||||
wantsErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "not-a-number",
|
||||
cli: "x",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "invalid number: x",
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
cli: "123",
|
||||
wants: listOpts{
|
||||
number: 123,
|
||||
limit: 30,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "owner",
|
||||
cli: "--owner monalisa",
|
||||
wants: listOpts{
|
||||
owner: "monalisa",
|
||||
limit: 30,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
cli: "--format json",
|
||||
wants: listOpts{
|
||||
format: "json",
|
||||
limit: 30,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts listOpts
|
||||
cmd := NewCmdList(f, func(config listConfig) error {
|
||||
gotOpts = config.opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Equal(t, tt.wantsErrMsg, err.Error())
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.number, gotOpts.number)
|
||||
assert.Equal(t, tt.wants.owner, gotOpts.owner)
|
||||
assert.Equal(t, tt.wants.limit, gotOpts.limit)
|
||||
assert.Equal(t, tt.wants.format, gotOpts.format)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunList_User(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// list project fields
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"number": 1,
|
||||
"firstItems": queries.LimitMax,
|
||||
"afterItems": nil,
|
||||
"firstFields": queries.LimitDefault,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"fields": map[string]interface{}{
|
||||
"nodes": []map[string]interface{}{
|
||||
{
|
||||
"__typename": "ProjectV2Field",
|
||||
"name": "FieldTitle",
|
||||
"id": "field ID",
|
||||
},
|
||||
{
|
||||
"__typename": "ProjectV2SingleSelectField",
|
||||
"name": "Status",
|
||||
"id": "status ID",
|
||||
},
|
||||
{
|
||||
"__typename": "ProjectV2IterationField",
|
||||
"name": "Iterations",
|
||||
"id": "iteration ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
config := listConfig{
|
||||
tp: tableprinter.New(ios),
|
||||
opts: listOpts{
|
||||
number: 1,
|
||||
owner: "monalisa",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"FieldTitle\tProjectV2Field\tfield ID\nStatus\tProjectV2SingleSelectField\tstatus ID\nIterations\tProjectV2IterationField\titeration ID\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunList_Org(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get org ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"user"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// list project fields
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query OrgProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
"number": 1,
|
||||
"firstItems": queries.LimitMax,
|
||||
"afterItems": nil,
|
||||
"firstFields": queries.LimitDefault,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"fields": map[string]interface{}{
|
||||
"nodes": []map[string]interface{}{
|
||||
{
|
||||
"__typename": "ProjectV2Field",
|
||||
"name": "FieldTitle",
|
||||
"id": "field ID",
|
||||
},
|
||||
{
|
||||
"__typename": "ProjectV2SingleSelectField",
|
||||
"name": "Status",
|
||||
"id": "status ID",
|
||||
},
|
||||
{
|
||||
"__typename": "ProjectV2IterationField",
|
||||
"name": "Iterations",
|
||||
"id": "iteration ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
config := listConfig{
|
||||
tp: tableprinter.New(ios),
|
||||
opts: listOpts{
|
||||
number: 1,
|
||||
owner: "github",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"FieldTitle\tProjectV2Field\tfield ID\nStatus\tProjectV2SingleSelectField\tstatus ID\nIterations\tProjectV2IterationField\titeration ID\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunList_Me(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// list project fields
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"number": 1,
|
||||
"firstItems": queries.LimitMax,
|
||||
"afterItems": nil,
|
||||
"firstFields": queries.LimitDefault,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"fields": map[string]interface{}{
|
||||
"nodes": []map[string]interface{}{
|
||||
{
|
||||
"__typename": "ProjectV2Field",
|
||||
"name": "FieldTitle",
|
||||
"id": "field ID",
|
||||
},
|
||||
{
|
||||
"__typename": "ProjectV2SingleSelectField",
|
||||
"name": "Status",
|
||||
"id": "status ID",
|
||||
},
|
||||
{
|
||||
"__typename": "ProjectV2IterationField",
|
||||
"name": "Iterations",
|
||||
"id": "iteration ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
config := listConfig{
|
||||
tp: tableprinter.New(ios),
|
||||
opts: listOpts{
|
||||
number: 1,
|
||||
owner: "@me",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"FieldTitle\tProjectV2Field\tfield ID\nStatus\tProjectV2SingleSelectField\tstatus ID\nIterations\tProjectV2IterationField\titeration ID\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunList_Empty(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// list project fields
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"number": 1,
|
||||
"firstItems": queries.LimitMax,
|
||||
"afterItems": nil,
|
||||
"firstFields": queries.LimitDefault,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"fields": map[string]interface{}{
|
||||
"nodes": nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
config := listConfig{
|
||||
tp: tableprinter.New(ios),
|
||||
opts: listOpts{
|
||||
number: 1,
|
||||
owner: "@me",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.EqualError(
|
||||
t,
|
||||
err,
|
||||
"Project 1 for owner @me has no fields")
|
||||
}
|
||||
144
pkg/cmd/project/item-add/item_add.go
Normal file
144
pkg/cmd/project/item-add/item_add.go
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
package itemadd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type addItemOpts struct {
|
||||
owner string
|
||||
number int32
|
||||
itemURL string
|
||||
projectID string
|
||||
itemID string
|
||||
format string
|
||||
}
|
||||
|
||||
type addItemConfig struct {
|
||||
client *queries.Client
|
||||
opts addItemOpts
|
||||
io *iostreams.IOStreams
|
||||
}
|
||||
|
||||
type addProjectItemMutation struct {
|
||||
CreateProjectItem struct {
|
||||
ProjectV2Item queries.ProjectItem `graphql:"item"`
|
||||
} `graphql:"addProjectV2ItemById(input:$input)"`
|
||||
}
|
||||
|
||||
func NewCmdAddItem(f *cmdutil.Factory, runF func(config addItemConfig) error) *cobra.Command {
|
||||
opts := addItemOpts{}
|
||||
addItemCmd := &cobra.Command{
|
||||
Short: "Add a pull request or an issue to a project",
|
||||
Use: "item-add [<number>]",
|
||||
Example: heredoc.Doc(`
|
||||
# add an item to monalisa's project "1"
|
||||
gh project item-add 1 --owner monalisa --url https://github.com/monalisa/myproject/issues/23
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := client.New(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
num, err := strconv.ParseInt(args[0], 10, 32)
|
||||
if err != nil {
|
||||
return cmdutil.FlagErrorf("invalid number: %v", args[0])
|
||||
}
|
||||
opts.number = int32(num)
|
||||
}
|
||||
|
||||
config := addItemConfig{
|
||||
client: client,
|
||||
opts: opts,
|
||||
io: f.IOStreams,
|
||||
}
|
||||
|
||||
// allow testing of the command without actually running it
|
||||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
return runAddItem(config)
|
||||
},
|
||||
}
|
||||
|
||||
addItemCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner. Use \"@me\" for the current user.")
|
||||
addItemCmd.Flags().StringVar(&opts.itemURL, "url", "", "URL of the issue or pull request to add to the project")
|
||||
cmdutil.StringEnumFlag(addItemCmd, &opts.format, "format", "", "", []string{"json"}, "Output format")
|
||||
|
||||
_ = addItemCmd.MarkFlagRequired("url")
|
||||
|
||||
return addItemCmd
|
||||
}
|
||||
|
||||
func runAddItem(config addItemConfig) error {
|
||||
canPrompt := config.io.CanPrompt()
|
||||
owner, err := config.client.NewOwner(canPrompt, config.opts.owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := config.client.NewProject(canPrompt, owner, config.opts.number, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.opts.projectID = project.ID
|
||||
|
||||
itemID, err := config.client.IssueOrPullRequestID(config.opts.itemURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.opts.itemID = itemID
|
||||
|
||||
query, variables := addItemArgs(config)
|
||||
err = config.client.Mutate("AddItem", query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printJSON(config, query.CreateProjectItem.ProjectV2Item)
|
||||
}
|
||||
|
||||
return printResults(config, query.CreateProjectItem.ProjectV2Item)
|
||||
|
||||
}
|
||||
|
||||
func addItemArgs(config addItemConfig) (*addProjectItemMutation, map[string]interface{}) {
|
||||
return &addProjectItemMutation{}, map[string]interface{}{
|
||||
"input": githubv4.AddProjectV2ItemByIdInput{
|
||||
ProjectID: githubv4.ID(config.opts.projectID),
|
||||
ContentID: githubv4.ID(config.opts.itemID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func printResults(config addItemConfig, item queries.ProjectItem) error {
|
||||
if !config.io.IsStdoutTTY() {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := fmt.Fprintf(config.io.Out, "Added item\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func printJSON(config addItemConfig, item queries.ProjectItem) error {
|
||||
b, err := format.JSONProjectItem(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = config.io.Out.Write(b)
|
||||
return err
|
||||
}
|
||||
426
pkg/cmd/project/item-add/item_add_test.go
Normal file
426
pkg/cmd/project/item-add/item_add_test.go
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
package itemadd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestNewCmdaddItem(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants addItemOpts
|
||||
wantsErr bool
|
||||
wantsErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "missing-url",
|
||||
cli: "",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "required flag(s) \"url\" not set",
|
||||
},
|
||||
{
|
||||
name: "not-a-number",
|
||||
cli: "x --url github.com/cli/cli",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "invalid number: x",
|
||||
},
|
||||
{
|
||||
name: "url",
|
||||
cli: "--url github.com/cli/cli",
|
||||
wants: addItemOpts{
|
||||
itemURL: "github.com/cli/cli",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
cli: "123 --url github.com/cli/cli",
|
||||
wants: addItemOpts{
|
||||
number: 123,
|
||||
itemURL: "github.com/cli/cli",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "owner",
|
||||
cli: "--owner monalisa --url github.com/cli/cli",
|
||||
wants: addItemOpts{
|
||||
owner: "monalisa",
|
||||
itemURL: "github.com/cli/cli",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
cli: "--format json --url github.com/cli/cli",
|
||||
wants: addItemOpts{
|
||||
format: "json",
|
||||
itemURL: "github.com/cli/cli",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts addItemOpts
|
||||
cmd := NewCmdAddItem(f, func(config addItemConfig) error {
|
||||
gotOpts = config.opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.wantsErrMsg, err.Error())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.number, gotOpts.number)
|
||||
assert.Equal(t, tt.wants.owner, gotOpts.owner)
|
||||
assert.Equal(t, tt.wants.itemURL, gotOpts.itemURL)
|
||||
assert.Equal(t, tt.wants.format, gotOpts.format)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAddItem_User(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get item ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query GetIssueOrPullRequest.*",
|
||||
"variables": map[string]interface{}{
|
||||
"url": "https://github.com/cli/go-gh/issues/1",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"resource": map[string]interface{}{
|
||||
"id": "item ID",
|
||||
"__typename": "Issue",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// create item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation AddItem.*","variables":{"input":{"projectId":"an ID","contentId":"item ID"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"addProjectV2ItemById": map[string]interface{}{
|
||||
"item": map[string]interface{}{
|
||||
"id": "item ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := addItemConfig{
|
||||
opts: addItemOpts{
|
||||
owner: "monalisa",
|
||||
number: 1,
|
||||
itemURL: "https://github.com/cli/go-gh/issues/1",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runAddItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Added item\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunAddItem_Org(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get org ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"user"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query OrgProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get item ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query GetIssueOrPullRequest.*",
|
||||
"variables": map[string]interface{}{
|
||||
"url": "https://github.com/cli/go-gh/issues/1",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"resource": map[string]interface{}{
|
||||
"id": "item ID",
|
||||
"__typename": "Issue",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// create item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation AddItem.*","variables":{"input":{"projectId":"an ID","contentId":"item ID"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"addProjectV2ItemById": map[string]interface{}{
|
||||
"item": map[string]interface{}{
|
||||
"id": "item ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := addItemConfig{
|
||||
opts: addItemOpts{
|
||||
owner: "github",
|
||||
number: 1,
|
||||
itemURL: "https://github.com/cli/go-gh/issues/1",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runAddItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Added item\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunAddItem_Me(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get item ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query GetIssueOrPullRequest.*",
|
||||
"variables": map[string]interface{}{
|
||||
"url": "https://github.com/cli/go-gh/pull/1",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"resource": map[string]interface{}{
|
||||
"id": "item ID",
|
||||
"__typename": "PullRequest",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// create item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation AddItem.*","variables":{"input":{"projectId":"an ID","contentId":"item ID"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"addProjectV2ItemById": map[string]interface{}{
|
||||
"item": map[string]interface{}{
|
||||
"id": "item ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := addItemConfig{
|
||||
opts: addItemOpts{
|
||||
owner: "@me",
|
||||
number: 1,
|
||||
itemURL: "https://github.com/cli/go-gh/pull/1",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runAddItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Added item\n",
|
||||
stdout.String())
|
||||
}
|
||||
171
pkg/cmd/project/item-archive/item_archive.go
Normal file
171
pkg/cmd/project/item-archive/item_archive.go
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
package itemarchive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type archiveItemOpts struct {
|
||||
owner string
|
||||
number int32
|
||||
undo bool
|
||||
itemID string
|
||||
projectID string
|
||||
format string
|
||||
}
|
||||
|
||||
type archiveItemConfig struct {
|
||||
client *queries.Client
|
||||
opts archiveItemOpts
|
||||
io *iostreams.IOStreams
|
||||
}
|
||||
|
||||
type archiveProjectItemMutation struct {
|
||||
ArchiveProjectItem struct {
|
||||
ProjectV2Item queries.ProjectItem `graphql:"item"`
|
||||
} `graphql:"archiveProjectV2Item(input:$input)"`
|
||||
}
|
||||
|
||||
type unarchiveProjectItemMutation struct {
|
||||
UnarchiveProjectItem struct {
|
||||
ProjectV2Item queries.ProjectItem `graphql:"item"`
|
||||
} `graphql:"unarchiveProjectV2Item(input:$input)"`
|
||||
}
|
||||
|
||||
func NewCmdArchiveItem(f *cmdutil.Factory, runF func(config archiveItemConfig) error) *cobra.Command {
|
||||
opts := archiveItemOpts{}
|
||||
archiveItemCmd := &cobra.Command{
|
||||
Short: "Archive an item in a project",
|
||||
Use: "item-archive [<number>]",
|
||||
Example: heredoc.Doc(`
|
||||
# archive an item in the current user's project "1"
|
||||
gh project item-archive 1 --owner "@me" --id <item-ID>
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := client.New(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
num, err := strconv.ParseInt(args[0], 10, 32)
|
||||
if err != nil {
|
||||
return cmdutil.FlagErrorf("invalid number: %v", args[0])
|
||||
}
|
||||
opts.number = int32(num)
|
||||
}
|
||||
|
||||
config := archiveItemConfig{
|
||||
client: client,
|
||||
opts: opts,
|
||||
io: f.IOStreams,
|
||||
}
|
||||
|
||||
// allow testing of the command without actually running it
|
||||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
return runArchiveItem(config)
|
||||
},
|
||||
}
|
||||
|
||||
archiveItemCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner. Use \"@me\" for the current user.")
|
||||
archiveItemCmd.Flags().StringVar(&opts.itemID, "id", "", "ID of the item to archive")
|
||||
archiveItemCmd.Flags().BoolVar(&opts.undo, "undo", false, "Unarchive an item")
|
||||
cmdutil.StringEnumFlag(archiveItemCmd, &opts.format, "format", "", "", []string{"json"}, "Output format")
|
||||
|
||||
_ = archiveItemCmd.MarkFlagRequired("id")
|
||||
|
||||
return archiveItemCmd
|
||||
}
|
||||
|
||||
func runArchiveItem(config archiveItemConfig) error {
|
||||
canPrompt := config.io.CanPrompt()
|
||||
owner, err := config.client.NewOwner(canPrompt, config.opts.owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := config.client.NewProject(canPrompt, owner, config.opts.number, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.opts.projectID = project.ID
|
||||
|
||||
if config.opts.undo {
|
||||
query, variables := unarchiveItemArgs(config, config.opts.itemID)
|
||||
err = config.client.Mutate("UnarchiveProjectItem", query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printJSON(config, query.UnarchiveProjectItem.ProjectV2Item)
|
||||
}
|
||||
|
||||
return printResults(config, query.UnarchiveProjectItem.ProjectV2Item)
|
||||
}
|
||||
query, variables := archiveItemArgs(config)
|
||||
err = config.client.Mutate("ArchiveProjectItem", query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printJSON(config, query.ArchiveProjectItem.ProjectV2Item)
|
||||
}
|
||||
|
||||
return printResults(config, query.ArchiveProjectItem.ProjectV2Item)
|
||||
}
|
||||
|
||||
func archiveItemArgs(config archiveItemConfig) (*archiveProjectItemMutation, map[string]interface{}) {
|
||||
return &archiveProjectItemMutation{}, map[string]interface{}{
|
||||
"input": githubv4.ArchiveProjectV2ItemInput{
|
||||
ProjectID: githubv4.ID(config.opts.projectID),
|
||||
ItemID: githubv4.ID(config.opts.itemID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func unarchiveItemArgs(config archiveItemConfig, itemID string) (*unarchiveProjectItemMutation, map[string]interface{}) {
|
||||
return &unarchiveProjectItemMutation{}, map[string]interface{}{
|
||||
"input": githubv4.UnarchiveProjectV2ItemInput{
|
||||
ProjectID: githubv4.ID(config.opts.projectID),
|
||||
ItemID: githubv4.ID(itemID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func printResults(config archiveItemConfig, item queries.ProjectItem) error {
|
||||
if !config.io.IsStdoutTTY() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if config.opts.undo {
|
||||
_, err := fmt.Fprintf(config.io.Out, "Unarchived item\n")
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := fmt.Fprintf(config.io.Out, "Archived item\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func printJSON(config archiveItemConfig, item queries.ProjectItem) error {
|
||||
b, err := format.JSONProjectItem(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = config.io.Out.Write(b)
|
||||
return err
|
||||
}
|
||||
639
pkg/cmd/project/item-archive/item_archive_test.go
Normal file
639
pkg/cmd/project/item-archive/item_archive_test.go
Normal file
|
|
@ -0,0 +1,639 @@
|
|||
package itemarchive
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestNewCmdarchiveItem(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants archiveItemOpts
|
||||
wantsErr bool
|
||||
wantsErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "missing-id",
|
||||
cli: "",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "required flag(s) \"id\" not set",
|
||||
},
|
||||
{
|
||||
name: "not-a-number",
|
||||
cli: "x --id 123",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "invalid number: x",
|
||||
},
|
||||
{
|
||||
name: "id",
|
||||
cli: "--id 123",
|
||||
wants: archiveItemOpts{
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
cli: "456 --id 123",
|
||||
wants: archiveItemOpts{
|
||||
number: 456,
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "owner",
|
||||
cli: "--owner monalisa --id 123",
|
||||
wants: archiveItemOpts{
|
||||
owner: "monalisa",
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "undo",
|
||||
cli: "--undo --id 123",
|
||||
wants: archiveItemOpts{
|
||||
undo: true,
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
cli: "--format json --id 123",
|
||||
wants: archiveItemOpts{
|
||||
format: "json",
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts archiveItemOpts
|
||||
cmd := NewCmdArchiveItem(f, func(config archiveItemConfig) error {
|
||||
gotOpts = config.opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.wantsErrMsg, err.Error())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.number, gotOpts.number)
|
||||
assert.Equal(t, tt.wants.owner, gotOpts.owner)
|
||||
assert.Equal(t, tt.wants.itemID, gotOpts.itemID)
|
||||
assert.Equal(t, tt.wants.undo, gotOpts.undo)
|
||||
assert.Equal(t, tt.wants.format, gotOpts.format)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunArchive_User(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// archive item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation ArchiveProjectItem.*","variables":{"input":{"projectId":"an ID","itemId":"item ID"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"archiveProjectV2Item": map[string]interface{}{
|
||||
"item": map[string]interface{}{
|
||||
"id": "item ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := archiveItemConfig{
|
||||
opts: archiveItemOpts{
|
||||
owner: "monalisa",
|
||||
number: 1,
|
||||
itemID: "item ID",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runArchiveItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Archived item\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunArchive_Org(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get org ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"user"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query OrgProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// archive item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation ArchiveProjectItem.*","variables":{"input":{"projectId":"an ID","itemId":"item ID"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"archiveProjectV2Item": map[string]interface{}{
|
||||
"item": map[string]interface{}{
|
||||
"id": "item ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := archiveItemConfig{
|
||||
opts: archiveItemOpts{
|
||||
owner: "github",
|
||||
number: 1,
|
||||
itemID: "item ID",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runArchiveItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Archived item\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunArchive_Me(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// archive item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation ArchiveProjectItem.*","variables":{"input":{"projectId":"an ID","itemId":"item ID"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"archiveProjectV2Item": map[string]interface{}{
|
||||
"item": map[string]interface{}{
|
||||
"id": "item ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := archiveItemConfig{
|
||||
opts: archiveItemOpts{
|
||||
owner: "@me",
|
||||
number: 1,
|
||||
itemID: "item ID",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runArchiveItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Archived item\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunArchive_User_Undo(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// archive item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation UnarchiveProjectItem.*","variables":{"input":{"projectId":"an ID","itemId":"item ID"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"unarchiveProjectV2Item": map[string]interface{}{
|
||||
"item": map[string]interface{}{
|
||||
"id": "item ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := archiveItemConfig{
|
||||
opts: archiveItemOpts{
|
||||
owner: "monalisa",
|
||||
number: 1,
|
||||
itemID: "item ID",
|
||||
undo: true,
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runArchiveItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Unarchived item\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunArchive_Org_Undo(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get org ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"user"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query OrgProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// archive item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation UnarchiveProjectItem.*","variables":{"input":{"projectId":"an ID","itemId":"item ID"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"unarchiveProjectV2Item": map[string]interface{}{
|
||||
"item": map[string]interface{}{
|
||||
"id": "item ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := archiveItemConfig{
|
||||
opts: archiveItemOpts{
|
||||
owner: "github",
|
||||
number: 1,
|
||||
itemID: "item ID",
|
||||
undo: true,
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runArchiveItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Unarchived item\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunArchive_Me_Undo(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// archive item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation UnarchiveProjectItem.*","variables":{"input":{"projectId":"an ID","itemId":"item ID"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"unarchiveProjectV2Item": map[string]interface{}{
|
||||
"item": map[string]interface{}{
|
||||
"id": "item ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := archiveItemConfig{
|
||||
opts: archiveItemOpts{
|
||||
owner: "@me",
|
||||
number: 1,
|
||||
itemID: "item ID",
|
||||
undo: true,
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runArchiveItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Unarchived item\n",
|
||||
stdout.String())
|
||||
}
|
||||
140
pkg/cmd/project/item-create/item_create.go
Normal file
140
pkg/cmd/project/item-create/item_create.go
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
package itemcreate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type createItemOpts struct {
|
||||
title string
|
||||
body string
|
||||
owner string
|
||||
number int32
|
||||
projectID string
|
||||
format string
|
||||
}
|
||||
|
||||
type createItemConfig struct {
|
||||
client *queries.Client
|
||||
opts createItemOpts
|
||||
io *iostreams.IOStreams
|
||||
}
|
||||
|
||||
type createProjectDraftItemMutation struct {
|
||||
CreateProjectDraftItem struct {
|
||||
ProjectV2Item queries.ProjectItem `graphql:"projectItem"`
|
||||
} `graphql:"addProjectV2DraftIssue(input:$input)"`
|
||||
}
|
||||
|
||||
func NewCmdCreateItem(f *cmdutil.Factory, runF func(config createItemConfig) error) *cobra.Command {
|
||||
opts := createItemOpts{}
|
||||
createItemCmd := &cobra.Command{
|
||||
Short: "Create a draft issue item in a project",
|
||||
Use: "item-create [<number>]",
|
||||
Example: heredoc.Doc(`
|
||||
# create a draft issue in the current user's project "1"
|
||||
gh project item-create 1 --owner "@me" --title "new item" --body "new item body"
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := client.New(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
num, err := strconv.ParseInt(args[0], 10, 32)
|
||||
if err != nil {
|
||||
return cmdutil.FlagErrorf("invalid number: %v", args[0])
|
||||
}
|
||||
opts.number = int32(num)
|
||||
}
|
||||
|
||||
config := createItemConfig{
|
||||
client: client,
|
||||
opts: opts,
|
||||
io: f.IOStreams,
|
||||
}
|
||||
|
||||
// allow testing of the command without actually running it
|
||||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
return runCreateItem(config)
|
||||
},
|
||||
}
|
||||
|
||||
createItemCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner. Use \"@me\" for the current user.")
|
||||
createItemCmd.Flags().StringVar(&opts.title, "title", "", "Title for the draft issue")
|
||||
createItemCmd.Flags().StringVar(&opts.body, "body", "", "Body for the draft issue")
|
||||
cmdutil.StringEnumFlag(createItemCmd, &opts.format, "format", "", "", []string{"json"}, "Output format")
|
||||
|
||||
_ = createItemCmd.MarkFlagRequired("title")
|
||||
|
||||
return createItemCmd
|
||||
}
|
||||
|
||||
func runCreateItem(config createItemConfig) error {
|
||||
canPrompt := config.io.CanPrompt()
|
||||
owner, err := config.client.NewOwner(canPrompt, config.opts.owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := config.client.NewProject(canPrompt, owner, config.opts.number, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.opts.projectID = project.ID
|
||||
|
||||
query, variables := createDraftIssueArgs(config)
|
||||
|
||||
err = config.client.Mutate("CreateDraftItem", query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printJSON(config, query.CreateProjectDraftItem.ProjectV2Item)
|
||||
}
|
||||
|
||||
return printResults(config, query.CreateProjectDraftItem.ProjectV2Item)
|
||||
}
|
||||
|
||||
func createDraftIssueArgs(config createItemConfig) (*createProjectDraftItemMutation, map[string]interface{}) {
|
||||
return &createProjectDraftItemMutation{}, map[string]interface{}{
|
||||
"input": githubv4.AddProjectV2DraftIssueInput{
|
||||
Body: githubv4.NewString(githubv4.String(config.opts.body)),
|
||||
ProjectID: githubv4.ID(config.opts.projectID),
|
||||
Title: githubv4.String(config.opts.title),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func printResults(config createItemConfig, item queries.ProjectItem) error {
|
||||
if !config.io.IsStdoutTTY() {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := fmt.Fprintf(config.io.Out, "Created item\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func printJSON(config createItemConfig, item queries.ProjectItem) error {
|
||||
b, err := format.JSONProjectItem(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = config.io.Out.Write(b)
|
||||
return err
|
||||
}
|
||||
374
pkg/cmd/project/item-create/item_create_test.go
Normal file
374
pkg/cmd/project/item-create/item_create_test.go
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
package itemcreate
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestNewCmdCreateItem(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants createItemOpts
|
||||
wantsErr bool
|
||||
wantsErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "missing-title",
|
||||
cli: "",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "required flag(s) \"title\" not set",
|
||||
},
|
||||
{
|
||||
name: "not-a-number",
|
||||
cli: "x --title t",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "invalid number: x",
|
||||
},
|
||||
{
|
||||
name: "title",
|
||||
cli: "--title t",
|
||||
wants: createItemOpts{
|
||||
title: "t",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
cli: "123 --title t",
|
||||
wants: createItemOpts{
|
||||
number: 123,
|
||||
title: "t",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "owner",
|
||||
cli: "--owner monalisa --title t",
|
||||
wants: createItemOpts{
|
||||
owner: "monalisa",
|
||||
title: "t",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "body",
|
||||
cli: "--body b --title t",
|
||||
wants: createItemOpts{
|
||||
body: "b",
|
||||
title: "t",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
cli: "--format json --title t",
|
||||
wants: createItemOpts{
|
||||
format: "json",
|
||||
title: "t",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts createItemOpts
|
||||
cmd := NewCmdCreateItem(f, func(config createItemConfig) error {
|
||||
gotOpts = config.opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.wantsErrMsg, err.Error())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.number, gotOpts.number)
|
||||
assert.Equal(t, tt.wants.owner, gotOpts.owner)
|
||||
assert.Equal(t, tt.wants.title, gotOpts.title)
|
||||
assert.Equal(t, tt.wants.format, gotOpts.format)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCreateItem_Draft_User(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// create item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CreateDraftItem.*","variables":{"input":{"projectId":"an ID","title":"a title","body":""}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"addProjectV2DraftIssue": map[string]interface{}{
|
||||
"projectItem": map[string]interface{}{
|
||||
"id": "item ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := createItemConfig{
|
||||
opts: createItemOpts{
|
||||
title: "a title",
|
||||
owner: "monalisa",
|
||||
number: 1,
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runCreateItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Created item\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunCreateItem_Draft_Org(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get org ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"user"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query OrgProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// create item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CreateDraftItem.*","variables":{"input":{"projectId":"an ID","title":"a title","body":""}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"addProjectV2DraftIssue": map[string]interface{}{
|
||||
"projectItem": map[string]interface{}{
|
||||
"id": "item ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := createItemConfig{
|
||||
opts: createItemOpts{
|
||||
title: "a title",
|
||||
owner: "github",
|
||||
number: 1,
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runCreateItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Created item\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunCreateItem_Draft_Me(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// create item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation CreateDraftItem.*","variables":{"input":{"projectId":"an ID","title":"a title","body":"a body"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"addProjectV2DraftIssue": map[string]interface{}{
|
||||
"projectItem": map[string]interface{}{
|
||||
"id": "item ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := createItemConfig{
|
||||
opts: createItemOpts{
|
||||
title: "a title",
|
||||
owner: "@me",
|
||||
number: 1,
|
||||
body: "a body",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runCreateItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Created item\n",
|
||||
stdout.String())
|
||||
}
|
||||
131
pkg/cmd/project/item-delete/item_delete.go
Normal file
131
pkg/cmd/project/item-delete/item_delete.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package itemdelete
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type deleteItemOpts struct {
|
||||
owner string
|
||||
number int32
|
||||
itemID string
|
||||
projectID string
|
||||
format string
|
||||
}
|
||||
|
||||
type deleteItemConfig struct {
|
||||
client *queries.Client
|
||||
opts deleteItemOpts
|
||||
io *iostreams.IOStreams
|
||||
}
|
||||
|
||||
type deleteProjectItemMutation struct {
|
||||
DeleteProjectItem struct {
|
||||
DeletedItemId githubv4.ID `graphql:"deletedItemId"`
|
||||
} `graphql:"deleteProjectV2Item(input:$input)"`
|
||||
}
|
||||
|
||||
func NewCmdDeleteItem(f *cmdutil.Factory, runF func(config deleteItemConfig) error) *cobra.Command {
|
||||
opts := deleteItemOpts{}
|
||||
deleteItemCmd := &cobra.Command{
|
||||
Short: "Delete an item from a project by ID",
|
||||
Use: "item-delete [<number>]",
|
||||
Example: heredoc.Doc(`
|
||||
# delete an item in the current user's project "1"
|
||||
gh project item-delete 1 --owner "@me" --id <item-ID>
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := client.New(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
num, err := strconv.ParseInt(args[0], 10, 32)
|
||||
if err != nil {
|
||||
return cmdutil.FlagErrorf("invalid number: %v", args[0])
|
||||
}
|
||||
opts.number = int32(num)
|
||||
}
|
||||
|
||||
config := deleteItemConfig{
|
||||
client: client,
|
||||
opts: opts,
|
||||
io: f.IOStreams,
|
||||
}
|
||||
|
||||
// allow testing of the command without actually running it
|
||||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
return runDeleteItem(config)
|
||||
},
|
||||
}
|
||||
|
||||
deleteItemCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner. Use \"@me\" for the current user.")
|
||||
deleteItemCmd.Flags().StringVar(&opts.itemID, "id", "", "ID of the item to delete")
|
||||
cmdutil.StringEnumFlag(deleteItemCmd, &opts.format, "format", "", "", []string{"json"}, "Output format")
|
||||
|
||||
_ = deleteItemCmd.MarkFlagRequired("id")
|
||||
|
||||
return deleteItemCmd
|
||||
}
|
||||
|
||||
func runDeleteItem(config deleteItemConfig) error {
|
||||
canPrompt := config.io.CanPrompt()
|
||||
owner, err := config.client.NewOwner(canPrompt, config.opts.owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
project, err := config.client.NewProject(canPrompt, owner, config.opts.number, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.opts.projectID = project.ID
|
||||
|
||||
query, variables := deleteItemArgs(config)
|
||||
err = config.client.Mutate("DeleteProjectItem", query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printJSON(config, query.DeleteProjectItem.DeletedItemId)
|
||||
}
|
||||
|
||||
return printResults(config)
|
||||
|
||||
}
|
||||
|
||||
func deleteItemArgs(config deleteItemConfig) (*deleteProjectItemMutation, map[string]interface{}) {
|
||||
return &deleteProjectItemMutation{}, map[string]interface{}{
|
||||
"input": githubv4.DeleteProjectV2ItemInput{
|
||||
ProjectID: githubv4.ID(config.opts.projectID),
|
||||
ItemID: githubv4.ID(config.opts.itemID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func printResults(config deleteItemConfig) error {
|
||||
if !config.io.IsStdoutTTY() {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := fmt.Fprintf(config.io.Out, "Deleted item\n")
|
||||
return err
|
||||
}
|
||||
|
||||
func printJSON(config deleteItemConfig, ID githubv4.ID) error {
|
||||
_, err := config.io.Out.Write([]byte(fmt.Sprintf(`{"id": "%s"}`, ID)))
|
||||
return err
|
||||
}
|
||||
359
pkg/cmd/project/item-delete/item_delete_test.go
Normal file
359
pkg/cmd/project/item-delete/item_delete_test.go
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
package itemdelete
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestNewCmdDeleteItem(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants deleteItemOpts
|
||||
wantsErr bool
|
||||
wantsErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "missing-id",
|
||||
cli: "",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "required flag(s) \"id\" not set",
|
||||
},
|
||||
{
|
||||
name: "not-a-number",
|
||||
cli: "x --id 123",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "invalid number: x",
|
||||
},
|
||||
{
|
||||
name: "item-id",
|
||||
cli: "--id 123",
|
||||
wants: deleteItemOpts{
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
cli: "456 --id 123",
|
||||
wants: deleteItemOpts{
|
||||
number: 456,
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "owner",
|
||||
cli: "--owner monalisa --id 123",
|
||||
wants: deleteItemOpts{
|
||||
owner: "monalisa",
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
cli: "--format json --id 123",
|
||||
wants: deleteItemOpts{
|
||||
format: "json",
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts deleteItemOpts
|
||||
cmd := NewCmdDeleteItem(f, func(config deleteItemConfig) error {
|
||||
gotOpts = config.opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.wantsErrMsg, err.Error())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.number, gotOpts.number)
|
||||
assert.Equal(t, tt.wants.owner, gotOpts.owner)
|
||||
assert.Equal(t, tt.wants.itemID, gotOpts.itemID)
|
||||
assert.Equal(t, tt.wants.format, gotOpts.format)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunDelete_User(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// delete item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation DeleteProjectItem.*","variables":{"input":{"projectId":"an ID","itemId":"item ID"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"deleteProjectV2Item": map[string]interface{}{
|
||||
"deletedItemId": "item ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := deleteItemConfig{
|
||||
opts: deleteItemOpts{
|
||||
owner: "monalisa",
|
||||
number: 1,
|
||||
itemID: "item ID",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runDeleteItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Deleted item\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunDelete_Org(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get org ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"user"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query OrgProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// delete item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation DeleteProjectItem.*","variables":{"input":{"projectId":"an ID","itemId":"item ID"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"deleteProjectV2Item": map[string]interface{}{
|
||||
"deletedItemId": "item ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := deleteItemConfig{
|
||||
opts: deleteItemOpts{
|
||||
owner: "github",
|
||||
number: 1,
|
||||
itemID: "item ID",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runDeleteItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Deleted item\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunDelete_Me(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// get project ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerProject.*",
|
||||
"variables": map[string]interface{}{
|
||||
"number": 1,
|
||||
"firstItems": 0,
|
||||
"afterItems": nil,
|
||||
"firstFields": 0,
|
||||
"afterFields": nil,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// delete item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation DeleteProjectItem.*","variables":{"input":{"projectId":"an ID","itemId":"item ID"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"deleteProjectV2Item": map[string]interface{}{
|
||||
"deletedItemId": "item ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
config := deleteItemConfig{
|
||||
opts: deleteItemOpts{
|
||||
owner: "@me",
|
||||
number: 1,
|
||||
itemID: "item ID",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runDeleteItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Deleted item\n",
|
||||
stdout.String())
|
||||
}
|
||||
253
pkg/cmd/project/item-edit/item_edit.go
Normal file
253
pkg/cmd/project/item-edit/item_edit.go
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
package itemedit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/shurcooL/githubv4"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type editItemOpts struct {
|
||||
// updateDraftIssue
|
||||
title string
|
||||
body string
|
||||
itemID string
|
||||
// updateItem
|
||||
fieldID string
|
||||
projectID string
|
||||
text string
|
||||
number float32
|
||||
date string
|
||||
singleSelectOptionID string
|
||||
iterationID string
|
||||
// format
|
||||
format string
|
||||
}
|
||||
|
||||
type editItemConfig struct {
|
||||
io *iostreams.IOStreams
|
||||
client *queries.Client
|
||||
opts editItemOpts
|
||||
}
|
||||
|
||||
type EditProjectDraftIssue struct {
|
||||
UpdateProjectV2DraftIssue struct {
|
||||
DraftIssue queries.DraftIssue `graphql:"draftIssue"`
|
||||
} `graphql:"updateProjectV2DraftIssue(input:$input)"`
|
||||
}
|
||||
|
||||
type UpdateProjectV2FieldValue struct {
|
||||
Update struct {
|
||||
Item queries.ProjectItem `graphql:"projectV2Item"`
|
||||
} `graphql:"updateProjectV2ItemFieldValue(input:$input)"`
|
||||
}
|
||||
|
||||
func NewCmdEditItem(f *cmdutil.Factory, runF func(config editItemConfig) error) *cobra.Command {
|
||||
opts := editItemOpts{}
|
||||
editItemCmd := &cobra.Command{
|
||||
Use: "item-edit",
|
||||
Short: "Edit an item in a project",
|
||||
Long: heredoc.Doc(`
|
||||
Edit either a draft issue or a project item. Both usages require the ID of the item to edit.
|
||||
|
||||
For non-draft issues, the ID of the project is also required, and only a single field value can be updated per invocation.
|
||||
`),
|
||||
Example: heredoc.Doc(`
|
||||
# edit an item's text field value
|
||||
gh project item-edit --id <item-ID> --field-id <field-ID> --project-id <project-ID> --text "new text"
|
||||
`),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := cmdutil.MutuallyExclusive(
|
||||
"only one of `--text`, `--number`, `--date`, `--single-select-option-id` or `--iteration-id` may be used",
|
||||
opts.text != "",
|
||||
opts.number != 0,
|
||||
opts.date != "",
|
||||
opts.singleSelectOptionID != "",
|
||||
opts.iterationID != "",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := client.New(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := editItemConfig{
|
||||
io: f.IOStreams,
|
||||
client: client,
|
||||
opts: opts,
|
||||
}
|
||||
|
||||
// allow testing of the command without actually running it
|
||||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
return runEditItem(config)
|
||||
},
|
||||
}
|
||||
|
||||
editItemCmd.Flags().StringVar(&opts.itemID, "id", "", "ID of the item to edit")
|
||||
cmdutil.StringEnumFlag(editItemCmd, &opts.format, "format", "", "", []string{"json"}, "Output format")
|
||||
|
||||
editItemCmd.Flags().StringVar(&opts.title, "title", "", "Title of the draft issue item")
|
||||
editItemCmd.Flags().StringVar(&opts.body, "body", "", "Body of the draft issue item")
|
||||
|
||||
editItemCmd.Flags().StringVar(&opts.fieldID, "field-id", "", "ID of the field to update")
|
||||
editItemCmd.Flags().StringVar(&opts.projectID, "project-id", "", "ID of the project to which the field belongs to")
|
||||
editItemCmd.Flags().StringVar(&opts.text, "text", "", "Text value for the field")
|
||||
editItemCmd.Flags().Float32Var(&opts.number, "number", 0, "Number value for the field")
|
||||
editItemCmd.Flags().StringVar(&opts.date, "date", "", "Date value for the field (YYYY-MM-DD)")
|
||||
editItemCmd.Flags().StringVar(&opts.singleSelectOptionID, "single-select-option-id", "", "ID of the single select option value to set on the field")
|
||||
editItemCmd.Flags().StringVar(&opts.iterationID, "iteration-id", "", "ID of the iteration value to set on the field")
|
||||
|
||||
_ = editItemCmd.MarkFlagRequired("id")
|
||||
|
||||
return editItemCmd
|
||||
}
|
||||
|
||||
func runEditItem(config editItemConfig) error {
|
||||
// update draft issue
|
||||
if config.opts.title != "" || config.opts.body != "" {
|
||||
if !strings.HasPrefix(config.opts.itemID, "DI_") {
|
||||
return cmdutil.FlagErrorf("ID must be the ID of the draft issue content which is prefixed with `DI_`")
|
||||
}
|
||||
|
||||
query, variables := buildEditDraftIssue(config)
|
||||
|
||||
err := config.client.Mutate("EditDraftIssueItem", query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printDraftIssueJSON(config, query.UpdateProjectV2DraftIssue.DraftIssue)
|
||||
}
|
||||
|
||||
return printDraftIssueResults(config, query.UpdateProjectV2DraftIssue.DraftIssue)
|
||||
}
|
||||
|
||||
// update item values
|
||||
if config.opts.text != "" || config.opts.number != 0 || config.opts.date != "" || config.opts.singleSelectOptionID != "" || config.opts.iterationID != "" {
|
||||
if config.opts.fieldID == "" {
|
||||
return cmdutil.FlagErrorf("field-id must be provided")
|
||||
}
|
||||
if config.opts.projectID == "" {
|
||||
// TODO: offer to fetch interactively
|
||||
return cmdutil.FlagErrorf("project-id must be provided")
|
||||
}
|
||||
|
||||
var parsedDate time.Time
|
||||
if config.opts.date != "" {
|
||||
date, err := time.Parse("2006-01-02", config.opts.date)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parsedDate = date
|
||||
}
|
||||
|
||||
query, variables := buildUpdateItem(config, parsedDate)
|
||||
err := config.client.Mutate("UpdateItemValues", query, variables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printItemJSON(config, &query.Update.Item)
|
||||
}
|
||||
|
||||
return printItemResults(config, &query.Update.Item)
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintln(config.io.ErrOut, "error: no changes to make"); err != nil {
|
||||
return err
|
||||
}
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
|
||||
func buildEditDraftIssue(config editItemConfig) (*EditProjectDraftIssue, map[string]interface{}) {
|
||||
return &EditProjectDraftIssue{}, map[string]interface{}{
|
||||
"input": githubv4.UpdateProjectV2DraftIssueInput{
|
||||
Body: githubv4.NewString(githubv4.String(config.opts.body)),
|
||||
DraftIssueID: githubv4.ID(config.opts.itemID),
|
||||
Title: githubv4.NewString(githubv4.String(config.opts.title)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildUpdateItem(config editItemConfig, date time.Time) (*UpdateProjectV2FieldValue, map[string]interface{}) {
|
||||
var value githubv4.ProjectV2FieldValue
|
||||
if config.opts.text != "" {
|
||||
value = githubv4.ProjectV2FieldValue{
|
||||
Text: githubv4.NewString(githubv4.String(config.opts.text)),
|
||||
}
|
||||
} else if config.opts.number != 0 {
|
||||
value = githubv4.ProjectV2FieldValue{
|
||||
Number: githubv4.NewFloat(githubv4.Float(config.opts.number)),
|
||||
}
|
||||
} else if config.opts.date != "" {
|
||||
value = githubv4.ProjectV2FieldValue{
|
||||
Date: githubv4.NewDate(githubv4.Date{Time: date}),
|
||||
}
|
||||
} else if config.opts.singleSelectOptionID != "" {
|
||||
value = githubv4.ProjectV2FieldValue{
|
||||
SingleSelectOptionID: githubv4.NewString(githubv4.String(config.opts.singleSelectOptionID)),
|
||||
}
|
||||
} else if config.opts.iterationID != "" {
|
||||
value = githubv4.ProjectV2FieldValue{
|
||||
IterationID: githubv4.NewString(githubv4.String(config.opts.iterationID)),
|
||||
}
|
||||
}
|
||||
|
||||
return &UpdateProjectV2FieldValue{}, map[string]interface{}{
|
||||
"input": githubv4.UpdateProjectV2ItemFieldValueInput{
|
||||
ProjectID: githubv4.ID(config.opts.projectID),
|
||||
ItemID: githubv4.ID(config.opts.itemID),
|
||||
FieldID: githubv4.ID(config.opts.fieldID),
|
||||
Value: value,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func printDraftIssueResults(config editItemConfig, item queries.DraftIssue) error {
|
||||
if !config.io.IsStdoutTTY() {
|
||||
return nil
|
||||
}
|
||||
_, err := fmt.Fprintf(config.io.Out, "Edited draft issue %q\n", item.Title)
|
||||
return err
|
||||
}
|
||||
|
||||
func printDraftIssueJSON(config editItemConfig, item queries.DraftIssue) error {
|
||||
b, err := format.JSONProjectDraftIssue(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = config.io.Out.Write(b)
|
||||
return err
|
||||
}
|
||||
|
||||
func printItemResults(config editItemConfig, item *queries.ProjectItem) error {
|
||||
if !config.io.IsStdoutTTY() {
|
||||
return nil
|
||||
}
|
||||
_, err := fmt.Fprintf(config.io.Out, "Edited item %q\n", item.Title())
|
||||
return err
|
||||
}
|
||||
|
||||
func printItemJSON(config editItemConfig, item *queries.ProjectItem) error {
|
||||
b, err := format.JSONProjectItem(*item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = config.io.Out.Write(b)
|
||||
return err
|
||||
|
||||
}
|
||||
476
pkg/cmd/project/item-edit/item_edit_test.go
Normal file
476
pkg/cmd/project/item-edit/item_edit_test.go
Normal file
|
|
@ -0,0 +1,476 @@
|
|||
package itemedit
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestNewCmdeditItem(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants editItemOpts
|
||||
wantsErr bool
|
||||
wantsErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "missing-id",
|
||||
cli: "",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "required flag(s) \"id\" not set",
|
||||
},
|
||||
{
|
||||
name: "invalid-flags",
|
||||
cli: "--id 123 --text t --date 2023-01-01",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "only one of `--text`, `--number`, `--date`, `--single-select-option-id` or `--iteration-id` may be used",
|
||||
},
|
||||
{
|
||||
name: "item-id",
|
||||
cli: "--id 123",
|
||||
wants: editItemOpts{
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
cli: "--number 456 --id 123",
|
||||
wants: editItemOpts{
|
||||
number: 456,
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "field-id",
|
||||
cli: "--field-id FIELD_ID --id 123",
|
||||
wants: editItemOpts{
|
||||
fieldID: "FIELD_ID",
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "project-id",
|
||||
cli: "--project-id PROJECT_ID --id 123",
|
||||
wants: editItemOpts{
|
||||
projectID: "PROJECT_ID",
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "text",
|
||||
cli: "--text t --id 123",
|
||||
wants: editItemOpts{
|
||||
text: "t",
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "date",
|
||||
cli: "--date 2023-01-01 --id 123",
|
||||
wants: editItemOpts{
|
||||
date: "2023-01-01",
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single-select-option-id",
|
||||
cli: "--single-select-option-id OPTION_ID --id 123",
|
||||
wants: editItemOpts{
|
||||
singleSelectOptionID: "OPTION_ID",
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "iteration-id",
|
||||
cli: "--iteration-id ITERATION_ID --id 123",
|
||||
wants: editItemOpts{
|
||||
iterationID: "ITERATION_ID",
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
cli: "--format json --id 123",
|
||||
wants: editItemOpts{
|
||||
format: "json",
|
||||
itemID: "123",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts editItemOpts
|
||||
cmd := NewCmdEditItem(f, func(config editItemConfig) error {
|
||||
gotOpts = config.opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.wantsErrMsg, err.Error())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.number, gotOpts.number)
|
||||
assert.Equal(t, tt.wants.itemID, gotOpts.itemID)
|
||||
assert.Equal(t, tt.wants.format, gotOpts.format)
|
||||
assert.Equal(t, tt.wants.title, gotOpts.title)
|
||||
assert.Equal(t, tt.wants.fieldID, gotOpts.fieldID)
|
||||
assert.Equal(t, tt.wants.projectID, gotOpts.projectID)
|
||||
assert.Equal(t, tt.wants.text, gotOpts.text)
|
||||
assert.Equal(t, tt.wants.number, gotOpts.number)
|
||||
assert.Equal(t, tt.wants.date, gotOpts.date)
|
||||
assert.Equal(t, tt.wants.singleSelectOptionID, gotOpts.singleSelectOptionID)
|
||||
assert.Equal(t, tt.wants.iterationID, gotOpts.iterationID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunItemEdit_Draft(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// edit item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation EditDraftIssueItem.*","variables":{"input":{"draftIssueId":"DI_item_id","title":"a title","body":"a new body"}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"updateProjectV2DraftIssue": map[string]interface{}{
|
||||
"draftIssue": map[string]interface{}{
|
||||
"title": "a title",
|
||||
"body": "a new body",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
|
||||
config := editItemConfig{
|
||||
io: ios,
|
||||
opts: editItemOpts{
|
||||
title: "a title",
|
||||
body: "a new body",
|
||||
itemID: "DI_item_id",
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runEditItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Edited draft issue \"a title\"\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunItemEdit_Text(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// edit item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation UpdateItemValues.*","variables":{"input":{"projectId":"project_id","itemId":"item_id","fieldId":"field_id","value":{"text":"item text"}}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"updateProjectV2ItemFieldValue": map[string]interface{}{
|
||||
"projectV2Item": map[string]interface{}{
|
||||
"ID": "item_id",
|
||||
"content": map[string]interface{}{
|
||||
"__typename": "Issue",
|
||||
"body": "body",
|
||||
"title": "title",
|
||||
"number": 1,
|
||||
"repository": map[string]interface{}{
|
||||
"nameWithOwner": "my-repo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
config := editItemConfig{
|
||||
io: ios,
|
||||
opts: editItemOpts{
|
||||
text: "item text",
|
||||
itemID: "item_id",
|
||||
projectID: "project_id",
|
||||
fieldID: "field_id",
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runEditItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", stdout.String())
|
||||
}
|
||||
|
||||
func TestRunItemEdit_Number(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// edit item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation UpdateItemValues.*","variables":{"input":{"projectId":"project_id","itemId":"item_id","fieldId":"field_id","value":{"number":2}}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"updateProjectV2ItemFieldValue": map[string]interface{}{
|
||||
"projectV2Item": map[string]interface{}{
|
||||
"ID": "item_id",
|
||||
"content": map[string]interface{}{
|
||||
"__typename": "Issue",
|
||||
"body": "body",
|
||||
"title": "title",
|
||||
"number": 1,
|
||||
"repository": map[string]interface{}{
|
||||
"nameWithOwner": "my-repo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
|
||||
config := editItemConfig{
|
||||
io: ios,
|
||||
opts: editItemOpts{
|
||||
number: 2,
|
||||
itemID: "item_id",
|
||||
projectID: "project_id",
|
||||
fieldID: "field_id",
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runEditItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Edited item \"title\"\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunItemEdit_Date(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// edit item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation UpdateItemValues.*","variables":{"input":{"projectId":"project_id","itemId":"item_id","fieldId":"field_id","value":{"date":"2023-01-01T00:00:00Z"}}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"updateProjectV2ItemFieldValue": map[string]interface{}{
|
||||
"projectV2Item": map[string]interface{}{
|
||||
"ID": "item_id",
|
||||
"content": map[string]interface{}{
|
||||
"__typename": "Issue",
|
||||
"body": "body",
|
||||
"title": "title",
|
||||
"number": 1,
|
||||
"repository": map[string]interface{}{
|
||||
"nameWithOwner": "my-repo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
config := editItemConfig{
|
||||
io: ios,
|
||||
opts: editItemOpts{
|
||||
date: "2023-01-01",
|
||||
itemID: "item_id",
|
||||
projectID: "project_id",
|
||||
fieldID: "field_id",
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runEditItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", stdout.String())
|
||||
}
|
||||
|
||||
func TestRunItemEdit_SingleSelect(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// edit item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation UpdateItemValues.*","variables":{"input":{"projectId":"project_id","itemId":"item_id","fieldId":"field_id","value":{"singleSelectOptionId":"option_id"}}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"updateProjectV2ItemFieldValue": map[string]interface{}{
|
||||
"projectV2Item": map[string]interface{}{
|
||||
"ID": "item_id",
|
||||
"content": map[string]interface{}{
|
||||
"__typename": "Issue",
|
||||
"body": "body",
|
||||
"title": "title",
|
||||
"number": 1,
|
||||
"repository": map[string]interface{}{
|
||||
"nameWithOwner": "my-repo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
config := editItemConfig{
|
||||
io: ios,
|
||||
opts: editItemOpts{
|
||||
singleSelectOptionID: "option_id",
|
||||
itemID: "item_id",
|
||||
projectID: "project_id",
|
||||
fieldID: "field_id",
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runEditItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", stdout.String())
|
||||
}
|
||||
|
||||
func TestRunItemEdit_Iteration(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// edit item
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
BodyString(`{"query":"mutation UpdateItemValues.*","variables":{"input":{"projectId":"project_id","itemId":"item_id","fieldId":"field_id","value":{"iterationId":"option_id"}}}}`).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"updateProjectV2ItemFieldValue": map[string]interface{}{
|
||||
"projectV2Item": map[string]interface{}{
|
||||
"ID": "item_id",
|
||||
"content": map[string]interface{}{
|
||||
"__typename": "Issue",
|
||||
"body": "body",
|
||||
"title": "title",
|
||||
"number": 1,
|
||||
"repository": map[string]interface{}{
|
||||
"nameWithOwner": "my-repo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
|
||||
config := editItemConfig{
|
||||
io: ios,
|
||||
opts: editItemOpts{
|
||||
iterationID: "option_id",
|
||||
itemID: "item_id",
|
||||
projectID: "project_id",
|
||||
fieldID: "field_id",
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runEditItem(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Edited item \"title\"\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunItemEdit_NoChanges(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
ios.SetStdoutTTY(true)
|
||||
|
||||
config := editItemConfig{
|
||||
io: ios,
|
||||
opts: editItemOpts{},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runEditItem(config)
|
||||
assert.Error(t, err, "SilentError")
|
||||
assert.Equal(t, "", stdout.String())
|
||||
assert.Equal(t, "error: no changes to make\n", stderr.String())
|
||||
}
|
||||
|
||||
func TestRunItemEdit_InvalidID(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
client := queries.NewTestClient()
|
||||
config := editItemConfig{
|
||||
opts: editItemOpts{
|
||||
title: "a title",
|
||||
body: "a new body",
|
||||
itemID: "item_id",
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runEditItem(config)
|
||||
assert.Error(t, err, "ID must be the ID of the draft issue content which is prefixed with `DI_`")
|
||||
}
|
||||
136
pkg/cmd/project/item-list/item_list.go
Normal file
136
pkg/cmd/project/item-list/item_list.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package itemlist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type listOpts struct {
|
||||
limit int
|
||||
owner string
|
||||
number int32
|
||||
format string
|
||||
}
|
||||
|
||||
type listConfig struct {
|
||||
io *iostreams.IOStreams
|
||||
tp *tableprinter.TablePrinter
|
||||
client *queries.Client
|
||||
opts listOpts
|
||||
}
|
||||
|
||||
func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.Command {
|
||||
opts := listOpts{}
|
||||
listCmd := &cobra.Command{
|
||||
Short: "List the items in a project",
|
||||
Use: "item-list [<number>]",
|
||||
Example: heredoc.Doc(`
|
||||
# list the items in the current users's project "1"
|
||||
gh project item-list 1 --owner "@me"
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := client.New(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
num, err := strconv.ParseInt(args[0], 10, 32)
|
||||
if err != nil {
|
||||
return cmdutil.FlagErrorf("invalid number: %v", args[0])
|
||||
}
|
||||
opts.number = int32(num)
|
||||
}
|
||||
|
||||
t := tableprinter.New(f.IOStreams)
|
||||
config := listConfig{
|
||||
io: f.IOStreams,
|
||||
tp: t,
|
||||
client: client,
|
||||
opts: opts,
|
||||
}
|
||||
|
||||
// allow testing of the command without actually running it
|
||||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
return runList(config)
|
||||
},
|
||||
}
|
||||
|
||||
listCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner. Use \"@me\" for the current user.")
|
||||
cmdutil.StringEnumFlag(listCmd, &opts.format, "format", "", "", []string{"json"}, "Output format")
|
||||
listCmd.Flags().IntVarP(&opts.limit, "limit", "L", queries.LimitDefault, "Maximum number of items to fetch")
|
||||
|
||||
return listCmd
|
||||
}
|
||||
|
||||
func runList(config listConfig) error {
|
||||
canPrompt := config.io.CanPrompt()
|
||||
owner, err := config.client.NewOwner(canPrompt, config.opts.owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// no need to fetch the project if we already have the number
|
||||
if config.opts.number == 0 {
|
||||
project, err := config.client.NewProject(canPrompt, owner, config.opts.number, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.opts.number = project.Number
|
||||
}
|
||||
|
||||
project, err := config.client.ProjectItems(owner, config.opts.number, config.opts.limit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printJSON(config, project)
|
||||
}
|
||||
|
||||
return printResults(config, project.Items.Nodes, owner.Login)
|
||||
}
|
||||
|
||||
func printResults(config listConfig, items []queries.ProjectItem, login string) error {
|
||||
if len(items) == 0 {
|
||||
return cmdutil.NewNoResultsError(fmt.Sprintf("Project %d for owner %s has no items", config.opts.number, login))
|
||||
}
|
||||
|
||||
config.tp.HeaderRow("Type", "Title", "Number", "Repository", "ID")
|
||||
|
||||
for _, i := range items {
|
||||
config.tp.AddField(i.Type())
|
||||
config.tp.AddField(i.Title())
|
||||
if i.Number() == 0 {
|
||||
config.tp.AddField("")
|
||||
} else {
|
||||
config.tp.AddField(strconv.Itoa(i.Number()))
|
||||
}
|
||||
config.tp.AddField(i.Repo())
|
||||
config.tp.AddField(i.ID(), tableprinter.WithTruncate(nil))
|
||||
config.tp.EndRow()
|
||||
}
|
||||
|
||||
return config.tp.Render()
|
||||
}
|
||||
|
||||
func printJSON(config listConfig, project *queries.Project) error {
|
||||
b, err := format.JSONProjectDetailedItems(project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = config.io.Out.Write(b)
|
||||
return err
|
||||
}
|
||||
402
pkg/cmd/project/item-list/item_list_test.go
Normal file
402
pkg/cmd/project/item-list/item_list_test.go
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
package itemlist
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestNewCmdList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants listOpts
|
||||
wantsErr bool
|
||||
wantsErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "not-a-number",
|
||||
cli: "x",
|
||||
wantsErr: true,
|
||||
wantsErrMsg: "invalid number: x",
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
cli: "123",
|
||||
wants: listOpts{
|
||||
number: 123,
|
||||
limit: 30,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "owner",
|
||||
cli: "--owner monalisa",
|
||||
wants: listOpts{
|
||||
owner: "monalisa",
|
||||
limit: 30,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
cli: "--format json",
|
||||
wants: listOpts{
|
||||
format: "json",
|
||||
limit: 30,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts listOpts
|
||||
cmd := NewCmdList(f, func(config listConfig) error {
|
||||
gotOpts = config.opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.wantsErrMsg, err.Error())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.number, gotOpts.number)
|
||||
assert.Equal(t, tt.wants.owner, gotOpts.owner)
|
||||
assert.Equal(t, tt.wants.format, gotOpts.format)
|
||||
assert.Equal(t, tt.wants.limit, gotOpts.limit)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunList_User(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// list project items
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserProjectWithItems.*",
|
||||
"variables": map[string]interface{}{
|
||||
"firstItems": queries.LimitDefault,
|
||||
"afterItems": nil,
|
||||
"firstFields": queries.LimitMax,
|
||||
"afterFields": nil,
|
||||
"login": "monalisa",
|
||||
"number": 1,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"items": map[string]interface{}{
|
||||
"nodes": []map[string]interface{}{
|
||||
{
|
||||
"id": "issue ID",
|
||||
"content": map[string]interface{}{
|
||||
"__typename": "Issue",
|
||||
"title": "an issue",
|
||||
"number": 1,
|
||||
"repository": map[string]string{
|
||||
"nameWithOwner": "cli/go-gh",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "pull request ID",
|
||||
"content": map[string]interface{}{
|
||||
"__typename": "PullRequest",
|
||||
"title": "a pull request",
|
||||
"number": 2,
|
||||
"repository": map[string]string{
|
||||
"nameWithOwner": "cli/go-gh",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "draft issue ID",
|
||||
"content": map[string]interface{}{
|
||||
"title": "draft issue",
|
||||
"__typename": "DraftIssue",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
config := listConfig{
|
||||
tp: tableprinter.New(ios),
|
||||
opts: listOpts{
|
||||
number: 1,
|
||||
owner: "monalisa",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Issue\tan issue\t1\tcli/go-gh\tissue ID\nPullRequest\ta pull request\t2\tcli/go-gh\tpull request ID\nDraftIssue\tdraft issue\t\t\tdraft issue ID\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunList_Org(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get org ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"user"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// list project items
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query OrgProjectWithItems.*",
|
||||
"variables": map[string]interface{}{
|
||||
"firstItems": queries.LimitDefault,
|
||||
"afterItems": nil,
|
||||
"firstFields": queries.LimitMax,
|
||||
"afterFields": nil,
|
||||
"login": "github",
|
||||
"number": 1,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"items": map[string]interface{}{
|
||||
"nodes": []map[string]interface{}{
|
||||
{
|
||||
"id": "issue ID",
|
||||
"content": map[string]interface{}{
|
||||
"__typename": "Issue",
|
||||
"title": "an issue",
|
||||
"number": 1,
|
||||
"repository": map[string]string{
|
||||
"nameWithOwner": "cli/go-gh",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "pull request ID",
|
||||
"content": map[string]interface{}{
|
||||
"__typename": "PullRequest",
|
||||
"title": "a pull request",
|
||||
"number": 2,
|
||||
"repository": map[string]string{
|
||||
"nameWithOwner": "cli/go-gh",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "draft issue ID",
|
||||
"content": map[string]interface{}{
|
||||
"title": "draft issue",
|
||||
"__typename": "DraftIssue",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
config := listConfig{
|
||||
tp: tableprinter.New(ios),
|
||||
opts: listOpts{
|
||||
number: 1,
|
||||
owner: "github",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Issue\tan issue\t1\tcli/go-gh\tissue ID\nPullRequest\ta pull request\t2\tcli/go-gh\tpull request ID\nDraftIssue\tdraft issue\t\t\tdraft issue ID\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunList_Me(t *testing.T) {
|
||||
defer gock.Off()
|
||||
// gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// list project items
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerProjectWithItems.*",
|
||||
"variables": map[string]interface{}{
|
||||
"firstItems": queries.LimitDefault,
|
||||
"afterItems": nil,
|
||||
"firstFields": queries.LimitMax,
|
||||
"afterFields": nil,
|
||||
"number": 1,
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"items": map[string]interface{}{
|
||||
"nodes": []map[string]interface{}{
|
||||
{
|
||||
"id": "issue ID",
|
||||
"content": map[string]interface{}{
|
||||
"__typename": "Issue",
|
||||
"title": "an issue",
|
||||
"number": 1,
|
||||
"repository": map[string]string{
|
||||
"nameWithOwner": "cli/go-gh",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "pull request ID",
|
||||
"content": map[string]interface{}{
|
||||
"__typename": "PullRequest",
|
||||
"title": "a pull request",
|
||||
"number": 2,
|
||||
"repository": map[string]string{
|
||||
"nameWithOwner": "cli/go-gh",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "draft issue ID",
|
||||
"content": map[string]interface{}{
|
||||
"title": "draft issue",
|
||||
"__typename": "DraftIssue",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
config := listConfig{
|
||||
tp: tableprinter.New(ios),
|
||||
opts: listOpts{
|
||||
number: 1,
|
||||
owner: "@me",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Issue\tan issue\t1\tcli/go-gh\tissue ID\nPullRequest\ta pull request\t2\tcli/go-gh\tpull request ID\nDraftIssue\tdraft issue\t\t\tdraft issue ID\n",
|
||||
stdout.String())
|
||||
}
|
||||
186
pkg/cmd/project/list/list.go
Normal file
186
pkg/cmd/project/list/list.go
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type listOpts struct {
|
||||
limit int
|
||||
web bool
|
||||
owner string
|
||||
closed bool
|
||||
format string
|
||||
}
|
||||
|
||||
type listConfig struct {
|
||||
tp *tableprinter.TablePrinter
|
||||
client *queries.Client
|
||||
opts listOpts
|
||||
URLOpener func(string) error
|
||||
io *iostreams.IOStreams
|
||||
}
|
||||
|
||||
func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.Command {
|
||||
opts := listOpts{}
|
||||
listCmd := &cobra.Command{
|
||||
Short: "List the projects for an owner",
|
||||
Use: "list",
|
||||
Example: heredoc.Doc(`
|
||||
# list the current user's projects
|
||||
gh project list
|
||||
|
||||
# list the projects for org github including closed projects
|
||||
gh project list --owner github --closed
|
||||
`),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := client.New(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
URLOpener := func(url string) error {
|
||||
return f.Browser.Browse(url)
|
||||
}
|
||||
t := tableprinter.New(f.IOStreams)
|
||||
config := listConfig{
|
||||
tp: t,
|
||||
client: client,
|
||||
opts: opts,
|
||||
URLOpener: URLOpener,
|
||||
io: f.IOStreams,
|
||||
}
|
||||
|
||||
// allow testing of the command without actually running it
|
||||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
return runList(config)
|
||||
},
|
||||
}
|
||||
|
||||
listCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner")
|
||||
listCmd.Flags().BoolVarP(&opts.closed, "closed", "", false, "Include closed projects")
|
||||
listCmd.Flags().BoolVarP(&opts.web, "web", "w", false, "Open projects list in the browser")
|
||||
cmdutil.StringEnumFlag(listCmd, &opts.format, "format", "", "", []string{"json"}, "Output format")
|
||||
listCmd.Flags().IntVarP(&opts.limit, "limit", "L", queries.LimitDefault, "Maximum number of projects to fetch")
|
||||
|
||||
return listCmd
|
||||
}
|
||||
|
||||
func runList(config listConfig) error {
|
||||
if config.opts.web {
|
||||
url, err := buildURL(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := config.URLOpener(url); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if config.opts.owner == "" {
|
||||
config.opts.owner = "@me"
|
||||
}
|
||||
canPrompt := config.io.CanPrompt()
|
||||
owner, err := config.client.NewOwner(canPrompt, config.opts.owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
projects, totalCount, err := config.client.Projects(config.opts.owner, owner.Type, config.opts.limit, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
projects = filterProjects(projects, config)
|
||||
|
||||
if config.opts.format == "json" {
|
||||
return printJSON(config, projects, totalCount)
|
||||
}
|
||||
|
||||
return printResults(config, projects, owner.Login)
|
||||
}
|
||||
|
||||
// TODO: support non-github.com hostnames
|
||||
func buildURL(config listConfig) (string, error) {
|
||||
var url string
|
||||
if config.opts.owner == "@me" || config.opts.owner == "" {
|
||||
owner, err := config.client.ViewerLoginName()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
url = fmt.Sprintf("https://github.com/users/%s/projects", owner)
|
||||
} else {
|
||||
_, ownerType, err := config.client.OwnerIDAndType(config.opts.owner)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if ownerType == queries.UserOwner {
|
||||
url = fmt.Sprintf("https://github.com/users/%s/projects", config.opts.owner)
|
||||
} else {
|
||||
url = fmt.Sprintf("https://github.com/orgs/%s/projects", config.opts.owner)
|
||||
}
|
||||
}
|
||||
|
||||
if config.opts.closed {
|
||||
return url + "?query=is%3Aclosed", nil
|
||||
}
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func filterProjects(nodes []queries.Project, config listConfig) []queries.Project {
|
||||
projects := make([]queries.Project, 0, len(nodes))
|
||||
for _, p := range nodes {
|
||||
if !config.opts.closed && p.Closed {
|
||||
continue
|
||||
}
|
||||
projects = append(projects, p)
|
||||
}
|
||||
return projects
|
||||
}
|
||||
|
||||
func printResults(config listConfig, projects []queries.Project, owner string) error {
|
||||
if len(projects) == 0 {
|
||||
return cmdutil.NewNoResultsError(fmt.Sprintf("No projects found for %s", owner))
|
||||
}
|
||||
|
||||
config.tp.HeaderRow("Number", "Title", "State", "ID")
|
||||
|
||||
for _, p := range projects {
|
||||
config.tp.AddField(strconv.Itoa(int(p.Number)), tableprinter.WithTruncate(nil))
|
||||
config.tp.AddField(p.Title)
|
||||
var state string
|
||||
if p.Closed {
|
||||
state = "closed"
|
||||
} else {
|
||||
state = "open"
|
||||
}
|
||||
config.tp.AddField(state)
|
||||
config.tp.AddField(p.ID, tableprinter.WithTruncate(nil))
|
||||
config.tp.EndRow()
|
||||
}
|
||||
|
||||
return config.tp.Render()
|
||||
}
|
||||
|
||||
func printJSON(config listConfig, projects []queries.Project, totalCount int) error {
|
||||
b, err := format.JSONProjects(projects, totalCount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = config.io.Out.Write(b)
|
||||
return err
|
||||
}
|
||||
737
pkg/cmd/project/list/list_test.go
Normal file
737
pkg/cmd/project/list/list_test.go
Normal file
|
|
@ -0,0 +1,737 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestNewCmdlist(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cli string
|
||||
wants listOpts
|
||||
wantsErr bool
|
||||
wantsErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "owner",
|
||||
cli: "--owner monalisa",
|
||||
wants: listOpts{
|
||||
owner: "monalisa",
|
||||
limit: 30,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "closed",
|
||||
cli: "--closed",
|
||||
wants: listOpts{
|
||||
closed: true,
|
||||
limit: 30,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "web",
|
||||
cli: "--web",
|
||||
wants: listOpts{
|
||||
web: true,
|
||||
limit: 30,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
cli: "--format json",
|
||||
wants: listOpts{
|
||||
format: "json",
|
||||
limit: 30,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
os.Setenv("GH_TOKEN", "auth-token")
|
||||
defer os.Unsetenv("GH_TOKEN")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
}
|
||||
|
||||
argv, err := shlex.Split(tt.cli)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotOpts listOpts
|
||||
cmd := NewCmdList(f, func(config listConfig) error {
|
||||
gotOpts = config.opts
|
||||
return nil
|
||||
})
|
||||
|
||||
cmd.SetArgs(argv)
|
||||
_, err = cmd.ExecuteC()
|
||||
if tt.wantsErr {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.wantsErrMsg, err.Error())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.wants.owner, gotOpts.owner)
|
||||
assert.Equal(t, tt.wants.closed, gotOpts.closed)
|
||||
assert.Equal(t, tt.wants.web, gotOpts.web)
|
||||
assert.Equal(t, tt.wants.limit, gotOpts.limit)
|
||||
assert.Equal(t, tt.wants.format, gotOpts.format)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunList(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"projectsV2": map[string]interface{}{
|
||||
"nodes": []interface{}{
|
||||
map[string]interface{}{
|
||||
"title": "Project 1",
|
||||
"shortDescription": "Short description 1",
|
||||
"url": "url1",
|
||||
"closed": false,
|
||||
"ID": "id-1",
|
||||
"number": 1,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"title": "Project 2",
|
||||
"shortDescription": "",
|
||||
"url": "url2",
|
||||
"closed": true,
|
||||
"ID": "id-2",
|
||||
"number": 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
config := listConfig{
|
||||
tp: tableprinter.New(ios),
|
||||
opts: listOpts{
|
||||
owner: "monalisa",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"1\tProject 1\topen\tid-1\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunList_Me(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"projectsV2": map[string]interface{}{
|
||||
"nodes": []interface{}{
|
||||
map[string]interface{}{
|
||||
"title": "Project 1",
|
||||
"shortDescription": "Short description 1",
|
||||
"url": "url1",
|
||||
"closed": false,
|
||||
"ID": "id-1",
|
||||
"number": 1,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"title": "Project 2",
|
||||
"shortDescription": "",
|
||||
"url": "url2",
|
||||
"closed": true,
|
||||
"ID": "id-2",
|
||||
"number": 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
config := listConfig{
|
||||
tp: tableprinter.New(ios),
|
||||
opts: listOpts{
|
||||
owner: "@me",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"1\tProject 1\topen\tid-1\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunListViewer(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query ViewerOwner.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"projectsV2": map[string]interface{}{
|
||||
"nodes": []interface{}{
|
||||
map[string]interface{}{
|
||||
"title": "Project 1",
|
||||
"shortDescription": "Short description 1",
|
||||
"url": "url1",
|
||||
"closed": false,
|
||||
"ID": "id-1",
|
||||
"number": 1,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"title": "Project 2",
|
||||
"shortDescription": "",
|
||||
"url": "url2",
|
||||
"closed": true,
|
||||
"ID": "id-2",
|
||||
"number": 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
config := listConfig{
|
||||
tp: tableprinter.New(ios),
|
||||
opts: listOpts{},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"1\tProject 1\topen\tid-1\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunListOrg(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get org ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"user"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"projectsV2": map[string]interface{}{
|
||||
"nodes": []interface{}{
|
||||
map[string]interface{}{
|
||||
"title": "Project 1",
|
||||
"shortDescription": "Short description 1",
|
||||
"url": "url1",
|
||||
"closed": false,
|
||||
"ID": "id-1",
|
||||
"number": 1,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"title": "Project 2",
|
||||
"shortDescription": "",
|
||||
"url": "url2",
|
||||
"closed": true,
|
||||
"ID": "id-2",
|
||||
"number": 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
config := listConfig{
|
||||
tp: tableprinter.New(ios),
|
||||
opts: listOpts{
|
||||
owner: "github",
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"1\tProject 1\topen\tid-1\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunListEmpty(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query Viewer.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
"login": "theviewer",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"projectsV2": map[string]interface{}{
|
||||
"nodes": []interface{}{},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
config := listConfig{
|
||||
tp: tableprinter.New(ios),
|
||||
opts: listOpts{},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.EqualError(
|
||||
t,
|
||||
err,
|
||||
"No projects found for @me")
|
||||
}
|
||||
|
||||
func TestRunListWithClosed(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
"projectsV2": map[string]interface{}{
|
||||
"nodes": []interface{}{
|
||||
map[string]interface{}{
|
||||
"title": "Project 1",
|
||||
"shortDescription": "Short description 1",
|
||||
"url": "url1",
|
||||
"closed": false,
|
||||
"ID": "id-1",
|
||||
"number": 1,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"title": "Project 2",
|
||||
"shortDescription": "",
|
||||
"url": "url2",
|
||||
"closed": true,
|
||||
"ID": "id-2",
|
||||
"number": 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
config := listConfig{
|
||||
tp: tableprinter.New(ios),
|
||||
opts: listOpts{
|
||||
owner: "monalisa",
|
||||
closed: true,
|
||||
},
|
||||
client: client,
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"1\tProject 1\topen\tid-1\n2\tProject 2\tclosed\tid-2\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunListWeb_User(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
buf := bytes.Buffer{}
|
||||
config := listConfig{
|
||||
opts: listOpts{
|
||||
owner: "monalisa",
|
||||
web: true,
|
||||
},
|
||||
URLOpener: func(url string) error {
|
||||
buf.WriteString(url)
|
||||
return nil
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/users/monalisa/projects", buf.String())
|
||||
}
|
||||
|
||||
func TestRunListWeb_Org(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
// get org ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "github",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"organization": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"user"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
buf := bytes.Buffer{}
|
||||
config := listConfig{
|
||||
opts: listOpts{
|
||||
owner: "github",
|
||||
web: true,
|
||||
},
|
||||
URLOpener: func(url string) error {
|
||||
buf.WriteString(url)
|
||||
return nil
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/orgs/github/projects", buf.String())
|
||||
}
|
||||
|
||||
func TestRunListWeb_Me(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query Viewer.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
"login": "theviewer",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
buf := bytes.Buffer{}
|
||||
config := listConfig{
|
||||
opts: listOpts{
|
||||
owner: "@me",
|
||||
web: true,
|
||||
},
|
||||
URLOpener: func(url string) error {
|
||||
buf.WriteString(url)
|
||||
return nil
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/users/theviewer/projects", buf.String())
|
||||
}
|
||||
|
||||
func TestRunListWeb_Empty(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query Viewer.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
"login": "theviewer",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
buf := bytes.Buffer{}
|
||||
config := listConfig{
|
||||
opts: listOpts{
|
||||
web: true,
|
||||
},
|
||||
URLOpener: func(url string) error {
|
||||
buf.WriteString(url)
|
||||
return nil
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/users/theviewer/projects", buf.String())
|
||||
}
|
||||
|
||||
func TestRunListWeb_Closed(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
// get viewer ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query Viewer.*",
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
"login": "theviewer",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
buf := bytes.Buffer{}
|
||||
config := listConfig{
|
||||
opts: listOpts{
|
||||
web: true,
|
||||
closed: true,
|
||||
},
|
||||
URLOpener: func(url string) error {
|
||||
buf.WriteString(url)
|
||||
return nil
|
||||
},
|
||||
client: client,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/users/theviewer/projects?query=is%3Aclosed", buf.String())
|
||||
}
|
||||
61
pkg/cmd/project/project.go
Normal file
61
pkg/cmd/project/project.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package project
|
||||
|
||||
import (
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
cmdClose "github.com/cli/cli/v2/pkg/cmd/project/close"
|
||||
cmdCopy "github.com/cli/cli/v2/pkg/cmd/project/copy"
|
||||
cmdCreate "github.com/cli/cli/v2/pkg/cmd/project/create"
|
||||
cmdDelete "github.com/cli/cli/v2/pkg/cmd/project/delete"
|
||||
cmdEdit "github.com/cli/cli/v2/pkg/cmd/project/edit"
|
||||
cmdFieldCreate "github.com/cli/cli/v2/pkg/cmd/project/field-create"
|
||||
cmdFieldDelete "github.com/cli/cli/v2/pkg/cmd/project/field-delete"
|
||||
cmdFieldList "github.com/cli/cli/v2/pkg/cmd/project/field-list"
|
||||
cmdItemAdd "github.com/cli/cli/v2/pkg/cmd/project/item-add"
|
||||
cmdItemArchive "github.com/cli/cli/v2/pkg/cmd/project/item-archive"
|
||||
cmdItemCreate "github.com/cli/cli/v2/pkg/cmd/project/item-create"
|
||||
cmdItemDelete "github.com/cli/cli/v2/pkg/cmd/project/item-delete"
|
||||
cmdItemEdit "github.com/cli/cli/v2/pkg/cmd/project/item-edit"
|
||||
cmdItemList "github.com/cli/cli/v2/pkg/cmd/project/item-list"
|
||||
cmdList "github.com/cli/cli/v2/pkg/cmd/project/list"
|
||||
cmdView "github.com/cli/cli/v2/pkg/cmd/project/view"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCmdProject(f *cmdutil.Factory) *cobra.Command {
|
||||
var cmd = &cobra.Command{
|
||||
Use: "project <command> [flags]",
|
||||
Short: "Work with GitHub Projects.",
|
||||
Long: "Work with GitHub Projects. Note that the token you are using must have 'project' scope, which is not set by default. You can verify your token scope by running 'gh auth status' and add the project scope by running 'gh auth refresh -s project'.",
|
||||
Example: heredoc.Doc(`
|
||||
$ gh project create --owner monalisa --title "Roadmap"
|
||||
$ gh project view 1 --owner cli --web
|
||||
$ gh project field-list 1 --owner cli
|
||||
$ gh project item-list 1 --owner cli
|
||||
`),
|
||||
GroupID: "core",
|
||||
}
|
||||
|
||||
cmd.AddCommand(cmdList.NewCmdList(f, nil))
|
||||
cmd.AddCommand(cmdCreate.NewCmdCreate(f, nil))
|
||||
cmd.AddCommand(cmdCopy.NewCmdCopy(f, nil))
|
||||
cmd.AddCommand(cmdClose.NewCmdClose(f, nil))
|
||||
cmd.AddCommand(cmdDelete.NewCmdDelete(f, nil))
|
||||
cmd.AddCommand(cmdEdit.NewCmdEdit(f, nil))
|
||||
cmd.AddCommand(cmdView.NewCmdView(f, nil))
|
||||
|
||||
// items
|
||||
cmd.AddCommand(cmdItemList.NewCmdList(f, nil))
|
||||
cmd.AddCommand(cmdItemCreate.NewCmdCreateItem(f, nil))
|
||||
cmd.AddCommand(cmdItemAdd.NewCmdAddItem(f, nil))
|
||||
cmd.AddCommand(cmdItemEdit.NewCmdEditItem(f, nil))
|
||||
cmd.AddCommand(cmdItemArchive.NewCmdArchiveItem(f, nil))
|
||||
cmd.AddCommand(cmdItemDelete.NewCmdDeleteItem(f, nil))
|
||||
|
||||
// fields
|
||||
cmd.AddCommand(cmdFieldList.NewCmdList(f, nil))
|
||||
cmd.AddCommand(cmdFieldCreate.NewCmdCreateField(f, nil))
|
||||
cmd.AddCommand(cmdFieldDelete.NewCmdDeleteField(f, nil))
|
||||
|
||||
return cmd
|
||||
}
|
||||
22
pkg/cmd/project/shared/client/client.go
Normal file
22
pkg/cmd/project/shared/client/client.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
)
|
||||
|
||||
func New(f *cmdutil.Factory) (*queries.Client, error) {
|
||||
if f.HttpClient == nil {
|
||||
// This is for compatibility with tests that exercise Cobra command functionality.
|
||||
// These tests do not define a `HttpClient` nor do they need to.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
httpClient, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return queries.NewClient(httpClient, os.Getenv("GH_HOST"), f.IOStreams), nil
|
||||
}
|
||||
365
pkg/cmd/project/shared/format/json.go
Normal file
365
pkg/cmd/project/shared/format/json.go
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
package format
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
)
|
||||
|
||||
// JSONProject serializes a Project to JSON.
|
||||
func JSONProject(project queries.Project) ([]byte, error) {
|
||||
return json.Marshal(projectJSON{
|
||||
Number: project.Number,
|
||||
URL: project.URL,
|
||||
ShortDescription: project.ShortDescription,
|
||||
Public: project.Public,
|
||||
Closed: project.Closed,
|
||||
Title: project.Title,
|
||||
ID: project.ID,
|
||||
Readme: project.Readme,
|
||||
Items: struct {
|
||||
TotalCount int `json:"totalCount"`
|
||||
}{
|
||||
TotalCount: project.Items.TotalCount,
|
||||
},
|
||||
Fields: struct {
|
||||
TotalCount int `json:"totalCount"`
|
||||
}{
|
||||
TotalCount: project.Fields.TotalCount,
|
||||
},
|
||||
Owner: struct {
|
||||
Type string `json:"type"`
|
||||
Login string `json:"login"`
|
||||
}{
|
||||
Type: project.OwnerType(),
|
||||
Login: project.OwnerLogin(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// JSONProjects serializes a slice of Projects to JSON.
|
||||
// JSON fields are `totalCount` and `projects`.
|
||||
func JSONProjects(projects []queries.Project, totalCount int) ([]byte, error) {
|
||||
var result []projectJSON
|
||||
for _, p := range projects {
|
||||
result = append(result, projectJSON{
|
||||
Number: p.Number,
|
||||
URL: p.URL,
|
||||
ShortDescription: p.ShortDescription,
|
||||
Public: p.Public,
|
||||
Closed: p.Closed,
|
||||
Title: p.Title,
|
||||
ID: p.ID,
|
||||
Readme: p.Readme,
|
||||
Items: struct {
|
||||
TotalCount int `json:"totalCount"`
|
||||
}{
|
||||
TotalCount: p.Items.TotalCount,
|
||||
},
|
||||
Fields: struct {
|
||||
TotalCount int `json:"totalCount"`
|
||||
}{
|
||||
TotalCount: p.Fields.TotalCount,
|
||||
},
|
||||
Owner: struct {
|
||||
Type string `json:"type"`
|
||||
Login string `json:"login"`
|
||||
}{
|
||||
Type: p.OwnerType(),
|
||||
Login: p.OwnerLogin(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return json.Marshal(struct {
|
||||
Projects []projectJSON `json:"projects"`
|
||||
TotalCount int `json:"totalCount"`
|
||||
}{
|
||||
Projects: result,
|
||||
TotalCount: totalCount,
|
||||
})
|
||||
}
|
||||
|
||||
type projectJSON struct {
|
||||
Number int32 `json:"number"`
|
||||
URL string `json:"url"`
|
||||
ShortDescription string `json:"shortDescription"`
|
||||
Public bool `json:"public"`
|
||||
Closed bool `json:"closed"`
|
||||
Title string `json:"title"`
|
||||
ID string `json:"id"`
|
||||
Readme string `json:"readme"`
|
||||
Items struct {
|
||||
TotalCount int `json:"totalCount"`
|
||||
} `graphql:"items(first: 100)" json:"items"`
|
||||
Fields struct {
|
||||
TotalCount int `json:"totalCount"`
|
||||
} `graphql:"fields(first:100)" json:"fields"`
|
||||
Owner struct {
|
||||
Type string `json:"type"`
|
||||
Login string `json:"login"`
|
||||
} `json:"owner"`
|
||||
}
|
||||
|
||||
// JSONProjectField serializes a ProjectField to JSON.
|
||||
func JSONProjectField(field queries.ProjectField) ([]byte, error) {
|
||||
val := projectFieldJSON{
|
||||
ID: field.ID(),
|
||||
Name: field.Name(),
|
||||
Type: field.Type(),
|
||||
}
|
||||
for _, o := range field.Options() {
|
||||
val.Options = append(val.Options, singleSelectOptionJSON{
|
||||
Name: o.Name,
|
||||
ID: o.ID,
|
||||
})
|
||||
}
|
||||
|
||||
return json.Marshal(val)
|
||||
}
|
||||
|
||||
// JSONProjectFields serializes a slice of ProjectFields to JSON.
|
||||
// JSON fields are `totalCount` and `fields`.
|
||||
func JSONProjectFields(project *queries.Project) ([]byte, error) {
|
||||
var result []projectFieldJSON
|
||||
for _, f := range project.Fields.Nodes {
|
||||
val := projectFieldJSON{
|
||||
ID: f.ID(),
|
||||
Name: f.Name(),
|
||||
Type: f.Type(),
|
||||
}
|
||||
for _, o := range f.Options() {
|
||||
val.Options = append(val.Options, singleSelectOptionJSON{
|
||||
Name: o.Name,
|
||||
ID: o.ID,
|
||||
})
|
||||
}
|
||||
|
||||
result = append(result, val)
|
||||
}
|
||||
|
||||
return json.Marshal(struct {
|
||||
Fields []projectFieldJSON `json:"fields"`
|
||||
TotalCount int `json:"totalCount"`
|
||||
}{
|
||||
Fields: result,
|
||||
TotalCount: project.Fields.TotalCount,
|
||||
})
|
||||
}
|
||||
|
||||
type projectFieldJSON struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Options []singleSelectOptionJSON `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
type singleSelectOptionJSON struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// JSONProjectItem serializes a ProjectItem to JSON.
|
||||
func JSONProjectItem(item queries.ProjectItem) ([]byte, error) {
|
||||
return json.Marshal(projectItemJSON{
|
||||
ID: item.ID(),
|
||||
Title: item.Title(),
|
||||
Body: item.Body(),
|
||||
Type: item.Type(),
|
||||
URL: item.URL(),
|
||||
})
|
||||
}
|
||||
|
||||
type projectItemJSON struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// JSONProjectDraftIssue serializes a DraftIssue to JSON.
|
||||
// This is needed because the field for
|
||||
// https://docs.github.com/en/graphql/reference/mutations#updateprojectv2draftissue
|
||||
// is a DraftIssue https://docs.github.com/en/graphql/reference/objects#draftissue
|
||||
// and not a ProjectV2Item https://docs.github.com/en/graphql/reference/objects#projectv2item
|
||||
func JSONProjectDraftIssue(item queries.DraftIssue) ([]byte, error) {
|
||||
|
||||
return json.Marshal(draftIssueJSON{
|
||||
ID: item.ID,
|
||||
Title: item.Title,
|
||||
Body: item.Body,
|
||||
Type: "DraftIssue",
|
||||
})
|
||||
}
|
||||
|
||||
type draftIssueJSON struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func projectItemContent(p queries.ProjectItem) any {
|
||||
switch p.Content.TypeName {
|
||||
case "DraftIssue":
|
||||
return struct {
|
||||
Type string `json:"type"`
|
||||
Body string `json:"body"`
|
||||
Title string `json:"title"`
|
||||
}{
|
||||
Type: p.Type(),
|
||||
Body: p.Body(),
|
||||
Title: p.Title(),
|
||||
}
|
||||
case "Issue":
|
||||
return struct {
|
||||
Type string `json:"type"`
|
||||
Body string `json:"body"`
|
||||
Title string `json:"title"`
|
||||
Number int `json:"number"`
|
||||
Repository string `json:"repository"`
|
||||
URL string `json:"url"`
|
||||
}{
|
||||
Type: p.Type(),
|
||||
Body: p.Body(),
|
||||
Title: p.Title(),
|
||||
Number: p.Number(),
|
||||
Repository: p.Repo(),
|
||||
URL: p.URL(),
|
||||
}
|
||||
case "PullRequest":
|
||||
return struct {
|
||||
Type string `json:"type"`
|
||||
Body string `json:"body"`
|
||||
Title string `json:"title"`
|
||||
Number int `json:"number"`
|
||||
Repository string `json:"repository"`
|
||||
URL string `json:"url"`
|
||||
}{
|
||||
Type: p.Type(),
|
||||
Body: p.Body(),
|
||||
Title: p.Title(),
|
||||
Number: p.Number(),
|
||||
Repository: p.Repo(),
|
||||
URL: p.URL(),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func projectFieldValueData(v queries.FieldValueNodes) any {
|
||||
switch v.Type {
|
||||
case "ProjectV2ItemFieldDateValue":
|
||||
return v.ProjectV2ItemFieldDateValue.Date
|
||||
case "ProjectV2ItemFieldIterationValue":
|
||||
return struct {
|
||||
StartDate string `json:"startDate"`
|
||||
Duration int `json:"duration"`
|
||||
}{
|
||||
StartDate: v.ProjectV2ItemFieldIterationValue.StartDate,
|
||||
Duration: v.ProjectV2ItemFieldIterationValue.Duration,
|
||||
}
|
||||
case "ProjectV2ItemFieldNumberValue":
|
||||
return v.ProjectV2ItemFieldNumberValue.Number
|
||||
case "ProjectV2ItemFieldSingleSelectValue":
|
||||
return v.ProjectV2ItemFieldSingleSelectValue.Name
|
||||
case "ProjectV2ItemFieldTextValue":
|
||||
return v.ProjectV2ItemFieldTextValue.Text
|
||||
case "ProjectV2ItemFieldMilestoneValue":
|
||||
return struct {
|
||||
Description string `json:"description"`
|
||||
DueOn string `json:"dueOn"`
|
||||
}{
|
||||
Description: v.ProjectV2ItemFieldMilestoneValue.Milestone.Description,
|
||||
DueOn: v.ProjectV2ItemFieldMilestoneValue.Milestone.DueOn,
|
||||
}
|
||||
case "ProjectV2ItemFieldLabelValue":
|
||||
names := make([]string, 0)
|
||||
for _, p := range v.ProjectV2ItemFieldLabelValue.Labels.Nodes {
|
||||
names = append(names, p.Name)
|
||||
}
|
||||
return names
|
||||
case "ProjectV2ItemFieldPullRequestValue":
|
||||
urls := make([]string, 0)
|
||||
for _, p := range v.ProjectV2ItemFieldPullRequestValue.PullRequests.Nodes {
|
||||
urls = append(urls, p.Url)
|
||||
}
|
||||
return urls
|
||||
case "ProjectV2ItemFieldRepositoryValue":
|
||||
return v.ProjectV2ItemFieldRepositoryValue.Repository.Url
|
||||
case "ProjectV2ItemFieldUserValue":
|
||||
logins := make([]string, 0)
|
||||
for _, p := range v.ProjectV2ItemFieldUserValue.Users.Nodes {
|
||||
logins = append(logins, p.Login)
|
||||
}
|
||||
return logins
|
||||
case "ProjectV2ItemFieldReviewerValue":
|
||||
names := make([]string, 0)
|
||||
for _, p := range v.ProjectV2ItemFieldReviewerValue.Reviewers.Nodes {
|
||||
if p.Type == "Team" {
|
||||
names = append(names, p.Team.Name)
|
||||
} else if p.Type == "User" {
|
||||
names = append(names, p.User.Login)
|
||||
}
|
||||
}
|
||||
return names
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// serialize creates a map from field to field values
|
||||
func serializeProjectWithItems(project *queries.Project) []map[string]any {
|
||||
fields := make(map[string]string)
|
||||
|
||||
// make a map of fields by ID
|
||||
for _, f := range project.Fields.Nodes {
|
||||
fields[f.ID()] = CamelCase(f.Name())
|
||||
}
|
||||
itemsSlice := make([]map[string]any, 0)
|
||||
|
||||
// for each value, look up the name by ID
|
||||
// and set the value to the field value
|
||||
for _, i := range project.Items.Nodes {
|
||||
o := make(map[string]any)
|
||||
o["id"] = i.Id
|
||||
o["content"] = projectItemContent(i)
|
||||
for _, v := range i.FieldValues.Nodes {
|
||||
id := v.ID()
|
||||
value := projectFieldValueData(v)
|
||||
|
||||
o[fields[id]] = value
|
||||
}
|
||||
itemsSlice = append(itemsSlice, o)
|
||||
}
|
||||
return itemsSlice
|
||||
}
|
||||
|
||||
// JSONProjectWithItems returns a detailed JSON representation of project items.
|
||||
// JSON fields are `totalCount` and `items`.
|
||||
func JSONProjectDetailedItems(project *queries.Project) ([]byte, error) {
|
||||
items := serializeProjectWithItems(project)
|
||||
return json.Marshal(struct {
|
||||
Items []map[string]any `json:"items"`
|
||||
TotalCount int `json:"totalCount"`
|
||||
}{
|
||||
Items: items,
|
||||
TotalCount: project.Items.TotalCount,
|
||||
})
|
||||
}
|
||||
|
||||
// CamelCase converts a string to camelCase, which is useful for turning Go field names to JSON keys.
|
||||
func CamelCase(s string) string {
|
||||
if len(s) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(s) == 1 {
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
return strings.ToLower(s[0:1]) + s[1:]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue