@@ -24,6 +24,7 @@ type CloseOptions struct {
2424 IssueNumber int
2525 Comment string
2626 Reason string
27+ DuplicateOf string
2728
2829 Detector fd.Detector
2930}
@@ -55,6 +56,13 @@ func NewCmdClose(f *cmdutil.Factory, runF func(*CloseOptions) error) *cobra.Comm
5556 }
5657
5758 opts .IssueNumber = issueNumber
59+ if opts .DuplicateOf != "" {
60+ if opts .Reason == "" {
61+ opts .Reason = "duplicate"
62+ } else if opts .Reason != "duplicate" {
63+ return cmdutil .FlagErrorf ("`--duplicate-of` can only be used with `--reason duplicate`" )
64+ }
65+ }
5866
5967 if runF != nil {
6068 return runF (opts )
@@ -64,13 +72,22 @@ func NewCmdClose(f *cmdutil.Factory, runF func(*CloseOptions) error) *cobra.Comm
6472 }
6573
6674 cmd .Flags ().StringVarP (& opts .Comment , "comment" , "c" , "" , "Leave a closing comment" )
67- cmdutil .StringEnumFlag (cmd , & opts .Reason , "reason" , "r" , "" , []string {"completed" , "not planned" }, "Reason for closing" )
75+ cmdutil .StringEnumFlag (cmd , & opts .Reason , "reason" , "r" , "" , []string {"completed" , "not planned" , "duplicate" }, "Reason for closing" )
76+ cmd .Flags ().StringVar (& opts .DuplicateOf , "duplicate-of" , "" , "Mark as duplicate of another issue by number or URL" )
6877
6978 return cmd
7079}
7180
7281func closeRun (opts * CloseOptions ) error {
7382 cs := opts .IO .ColorScheme ()
83+ closeReason := opts .Reason
84+ if opts .DuplicateOf != "" {
85+ if closeReason == "" {
86+ closeReason = "duplicate"
87+ } else if closeReason != "duplicate" {
88+ return cmdutil .FlagErrorf ("`--duplicate-of` can only be used with `--reason duplicate`" )
89+ }
90+ }
7491
7592 httpClient , err := opts .HttpClient ()
7693 if err != nil {
@@ -92,6 +109,32 @@ func closeRun(opts *CloseOptions) error {
92109 return nil
93110 }
94111
112+ var duplicateIssueID string
113+ if opts .DuplicateOf != "" {
114+ if issue .IsPullRequest () {
115+ return cmdutil .FlagErrorf ("`--duplicate-of` is only supported for issues" )
116+ }
117+ duplicateIssueNumber , duplicateRepo , err := shared .ParseIssueFromArg (opts .DuplicateOf )
118+ if err != nil {
119+ return cmdutil .FlagErrorf ("invalid value for `--duplicate-of`: %v" , err )
120+ }
121+ duplicateIssueRepo := baseRepo
122+ if parsedRepo , present := duplicateRepo .Value (); present {
123+ duplicateIssueRepo = parsedRepo
124+ }
125+ if ghrepo .IsSame (baseRepo , duplicateIssueRepo ) && issue .Number == duplicateIssueNumber {
126+ return cmdutil .FlagErrorf ("`--duplicate-of` cannot reference the current issue" )
127+ }
128+ duplicateIssue , err := shared .FindIssueOrPR (httpClient , duplicateIssueRepo , duplicateIssueNumber , []string {"id" })
129+ if err != nil {
130+ return err
131+ }
132+ if duplicateIssue .IsPullRequest () {
133+ return cmdutil .FlagErrorf ("`--duplicate-of` must reference an issue" )
134+ }
135+ duplicateIssueID = duplicateIssue .ID
136+ }
137+
95138 if opts .Comment != "" {
96139 commentOpts := & prShared.CommentableOptions {
97140 Body : opts .Comment ,
@@ -108,7 +151,7 @@ func closeRun(opts *CloseOptions) error {
108151 }
109152 }
110153
111- err = apiClose (httpClient , baseRepo , issue , opts .Detector , opts . Reason )
154+ err = apiClose (httpClient , baseRepo , issue , opts .Detector , closeReason , duplicateIssueID )
112155 if err != nil {
113156 return err
114157 }
@@ -118,12 +161,12 @@ func closeRun(opts *CloseOptions) error {
118161 return nil
119162}
120163
121- func apiClose (httpClient * http.Client , repo ghrepo.Interface , issue * api.Issue , detector fd.Detector , reason string ) error {
164+ func apiClose (httpClient * http.Client , repo ghrepo.Interface , issue * api.Issue , detector fd.Detector , reason string , duplicateIssueID string ) error {
122165 if issue .IsPullRequest () {
123166 return api .PullRequestClose (httpClient , repo , issue .ID )
124167 }
125168
126- if reason != "" {
169+ if reason != "" || duplicateIssueID != "" {
127170 if detector == nil {
128171 cachedClient := api .NewCachedHTTPClient (httpClient , time .Hour * 24 )
129172 detector = fd .NewDetector (cachedClient , repo .RepoHost ())
@@ -135,6 +178,15 @@ func apiClose(httpClient *http.Client, repo ghrepo.Interface, issue *api.Issue,
135178 // TODO stateReasonCleanup
136179 if ! features .StateReason {
137180 // If StateReason is not supported silently close issue without setting StateReason.
181+ if duplicateIssueID != "" {
182+ return fmt .Errorf ("closing as duplicate is not supported on %s" , repo .RepoHost ())
183+ }
184+ reason = ""
185+ } else if reason == "duplicate" && ! features .StateReasonDuplicate {
186+ if duplicateIssueID != "" {
187+ return fmt .Errorf ("closing as duplicate is not supported on %s" , repo .RepoHost ())
188+ }
189+ // If DUPLICATE is not supported silently close issue without setting StateReason.
138190 reason = ""
139191 }
140192 }
@@ -144,6 +196,8 @@ func apiClose(httpClient *http.Client, repo ghrepo.Interface, issue *api.Issue,
144196 // If no reason is specified do not set it.
145197 case "not planned" :
146198 reason = "NOT_PLANNED"
199+ case "duplicate" :
200+ reason = "DUPLICATE"
147201 default :
148202 reason = "COMPLETED"
149203 }
@@ -158,8 +212,9 @@ func apiClose(httpClient *http.Client, repo ghrepo.Interface, issue *api.Issue,
158212
159213 variables := map [string ]interface {}{
160214 "input" : CloseIssueInput {
161- IssueID : issue .ID ,
162- StateReason : reason ,
215+ IssueID : issue .ID ,
216+ StateReason : reason ,
217+ DuplicateIssueID : duplicateIssueID ,
163218 },
164219 }
165220
@@ -168,6 +223,7 @@ func apiClose(httpClient *http.Client, repo ghrepo.Interface, issue *api.Issue,
168223}
169224
170225type CloseIssueInput struct {
171- IssueID string `json:"issueId"`
172- StateReason string `json:"stateReason,omitempty"`
226+ IssueID string `json:"issueId"`
227+ StateReason string `json:"stateReason,omitempty"`
228+ DuplicateIssueID string `json:"duplicateIssueId,omitempty"`
173229}
0 commit comments