├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── cargo-issue ├── Cargo.toml └── src │ ├── main.rs │ └── tests.rs ├── issue ├── Cargo.toml └── src │ └── lib.rs ├── lib ├── Cargo.toml └── src │ ├── config.rs │ └── lib.rs ├── macros ├── Cargo.toml └── src │ └── lib.rs └── tests ├── Cargo.toml ├── test.rs └── ui ├── github_active_issue.rs ├── github_closed_issue.rs ├── github_closed_issue.stderr ├── github_issue_not_found.rs └── github_issue_not_found.stderr /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Fmt 20 | run: cargo fmt --all -- --check 21 | - name: Lint 22 | run: cargo clippy --all 23 | - name: Build 24 | run: cargo build --all --verbose 25 | - name: Run tests 26 | run: cargo test --all --verbose 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .idea -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "issue", 4 | "cargo-issue", 5 | "lib", 6 | "macros", 7 | ] 8 | 9 | exclude = [ 10 | "tests", 11 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Karel Lubertus Kubat 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Issue-rs 2 | 3 | Ever added a todo based on an open issue (perhaps in one of your dependencies)? Track the issue and be warned when it is 4 | closed! 5 | 6 | 7 | ```rust 8 | // Our trait implementation never returns an error, but until the `nevertype` 9 | // is stabilized, we need to use the unit type. 10 | #[issue::track(url="https://github.com/rust-lang/rust/issues/35121")] 11 | type Result = core::result::Result; 12 | ``` 13 | 14 | Once the tracked issue is resolved, a warning will be emitted during compile time. 15 | 16 | # CI and Configuration 17 | 18 | Locally it is recommended to always run the tracked issue. Alternatively, setting the environment variable 19 | `ISSUE_RS_IGNORE` to any value will disable it entirely. 20 | 21 | For reproducible builds, set `ISSUE_RS_IGNORE` and use the [cargo-issue](https://crates.io/crates/cargo-issue) subcommand 22 | as a separate step in CI. This will still require network connectivity however. `cargo-issue` offers higher performance 23 | by concurrently tracking issues, which for large codebases, with many tracked issues, can significantly improve 24 | performance as well. 25 | 26 | ## TODO 27 | 28 | - [ ] Support Gitlab 29 | - [ ] Support arbitrary URLS/private instances 30 | - [ ] Authentication for private repos 31 | -------------------------------------------------------------------------------- /cargo-issue/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-issue" 3 | version = "0.1.4" 4 | edition = "2018" 5 | authors = ["kaiserkarel"] 6 | categories = ["development-tools", "development-tools::build-utils"] 7 | keywords = ["issue", "issue-tracking", "github", "gitlab"] 8 | readme = "../README.md" 9 | repository = "https://github.com/kaiserkarel/issue" 10 | license-file = "../LICENSE" 11 | description = "CLI for issue-rs" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | issue = { version = "0.1.4", path = "../issue" } 17 | tokio = { version = "1", features = ["full"] } 18 | structopt = { version = "0.3", default-features = false} 19 | serde = {version = "1", features = ["derive"]} 20 | serde_json = "1.0.64" 21 | tokio-stream = "0.1.7" 22 | futures = "0.3.15" 23 | reqwest = "0.11.4" 24 | anyhow = "1.0.42" 25 | 26 | [dev-dependencies] 27 | assert_cmd = "1.0.7" 28 | -------------------------------------------------------------------------------- /cargo-issue/src/main.rs: -------------------------------------------------------------------------------- 1 | use futures::stream::StreamExt; 2 | use issue::{GithubIssue, APP_USER_AGENT, CONFIG_ENV}; 3 | use serde_json::Deserializer; 4 | use std::process::{Command, Stdio}; 5 | use std::sync::atomic::{AtomicI32, Ordering}; 6 | use std::sync::Arc; 7 | use structopt::StructOpt; 8 | use tokio::time::Duration; 9 | use tokio_stream::{self as stream}; 10 | 11 | #[cfg(test)] 12 | mod tests; 13 | 14 | #[derive(StructOpt, Debug)] 15 | #[structopt( 16 | name = "issue", 17 | bin_name = "cargo", 18 | about = "Track open issues and be warned once they're closed." 19 | )] 20 | pub enum Cli { 21 | /// Track open issues and be warned once they're closed. 22 | #[structopt(name = "issue")] 23 | Issue(Cmd), 24 | } 25 | 26 | #[derive(StructOpt, Debug, Clone)] 27 | pub enum Cmd { 28 | /// Checks all tracked issues in the project. More efficient for large projects with many tracked 29 | /// issues, and the recommend way to run `issue-rs` in CI. 30 | Check { 31 | /// The cargo package to check. 32 | #[structopt(short, long)] 33 | package: Option, 34 | /// The manifest path of the cargo package check. 35 | #[structopt(short, long)] 36 | manifest_path: Option, 37 | }, 38 | /// Lists all tracked issues, regardless of their status. Useful to get an overview of the 39 | /// technical debt and dependencies on issues. 40 | #[structopt(name = "list")] 41 | List { 42 | /// The cargo package to check. 43 | #[structopt(short, long)] 44 | package: Option, 45 | /// The manifest path of the cargo package check. 46 | #[structopt(short, long)] 47 | manifest_path: Option, 48 | }, 49 | } 50 | 51 | #[tokio::main] 52 | async fn main() -> Result<(), anyhow::Error> { 53 | let Cli::Issue(opts) = Cli::from_args(); 54 | 55 | let (package, manifest_path) = match opts.clone() { 56 | Cmd::Check { 57 | package, 58 | manifest_path, 59 | } => (package, manifest_path), 60 | Cmd::List { 61 | package, 62 | manifest_path, 63 | } => (package, manifest_path), 64 | }; 65 | 66 | let cargo = std::env::var("CARGO").expect("cargo not found"); 67 | let mut command = Command::new(cargo); 68 | command.env( 69 | CONFIG_ENV, 70 | serde_json::to_string(&issue::Mode::Pipe).expect("serializing configuration"), 71 | ); 72 | command.arg("--quiet"); 73 | command.arg("check"); 74 | 75 | package 76 | .as_ref() 77 | .map(|package| command.args(["-p", package])); 78 | 79 | manifest_path 80 | .as_ref() 81 | .map(|manifest_path| command.args(["--manifest-path", manifest_path])); 82 | 83 | command.stdout(Stdio::piped()); 84 | 85 | let mut handle = command.spawn().unwrap(); 86 | let stdout = handle.stdout.as_mut().unwrap(); 87 | let stream = Deserializer::from_reader(stdout).into_iter::(); 88 | 89 | // Ensures that cargo's build output is not interleaved with the actual issues being printed. 90 | tokio::time::sleep(Duration::from_millis(100)).await; 91 | 92 | let exit_code = Arc::new(AtomicI32::new(0)); 93 | 94 | stream::iter(stream) 95 | .for_each_concurrent(None, |issue| { 96 | let opts = opts.clone(); 97 | let exit_code = exit_code.clone(); 98 | async move { 99 | let issue = issue.expect("deserializing cargo check stdout"); 100 | match opts { 101 | Cmd::List { .. } => println!("{}", &issue), 102 | Cmd::Check { .. } => { 103 | #[allow(clippy::blocks_in_if_conditions)] 104 | if is_closed(&issue).await.unwrap_or_else(|e| { 105 | eprintln!("unable to access issue: {}", e); 106 | exit_code.store(1, Ordering::SeqCst); 107 | false 108 | }) { 109 | eprintln!("Closed issue: {}", &issue.url); 110 | exit_code.store(1, Ordering::SeqCst); 111 | } 112 | } 113 | } 114 | } 115 | }) 116 | .await; 117 | 118 | handle.wait().unwrap(); 119 | 120 | if exit_code.load(Ordering::SeqCst) == 1 { 121 | anyhow::bail!("encountered errors") 122 | } 123 | Ok(()) 124 | } 125 | 126 | pub async fn is_closed(issue: &issue::Issue) -> Result { 127 | let client = reqwest::ClientBuilder::new() 128 | .user_agent(APP_USER_AGENT) 129 | .build() 130 | .unwrap(); 131 | 132 | let response = client.get(issue.canonicalize_url()).send().await?; 133 | 134 | if !response.status().is_success() { 135 | anyhow::bail!( 136 | "failed to fetch issue: {}", 137 | response 138 | .text() 139 | .await 140 | .unwrap_or_else(|e| format!("no response found: {}", e)) 141 | ) 142 | } 143 | 144 | let issue: GithubIssue = response.json().await?; 145 | 146 | Ok(issue.closed_at.is_some()) 147 | } 148 | -------------------------------------------------------------------------------- /cargo-issue/src/tests.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::Command; 2 | 3 | #[test] 4 | fn test_check() { 5 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); 6 | cmd.arg("issue"); 7 | cmd.arg("check"); 8 | cmd.args(["--manifest-path", "../tests/Cargo.toml"]); 9 | 10 | let output = cmd.assert(); 11 | output.success().stderr(""); 12 | } 13 | 14 | #[test] 15 | fn test_list() { 16 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); 17 | cmd.arg("issue"); 18 | cmd.arg("list"); 19 | cmd.args(["--manifest-path", "../tests/Cargo.toml"]); 20 | 21 | let output = cmd.assert(); 22 | output.success().stderr(""); 23 | } 24 | -------------------------------------------------------------------------------- /issue/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "issue" 3 | version = "0.1.4" 4 | edition = "2018" 5 | categories = ["development-tools", "development-tools::build-utils"] 6 | keywords = ["issue", "issue-tracking", "github", "gitlab"] 7 | readme = "../README.md" 8 | repository = "https://github.com/kaiserkarel/issue" 9 | license-file = "../LICENSE" 10 | description = "Tracks open issues during compile time and emits warnings if issues are closed" 11 | 12 | [dependencies] 13 | macros = { package = "issue-macros", version = "0.1.1", path = "../macros" } 14 | lib = { version = "0.1.2", path = "../lib", package = "cargo-issue-lib" } 15 | -------------------------------------------------------------------------------- /issue/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, clippy::pedantic, clippy::nursery, missing_docs)] 2 | 3 | //! Ever added a todo based on an open issue (perhaps in one of your dependencies)? Track the issue and be warned when it is 4 | //! closed! 5 | //! 6 | //! ```rust 7 | //! 8 | //! // Our trait implementation never returns an error, but until the `nevertype` 9 | //! // is stabilized, we need to use the unit type. 10 | //! #[issue::track(url="https://github.com/rust-lang/rust/issues/35121")] 11 | //! type Result = core::result::Result; 12 | //! ``` 13 | //! 14 | //! Once the tracked issue is resolved, a warning will be emitted during compile time. 15 | pub use ::macros::track; 16 | 17 | pub use ::lib::{GithubIssue, Issue, Level, Mode, APP_USER_AGENT, CONFIG_ENV}; 18 | -------------------------------------------------------------------------------- /lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-issue-lib" 3 | version = "0.1.4" 4 | edition = "2018" 5 | categories = ["development-tools", "development-tools::build-utils"] 6 | keywords = ["issue", "issue-tracking", "github", "gitlab"] 7 | readme = "../README.md" 8 | repository = "https://github.com/kaiserkarel/issue" 9 | license-file = "../LICENSE" 10 | description = "Tracks open issues during compile time and emits warnings if issues are closed" 11 | 12 | [dependencies] 13 | serde = {version = "1", features = ["derive"]} 14 | serde_json = "1.0.64" 15 | url = "2.2.2" 16 | reqwest = { version = "0.11.4", features = ["blocking", "rustls-tls", "json"]} 17 | anyhow = "1.0.42" 18 | itertools = "0.10.1" 19 | -------------------------------------------------------------------------------- /lib/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 4 | #[cfg_attr(structopt, derive(structopt::StructOpt))] 5 | pub enum Mode { 6 | /// Checks all tracked issues. More efficient than running the track macros during build time, 7 | /// as it can asynchronously obtain all information. 8 | Pipe, 9 | /// Runs checks during build time. `Level::Warn` corresponds to build warnings, `Level::Error` 10 | /// to build errors. 11 | Emit(Level), 12 | /// Performs no actions after parsing the attribute. 13 | Noop, 14 | } 15 | 16 | impl FromStr for Mode { 17 | type Err = &'static str; 18 | 19 | fn from_str(s: &str) -> Result { 20 | match s { 21 | "Pipe" => Ok(Mode::Pipe), 22 | "Emit(Warn)" => Ok(Mode::Emit(Level::Warn)), 23 | "Emit(Error)" => Ok(Mode::Emit(Level::Error)), 24 | _ => Err("unrecognized Mode"), 25 | } 26 | } 27 | } 28 | 29 | impl Default for Mode { 30 | fn default() -> Self { 31 | Mode::Emit(Level::Warn) 32 | } 33 | } 34 | 35 | #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] 36 | pub enum Level { 37 | Warn, 38 | Error, 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | 3 | pub use config::{Level, Mode}; 4 | use itertools::Itertools; 5 | use std::fmt::{Display, Formatter}; 6 | 7 | pub const CONFIG_ENV: &str = "ISSUE_RS::Config::Mode"; 8 | pub const IGNORE_ENV: &str = "ISSUE_RS_IGNORE"; 9 | 10 | pub fn get_mode() -> Option { 11 | if let Ok(var) = std::env::var(CONFIG_ENV) { 12 | serde_json::from_str(&var).expect("reading Mode from configuration") 13 | } else if std::env::var(IGNORE_ENV).is_ok() { 14 | Some(Mode::Noop) 15 | } else { 16 | Some(Default::default()) 17 | } 18 | } 19 | 20 | pub const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); 21 | 22 | #[derive(Debug, serde::Serialize, serde::Deserialize)] 23 | pub struct Issue { 24 | pub url: String, 25 | } 26 | 27 | impl Display for Issue { 28 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 29 | write!(f, "issue: {}", self.url) 30 | } 31 | } 32 | 33 | // Both Github and Gitlab use the `closed_at` field to identify closed issues. 34 | #[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] 35 | pub struct GithubIssue { 36 | pub closed_at: Option, 37 | } 38 | 39 | impl Issue { 40 | pub fn canonicalize_url(&self) -> url::Url { 41 | let url = url::Url::parse(&self.url).unwrap(); 42 | 43 | if url.host_str().unwrap().contains("github") { 44 | let path: String = Itertools::intersperse(url.path_segments().unwrap(), "/").collect(); 45 | return url::Url::parse(&format!("https://api.github.com/repos/{}", path)).unwrap(); 46 | } 47 | unreachable!("only github public repositories are currently supported") 48 | } 49 | 50 | pub fn is_closed(&self) -> Result { 51 | let client = reqwest::blocking::ClientBuilder::new() 52 | .user_agent(APP_USER_AGENT) 53 | .build() 54 | .unwrap(); 55 | 56 | let response = client.get(self.canonicalize_url()).send()?; 57 | 58 | if !response.status().is_success() { 59 | anyhow::bail!( 60 | "failed to fetch issue: {}", 61 | response 62 | .text() 63 | .unwrap_or_else(|e| format!("no response found: {}", e)) 64 | ) 65 | } 66 | 67 | let issue: GithubIssue = response.json()?; 68 | 69 | Ok(issue.closed_at.is_some()) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "issue-macros" 3 | version = "0.1.4" 4 | edition = "2018" 5 | license-file = "../LICENSE" 6 | description = "Procedural macro implementation for `issue`." 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | lib = { version = "0.1.2", path = "../lib", package = "cargo-issue-lib" } 15 | 16 | darling = "0.13.0" 17 | syn = "1.0.73" 18 | quote = "1.0.9" 19 | serde = { version = "1", features = ["derive"]} 20 | anyhow = "1.0.42" 21 | proc-macro-error = { version = "1", default-features = false } 22 | itertools = "0.10.1" 23 | lazy_static = "1.4.0" 24 | serde_json = "1.0.64" 25 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use darling::FromMeta; 2 | use lazy_static::lazy_static; 3 | use lib::{Level, Mode}; 4 | use proc_macro::TokenStream; 5 | use proc_macro_error::{emit_error, emit_warning, proc_macro_error}; 6 | use syn::{parse_macro_input, spanned::Spanned, AttributeArgs}; 7 | 8 | #[cfg(not(debug_assertions))] 9 | lazy_static! { 10 | static ref MODE: Mode = lib::get_mode().unwrap_or(Mode::Noop); 11 | } 12 | 13 | #[cfg(debug_assertions)] 14 | lazy_static! { 15 | static ref MODE: Mode = lib::get_mode().unwrap_or(Mode::Emit(Level::Warn)); 16 | } 17 | 18 | /// Fetches the issue from Github and emits a warning or error if it is closed depending on the 19 | /// `ISSUE_HARD_FAIL` environment variable. 20 | #[proc_macro_error] 21 | #[proc_macro_attribute] 22 | pub fn track(args: TokenStream, input: TokenStream) -> TokenStream { 23 | let attr_args = parse_macro_input!(args as AttributeArgs); 24 | 25 | let issue: lib::Issue = match Issue::from_list(&attr_args) { 26 | Ok(v) => v.into(), 27 | Err(e) => { 28 | return TokenStream::from(e.write_errors()); 29 | } 30 | }; 31 | 32 | match *MODE { 33 | Mode::Noop => (), 34 | Mode::Pipe => println!( 35 | "{}", 36 | serde_json::to_string(&issue).expect("serializing issue") 37 | ), 38 | Mode::Emit(Level::Warn) => match issue.is_closed() { 39 | Err(e) => { 40 | emit_warning!( 41 | attr_args[0].span(), 42 | "unable to access {}\n {}", 43 | issue.url, 44 | e 45 | ) 46 | } 47 | Ok(true) => { 48 | emit_warning!(attr_args[0].span(), "issue {} has been closed", issue.url) 49 | } 50 | _ => (), 51 | }, 52 | Mode::Emit(Level::Error) => match issue.is_closed() { 53 | Err(e) => { 54 | emit_error!( 55 | attr_args[0].span(), 56 | "unable to access {}\n {}", 57 | issue.url, 58 | e 59 | ) 60 | } 61 | Ok(true) => { 62 | emit_error!(attr_args[0].span(), "issue {} has been closed", issue.url) 63 | } 64 | _ => (), 65 | }, 66 | } 67 | input 68 | } 69 | 70 | #[derive(Default, Debug, FromMeta, serde::Serialize)] 71 | #[darling(default)] 72 | struct Issue { 73 | pub url: String, 74 | } 75 | 76 | impl From for lib::Issue { 77 | fn from(issue: Issue) -> Self { 78 | lib::Issue { url: issue.url } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "macros-test" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | 7 | [lib] 8 | path = "test.rs" 9 | name = "tests" 10 | 11 | [dependencies] 12 | issue = { path = "../issue" } 13 | 14 | [dev-dependencies] 15 | issue = { path = "../issue" } 16 | trybuild = "1.0.42" 17 | 18 | [features] 19 | default = [] 20 | integration = [] -------------------------------------------------------------------------------- /tests/test.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | #[test] 4 | fn test() { 5 | let t = trybuild::TestCases::new(); 6 | 7 | t.pass("ui/github_active_issue.rs"); 8 | 9 | std::env::set_var("ISSUE_RS::Config::Mode", r#"{"Emit":"Warn"}"#); 10 | t.pass("ui/github_closed_issue.rs"); 11 | t.pass("ui/github_issue_not_found.rs"); 12 | } 13 | } 14 | 15 | #[cfg(feature = "integration")] 16 | mod integration { 17 | #[issue::track(url = "https://github.com/KaiserKarel/issue/issues/1")] 18 | mod active {} 19 | 20 | #[issue::track(url = "https://github.com/KaiserKarel/issue/issues/2")] 21 | mod closed {} 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/ui/github_active_issue.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | #[issue::track(url="https://github.com/KaiserKarel/issue/issues/1")] 4 | fn main() { 5 | 6 | } -------------------------------------------------------------------------------- /tests/ui/github_closed_issue.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | #[issue::track(url="https://github.com/KaiserKarel/issue/issues/2")] 4 | fn main() { 5 | 6 | } -------------------------------------------------------------------------------- /tests/ui/github_closed_issue.stderr: -------------------------------------------------------------------------------- 1 | error: issue https://github.com/KaiserKarel/issue/issues/2 has been closed 2 | --> $DIR/github_closed_issue.rs:3:16 3 | | 4 | 3 | #[issue::track(url="https://github.com/KaiserKarel/issue/issues/2")] 5 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 6 | -------------------------------------------------------------------------------- /tests/ui/github_issue_not_found.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | #[issue::track(url="https://github.com/KaiserKarel/issue/issues/0")] 4 | fn main() { 5 | 6 | } -------------------------------------------------------------------------------- /tests/ui/github_issue_not_found.stderr: -------------------------------------------------------------------------------- 1 | warning: unable to access https://github.com/KaiserKarel/issue/issues/0 2 | failed to fetch issue: {"message":"Not Found","documentation_url":"https://docs.github.com/rest/reference/issues#get-an-issue"} 3 | --> $DIR/github_issue_not_found.rs:3:16 4 | | 5 | 3 | #[issue::track(url="https://github.com/KaiserKarel/issue/issues/0")] 6 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 7 | --------------------------------------------------------------------------------