@@ -426,6 +426,10 @@ impl IssueRepository {
426426 )
427427 }
428428
429+ fn full_repo_name ( & self ) -> String {
430+ format ! ( "{}/{}" , self . organization, self . repository)
431+ }
432+
429433 async fn has_label ( & self , client : & GithubClient , label : & str ) -> anyhow:: Result < bool > {
430434 #[ allow( clippy:: redundant_pattern_matching) ]
431435 let url = format ! ( "{}/labels/{}" , self . url( ) , label) ;
@@ -745,49 +749,25 @@ impl Issue {
745749 Ok ( ( ) )
746750 }
747751
752+ /// Sets the milestone of the issue or PR.
753+ ///
754+ /// This will create the milestone if it does not exist. The new milestone
755+ /// will start in the "open" state.
748756 pub async fn set_milestone ( & self , client : & GithubClient , title : & str ) -> anyhow:: Result < ( ) > {
749757 log:: trace!(
750758 "Setting milestone for rust-lang/rust#{} to {}" ,
751759 self . number,
752760 title
753761 ) ;
754762
755- let create_url = format ! ( "{}/milestones" , self . repository( ) . url( ) ) ;
756- let resp = client
757- . send_req (
758- client
759- . post ( & create_url)
760- . body ( serde_json:: to_vec ( & MilestoneCreateBody { title } ) . unwrap ( ) ) ,
761- )
762- . await ;
763- // Explicitly do *not* try to return Err(...) if this fails -- that's
764- // fine, it just means the milestone was already created.
765- log:: trace!( "Created milestone: {:?}" , resp) ;
766-
767- let list_url = format ! ( "{}/milestones" , self . repository( ) . url( ) ) ;
768- let milestone_list: Vec < Milestone > = client. json ( client. get ( & list_url) ) . await ?;
769- let milestone_no = if let Some ( milestone) = milestone_list. iter ( ) . find ( |v| v. title == title)
770- {
771- milestone. number
772- } else {
773- anyhow:: bail!(
774- "Despite just creating milestone {} on {}, it does not exist?" ,
775- title,
776- self . repository( )
777- )
778- } ;
763+ let full_repo_name = self . repository ( ) . full_repo_name ( ) ;
764+ let milestone = client
765+ . get_or_create_milestone ( & full_repo_name, title, "open" )
766+ . await ?;
779767
780- #[ derive( serde:: Serialize ) ]
781- struct SetMilestone {
782- milestone : u64 ,
783- }
784- let url = format ! ( "{}/issues/{}" , self . repository( ) . url( ) , self . number) ;
785768 client
786- . send_req ( client. patch ( & url) . json ( & SetMilestone {
787- milestone : milestone_no,
788- } ) )
789- . await
790- . context ( "failed to set milestone" ) ?;
769+ . set_milestone ( & full_repo_name, & milestone, self . number )
770+ . await ?;
791771 Ok ( ( ) )
792772 }
793773
@@ -886,11 +866,6 @@ pub struct PullRequestFile {
886866 pub blob_url : String ,
887867}
888868
889- #[ derive( serde:: Serialize ) ]
890- struct MilestoneCreateBody < ' a > {
891- title : & ' a str ,
892- }
893-
894869#[ derive( Debug , serde:: Deserialize ) ]
895870pub struct Milestone {
896871 number : u64 ,
@@ -1246,6 +1221,33 @@ impl Repository {
12461221 )
12471222 }
12481223
1224+ /// Returns a list of commits between the SHA ranges of start (exclusive)
1225+ /// and end (inclusive).
1226+ pub async fn commits_in_range (
1227+ & self ,
1228+ client : & GithubClient ,
1229+ start : & str ,
1230+ end : & str ,
1231+ ) -> anyhow:: Result < Vec < GithubCommit > > {
1232+ let mut commits = Vec :: new ( ) ;
1233+ let mut page = 1 ;
1234+ loop {
1235+ let url = format ! ( "{}/commits?sha={end}&per_page=100&page={page}" , self . url( ) ) ;
1236+ let mut this_page: Vec < GithubCommit > = client
1237+ . json ( client. get ( & url) )
1238+ . await
1239+ . with_context ( || format ! ( "failed to fetch commits for {url}" ) ) ?;
1240+ if let Some ( idx) = this_page. iter ( ) . position ( |commit| commit. sha == start) {
1241+ this_page. truncate ( idx) ;
1242+ commits. extend ( this_page) ;
1243+ return Ok ( commits) ;
1244+ } else {
1245+ commits. extend ( this_page) ;
1246+ }
1247+ page += 1 ;
1248+ }
1249+ }
1250+
12491251 /// Retrieves a git commit for the given SHA.
12501252 pub async fn git_commit ( & self , client : & GithubClient , sha : & str ) -> anyhow:: Result < GitCommit > {
12511253 let url = format ! ( "{}/git/commits/{sha}" , self . url( ) ) ;
@@ -1616,6 +1618,40 @@ impl Repository {
16161618 } ) ?;
16171619 Ok ( ( ) )
16181620 }
1621+
1622+ /// Get or create a [`Milestone`].
1623+ ///
1624+ /// This will not change the state if it already exists.
1625+ pub async fn get_or_create_milestone (
1626+ & self ,
1627+ client : & GithubClient ,
1628+ title : & str ,
1629+ state : & str ,
1630+ ) -> anyhow:: Result < Milestone > {
1631+ client
1632+ . get_or_create_milestone ( & self . full_name , title, state)
1633+ . await
1634+ }
1635+
1636+ /// Set the milestone of an issue or PR.
1637+ pub async fn set_milestone (
1638+ & self ,
1639+ client : & GithubClient ,
1640+ milestone : & Milestone ,
1641+ issue_num : u64 ,
1642+ ) -> anyhow:: Result < ( ) > {
1643+ client
1644+ . set_milestone ( & self . full_name , milestone, issue_num)
1645+ . await
1646+ }
1647+
1648+ pub async fn get_issue ( & self , client : & GithubClient , issue_num : u64 ) -> anyhow:: Result < Issue > {
1649+ let url = format ! ( "{}/pulls/{issue_num}" , self . url( ) ) ;
1650+ client
1651+ . json ( client. get ( & url) )
1652+ . await
1653+ . with_context ( || format ! ( "{} failed to get issue {issue_num}" , self . full_name) )
1654+ }
16191655}
16201656
16211657pub struct Query < ' a > {
@@ -2126,6 +2162,83 @@ impl GithubClient {
21262162 . await
21272163 . with_context ( || format ! ( "{} failed to get repo" , full_name) )
21282164 }
2165+
2166+ /// Get or create a [`Milestone`].
2167+ ///
2168+ /// This will not change the state if it already exists.
2169+ async fn get_or_create_milestone (
2170+ & self ,
2171+ full_repo_name : & str ,
2172+ title : & str ,
2173+ state : & str ,
2174+ ) -> anyhow:: Result < Milestone > {
2175+ let url = format ! (
2176+ "{}/repos/{full_repo_name}/milestones" ,
2177+ Repository :: GITHUB_API_URL
2178+ ) ;
2179+ let resp = self
2180+ . send_req ( self . post ( & url) . json ( & serde_json:: json!( {
2181+ "title" : title,
2182+ "state" : state,
2183+ } ) ) )
2184+ . await ;
2185+ match resp {
2186+ Ok ( ( body, _dbg) ) => {
2187+ let milestone = serde_json:: from_slice ( & body) ?;
2188+ log:: trace!( "Created milestone: {milestone:?}" ) ;
2189+ return Ok ( milestone) ;
2190+ }
2191+ Err ( e) => {
2192+ if e. downcast_ref :: < reqwest:: Error > ( ) . map_or ( false , |e| {
2193+ matches ! ( e. status( ) , Some ( StatusCode :: UNPROCESSABLE_ENTITY ) )
2194+ } ) {
2195+ // fall-through, it already exists
2196+ } else {
2197+ return Err ( e. context ( format ! (
2198+ "failed to create milestone {url} with title {title}"
2199+ ) ) ) ;
2200+ }
2201+ }
2202+ }
2203+ // In the case where it already exists, we need to search for its number.
2204+ let mut page = 1 ;
2205+ loop {
2206+ let url = format ! (
2207+ "{}/repos/{full_repo_name}/milestones?page={page}&state=all" ,
2208+ Repository :: GITHUB_API_URL
2209+ ) ;
2210+ let milestones: Vec < Milestone > = self
2211+ . json ( self . get ( & url) )
2212+ . await
2213+ . with_context ( || format ! ( "failed to get milestones {url} searching for {title}" ) ) ?;
2214+ if milestones. is_empty ( ) {
2215+ anyhow:: bail!( "expected to find milestone with title {title}" ) ;
2216+ }
2217+ if let Some ( milestone) = milestones. into_iter ( ) . find ( |m| m. title == title) {
2218+ return Ok ( milestone) ;
2219+ }
2220+ page += 1 ;
2221+ }
2222+ }
2223+
2224+ /// Set the milestone of an issue or PR.
2225+ async fn set_milestone (
2226+ & self ,
2227+ full_repo_name : & str ,
2228+ milestone : & Milestone ,
2229+ issue_num : u64 ,
2230+ ) -> anyhow:: Result < ( ) > {
2231+ let url = format ! (
2232+ "{}/repos/{full_repo_name}/issues/{issue_num}" ,
2233+ Repository :: GITHUB_API_URL
2234+ ) ;
2235+ self . send_req ( self . patch ( & url) . json ( & serde_json:: json!( {
2236+ "milestone" : milestone. number
2237+ } ) ) )
2238+ . await
2239+ . with_context ( || format ! ( "failed to set milestone for {url} to milestone {milestone:?}" ) ) ?;
2240+ Ok ( ( ) )
2241+ }
21292242}
21302243
21312244#[ derive( Debug , serde:: Deserialize ) ]
0 commit comments