1package create 2 3import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "net/http" 9 "path/filepath" 10 "testing" 11 12 "github.com/MakeNowJust/heredoc" 13 "github.com/cli/cli/v2/api" 14 "github.com/cli/cli/v2/context" 15 "github.com/cli/cli/v2/git" 16 "github.com/cli/cli/v2/internal/config" 17 "github.com/cli/cli/v2/internal/ghrepo" 18 "github.com/cli/cli/v2/internal/run" 19 "github.com/cli/cli/v2/pkg/cmd/pr/shared" 20 prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared" 21 "github.com/cli/cli/v2/pkg/cmdutil" 22 "github.com/cli/cli/v2/pkg/httpmock" 23 "github.com/cli/cli/v2/pkg/iostreams" 24 "github.com/cli/cli/v2/pkg/prompt" 25 "github.com/cli/cli/v2/test" 26 "github.com/google/shlex" 27 "github.com/stretchr/testify/assert" 28 "github.com/stretchr/testify/require" 29) 30 31func TestNewCmdCreate(t *testing.T) { 32 tmpFile := filepath.Join(t.TempDir(), "my-body.md") 33 err := ioutil.WriteFile(tmpFile, []byte("a body from file"), 0600) 34 require.NoError(t, err) 35 36 tests := []struct { 37 name string 38 tty bool 39 stdin string 40 cli string 41 wantsErr bool 42 wantsOpts CreateOptions 43 }{ 44 { 45 name: "empty non-tty", 46 tty: false, 47 cli: "", 48 wantsErr: true, 49 }, 50 { 51 name: "empty tty", 52 tty: true, 53 cli: "", 54 wantsErr: false, 55 wantsOpts: CreateOptions{ 56 Title: "", 57 TitleProvided: false, 58 Body: "", 59 BodyProvided: false, 60 Autofill: false, 61 RecoverFile: "", 62 WebMode: false, 63 IsDraft: false, 64 BaseBranch: "", 65 HeadBranch: "", 66 MaintainerCanModify: true, 67 }, 68 }, 69 { 70 name: "body from stdin", 71 tty: false, 72 stdin: "this is on standard input", 73 cli: "-t mytitle -F -", 74 wantsErr: false, 75 wantsOpts: CreateOptions{ 76 Title: "mytitle", 77 TitleProvided: true, 78 Body: "this is on standard input", 79 BodyProvided: true, 80 Autofill: false, 81 RecoverFile: "", 82 WebMode: false, 83 IsDraft: false, 84 BaseBranch: "", 85 HeadBranch: "", 86 MaintainerCanModify: true, 87 }, 88 }, 89 { 90 name: "body from file", 91 tty: false, 92 cli: fmt.Sprintf("-t mytitle -F '%s'", tmpFile), 93 wantsErr: false, 94 wantsOpts: CreateOptions{ 95 Title: "mytitle", 96 TitleProvided: true, 97 Body: "a body from file", 98 BodyProvided: true, 99 Autofill: false, 100 RecoverFile: "", 101 WebMode: false, 102 IsDraft: false, 103 BaseBranch: "", 104 HeadBranch: "", 105 MaintainerCanModify: true, 106 }, 107 }, 108 } 109 for _, tt := range tests { 110 t.Run(tt.name, func(t *testing.T) { 111 io, stdin, stdout, stderr := iostreams.Test() 112 if tt.stdin != "" { 113 _, _ = stdin.WriteString(tt.stdin) 114 } else if tt.tty { 115 io.SetStdinTTY(true) 116 io.SetStdoutTTY(true) 117 } 118 119 f := &cmdutil.Factory{ 120 IOStreams: io, 121 } 122 123 var opts *CreateOptions 124 cmd := NewCmdCreate(f, func(o *CreateOptions) error { 125 opts = o 126 return nil 127 }) 128 129 args, err := shlex.Split(tt.cli) 130 require.NoError(t, err) 131 cmd.SetArgs(args) 132 _, err = cmd.ExecuteC() 133 if tt.wantsErr { 134 assert.Error(t, err) 135 return 136 } else { 137 require.NoError(t, err) 138 } 139 140 assert.Equal(t, "", stdout.String()) 141 assert.Equal(t, "", stderr.String()) 142 143 assert.Equal(t, tt.wantsOpts.Body, opts.Body) 144 assert.Equal(t, tt.wantsOpts.BodyProvided, opts.BodyProvided) 145 assert.Equal(t, tt.wantsOpts.Title, opts.Title) 146 assert.Equal(t, tt.wantsOpts.TitleProvided, opts.TitleProvided) 147 assert.Equal(t, tt.wantsOpts.Autofill, opts.Autofill) 148 assert.Equal(t, tt.wantsOpts.WebMode, opts.WebMode) 149 assert.Equal(t, tt.wantsOpts.RecoverFile, opts.RecoverFile) 150 assert.Equal(t, tt.wantsOpts.IsDraft, opts.IsDraft) 151 assert.Equal(t, tt.wantsOpts.MaintainerCanModify, opts.MaintainerCanModify) 152 assert.Equal(t, tt.wantsOpts.BaseBranch, opts.BaseBranch) 153 assert.Equal(t, tt.wantsOpts.HeadBranch, opts.HeadBranch) 154 }) 155 } 156} 157 158func runCommand(rt http.RoundTripper, remotes context.Remotes, branch string, isTTY bool, cli string) (*test.CmdOut, error) { 159 return runCommandWithRootDirOverridden(rt, remotes, branch, isTTY, cli, "") 160} 161 162func runCommandWithRootDirOverridden(rt http.RoundTripper, remotes context.Remotes, branch string, isTTY bool, cli string, rootDir string) (*test.CmdOut, error) { 163 io, _, stdout, stderr := iostreams.Test() 164 io.SetStdoutTTY(isTTY) 165 io.SetStdinTTY(isTTY) 166 io.SetStderrTTY(isTTY) 167 168 browser := &cmdutil.TestBrowser{} 169 factory := &cmdutil.Factory{ 170 IOStreams: io, 171 Browser: browser, 172 HttpClient: func() (*http.Client, error) { 173 return &http.Client{Transport: rt}, nil 174 }, 175 Config: func() (config.Config, error) { 176 return config.NewBlankConfig(), nil 177 }, 178 Remotes: func() (context.Remotes, error) { 179 if remotes != nil { 180 return remotes, nil 181 } 182 return context.Remotes{ 183 { 184 Remote: &git.Remote{ 185 Name: "origin", 186 Resolved: "base", 187 }, 188 Repo: ghrepo.New("OWNER", "REPO"), 189 }, 190 }, nil 191 }, 192 Branch: func() (string, error) { 193 return branch, nil 194 }, 195 } 196 197 cmd := NewCmdCreate(factory, func(opts *CreateOptions) error { 198 opts.RootDirOverride = rootDir 199 return createRun(opts) 200 }) 201 cmd.PersistentFlags().StringP("repo", "R", "", "") 202 203 argv, err := shlex.Split(cli) 204 if err != nil { 205 return nil, err 206 } 207 cmd.SetArgs(argv) 208 209 cmd.SetIn(&bytes.Buffer{}) 210 cmd.SetOut(ioutil.Discard) 211 cmd.SetErr(ioutil.Discard) 212 213 _, err = cmd.ExecuteC() 214 return &test.CmdOut{ 215 OutBuf: stdout, 216 ErrBuf: stderr, 217 BrowsedURL: browser.BrowsedURL(), 218 }, err 219} 220 221func initFakeHTTP() *httpmock.Registry { 222 return &httpmock.Registry{} 223} 224 225func TestPRCreate_nontty_web(t *testing.T) { 226 http := initFakeHTTP() 227 defer http.Verify(t) 228 229 http.StubRepoInfoResponse("OWNER", "REPO", "master") 230 231 cs, cmdTeardown := run.Stub() 232 defer cmdTeardown(t) 233 234 cs.Register(`git status --porcelain`, 0, "") 235 cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") 236 237 output, err := runCommand(http, nil, "feature", false, `--web --head=feature`) 238 require.NoError(t, err) 239 240 assert.Equal(t, "", output.String()) 241 assert.Equal(t, "", output.Stderr()) 242 assert.Equal(t, "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1", output.BrowsedURL) 243} 244 245func TestPRCreate_recover(t *testing.T) { 246 http := initFakeHTTP() 247 defer http.Verify(t) 248 249 http.StubRepoInfoResponse("OWNER", "REPO", "master") 250 shared.RunCommandFinder("feature", nil, nil) 251 http.Register( 252 httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`), 253 httpmock.StringResponse(` 254 { "data": { 255 "u000": { "login": "jillValentine", "id": "JILLID" }, 256 "repository": {}, 257 "organization": {} 258 } } 259 `)) 260 http.Register( 261 httpmock.GraphQL(`mutation PullRequestCreateRequestReviews\b`), 262 httpmock.GraphQLMutation(` 263 { "data": { "requestReviews": { 264 "clientMutationId": "" 265 } } } 266 `, func(inputs map[string]interface{}) { 267 assert.Equal(t, []interface{}{"JILLID"}, inputs["userIds"]) 268 })) 269 http.Register( 270 httpmock.GraphQL(`mutation PullRequestCreate\b`), 271 httpmock.GraphQLMutation(` 272 { "data": { "createPullRequest": { "pullRequest": { 273 "URL": "https://github.com/OWNER/REPO/pull/12" 274 } } } } 275 `, func(input map[string]interface{}) { 276 assert.Equal(t, "recovered title", input["title"].(string)) 277 assert.Equal(t, "recovered body", input["body"].(string)) 278 })) 279 280 cs, cmdTeardown := run.Stub() 281 defer cmdTeardown(t) 282 283 cs.Register(`git status --porcelain`, 0, "") 284 cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") 285 286 as, teardown := prompt.InitAskStubber() 287 defer teardown() 288 as.Stub([]*prompt.QuestionStub{ 289 { 290 Name: "Title", 291 Default: true, 292 }, 293 }) 294 as.Stub([]*prompt.QuestionStub{ 295 { 296 Name: "Body", 297 Default: true, 298 }, 299 }) 300 as.Stub([]*prompt.QuestionStub{ 301 { 302 Name: "confirmation", 303 Value: 0, 304 }, 305 }) 306 307 tmpfile, err := ioutil.TempFile(t.TempDir(), "testrecover*") 308 assert.NoError(t, err) 309 defer tmpfile.Close() 310 311 state := prShared.IssueMetadataState{ 312 Title: "recovered title", 313 Body: "recovered body", 314 Reviewers: []string{"jillValentine"}, 315 } 316 317 data, err := json.Marshal(state) 318 assert.NoError(t, err) 319 320 _, err = tmpfile.Write(data) 321 assert.NoError(t, err) 322 323 args := fmt.Sprintf("--recover '%s' -Hfeature", tmpfile.Name()) 324 325 output, err := runCommandWithRootDirOverridden(http, nil, "feature", true, args, "") 326 assert.NoError(t, err) 327 328 assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String()) 329} 330 331func TestPRCreate_nontty(t *testing.T) { 332 http := initFakeHTTP() 333 defer http.Verify(t) 334 335 http.StubRepoInfoResponse("OWNER", "REPO", "master") 336 shared.RunCommandFinder("feature", nil, nil) 337 http.Register( 338 httpmock.GraphQL(`mutation PullRequestCreate\b`), 339 httpmock.GraphQLMutation(` 340 { "data": { "createPullRequest": { "pullRequest": { 341 "URL": "https://github.com/OWNER/REPO/pull/12" 342 } } } }`, 343 func(input map[string]interface{}) { 344 assert.Equal(t, "REPOID", input["repositoryId"]) 345 assert.Equal(t, "my title", input["title"]) 346 assert.Equal(t, "my body", input["body"]) 347 assert.Equal(t, "master", input["baseRefName"]) 348 assert.Equal(t, "feature", input["headRefName"]) 349 }), 350 ) 351 352 cs, cmdTeardown := run.Stub() 353 defer cmdTeardown(t) 354 355 cs.Register(`git status --porcelain`, 0, "") 356 357 output, err := runCommand(http, nil, "feature", false, `-t "my title" -b "my body" -H feature`) 358 require.NoError(t, err) 359 360 assert.Equal(t, "", output.Stderr()) 361 assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String()) 362} 363 364func TestPRCreate(t *testing.T) { 365 http := initFakeHTTP() 366 defer http.Verify(t) 367 368 http.StubRepoInfoResponse("OWNER", "REPO", "master") 369 http.StubRepoResponse("OWNER", "REPO") 370 http.Register( 371 httpmock.GraphQL(`query UserCurrent\b`), 372 httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) 373 shared.RunCommandFinder("feature", nil, nil) 374 http.Register( 375 httpmock.GraphQL(`mutation PullRequestCreate\b`), 376 httpmock.GraphQLMutation(` 377 { "data": { "createPullRequest": { "pullRequest": { 378 "URL": "https://github.com/OWNER/REPO/pull/12" 379 } } } } 380 `, func(input map[string]interface{}) { 381 assert.Equal(t, "REPOID", input["repositoryId"].(string)) 382 assert.Equal(t, "my title", input["title"].(string)) 383 assert.Equal(t, "my body", input["body"].(string)) 384 assert.Equal(t, "master", input["baseRefName"].(string)) 385 assert.Equal(t, "feature", input["headRefName"].(string)) 386 })) 387 388 cs, cmdTeardown := run.Stub() 389 defer cmdTeardown(t) 390 391 cs.Register(`git status --porcelain`, 0, "") 392 cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") 393 cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") 394 cs.Register(`git push --set-upstream origin HEAD:feature`, 0, "") 395 396 ask, cleanupAsk := prompt.InitAskStubber() 397 defer cleanupAsk() 398 ask.StubOne(0) 399 400 output, err := runCommand(http, nil, "feature", true, `-t "my title" -b "my body"`) 401 require.NoError(t, err) 402 403 assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String()) 404 assert.Equal(t, "\nCreating pull request for feature into master in OWNER/REPO\n\n", output.Stderr()) 405} 406 407func TestPRCreate_NoMaintainerModify(t *testing.T) { 408 // TODO update this copypasta 409 http := initFakeHTTP() 410 defer http.Verify(t) 411 412 http.StubRepoInfoResponse("OWNER", "REPO", "master") 413 http.StubRepoResponse("OWNER", "REPO") 414 http.Register( 415 httpmock.GraphQL(`query UserCurrent\b`), 416 httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) 417 shared.RunCommandFinder("feature", nil, nil) 418 http.Register( 419 httpmock.GraphQL(`mutation PullRequestCreate\b`), 420 httpmock.GraphQLMutation(` 421 { "data": { "createPullRequest": { "pullRequest": { 422 "URL": "https://github.com/OWNER/REPO/pull/12" 423 } } } } 424 `, func(input map[string]interface{}) { 425 assert.Equal(t, false, input["maintainerCanModify"].(bool)) 426 assert.Equal(t, "REPOID", input["repositoryId"].(string)) 427 assert.Equal(t, "my title", input["title"].(string)) 428 assert.Equal(t, "my body", input["body"].(string)) 429 assert.Equal(t, "master", input["baseRefName"].(string)) 430 assert.Equal(t, "feature", input["headRefName"].(string)) 431 })) 432 433 cs, cmdTeardown := run.Stub() 434 defer cmdTeardown(t) 435 436 cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") 437 cs.Register(`git status --porcelain`, 0, "") 438 cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") 439 cs.Register(`git push --set-upstream origin HEAD:feature`, 0, "") 440 441 ask, cleanupAsk := prompt.InitAskStubber() 442 defer cleanupAsk() 443 ask.StubOne(0) 444 445 output, err := runCommand(http, nil, "feature", true, `-t "my title" -b "my body" --no-maintainer-edit`) 446 require.NoError(t, err) 447 448 assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String()) 449 assert.Equal(t, "\nCreating pull request for feature into master in OWNER/REPO\n\n", output.Stderr()) 450} 451 452func TestPRCreate_createFork(t *testing.T) { 453 http := initFakeHTTP() 454 defer http.Verify(t) 455 456 http.StubRepoInfoResponse("OWNER", "REPO", "master") 457 http.StubRepoResponse("OWNER", "REPO") 458 http.Register( 459 httpmock.GraphQL(`query UserCurrent\b`), 460 httpmock.StringResponse(`{"data": {"viewer": {"login": "monalisa"} } }`)) 461 shared.RunCommandFinder("feature", nil, nil) 462 http.Register( 463 httpmock.REST("POST", "repos/OWNER/REPO/forks"), 464 httpmock.StatusStringResponse(201, ` 465 { "node_id": "NODEID", 466 "name": "REPO", 467 "owner": {"login": "monalisa"} 468 } 469 `)) 470 http.Register( 471 httpmock.GraphQL(`mutation PullRequestCreate\b`), 472 httpmock.GraphQLMutation(` 473 { "data": { "createPullRequest": { "pullRequest": { 474 "URL": "https://github.com/OWNER/REPO/pull/12" 475 } } } } 476 `, func(input map[string]interface{}) { 477 assert.Equal(t, "REPOID", input["repositoryId"].(string)) 478 assert.Equal(t, "master", input["baseRefName"].(string)) 479 assert.Equal(t, "monalisa:feature", input["headRefName"].(string)) 480 })) 481 482 cs, cmdTeardown := run.Stub() 483 defer cmdTeardown(t) 484 485 cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") 486 cs.Register(`git status --porcelain`, 0, "") 487 cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") 488 cs.Register(`git remote add -f fork https://github.com/monalisa/REPO.git`, 0, "") 489 cs.Register(`git push --set-upstream fork HEAD:feature`, 0, "") 490 491 ask, cleanupAsk := prompt.InitAskStubber() 492 defer cleanupAsk() 493 ask.StubOne(1) 494 495 output, err := runCommand(http, nil, "feature", true, `-t title -b body`) 496 require.NoError(t, err) 497 498 assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String()) 499} 500 501func TestPRCreate_pushedToNonBaseRepo(t *testing.T) { 502 remotes := context.Remotes{ 503 { 504 Remote: &git.Remote{ 505 Name: "upstream", 506 Resolved: "base", 507 }, 508 Repo: ghrepo.New("OWNER", "REPO"), 509 }, 510 { 511 Remote: &git.Remote{ 512 Name: "origin", 513 Resolved: "base", 514 }, 515 Repo: ghrepo.New("monalisa", "REPO"), 516 }, 517 } 518 519 http := initFakeHTTP() 520 defer http.Verify(t) 521 522 http.StubRepoInfoResponse("OWNER", "REPO", "master") 523 shared.RunCommandFinder("feature", nil, nil) 524 http.Register( 525 httpmock.GraphQL(`mutation PullRequestCreate\b`), 526 httpmock.GraphQLMutation(` 527 { "data": { "createPullRequest": { "pullRequest": { 528 "URL": "https://github.com/OWNER/REPO/pull/12" 529 } } } } 530 `, func(input map[string]interface{}) { 531 assert.Equal(t, "REPOID", input["repositoryId"].(string)) 532 assert.Equal(t, "master", input["baseRefName"].(string)) 533 assert.Equal(t, "monalisa:feature", input["headRefName"].(string)) 534 })) 535 536 cs, cmdTeardown := run.Stub() 537 defer cmdTeardown(t) 538 539 cs.Register("git status", 0, "") 540 cs.Register(`git config --get-regexp \^branch\\\.feature\\\.`, 1, "") // determineTrackingBranch 541 cs.Register("git show-ref --verify", 0, heredoc.Doc(` 542 deadbeef HEAD 543 deadb00f refs/remotes/upstream/feature 544 deadbeef refs/remotes/origin/feature 545 `)) // determineTrackingBranch 546 547 _, cleanupAsk := prompt.InitAskStubber() 548 defer cleanupAsk() 549 550 output, err := runCommand(http, remotes, "feature", true, `-t title -b body`) 551 require.NoError(t, err) 552 553 assert.Equal(t, "\nCreating pull request for monalisa:feature into master in OWNER/REPO\n\n", output.Stderr()) 554 assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String()) 555} 556 557func TestPRCreate_pushedToDifferentBranchName(t *testing.T) { 558 http := initFakeHTTP() 559 defer http.Verify(t) 560 561 http.StubRepoInfoResponse("OWNER", "REPO", "master") 562 shared.RunCommandFinder("feature", nil, nil) 563 http.Register( 564 httpmock.GraphQL(`mutation PullRequestCreate\b`), 565 httpmock.GraphQLMutation(` 566 { "data": { "createPullRequest": { "pullRequest": { 567 "URL": "https://github.com/OWNER/REPO/pull/12" 568 } } } } 569 `, func(input map[string]interface{}) { 570 assert.Equal(t, "REPOID", input["repositoryId"].(string)) 571 assert.Equal(t, "master", input["baseRefName"].(string)) 572 assert.Equal(t, "my-feat2", input["headRefName"].(string)) 573 })) 574 575 cs, cmdTeardown := run.Stub() 576 defer cmdTeardown(t) 577 578 cs.Register("git status", 0, "") 579 cs.Register(`git config --get-regexp \^branch\\\.feature\\\.`, 0, heredoc.Doc(` 580 branch.feature.remote origin 581 branch.feature.merge refs/heads/my-feat2 582 `)) // determineTrackingBranch 583 cs.Register("git show-ref --verify", 0, heredoc.Doc(` 584 deadbeef HEAD 585 deadbeef refs/remotes/origin/my-feat2 586 `)) // determineTrackingBranch 587 588 _, cleanupAsk := prompt.InitAskStubber() 589 defer cleanupAsk() 590 591 output, err := runCommand(http, nil, "feature", true, `-t title -b body`) 592 require.NoError(t, err) 593 594 assert.Equal(t, "\nCreating pull request for my-feat2 into master in OWNER/REPO\n\n", output.Stderr()) 595 assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String()) 596} 597 598func TestPRCreate_nonLegacyTemplate(t *testing.T) { 599 http := initFakeHTTP() 600 defer http.Verify(t) 601 602 http.StubRepoInfoResponse("OWNER", "REPO", "master") 603 shared.RunCommandFinder("feature", nil, nil) 604 http.Register( 605 httpmock.GraphQL(`mutation PullRequestCreate\b`), 606 httpmock.GraphQLMutation(` 607 { "data": { "createPullRequest": { "pullRequest": { 608 "URL": "https://github.com/OWNER/REPO/pull/12" 609 } } } } 610 `, func(input map[string]interface{}) { 611 assert.Equal(t, "my title", input["title"].(string)) 612 assert.Equal(t, "- commit 1\n- commit 0\n\nFixes a bug and Closes an issue", input["body"].(string)) 613 })) 614 615 cs, cmdTeardown := run.Stub() 616 defer cmdTeardown(t) 617 618 cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "1234567890,commit 0\n2345678901,commit 1") 619 cs.Register(`git status --porcelain`, 0, "") 620 621 as, teardown := prompt.InitAskStubber() 622 defer teardown() 623 as.StubOne(0) // template 624 as.Stub([]*prompt.QuestionStub{ 625 { 626 Name: "Body", 627 Default: true, 628 }, 629 }) // body 630 as.Stub([]*prompt.QuestionStub{ 631 { 632 Name: "confirmation", 633 Value: 0, 634 }, 635 }) // confirm 636 637 output, err := runCommandWithRootDirOverridden(http, nil, "feature", true, `-t "my title" -H feature`, "./fixtures/repoWithNonLegacyPRTemplates") 638 require.NoError(t, err) 639 640 assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String()) 641} 642 643func TestPRCreate_metadata(t *testing.T) { 644 http := initFakeHTTP() 645 defer http.Verify(t) 646 647 http.StubRepoInfoResponse("OWNER", "REPO", "master") 648 shared.RunCommandFinder("feature", nil, nil) 649 http.Register( 650 httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`), 651 httpmock.StringResponse(` 652 { "data": { 653 "u000": { "login": "MonaLisa", "id": "MONAID" }, 654 "u001": { "login": "hubot", "id": "HUBOTID" }, 655 "repository": { 656 "l000": { "name": "bug", "id": "BUGID" }, 657 "l001": { "name": "TODO", "id": "TODOID" } 658 }, 659 "organization": { 660 "t000": { "slug": "core", "id": "COREID" }, 661 "t001": { "slug": "robots", "id": "ROBOTID" } 662 } 663 } } 664 `)) 665 http.Register( 666 httpmock.GraphQL(`query RepositoryMilestoneList\b`), 667 httpmock.StringResponse(` 668 { "data": { "repository": { "milestones": { 669 "nodes": [ 670 { "title": "GA", "id": "GAID" }, 671 { "title": "Big One.oh", "id": "BIGONEID" } 672 ], 673 "pageInfo": { "hasNextPage": false } 674 } } } } 675 `)) 676 http.Register( 677 httpmock.GraphQL(`query RepositoryProjectList\b`), 678 httpmock.StringResponse(` 679 { "data": { "repository": { "projects": { 680 "nodes": [ 681 { "name": "Cleanup", "id": "CLEANUPID" }, 682 { "name": "Roadmap", "id": "ROADMAPID" } 683 ], 684 "pageInfo": { "hasNextPage": false } 685 } } } } 686 `)) 687 http.Register( 688 httpmock.GraphQL(`query OrganizationProjectList\b`), 689 httpmock.StringResponse(` 690 { "data": { "organization": { "projects": { 691 "nodes": [], 692 "pageInfo": { "hasNextPage": false } 693 } } } } 694 `)) 695 http.Register( 696 httpmock.GraphQL(`mutation PullRequestCreate\b`), 697 httpmock.GraphQLMutation(` 698 { "data": { "createPullRequest": { "pullRequest": { 699 "id": "NEWPULLID", 700 "URL": "https://github.com/OWNER/REPO/pull/12" 701 } } } } 702 `, func(inputs map[string]interface{}) { 703 assert.Equal(t, "TITLE", inputs["title"]) 704 assert.Equal(t, "BODY", inputs["body"]) 705 if v, ok := inputs["assigneeIds"]; ok { 706 t.Errorf("did not expect assigneeIds: %v", v) 707 } 708 if v, ok := inputs["userIds"]; ok { 709 t.Errorf("did not expect userIds: %v", v) 710 } 711 })) 712 http.Register( 713 httpmock.GraphQL(`mutation PullRequestCreateMetadata\b`), 714 httpmock.GraphQLMutation(` 715 { "data": { "updatePullRequest": { 716 "clientMutationId": "" 717 } } } 718 `, func(inputs map[string]interface{}) { 719 assert.Equal(t, "NEWPULLID", inputs["pullRequestId"]) 720 assert.Equal(t, []interface{}{"MONAID"}, inputs["assigneeIds"]) 721 assert.Equal(t, []interface{}{"BUGID", "TODOID"}, inputs["labelIds"]) 722 assert.Equal(t, []interface{}{"ROADMAPID"}, inputs["projectIds"]) 723 assert.Equal(t, "BIGONEID", inputs["milestoneId"]) 724 })) 725 http.Register( 726 httpmock.GraphQL(`mutation PullRequestCreateRequestReviews\b`), 727 httpmock.GraphQLMutation(` 728 { "data": { "requestReviews": { 729 "clientMutationId": "" 730 } } } 731 `, func(inputs map[string]interface{}) { 732 assert.Equal(t, "NEWPULLID", inputs["pullRequestId"]) 733 assert.Equal(t, []interface{}{"HUBOTID", "MONAID"}, inputs["userIds"]) 734 assert.Equal(t, []interface{}{"COREID", "ROBOTID"}, inputs["teamIds"]) 735 assert.Equal(t, true, inputs["union"]) 736 })) 737 738 output, err := runCommand(http, nil, "feature", true, `-t TITLE -b BODY -H feature -a monalisa -l bug -l todo -p roadmap -m 'big one.oh' -r hubot -r monalisa -r /core -r /robots`) 739 assert.NoError(t, err) 740 741 assert.Equal(t, "https://github.com/OWNER/REPO/pull/12\n", output.String()) 742} 743 744func TestPRCreate_alreadyExists(t *testing.T) { 745 http := initFakeHTTP() 746 defer http.Verify(t) 747 748 http.StubRepoInfoResponse("OWNER", "REPO", "master") 749 shared.RunCommandFinder("feature", &api.PullRequest{URL: "https://github.com/OWNER/REPO/pull/123"}, ghrepo.New("OWNER", "REPO")) 750 751 _, err := runCommand(http, nil, "feature", true, `-t title -b body -H feature`) 752 assert.EqualError(t, err, "a pull request for branch \"feature\" into branch \"master\" already exists:\nhttps://github.com/OWNER/REPO/pull/123") 753} 754 755func TestPRCreate_web(t *testing.T) { 756 http := initFakeHTTP() 757 defer http.Verify(t) 758 759 http.StubRepoInfoResponse("OWNER", "REPO", "master") 760 http.StubRepoResponse("OWNER", "REPO") 761 http.Register( 762 httpmock.GraphQL(`query UserCurrent\b`), 763 httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) 764 765 cs, cmdTeardown := run.Stub() 766 defer cmdTeardown(t) 767 768 cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") 769 cs.Register(`git status --porcelain`, 0, "") 770 cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") 771 cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") 772 cs.Register(`git push --set-upstream origin HEAD:feature`, 0, "") 773 774 ask, cleanupAsk := prompt.InitAskStubber() 775 defer cleanupAsk() 776 ask.StubOne(0) 777 778 output, err := runCommand(http, nil, "feature", true, `--web`) 779 require.NoError(t, err) 780 781 assert.Equal(t, "", output.String()) 782 assert.Equal(t, "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n", output.Stderr()) 783 assert.Equal(t, "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1", output.BrowsedURL) 784} 785 786func TestPRCreate_webLongURL(t *testing.T) { 787 longBodyFile := filepath.Join(t.TempDir(), "long-body.txt") 788 err := ioutil.WriteFile(longBodyFile, make([]byte, 9216), 0600) 789 require.NoError(t, err) 790 791 http := initFakeHTTP() 792 defer http.Verify(t) 793 794 http.StubRepoInfoResponse("OWNER", "REPO", "master") 795 796 cs, cmdTeardown := run.Stub() 797 defer cmdTeardown(t) 798 799 cs.Register(`git status --porcelain`, 0, "") 800 cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") 801 802 _, err = runCommand(http, nil, "feature", false, fmt.Sprintf("--body-file '%s' --web --head=feature", longBodyFile)) 803 require.EqualError(t, err, "cannot open in browser: maximum URL length exceeded") 804} 805 806func TestPRCreate_webProject(t *testing.T) { 807 http := initFakeHTTP() 808 defer http.Verify(t) 809 810 http.StubRepoInfoResponse("OWNER", "REPO", "master") 811 http.StubRepoResponse("OWNER", "REPO") 812 http.Register( 813 httpmock.GraphQL(`query UserCurrent\b`), 814 httpmock.StringResponse(`{"data": {"viewer": {"login": "OWNER"} } }`)) 815 http.Register( 816 httpmock.GraphQL(`query RepositoryProjectList\b`), 817 httpmock.StringResponse(` 818 { "data": { "repository": { "projects": { 819 "nodes": [ 820 { "name": "Cleanup", "id": "CLEANUPID", "resourcePath": "/OWNER/REPO/projects/1" } 821 ], 822 "pageInfo": { "hasNextPage": false } 823 } } } } 824 `)) 825 http.Register( 826 httpmock.GraphQL(`query OrganizationProjectList\b`), 827 httpmock.StringResponse(` 828 { "data": { "organization": { "projects": { 829 "nodes": [ 830 { "name": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/1" } 831 ], 832 "pageInfo": { "hasNextPage": false } 833 } } } } 834 `)) 835 836 cs, cmdTeardown := run.Stub() 837 defer cmdTeardown(t) 838 839 cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") 840 cs.Register(`git status --porcelain`, 0, "") 841 cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") 842 cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") 843 cs.Register(`git push --set-upstream origin HEAD:feature`, 0, "") 844 845 ask, cleanupAsk := prompt.InitAskStubber() 846 defer cleanupAsk() 847 ask.StubOne(0) 848 849 output, err := runCommand(http, nil, "feature", true, `--web -p Triage`) 850 require.NoError(t, err) 851 852 assert.Equal(t, "", output.String()) 853 assert.Equal(t, "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n", output.Stderr()) 854 assert.Equal(t, "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1&projects=ORG%2F1", output.BrowsedURL) 855} 856 857func Test_determineTrackingBranch_empty(t *testing.T) { 858 cs, cmdTeardown := run.Stub() 859 defer cmdTeardown(t) 860 861 cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") 862 cs.Register(`git show-ref --verify -- HEAD`, 0, "abc HEAD") 863 864 remotes := context.Remotes{} 865 866 ref := determineTrackingBranch(remotes, "feature") 867 if ref != nil { 868 t.Errorf("expected nil result, got %v", ref) 869 } 870} 871 872func Test_determineTrackingBranch_noMatch(t *testing.T) { 873 cs, cmdTeardown := run.Stub() 874 defer cmdTeardown(t) 875 876 cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") 877 cs.Register("git show-ref --verify -- HEAD refs/remotes/origin/feature refs/remotes/upstream/feature", 0, "abc HEAD\nbca refs/remotes/origin/feature") 878 879 remotes := context.Remotes{ 880 &context.Remote{ 881 Remote: &git.Remote{Name: "origin"}, 882 Repo: ghrepo.New("hubot", "Spoon-Knife"), 883 }, 884 &context.Remote{ 885 Remote: &git.Remote{Name: "upstream"}, 886 Repo: ghrepo.New("octocat", "Spoon-Knife"), 887 }, 888 } 889 890 ref := determineTrackingBranch(remotes, "feature") 891 if ref != nil { 892 t.Errorf("expected nil result, got %v", ref) 893 } 894} 895 896func Test_determineTrackingBranch_hasMatch(t *testing.T) { 897 cs, cmdTeardown := run.Stub() 898 defer cmdTeardown(t) 899 900 cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") 901 cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature refs/remotes/upstream/feature$`, 0, heredoc.Doc(` 902 deadbeef HEAD 903 deadb00f refs/remotes/origin/feature 904 deadbeef refs/remotes/upstream/feature 905 `)) 906 907 remotes := context.Remotes{ 908 &context.Remote{ 909 Remote: &git.Remote{Name: "origin"}, 910 Repo: ghrepo.New("hubot", "Spoon-Knife"), 911 }, 912 &context.Remote{ 913 Remote: &git.Remote{Name: "upstream"}, 914 Repo: ghrepo.New("octocat", "Spoon-Knife"), 915 }, 916 } 917 918 ref := determineTrackingBranch(remotes, "feature") 919 if ref == nil { 920 t.Fatal("expected result, got nil") 921 } 922 923 assert.Equal(t, "upstream", ref.RemoteName) 924 assert.Equal(t, "feature", ref.BranchName) 925} 926 927func Test_determineTrackingBranch_respectTrackingConfig(t *testing.T) { 928 cs, cmdTeardown := run.Stub() 929 defer cmdTeardown(t) 930 931 cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, heredoc.Doc(` 932 branch.feature.remote origin 933 branch.feature.merge refs/heads/great-feat 934 `)) 935 cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/great-feat refs/remotes/origin/feature$`, 0, heredoc.Doc(` 936 deadbeef HEAD 937 deadb00f refs/remotes/origin/feature 938 `)) 939 940 remotes := context.Remotes{ 941 &context.Remote{ 942 Remote: &git.Remote{Name: "origin"}, 943 Repo: ghrepo.New("hubot", "Spoon-Knife"), 944 }, 945 } 946 947 ref := determineTrackingBranch(remotes, "feature") 948 if ref != nil { 949 t.Errorf("expected nil result, got %v", ref) 950 } 951} 952 953func Test_generateCompareURL(t *testing.T) { 954 tests := []struct { 955 name string 956 ctx CreateContext 957 state prShared.IssueMetadataState 958 want string 959 wantErr bool 960 }{ 961 { 962 name: "basic", 963 ctx: CreateContext{ 964 BaseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"), 965 BaseBranch: "main", 966 HeadBranchLabel: "feature", 967 }, 968 want: "https://github.com/OWNER/REPO/compare/main...feature?body=&expand=1", 969 wantErr: false, 970 }, 971 { 972 name: "with labels", 973 ctx: CreateContext{ 974 BaseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"), 975 BaseBranch: "a", 976 HeadBranchLabel: "b", 977 }, 978 state: prShared.IssueMetadataState{ 979 Labels: []string{"one", "two three"}, 980 }, 981 want: "https://github.com/OWNER/REPO/compare/a...b?body=&expand=1&labels=one%2Ctwo+three", 982 wantErr: false, 983 }, 984 { 985 name: "complex branch names", 986 ctx: CreateContext{ 987 BaseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"), 988 BaseBranch: "main/trunk", 989 HeadBranchLabel: "owner:feature", 990 }, 991 want: "https://github.com/OWNER/REPO/compare/main%2Ftrunk...owner%3Afeature?body=&expand=1", 992 wantErr: false, 993 }, 994 } 995 for _, tt := range tests { 996 t.Run(tt.name, func(t *testing.T) { 997 got, err := generateCompareURL(tt.ctx, tt.state) 998 if (err != nil) != tt.wantErr { 999 t.Errorf("generateCompareURL() error = %v, wantErr %v", err, tt.wantErr) 1000 return 1001 } 1002 if got != tt.want { 1003 t.Errorf("generateCompareURL() = %v, want %v", got, tt.want) 1004 } 1005 }) 1006 } 1007} 1008