|
1 | 1 | use crate::models::{NotificationReason, NotificationType}; |
2 | 2 | use chrono::{DateTime, Utc}; |
3 | 3 | use serde::{Deserialize, Serialize}; |
| 4 | +use url::Url; |
4 | 5 |
|
5 | 6 | #[derive(Debug, Clone, Serialize, Deserialize)] |
6 | 7 | pub struct Notification { |
@@ -168,14 +169,36 @@ impl Notification { |
168 | 169 | // API: https://api.github.com/repos/owner/repo/releases/12345 |
169 | 170 | // Web: https://github.com/owner/repo/releases/tag/v1.0.0 |
170 | 171 | if let Some(remainder) = api_url.strip_prefix(&prefix) { |
171 | | - let parts: Vec<&str> = remainder.splitn(4, '/').collect(); |
172 | | - if parts.len() >= 3 && parts[2] == "releases" { |
173 | | - let (owner, repo) = (parts[0], parts[1]); |
174 | | - let tag = &self.subject.title; |
175 | | - return Some(format!( |
176 | | - "{}/{}/{}/releases/tag/{}", |
177 | | - web_base, owner, repo, tag |
178 | | - )); |
| 172 | + let mut parts = remainder.split('/'); |
| 173 | + let owner = parts.next(); |
| 174 | + let repo = parts.next(); |
| 175 | + let resource = parts.next(); |
| 176 | + let release_id = parts.next(); |
| 177 | + let trailing = parts.next(); |
| 178 | + |
| 179 | + if let (Some(owner), Some(repo), Some("releases"), Some(release_id), None) = |
| 180 | + (owner, repo, resource, release_id, trailing) |
| 181 | + { |
| 182 | + if !matches!(self.notification_type(), NotificationType::Release) |
| 183 | + || release_id.parse::<u64>().is_err() |
| 184 | + { |
| 185 | + // Not a release-by-id subject URL; continue with generic conversion below. |
| 186 | + // This avoids rewriting paths such as /releases/assets/{id}. |
| 187 | + } else { |
| 188 | + let mut release_url = Url::parse(&format!( |
| 189 | + "{}/{}/{}/releases/tag/", |
| 190 | + web_base, owner, repo |
| 191 | + )) |
| 192 | + .ok()?; |
| 193 | + |
| 194 | + { |
| 195 | + let mut path = release_url.path_segments_mut().ok()?; |
| 196 | + path.pop_if_empty(); |
| 197 | + path.push(&self.subject.title); |
| 198 | + } |
| 199 | + |
| 200 | + return Some(release_url.into()); |
| 201 | + } |
179 | 202 | } |
180 | 203 | } |
181 | 204 |
|
@@ -304,4 +327,28 @@ mod tests { |
304 | 327 | Some("https://git.example.com/org/proj/releases/tag/v2.0.0".to_string()) |
305 | 328 | ); |
306 | 329 | } |
| 330 | + |
| 331 | + #[test] |
| 332 | + fn web_url_release_encodes_tag_segment() { |
| 333 | + let n = make_notification( |
| 334 | + Some("https://api.github.com/repos/owner/repo/releases/12345"), |
| 335 | + "release/v1.2.3", |
| 336 | + ); |
| 337 | + assert_eq!( |
| 338 | + n.web_url("github.com"), |
| 339 | + Some("https://github.com/owner/repo/releases/tag/release%2Fv1.2.3".to_string()) |
| 340 | + ); |
| 341 | + } |
| 342 | + |
| 343 | + #[test] |
| 344 | + fn web_url_release_assets_not_rewritten_to_tag() { |
| 345 | + let n = make_notification( |
| 346 | + Some("https://api.github.com/repos/owner/repo/releases/assets/42"), |
| 347 | + "v1.2.3", |
| 348 | + ); |
| 349 | + assert_eq!( |
| 350 | + n.web_url("github.com"), |
| 351 | + Some("https://github.com/owner/repo/releases/assets/42".to_string()) |
| 352 | + ); |
| 353 | + } |
307 | 354 | } |
0 commit comments