Skip to content

Commit 49f7223

Browse files
authored
Merge pull request cli#12376 from majiayu000/fix-11903-pr-create-same-ref
fix: error when head and base refs are identical in pr create
2 parents c34c47c + 0f4b9b0 commit 49f7223

2 files changed

Lines changed: 109 additions & 0 deletions

File tree

pkg/cmd/pr/create/create.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,16 @@ func (r forkableRefs) UnqualifiedHeadRef() string {
166166
return r.qualifiedHeadRef.BranchName()
167167
}
168168

169+
// isSameRef checks if the head and base refs point to the same ref in the same repository.
170+
// For cross-repository PRs (e.g., from a fork), the qualified head ref will contain
171+
// an owner prefix (owner:branch), so even if branch names match, they refer to different repos.
172+
func isSameRef(refs creationRefs) bool {
173+
if strings.Contains(refs.QualifiedHeadRef(), ":") {
174+
return false
175+
}
176+
return refs.UnqualifiedHeadRef() == refs.BaseRef()
177+
}
178+
169179
// CreateContext stores contextual data about the creation process and is for building up enough
170180
// data to create a pull request.
171181
type CreateContext struct {
@@ -367,6 +377,10 @@ func createRun(opts *CreateOptions) error {
367377
return err
368378
}
369379

380+
if isSameRef(ctx.PRRefs) {
381+
return fmt.Errorf("head branch %q is the same as base branch %q, cannot create a pull request", ctx.PRRefs.UnqualifiedHeadRef(), ctx.PRRefs.BaseRef())
382+
}
383+
370384
httpClient, err := opts.HttpClient()
371385
if err != nil {
372386
return err

pkg/cmd/pr/create/create_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,31 @@ func Test_createRun(t *testing.T) {
384384
},
385385
expectedOut: "https://github.com/OWNER/REPO/pull/12\n",
386386
},
387+
{
388+
name: "same head and base branch should error",
389+
setup: func(opts *CreateOptions, t *testing.T) func() {
390+
opts.TitleProvided = true
391+
opts.BodyProvided = true
392+
opts.Title = "my title"
393+
opts.Body = "my body"
394+
opts.HeadBranch = "master"
395+
return func() {}
396+
},
397+
wantErr: `head branch "master" is the same as base branch "master", cannot create a pull request`,
398+
},
399+
{
400+
name: "same head and base branch with explicit base should error",
401+
setup: func(opts *CreateOptions, t *testing.T) func() {
402+
opts.TitleProvided = true
403+
opts.BodyProvided = true
404+
opts.Title = "my title"
405+
opts.Body = "my body"
406+
opts.HeadBranch = "feature"
407+
opts.BaseBranch = "feature"
408+
return func() {}
409+
},
410+
wantErr: `head branch "feature" is the same as base branch "feature", cannot create a pull request`,
411+
},
387412
{
388413
name: "dry-run-nontty-with-default-base",
389414
tty: false,
@@ -2879,3 +2904,73 @@ func TestProjectsV1Deprecation(t *testing.T) {
28792904
})
28802905
})
28812906
}
2907+
2908+
func Test_isSameRef(t *testing.T) {
2909+
tests := []struct {
2910+
name string
2911+
refs creationRefs
2912+
expected bool
2913+
}{
2914+
{
2915+
name: "same branch in same repo",
2916+
refs: skipPushRefs{
2917+
qualifiedHeadRef: shared.NewQualifiedHeadRefWithoutOwner("main"),
2918+
baseRefs: baseRefs{
2919+
baseBranchName: "main",
2920+
},
2921+
},
2922+
expected: true,
2923+
},
2924+
{
2925+
name: "different branches in same repo",
2926+
refs: skipPushRefs{
2927+
qualifiedHeadRef: shared.NewQualifiedHeadRefWithoutOwner("feature"),
2928+
baseRefs: baseRefs{
2929+
baseBranchName: "main",
2930+
},
2931+
},
2932+
expected: false,
2933+
},
2934+
{
2935+
name: "same branch name in different repos (cross-repo PR)",
2936+
refs: skipPushRefs{
2937+
qualifiedHeadRef: shared.NewQualifiedHeadRef("other-owner", "main"),
2938+
baseRefs: baseRefs{
2939+
baseBranchName: "main",
2940+
},
2941+
},
2942+
expected: false,
2943+
},
2944+
{
2945+
name: "pushableRefs same branch same repo",
2946+
refs: pushableRefs{
2947+
headRepo: ghrepo.New("OWNER", "REPO"),
2948+
headBranchName: "main",
2949+
baseRefs: baseRefs{
2950+
baseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
2951+
baseBranchName: "main",
2952+
},
2953+
},
2954+
expected: true,
2955+
},
2956+
{
2957+
name: "pushableRefs same branch different repos (fork)",
2958+
refs: pushableRefs{
2959+
headRepo: ghrepo.New("FORK-OWNER", "REPO"),
2960+
headBranchName: "main",
2961+
baseRefs: baseRefs{
2962+
baseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
2963+
baseBranchName: "main",
2964+
},
2965+
},
2966+
expected: false,
2967+
},
2968+
}
2969+
2970+
for _, tt := range tests {
2971+
t.Run(tt.name, func(t *testing.T) {
2972+
result := isSameRef(tt.refs)
2973+
assert.Equal(t, tt.expected, result)
2974+
})
2975+
}
2976+
}

0 commit comments

Comments
 (0)