Sfoglia il codice sorgente

Merge branch 'master' into avoid-empty-lines-repetitions

apiraino 4 anni fa
parent
commit
d4a0d6117f
12 ha cambiato i file con 830 aggiunte e 66 eliminazioni
  1. 155 46
      Cargo.lock
  2. 1 0
      Cargo.toml
  3. 46 0
      src/changelogs/mod.rs
  4. 151 0
      src/changelogs/rustc.rs
  5. 12 0
      src/config.rs
  6. 104 4
      src/github.rs
  7. 28 0
      src/handlers.rs
  8. 167 0
      src/handlers/github_releases.rs
  9. 93 0
      src/handlers/milestone_prs.rs
  10. 2 1
      src/handlers/notification.rs
  11. 25 0
      src/lib.rs
  12. 46 15
      src/zulip.rs

+ 155 - 46
Cargo.lock

@@ -24,6 +24,15 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "ansi_term"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+dependencies = [
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "anyhow"
 version = "1.0.32"
@@ -177,6 +186,21 @@ dependencies = [
  "time",
 ]
 
+[[package]]
+name = "clap"
+version = "2.33.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
 [[package]]
 name = "cloudabi"
 version = "0.1.0"
@@ -186,6 +210,25 @@ dependencies = [
  "bitflags",
 ]
 
+[[package]]
+name = "comrak"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d325e4f2ffff52ca77d995bb675494d5364aa332499d5f7c7fbb28c25e671f6"
+dependencies = [
+ "clap",
+ "entities",
+ "lazy_static",
+ "pest",
+ "pest_derive",
+ "regex",
+ "shell-words",
+ "twoway",
+ "typed-arena",
+ "unicode_categories",
+ "xdg",
+]
+
 [[package]]
 name = "core-foundation"
 version = "0.7.0"
@@ -274,6 +317,12 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "entities"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
+
 [[package]]
 name = "env_logger"
 version = "0.7.1"
@@ -461,9 +510,9 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
-version = "0.1.14"
+version = "0.1.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
+checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6"
 dependencies = [
  "cfg-if",
  "libc",
@@ -527,12 +576,9 @@ dependencies = [
 
 [[package]]
 name = "hashbrown"
-version = "0.8.2"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25"
-dependencies = [
- "autocfg",
-]
+checksum = "00d63df3d41950fb462ed38308eea019113ad1508da725bbedcd0fa5a85ef5f7"
 
 [[package]]
 name = "hermit-abi"
@@ -681,9 +727,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "1.5.2"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e47a3566dd4fd4eec714ae6ceabdee0caec795be835c223d92c2d40f1e8cf1c"
+checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2"
 dependencies = [
  "autocfg",
  "hashbrown",
@@ -719,9 +765,9 @@ checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
 
 [[package]]
 name = "js-sys"
-version = "0.3.44"
+version = "0.3.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85a7e2c92a4804dd459b86c339278d0fe87cf93757fae222c3fa3ae75458bc73"
+checksum = "ca059e81d9486668f12d455a4ea6daa600bd408134cd17e3d3fb5a32d1f016f8"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -750,9 +796,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "libc"
-version = "0.2.76"
+version = "0.2.77"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "755456fae044e6fa1ebbbd1b3e902ae19e73097ed4ed87bb79934a867c007bc3"
+checksum = "f2f96b10ec2560088a8e76961b00d47107b3a625fecb76dedb29ee7ccbf98235"
 
 [[package]]
 name = "lock_api"
@@ -814,11 +860,12 @@ dependencies = [
 
 [[package]]
 name = "miniz_oxide"
-version = "0.4.1"
+version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d7559a8a40d0f97e1edea3220f698f78b1c5ab67532e49f68fde3910323b722"
+checksum = "c60c0dfe32c10b43a144bad8fc83538c52f58302c92300ea7ec7bf7b38d5a7b9"
 dependencies = [
  "adler",
+ "autocfg",
 ]
 
 [[package]]
@@ -883,9 +930,9 @@ dependencies = [
 
 [[package]]
 name = "net2"
-version = "0.2.34"
+version = "0.2.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2ba7c918ac76704fb42afcbbb43891e72731f3dcca3bef2a19786297baf14af7"
+checksum = "3ebc3ec692ed7c9a255596c67808dee269f64655d8baf7b4f0638e51ba1d6853"
 dependencies = [
  "cfg-if",
  "libc",
@@ -1202,9 +1249,9 @@ checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.19"
+version = "1.0.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12"
+checksum = "36e28516df94f3dd551a587da5357459d9b36d945a7c37c3557928c1c2ff2a2c"
 dependencies = [
  "unicode-xid",
 ]
@@ -1349,7 +1396,7 @@ dependencies = [
 [[package]]
 name = "rust_team_data"
 version = "1.0.0"
-source = "git+https://github.com/rust-lang/team#a56af9b9cf1d49718fe088ae2e33f95952ba974e"
+source = "git+https://github.com/rust-lang/team#0c25c992de7f5e1f9bcfd1768b8c1d151d6412e5"
 dependencies = [
  "indexmap",
  "serde",
@@ -1417,18 +1464,18 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.115"
+version = "1.0.116"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5"
+checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.115"
+version = "1.0.116"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48"
+checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1492,6 +1539,12 @@ dependencies = [
  "opaque-debug 0.3.0",
 ]
 
+[[package]]
+name = "shell-words"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fa3938c99da4914afedd13bf3d79bcb6c277d1b2c398d23257a304d9e1b074"
+
 [[package]]
 name = "siphasher"
 version = "0.3.3"
@@ -1534,9 +1587,9 @@ dependencies = [
 
 [[package]]
 name = "socket2"
-version = "0.3.12"
+version = "0.3.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918"
+checksum = "b1fa70dc5c8104ec096f4fe7ede7a221d35ae13dcd19ba1ad9a81d2cab9a1c44"
 dependencies = [
  "cfg-if",
  "libc",
@@ -1554,17 +1607,23 @@ dependencies = [
  "unicode-normalization",
 ]
 
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
 [[package]]
 name = "subtle"
-version = "2.2.3"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "502d53007c02d7605a05df1c1a73ee436952781653da5d0bf57ad608f66932c1"
+checksum = "343f3f510c2915908f155e94f17220b19ccfacf2a64a2a5d8004f2c3e311e7fd"
 
 [[package]]
 name = "syn"
-version = "1.0.39"
+version = "1.0.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "891d8d6567fe7c7f8835a3a98af4208f3846fba258c1bc3c31d6e506239f11f9"
+checksum = "963f7d3cc59b59b9325165add223142bbf1df27655d07789f109896d353d8350"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1609,6 +1668,15 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
 [[package]]
 name = "thread_local"
 version = "1.0.1"
@@ -1741,9 +1809,9 @@ dependencies = [
 
 [[package]]
 name = "tracing-core"
-version = "0.1.15"
+version = "0.1.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f0e00789804e99b20f12bc7003ca416309d28a6f495d6af58d1e2c2842461b5"
+checksum = "5bcf46c1f1f06aeea2d6b81f3c863d0930a596c86ad1920d4e5bad6dd1d7119a"
 dependencies = [
  "lazy_static",
 ]
@@ -1755,6 +1823,7 @@ dependencies = [
  "anyhow",
  "async-trait",
  "chrono",
+ "comrak",
  "dotenv",
  "env_logger",
  "futures",
@@ -1789,6 +1858,22 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
 
+[[package]]
+name = "twoway"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b40075910de3a912adbd80b5d8bad6ad10a23eeb1f5bf9d4006839e899ba5bc"
+dependencies = [
+ "memchr",
+ "unchecked-index",
+]
+
+[[package]]
+name = "typed-arena"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d"
+
 [[package]]
 name = "typenum"
 version = "1.12.0"
@@ -1801,6 +1886,12 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
 
+[[package]]
+name = "unchecked-index"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
+
 [[package]]
 name = "unicase"
 version = "2.6.0"
@@ -1840,6 +1931,12 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
 
+[[package]]
+name = "unicode_categories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
 [[package]]
 name = "url"
 version = "2.1.1"
@@ -1867,6 +1964,12 @@ version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c"
 
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
 [[package]]
 name = "version_check"
 version = "0.9.2"
@@ -1908,9 +2011,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.67"
+version = "0.2.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0563a9a4b071746dd5aedbc3a28c6fe9be4586fb3fbadb67c400d4f53c6b16c"
+checksum = "1ac64ead5ea5f05873d7c12b545865ca2b8d28adfc50a49b84770a3a97265d42"
 dependencies = [
  "cfg-if",
  "serde",
@@ -1920,9 +2023,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.67"
+version = "0.2.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc71e4c5efa60fb9e74160e89b93353bc24059999c0ae0fb03affc39770310b0"
+checksum = "f22b422e2a757c35a73774860af8e112bff612ce6cb604224e8e47641a9e4f68"
 dependencies = [
  "bumpalo",
  "lazy_static",
@@ -1935,9 +2038,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.17"
+version = "0.4.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95f8d235a77f880bcef268d379810ea6c0af2eacfa90b1ad5af731776e0c4699"
+checksum = "b7866cab0aa01de1edf8b5d7936938a7e397ee50ce24119aef3e1eaa3b6171da"
 dependencies = [
  "cfg-if",
  "js-sys",
@@ -1947,9 +2050,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.67"
+version = "0.2.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97c57cefa5fa80e2ba15641578b44d36e7a64279bc5ed43c6dbaf329457a2ed2"
+checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -1957,9 +2060,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.67"
+version = "0.2.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "841a6d1c35c6f596ccea1f82504a192a60378f64b3bb0261904ad8f2f5657556"
+checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1970,15 +2073,15 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.67"
+version = "0.2.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93b162580e34310e5931c4b792560108b10fd14d64915d7fff8ff00180e70092"
+checksum = "1d649a3145108d7d3fbcde896a468d1bd636791823c9921135218ad89be08307"
 
 [[package]]
 name = "web-sys"
-version = "0.3.44"
+version = "0.3.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dda38f4e5ca63eda02c059d243aa25b5f35ab98451e518c51612cd0f1bd19a47"
+checksum = "4bf6ef87ad7ae8008e15a355ce696bed26012b7caa21605188cfd8214ab51e2d"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -2045,3 +2148,9 @@ dependencies = [
  "winapi 0.2.8",
  "winapi-build",
 ]
+
+[[package]]
+name = "xdg"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57"

+ 1 - 0
Cargo.toml

@@ -34,6 +34,7 @@ postgres-native-tls = "0.3"
 native-tls = "0.2"
 serde_path_to_error = "0.1.2"
 octocrab = "0.5"
+comrak = "0.8.2"
 
 [dependencies.serde]
 version = "1"

+ 46 - 0
src/changelogs/mod.rs

@@ -0,0 +1,46 @@
+mod rustc;
+
+use comrak::{nodes::AstNode, Arena, ComrakOptions, ComrakRenderOptions};
+use std::collections::HashMap;
+
+#[derive(Copy, Clone, PartialEq, Eq, Debug, serde::Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub(crate) enum ChangelogFormat {
+    Rustc,
+}
+
+pub(crate) struct Changelog {
+    versions: HashMap<String, String>,
+}
+
+impl Changelog {
+    pub(crate) fn parse(format: ChangelogFormat, content: &str) -> anyhow::Result<Self> {
+        match format {
+            ChangelogFormat::Rustc => rustc::RustcFormat::new(&Arena::new()).parse(content),
+        }
+    }
+
+    pub(crate) fn version(&self, version: &str) -> Option<&str> {
+        self.versions.get(version).map(|s| s.as_str())
+    }
+}
+
+fn render_for_github_releases<'a>(document: &'a AstNode<'a>) -> anyhow::Result<String> {
+    let mut content = Vec::new();
+    comrak::format_commonmark(
+        document,
+        &ComrakOptions {
+            render: ComrakRenderOptions {
+                // Prevent column width line breaks from appearing in the generated release
+                // notes. GitHub Releases insert <br>s for every line break in the markdown,
+                // mangling the output.
+                width: std::usize::MAX,
+
+                ..ComrakRenderOptions::default()
+            },
+            ..ComrakOptions::default()
+        },
+        &mut content,
+    )?;
+    Ok(String::from_utf8(content)?)
+}

+ 151 - 0
src/changelogs/rustc.rs

@@ -0,0 +1,151 @@
+use super::Changelog;
+use comrak::{
+    nodes::{Ast, AstNode, NodeHeading, NodeValue},
+    Arena, ComrakOptions,
+};
+use std::cell::RefCell;
+use std::collections::HashMap;
+
+pub(super) struct RustcFormat<'a> {
+    arena: &'a Arena<AstNode<'a>>,
+    current_h1: Option<String>,
+    result: Changelog,
+}
+
+impl<'a> RustcFormat<'a> {
+    pub(super) fn new(arena: &'a Arena<AstNode<'a>>) -> Self {
+        RustcFormat {
+            arena,
+            current_h1: None,
+            result: Changelog {
+                versions: HashMap::new(),
+            },
+        }
+    }
+
+    pub(super) fn parse(mut self, content: &str) -> anyhow::Result<Changelog> {
+        let ast = comrak::parse_document(&self.arena, &content, &ComrakOptions::default());
+
+        let mut section_ast = Vec::new();
+        for child in ast.children() {
+            let child_data = child.data.borrow();
+
+            if let NodeValue::Heading(NodeHeading { level: 1, .. }) = child_data.value {
+                if let Some(h1) = self.current_h1.take() {
+                    self.store_version(h1, section_ast)?;
+                }
+
+                self.current_h1 = Some(String::from_utf8(child_data.content.clone())?);
+                section_ast = Vec::new();
+            } else {
+                section_ast.push(child);
+            }
+        }
+        if let Some(h1) = self.current_h1.take() {
+            self.store_version(h1, section_ast)?;
+        }
+
+        Ok(self.result)
+    }
+
+    fn store_version(&mut self, h1: String, body: Vec<&'a AstNode<'a>>) -> anyhow::Result<()> {
+        // Create a document with only the contents of this section
+        let document = self
+            .arena
+            .alloc(AstNode::new(RefCell::new(Ast::new(NodeValue::Document))));
+        for child in &body {
+            document.append(child);
+        }
+
+        let content = super::render_for_github_releases(document)?;
+
+        if let Some(version) = h1.split(' ').nth(1) {
+            self.result.versions.insert(version.to_string(), content);
+        } else {
+            println!("skipped version, invalid header: {}", h1);
+        }
+
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    const CONTENT: &str = "\
+Version 1.45.2 (2020-08-03)
+==========================
+
+* [Fix bindings in tuple struct patterns][74954]
+* [Link in another section][69033]
+* Very very very very very very very very very very very long line that has some
+  linebreaks here and there
+
+[74954]: https://github.com/rust-lang/rust/issues/74954
+
+Version 1.45.1 (2020-07-30)
+==========================
+
+* [Fix const propagation with references.][73613]
+* [rustfmt accepts rustfmt_skip in cfg_attr again.][73078]
+
+[73613]: https://github.com/rust-lang/rust/pull/73613
+[73078]: https://github.com/rust-lang/rust/issues/73078
+
+Version 1.44.0 (2020-06-04)
+==========================
+
+Language
+--------
+- [You can now use `async/.await` with `#[no_std]` enabled.][69033]
+
+**Syntax-only changes**
+
+- [Expansion-driven outline module parsing][69838]
+```rust
+#[cfg(FALSE)]
+mod foo {
+    mod bar {
+        mod baz; // `foo/bar/baz.rs` doesn't exist, but no error!
+    }
+}
+```
+
+These are still rejected semantically, so you will likely receive an error but
+these changes can be seen and parsed by macros and conditional compilation.
+
+Internal Only
+-------------
+These changes provide no direct user facing benefits, but represent significant
+improvements to the internals and overall performance of rustc and
+related tools.
+
+- [dep_graph Avoid allocating a set on when the number reads are small.][69778]
+
+[69033]: https://github.com/rust-lang/rust/pull/69033/
+[69838]: https://github.com/rust-lang/rust/pull/69838/
+[69778]: https://github.com/rust-lang/rust/pull/69778/
+";
+
+    const EXPECTED_1_45_2: &str = "\
+- [Fix bindings in tuple struct patterns](https://github.com/rust-lang/rust/issues/74954)
+- [Link in another section](https://github.com/rust-lang/rust/pull/69033/)
+- Very very very very very very very very very very very long line that has some linebreaks here and there
+";
+
+    #[test]
+    fn test_changelog_parsing() -> anyhow::Result<()> {
+        let arena = Arena::new();
+        let parsed = RustcFormat::new(&arena).parse(CONTENT)?;
+
+        // Ensure the right markdown is generated from each version
+        let version_1_45_2 = parsed.version("1.45.2").expect("missing version 1.45.2");
+        assert_eq!(EXPECTED_1_45_2, version_1_45_2);
+
+        let version_1_44_0 = parsed.version("1.44.0").expect("missing version 1.44.0");
+        assert!(version_1_44_0.contains("Avoid allocating a set"));
+
+        Ok(())
+    }
+}

+ 12 - 0
src/config.rs

@@ -1,3 +1,4 @@
+use crate::changelogs::ChangelogFormat;
 use crate::github::GithubClient;
 use std::collections::{HashMap, HashSet};
 use std::fmt;
@@ -25,6 +26,7 @@ pub(crate) struct Config {
     pub(crate) glacier: Option<GlacierConfig>,
     pub(crate) autolabel: Option<AutolabelConfig>,
     pub(crate) notify_zulip: Option<NotifyZulipConfig>,
+    pub(crate) github_releases: Option<GitHubReleasesConfig>,
 }
 
 #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
@@ -150,6 +152,15 @@ pub(crate) async fn get(gh: &GithubClient, repo: &str) -> Result<Arc<Config>, Co
     }
 }
 
+#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub(crate) struct GitHubReleasesConfig {
+    pub(crate) format: ChangelogFormat,
+    pub(crate) project_name: String,
+    pub(crate) changelog_path: String,
+    pub(crate) changelog_branch: String,
+}
+
 fn get_cached_config(repo: &str) -> Option<Result<Arc<Config>, ConfigurationError>> {
     let cache = CONFIG_CACHE.read().unwrap();
     cache.get(repo).and_then(|(config, fetch_time)| {
@@ -272,6 +283,7 @@ mod tests {
                 glacier: None,
                 autolabel: None,
                 notify_zulip: None,
+                github_releases: None,
             }
         );
     }

+ 104 - 4
src/github.rs

@@ -244,12 +244,16 @@ pub struct Issue {
     pub number: u64,
     pub body: String,
     created_at: chrono::DateTime<Utc>,
+    #[serde(default)]
+    pub merge_commit_sha: Option<String>,
     pub title: String,
     pub html_url: String,
     pub user: User,
     pub labels: Vec<Label>,
     pub assignees: Vec<User>,
     pub pull_request: Option<PullRequestDetails>,
+    #[serde(default)]
+    pub merged: bool,
     // API URL
     comments_url: String,
     #[serde(skip)]
@@ -543,6 +547,63 @@ impl Issue {
             .await?;
         Ok(())
     }
+
+    pub async fn set_milestone(&self, client: &GithubClient, title: &str) -> anyhow::Result<()> {
+        log::trace!(
+            "Setting milestone for rust-lang/rust#{} to {}",
+            self.number,
+            title
+        );
+
+        let create_url = format!("{}/milestones", self.repository().url());
+        let resp = client
+            .send_req(
+                client
+                    .post(&create_url)
+                    .body(serde_json::to_vec(&MilestoneCreateBody { title }).unwrap()),
+            )
+            .await;
+        // Explicitly do *not* try to return Err(...) if this fails -- that's
+        // fine, it just means the milestone was already created.
+        log::trace!("Created milestone: {:?}", resp);
+
+        let list_url = format!("{}/milestones", self.repository().url());
+        let milestone_list: Vec<Milestone> = client.json(client.get(&list_url)).await?;
+        let milestone_no = if let Some(milestone) = milestone_list.iter().find(|v| v.title == title)
+        {
+            milestone.number
+        } else {
+            anyhow::bail!(
+                "Despite just creating milestone {} on {}, it does not exist?",
+                title,
+                self.repository()
+            )
+        };
+
+        #[derive(serde::Serialize)]
+        struct SetMilestone {
+            milestone: u64,
+        }
+        let url = format!("{}/issues/{}", self.repository().url(), self.number);
+        client
+            ._send_req(client.patch(&url).json(&SetMilestone {
+                milestone: milestone_no,
+            }))
+            .await
+            .context("failed to set milestone")?;
+        Ok(())
+    }
+}
+
+#[derive(serde::Serialize)]
+struct MilestoneCreateBody<'a> {
+    title: &'a str,
+}
+
+#[derive(Debug, serde::Deserialize)]
+pub struct Milestone {
+    number: u64,
+    title: String,
 }
 
 #[derive(Debug, serde::Deserialize)]
@@ -600,7 +661,7 @@ pub struct IssueCommentEvent {
 }
 
 #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
-#[serde(rename_all = "lowercase")]
+#[serde(rename_all = "snake_case")]
 pub enum IssuesAction {
     Opened,
     Edited,
@@ -622,6 +683,7 @@ pub enum IssuesAction {
     ReviewRequestRemoved,
     ReadyForReview,
     Synchronize,
+    ConvertedToDraft,
 }
 
 #[derive(Debug, serde::Deserialize)]
@@ -785,61 +847,99 @@ pub enum QueryKind {
     Count,
 }
 
+#[derive(Debug, serde::Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum CreateKind {
+    Branch,
+    Tag,
+}
+
+#[derive(Debug, serde::Deserialize)]
+pub struct CreateEvent {
+    pub ref_type: CreateKind,
+    repository: Repository,
+    sender: User,
+}
+
+#[derive(Debug, serde::Deserialize)]
+pub struct PushEvent {
+    #[serde(rename = "ref")]
+    pub git_ref: String,
+    repository: Repository,
+    sender: User,
+}
+
 #[derive(Debug)]
 pub enum Event {
+    Create(CreateEvent),
     IssueComment(IssueCommentEvent),
     Issue(IssuesEvent),
+    Push(PushEvent),
 }
 
 impl Event {
     pub fn repo_name(&self) -> &str {
         match self {
+            Event::Create(event) => &event.repository.full_name,
             Event::IssueComment(event) => &event.repository.full_name,
             Event::Issue(event) => &event.repository.full_name,
+            Event::Push(event) => &event.repository.full_name,
         }
     }
 
     pub fn issue(&self) -> Option<&Issue> {
         match self {
+            Event::Create(_) => None,
             Event::IssueComment(event) => Some(&event.issue),
             Event::Issue(event) => Some(&event.issue),
+            Event::Push(_) => None,
         }
     }
 
     /// This will both extract from IssueComment events but also Issue events
     pub fn comment_body(&self) -> Option<&str> {
         match self {
+            Event::Create(_) => None,
             Event::Issue(e) => Some(&e.issue.body),
             Event::IssueComment(e) => Some(&e.comment.body),
+            Event::Push(_) => None,
         }
     }
 
     /// This will both extract from IssueComment events but also Issue events
     pub fn comment_from(&self) -> Option<&str> {
         match self {
+            Event::Create(_) => None,
             Event::Issue(e) => Some(&e.changes.as_ref()?.body.from),
             Event::IssueComment(e) => Some(&e.changes.as_ref()?.body.from),
+            Event::Push(_) => None,
         }
     }
 
     pub fn html_url(&self) -> Option<&str> {
         match self {
+            Event::Create(_) => None,
             Event::Issue(e) => Some(&e.issue.html_url),
             Event::IssueComment(e) => Some(&e.comment.html_url),
+            Event::Push(_) => None,
         }
     }
 
     pub fn user(&self) -> &User {
         match self {
+            Event::Create(e) => &e.sender,
             Event::Issue(e) => &e.issue.user,
             Event::IssueComment(e) => &e.comment.user,
+            Event::Push(e) => &e.sender,
         }
     }
 
-    pub fn time(&self) -> chrono::DateTime<FixedOffset> {
+    pub fn time(&self) -> Option<chrono::DateTime<FixedOffset>> {
         match self {
-            Event::Issue(e) => e.issue.created_at.into(),
-            Event::IssueComment(e) => e.comment.updated_at.into(),
+            Event::Create(_) => None,
+            Event::Issue(e) => Some(e.issue.created_at.into()),
+            Event::IssueComment(e) => Some(e.comment.updated_at.into()),
+            Event::Push(_) => None,
         }
     }
 }

+ 28 - 0
src/handlers.rs

@@ -25,8 +25,10 @@ impl fmt::Display for HandlerError {
 
 mod assign;
 mod autolabel;
+mod github_releases;
 mod glacier;
 mod major_change;
+mod milestone_prs;
 mod nominate;
 mod notification;
 mod notify_zulip;
@@ -63,6 +65,28 @@ pub async fn handle(ctx: &Context, event: &Event) -> Vec<HandlerError> {
         );
     }
 
+    if let Err(e) = milestone_prs::handle(ctx, event).await {
+        log::error!(
+            "failed to process event {:?} with milestone_prs handler: {:?}",
+            event,
+            e
+        );
+    }
+
+    if let Some(ghr_config) = config
+        .as_ref()
+        .ok()
+        .and_then(|c| c.github_releases.as_ref())
+    {
+        if let Err(e) = github_releases::handle(ctx, event, ghr_config).await {
+            log::error!(
+                "failed to process event {:?} with github_releases handler: {:?}",
+                event,
+                e
+            );
+        }
+    }
+
     errors
 }
 
@@ -125,6 +149,10 @@ macro_rules! command_handlers {
                     log::debug!("skipping event, comment was {:?}", e.action);
                     return;
                 }
+                Event::Push(_) | Event::Create(_) => {
+                    log::debug!("skipping unsupported event");
+                    return;
+                }
             }
 
             let input = Input::new(&body, &ctx.username);

+ 167 - 0
src/handlers/github_releases.rs

@@ -0,0 +1,167 @@
+use crate::{
+    changelogs::Changelog,
+    config::GitHubReleasesConfig,
+    github::{CreateEvent, CreateKind, Event},
+    handlers::Context,
+};
+use anyhow::Context as _;
+use octocrab::Page;
+use std::{collections::HashMap, time::Duration};
+
+pub(super) async fn handle(
+    ctx: &Context,
+    event: &Event,
+    config: &GitHubReleasesConfig,
+) -> anyhow::Result<()> {
+    // Only allow commit pushed to the changelog branch or tags being created.
+    match event {
+        Event::Push(push) if push.git_ref == format!("refs/heads/{}", config.changelog_branch) => {}
+        Event::Create(CreateEvent {
+            ref_type: CreateKind::Tag,
+            ..
+        }) => {}
+        _ => return Ok(()),
+    }
+
+    log::info!("handling github releases");
+
+    log::debug!("loading the changelog");
+    let content = load_changelog(ctx, event, config).await.with_context(|| {
+        format!(
+            "failed to load changelog file {} from repo {} in branch {}",
+            config.changelog_path,
+            event.repo_name(),
+            config.changelog_branch
+        )
+    })?;
+    let changelog = Changelog::parse(config.format, &content)?;
+
+    log::debug!("loading the git tags");
+    let tags = load_paginated(
+        ctx,
+        &format!("repos/{}/git/matching-refs/tags", event.repo_name()),
+        |git_ref: &GitRef| {
+            git_ref
+                .name
+                .strip_prefix("refs/tags/")
+                .unwrap_or(git_ref.name.as_str())
+                .to_string()
+        },
+    )
+    .await?;
+
+    log::debug!("loading the existing releases");
+    let releases = load_paginated(
+        ctx,
+        &format!("repos/{}/releases", event.repo_name()),
+        |release: &Release| release.tag_name.clone(),
+    )
+    .await?;
+
+    for tag in tags.keys() {
+        if let Some(expected_body) = changelog.version(tag) {
+            let expected_name = format!("{} {}", config.project_name, tag);
+
+            if let Some(release) = releases.get(tag) {
+                if release.name != expected_name || release.body != expected_body {
+                    log::info!("updating release {} on {}", tag, event.repo_name());
+                    let _: serde_json::Value = ctx
+                        .octocrab
+                        .patch(
+                            &release.url,
+                            Some(&serde_json::json!({
+                                "name": expected_name,
+                                "body": expected_body,
+                            })),
+                        )
+                        .await?;
+                } else {
+                    // Avoid waiting for the delay below.
+                    continue;
+                }
+            } else {
+                log::info!("creating release {} on {}", tag, event.repo_name());
+                let _: serde_json::Value = ctx
+                    .octocrab
+                    .post(
+                        format!("repos/{}/releases", event.repo_name()),
+                        Some(&serde_json::json!({
+                            "tag_name": tag,
+                            "name": expected_name,
+                            "body": expected_body,
+                        })),
+                    )
+                    .await?;
+            }
+
+            log::debug!("sleeping for one second to avoid hitting any rate limit");
+            tokio::time::delay_for(Duration::from_secs(1)).await;
+        } else {
+            log::trace!(
+                "skipping tag {} since it doesn't have a changelog entry",
+                tag
+            );
+        }
+    }
+
+    Ok(())
+}
+
+async fn load_changelog(
+    ctx: &Context,
+    event: &Event,
+    config: &GitHubReleasesConfig,
+) -> anyhow::Result<String> {
+    let resp = ctx
+        .github
+        .raw_file(
+            event.repo_name(),
+            &config.changelog_branch,
+            &config.changelog_path,
+        )
+        .await?
+        .ok_or_else(|| anyhow::Error::msg("missing file"))?;
+
+    Ok(String::from_utf8(resp)?)
+}
+
+async fn load_paginated<T, R, F>(ctx: &Context, url: &str, key: F) -> anyhow::Result<HashMap<R, T>>
+where
+    T: serde::de::DeserializeOwned,
+    R: Eq + PartialEq + std::hash::Hash,
+    F: Fn(&T) -> R,
+{
+    let mut current_page: Page<T> = ctx.octocrab.get::<Page<T>, _, ()>(url, None).await?;
+
+    let mut items = current_page
+        .take_items()
+        .into_iter()
+        .map(|val| (key(&val), val))
+        .collect::<HashMap<R, T>>();
+
+    while let Some(mut new_page) = ctx.octocrab.get_page::<T>(&current_page.next).await? {
+        items.extend(
+            new_page
+                .take_items()
+                .into_iter()
+                .map(|val| (key(&val), val)),
+        );
+        current_page = new_page;
+    }
+
+    Ok(items)
+}
+
+#[derive(Debug, serde::Deserialize)]
+struct GitRef {
+    #[serde(rename = "ref")]
+    name: String,
+}
+
+#[derive(Debug, serde::Deserialize)]
+struct Release {
+    url: String,
+    tag_name: String,
+    name: String,
+    body: String,
+}

+ 93 - 0
src/handlers/milestone_prs.rs

@@ -0,0 +1,93 @@
+use crate::{
+    github::{Event, IssuesAction},
+    handlers::Context,
+};
+use anyhow::Context as _;
+
+pub async fn handle(ctx: &Context, event: &Event) -> anyhow::Result<()> {
+    let e = if let Event::Issue(e) = event {
+        e
+    } else {
+        return Ok(());
+    };
+
+    // Only trigger on closed issues
+    if e.action != IssuesAction::Closed {
+        return Ok(());
+    }
+
+    let repo = e.issue.repository();
+    if repo.organization != "rust-lang" && repo.repository != "rust" {
+        return Ok(());
+    }
+
+    if !e.issue.merged {
+        log::trace!(
+            "Ignoring closing of rust-lang/rust#{}: not merged",
+            e.issue.number
+        );
+        return Ok(());
+    }
+
+    let merge_sha = if let Some(s) = &e.issue.merge_commit_sha {
+        s
+    } else {
+        log::error!(
+            "rust-lang/rust#{}: no merge_commit_sha in event",
+            e.issue.number
+        );
+        return Ok(());
+    };
+
+    // Fetch channel.rs from the upstream repository
+
+    let resp = ctx
+        .github
+        .raw()
+        .get(&format!(
+            "https://raw.githubusercontent.com/rust-lang/rust/{}/src/bootstrap/channel.rs",
+            merge_sha
+        ))
+        .send()
+        .await
+        .with_context(|| format!("retrieving channel.rs for {}", merge_sha))?;
+
+    let resp = resp
+        .text()
+        .await
+        .with_context(|| format!("deserializing text channel.rs for {}", merge_sha))?;
+
+    let prefix = r#"const CFG_RELEASE_NUM: &str = ""#;
+    let start = if let Some(idx) = resp.find(prefix) {
+        idx + prefix.len()
+    } else {
+        log::error!(
+            "No {:?} in contents of channel.rs at {:?}",
+            prefix,
+            merge_sha
+        );
+        return Ok(());
+    };
+    let after = &resp[start..];
+    let end = if let Some(idx) = after.find('"') {
+        idx
+    } else {
+        log::error!("No suffix in contents of channel.rs at {:?}", merge_sha);
+        return Ok(());
+    };
+    let version = &after[..end];
+    if !version.starts_with("1.") && version.len() < 8 {
+        log::error!("Weird version {:?} for {:?}", version, merge_sha);
+        return Ok(());
+    }
+
+    // Associate this merged PR with the version it merged into.
+    //
+    // Note that this should work for rollup-merged PRs too. It will *not*
+    // auto-update when merging a beta-backport, for example, but that seems
+    // fine; we can manually update without too much trouble in that case, and
+    // eventually automate it separately.
+    e.issue.set_milestone(&ctx.github, version).await?;
+
+    Ok(())
+}

+ 2 - 1
src/handlers/notification.rs

@@ -33,6 +33,7 @@ pub async fn handle(ctx: &Context, event: &Event) -> anyhow::Result<()> {
     let short_description = match event {
         Event::Issue(e) => e.issue.title.clone(),
         Event::IssueComment(e) => format!("Comment on {}", e.issue.title),
+        Event::Push(_) | Event::Create(_) => return Ok(()),
     };
 
     let mut caps = parser::get_mentions(body)
@@ -110,7 +111,7 @@ pub async fn handle(ctx: &Context, event: &Event) -> anyhow::Result<()> {
                     user_id: user.id.unwrap(),
                     origin_url: event.html_url().unwrap().to_owned(),
                     origin_html: body.to_owned(),
-                    time: event.time(),
+                    time: event.time().unwrap(),
                     short_description: Some(short_description.clone()),
                     team_name: team_name.clone(),
                 },

+ 25 - 0
src/lib.rs

@@ -10,6 +10,7 @@ use std::fmt;
 
 pub mod actions;
 pub mod agenda;
+mod changelogs;
 pub mod config;
 pub mod db;
 pub mod github;
@@ -29,6 +30,8 @@ pub enum EventName {
     PullRequestReviewComment,
     IssueComment,
     Issue,
+    Push,
+    Create,
     Other,
 }
 
@@ -41,6 +44,8 @@ impl std::str::FromStr for EventName {
             "issue_comment" => EventName::IssueComment,
             "pull_request" => EventName::PullRequest,
             "issues" => EventName::Issue,
+            "push" => EventName::Push,
+            "create" => EventName::Create,
             _ => EventName::Other,
         })
     }
@@ -57,6 +62,8 @@ impl fmt::Display for EventName {
                 EventName::IssueComment => "issue_comment",
                 EventName::Issue => "issues",
                 EventName::PullRequest => "pull_request",
+                EventName::Push => "push",
+                EventName::Create => "create",
                 EventName::Other => "other",
             }
         )
@@ -150,6 +157,24 @@ pub async fn webhook(
 
             github::Event::Issue(payload)
         }
+        EventName::Push => {
+            let payload = deserialize_payload::<github::PushEvent>(&payload)
+                .with_context(|| format!("{:?} failed to deserialize", event))
+                .map_err(anyhow::Error::from)?;
+
+            log::info!("handling push event {:?}", payload);
+
+            github::Event::Push(payload)
+        }
+        EventName::Create => {
+            let payload = deserialize_payload::<github::CreateEvent>(&payload)
+                .with_context(|| format!("{:?} failed to deserialize", event))
+                .map_err(anyhow::Error::from)?;
+
+            log::info!("handling create event {:?}", payload);
+
+            github::Event::Create(payload)
+        }
         // Other events need not be handled
         EventName::Other => {
             return Ok(false);

+ 46 - 15
src/zulip.rs

@@ -163,7 +163,14 @@ fn handle_command<'a>(
                     if word == "@**triagebot**" {
                         let next = words.next();
                         match next {
-                            Some("await") => return match post_waiter(&ctx, message_data).await {
+                            Some("end-topic") | Some("await") => return match post_waiter(&ctx, message_data, WaitingMessage::end_topic()).await {
+                                Ok(r) => r,
+                                Err(e) => serde_json::to_string(&Response {
+                                    content: &format!("Failed to await at this time: {:?}", e),
+                                })
+                                .unwrap(),
+                            },
+                            Some("end-meeting") => return match post_waiter(&ctx, message_data, WaitingMessage::end_meeting()).await {
                                 Ok(r) => r,
                                 Err(e) => serde_json::to_string(&Response {
                                     content: &format!("Failed to await at this time: {:?}", e),
@@ -658,7 +665,36 @@ impl<'a> AddReaction<'a> {
     }
 }
 
-async fn post_waiter(ctx: &Context, message: &Message) -> anyhow::Result<String> {
+struct WaitingMessage<'a> {
+    primary: &'a str,
+    emoji: &'a [&'a str],
+}
+
+impl WaitingMessage<'static> {
+    fn end_topic() -> Self {
+        WaitingMessage {
+            primary: "Does anyone have something to add on the current topic?\n\
+                  React with :working_on_it: if you have something to say.\n\
+                  React with :all_good: if not.",
+            emoji: &[":working_on_it:", ":all_good:"],
+        }
+    }
+
+    fn end_meeting() -> Self {
+        WaitingMessage {
+            primary: "Does anyone have something to bring up?\n\
+                  React with :working_on_it: if you have something to say.\n\
+                  React with :all_good: if you're ready to end the meeting.",
+            emoji: &[":working_on_it:", ":all_good:"],
+        }
+    }
+}
+
+async fn post_waiter(
+    ctx: &Context,
+    message: &Message,
+    waiting: WaitingMessage<'_>,
+) -> anyhow::Result<String> {
     let posted = MessageApiRequest {
         recipient: Recipient::Stream {
             id: message.stream_id.ok_or_else(|| {
@@ -668,9 +704,7 @@ async fn post_waiter(ctx: &Context, message: &Message) -> anyhow::Result<String>
                 anyhow::format_err!("private waiting not supported, missing topic")
             })?,
         },
-        content: "Does anyone have something to add on this topic, or should we move on?\n\
-                  React with :working_on_it: if you have something to say.\n\
-                  React with :all_good: if we should move on.",
+        content: waiting.primary,
     }
     .send(ctx.github.raw())
     .await?;
@@ -679,17 +713,14 @@ async fn post_waiter(ctx: &Context, message: &Message) -> anyhow::Result<String>
         .with_context(|| format!("{:?} did not deserialize as SentMessage", body))?
         .id;
 
-    let reaction_a = AddReaction {
-        message_id,
-        emoji_name: "working_on_it",
-    }
-    .send(&ctx.github.raw());
-    let reaction_b = AddReaction {
-        message_id,
-        emoji_name: "all_good",
+    for reaction in waiting.emoji {
+        AddReaction {
+            message_id,
+            emoji_name: reaction,
+        }
+        .send(&ctx.github.raw())
+        .await?;
     }
-    .send(&ctx.github.raw());
-    futures::try_join!(reaction_a, reaction_b,)?;
 
     Ok(serde_json::to_string(&ResponseNotRequired {
         response_not_required: true,