|
| 1 | +//! Definitions of bisection bounds. |
| 2 | +
|
| 3 | +use crate::toolchains::{ |
| 4 | + download_progress, parse_to_naive_date, Toolchain, NIGHTLY_SERVER, YYYY_MM_DD, |
| 5 | +}; |
| 6 | +use crate::GitDate; |
| 7 | +use crate::Opts; |
| 8 | +use crate::{today, EPOCH_COMMIT}; |
| 9 | +use anyhow::bail; |
| 10 | +use chrono::NaiveDate; |
| 11 | +use reqwest::blocking::Client; |
| 12 | +use std::io::Read; |
| 13 | +use std::str::FromStr; |
| 14 | + |
| 15 | +/// A bisection boundary. |
| 16 | +#[derive(Clone, Debug)] |
| 17 | +pub enum Bound { |
| 18 | + Commit(String), |
| 19 | + Date(GitDate), |
| 20 | +} |
| 21 | + |
| 22 | +impl FromStr for Bound { |
| 23 | + type Err = std::convert::Infallible; |
| 24 | + fn from_str(s: &str) -> Result<Self, Self::Err> { |
| 25 | + parse_to_naive_date(s) |
| 26 | + .map(Self::Date) |
| 27 | + .or_else(|_| Ok(Self::Commit(s.to_string()))) |
| 28 | + } |
| 29 | +} |
| 30 | + |
| 31 | +impl Bound { |
| 32 | + /// Returns the SHA of this boundary. |
| 33 | + /// |
| 34 | + /// For nightlies, this will fetch from the network. |
| 35 | + pub fn sha(&self) -> anyhow::Result<String> { |
| 36 | + match self { |
| 37 | + Bound::Commit(commit) => Ok(commit.clone()), |
| 38 | + Bound::Date(date) => { |
| 39 | + let date_str = date.format(YYYY_MM_DD); |
| 40 | + let url = |
| 41 | + format!("{NIGHTLY_SERVER}/{date_str}/channel-rust-nightly-git-commit-hash.txt"); |
| 42 | + |
| 43 | + eprintln!("fetching {url}"); |
| 44 | + let client = Client::new(); |
| 45 | + let name = format!("nightly manifest {date_str}"); |
| 46 | + let mut response = download_progress(&client, &name, &url)?; |
| 47 | + let mut commit = String::new(); |
| 48 | + response.read_to_string(&mut commit)?; |
| 49 | + |
| 50 | + eprintln!("converted {date_str} to {commit}"); |
| 51 | + |
| 52 | + Ok(commit) |
| 53 | + } |
| 54 | + } |
| 55 | + } |
| 56 | +} |
| 57 | + |
| 58 | +/// The starting bisection bounds. |
| 59 | +pub enum Bounds { |
| 60 | + /// Indicates to search backwards from the given date to find the start |
| 61 | + /// date where the regression does not occur. |
| 62 | + SearchNightlyBackwards { end: GitDate }, |
| 63 | + /// Search between two commits. |
| 64 | + Commits { start: String, end: String }, |
| 65 | + /// Search between two dates. |
| 66 | + Dates { start: GitDate, end: GitDate }, |
| 67 | +} |
| 68 | + |
| 69 | +impl Bounds { |
| 70 | + pub fn from_args(args: &Opts) -> anyhow::Result<Bounds> { |
| 71 | + let (start, end) = translate_tags(&args)?; |
| 72 | + let today = today(); |
| 73 | + let check_in_future = |which, date: &NaiveDate| -> anyhow::Result<()> { |
| 74 | + if date > &today { |
| 75 | + bail!( |
| 76 | + "{which} date should be on or before current date, \ |
| 77 | + got {which} date request: {date} and current date is {today}" |
| 78 | + ); |
| 79 | + } |
| 80 | + Ok(()) |
| 81 | + }; |
| 82 | + let bounds = match (start, end) { |
| 83 | + // Neither --start or --end specified. |
| 84 | + (None, None) => Bounds::SearchNightlyBackwards { |
| 85 | + end: installed_nightly_or_latest()?, |
| 86 | + }, |
| 87 | + |
| 88 | + // --start or --end is a commit |
| 89 | + (Some(Bound::Commit(start)), Some(Bound::Commit(end))) => { |
| 90 | + Bounds::Commits { start, end } |
| 91 | + } |
| 92 | + (Some(Bound::Commit(start)), None) => Bounds::Commits { |
| 93 | + start, |
| 94 | + end: "origin/master".to_string(), |
| 95 | + }, |
| 96 | + (None, Some(Bound::Commit(end))) => Bounds::Commits { |
| 97 | + start: EPOCH_COMMIT.to_string(), |
| 98 | + end, |
| 99 | + }, |
| 100 | + |
| 101 | + // --start or --end is a date |
| 102 | + (Some(Bound::Date(start)), Some(Bound::Date(end))) => { |
| 103 | + check_in_future("start", &start)?; |
| 104 | + check_in_future("end", &end)?; |
| 105 | + Bounds::Dates { start, end } |
| 106 | + } |
| 107 | + (Some(Bound::Date(start)), None) => { |
| 108 | + check_in_future("start", &start)?; |
| 109 | + Bounds::Dates { |
| 110 | + start, |
| 111 | + end: find_latest_nightly()?, |
| 112 | + } |
| 113 | + } |
| 114 | + (None, Some(Bound::Date(end))) => { |
| 115 | + check_in_future("end", &end)?; |
| 116 | + if args.by_commit { |
| 117 | + bail!("--by-commit with an end date requires --start to be specified"); |
| 118 | + } |
| 119 | + Bounds::SearchNightlyBackwards { end } |
| 120 | + } |
| 121 | + |
| 122 | + // Mixed not supported. |
| 123 | + (Some(Bound::Commit(_)), Some(Bound::Date(_))) |
| 124 | + | (Some(Bound::Date(_)), Some(Bound::Commit(_))) => bail!( |
| 125 | + "cannot take different types of bounds for start/end, \ |
| 126 | + got start: {:?} and end {:?}", |
| 127 | + args.start, |
| 128 | + args.end |
| 129 | + ), |
| 130 | + }; |
| 131 | + if let Bounds::Dates { start, end } = &bounds { |
| 132 | + if end < start { |
| 133 | + bail!("end should be after start, got start: {start} and end {end}"); |
| 134 | + } |
| 135 | + if args.by_commit { |
| 136 | + eprintln!("finding commit range that corresponds to dates specified"); |
| 137 | + let bounds = Bounds::Commits { |
| 138 | + start: date_to_sha(&start)?, |
| 139 | + end: date_to_sha(&end)?, |
| 140 | + }; |
| 141 | + return Ok(bounds); |
| 142 | + } |
| 143 | + } |
| 144 | + Ok(bounds) |
| 145 | + } |
| 146 | +} |
| 147 | + |
| 148 | +/// Translates a tag-like bound (such as `1.62.0`) to a `Bound::Date` so that |
| 149 | +/// bisecting works for versions older than 167 days. |
| 150 | +fn translate_tags(args: &Opts) -> anyhow::Result<(Option<Bound>, Option<Bound>)> { |
| 151 | + let is_tag = |bound: &Option<Bound>| -> bool { |
| 152 | + match bound { |
| 153 | + Some(Bound::Commit(commit)) => commit.contains('.'), |
| 154 | + None | Some(Bound::Date(_)) => false, |
| 155 | + } |
| 156 | + }; |
| 157 | + let is_datelike = |bound: &Option<Bound>| -> bool { |
| 158 | + matches!(bound, None | Some(Bound::Date(_))) || is_tag(bound) |
| 159 | + }; |
| 160 | + if !(is_datelike(&args.start) && is_datelike(&args.end)) { |
| 161 | + // If the user specified an actual commit for one bound, then don't |
| 162 | + // even try to convert the other bound to a date. |
| 163 | + return Ok((args.start.clone(), args.end.clone())); |
| 164 | + } |
| 165 | + let fixup = |which: &str, bound: &Option<Bound>| -> anyhow::Result<Option<Bound>> { |
| 166 | + if is_tag(bound) { |
| 167 | + if let Some(Bound::Commit(tag)) = bound { |
| 168 | + let date = args |
| 169 | + .access |
| 170 | + .repo() |
| 171 | + .bound_to_date(Bound::Commit(tag.clone()))?; |
| 172 | + eprintln!( |
| 173 | + "translating --{which}={tag} to {date}", |
| 174 | + date = date.format(YYYY_MM_DD) |
| 175 | + ); |
| 176 | + return Ok(Some(Bound::Date(date))); |
| 177 | + } |
| 178 | + } |
| 179 | + Ok(bound.clone()) |
| 180 | + }; |
| 181 | + Ok((fixup("start", &args.start)?, fixup("end", &args.end)?)) |
| 182 | +} |
| 183 | + |
| 184 | +/// Returns the commit SHA of the nightly associated with the given date. |
| 185 | +fn date_to_sha(date: &NaiveDate) -> anyhow::Result<String> { |
| 186 | + let date_str = date.format(YYYY_MM_DD); |
| 187 | + let url = format!("{NIGHTLY_SERVER}/{date_str}/channel-rust-nightly-git-commit-hash.txt"); |
| 188 | + |
| 189 | + eprintln!("fetching {url}"); |
| 190 | + let client = Client::new(); |
| 191 | + let name = format!("nightly manifest {date_str}"); |
| 192 | + let mut response = download_progress(&client, &name, &url)?; |
| 193 | + let mut commit = String::new(); |
| 194 | + response.read_to_string(&mut commit)?; |
| 195 | + |
| 196 | + eprintln!("converted {date_str} to {commit}"); |
| 197 | + |
| 198 | + Ok(commit) |
| 199 | +} |
| 200 | + |
| 201 | +/// Returns the date of the nightly toolchain currently installed. If no |
| 202 | +/// nightly is found, then it goes to the network to determine the date of the |
| 203 | +/// latest nightly. |
| 204 | +fn installed_nightly_or_latest() -> anyhow::Result<GitDate> { |
| 205 | + if let Some(date) = Toolchain::default_nightly() { |
| 206 | + return Ok(date); |
| 207 | + } |
| 208 | + find_latest_nightly() |
| 209 | +} |
| 210 | + |
| 211 | +/// Returns the date of the latest nightly (fetched from the network). |
| 212 | +fn find_latest_nightly() -> anyhow::Result<GitDate> { |
| 213 | + let url = format!("{NIGHTLY_SERVER}/channel-rust-nightly.toml"); |
| 214 | + eprintln!("fetching {url}"); |
| 215 | + let client = Client::new(); |
| 216 | + let mut response = download_progress(&client, "nightly manifest", &url)?; |
| 217 | + let mut manifest = String::new(); |
| 218 | + response.read_to_string(&mut manifest)?; |
| 219 | + let manifest: toml::Value = toml::from_str(&manifest)?; |
| 220 | + let date = manifest["date"].as_str().expect("date is a string"); |
| 221 | + let date = NaiveDate::parse_from_str(date, "%Y-%m-%d")?; |
| 222 | + eprintln!("determined the latest nightly is {date}"); |
| 223 | + Ok(date) |
| 224 | +} |
0 commit comments