| 180 | 180 | | print_list(&ctx.out, &items) |
| 181 | 181 | | } |
| 182 | 182 | | |
| 183 | + | /// What a linked repository that is gone, with no path kept, is called (the web's `DELETED_REPO`). |
| 184 | + | const DELETED_REPO: &str = "deleted repository"; |
| 185 | + | |
| 186 | + | /// A linked repository's path, or "deleted repository" when it was deleted before work items and kept none. |
| 187 | + | fn repo_path(repo: &Value) -> &str { |
| 188 | + | repo["path"].as_str().filter(|path| !path.is_empty()).unwrap_or(DELETED_REPO) |
| 189 | + | } |
| 190 | + | |
| 183 | 191 | | /// A work item's repositories, short: the first, and `+2` for the rest. |
| 184 | 192 | | fn repos_cell(item: &Value) -> String { |
| 185 | | − | let paths: Vec<&str> = item["repos"].as_array().into_iter().flatten().filter_map(|r| r["path"].as_str()).collect(); |
| 193 | + | let paths: Vec<&str> = item["repos"].as_array().into_iter().flatten().map(repo_path).collect(); |
| 186 | 194 | | match paths.as_slice() { |
| 187 | 195 | | [] => String::new(), |
| 188 | 196 | | [one] => (*one).to_owned(), |
| ⋯ |
| 349 | 357 | | .into_iter() |
| 350 | 358 | | .flatten() |
| 351 | 359 | | .filter(|r| r["role"] == "target" && r["landed"] != true) |
| 352 | | − | .filter_map(|r| r["path"].as_str().map(str::to_owned)) |
| 360 | + | .map(|r| repo_path(r).to_owned()) |
| 353 | 361 | | .collect() |
| 354 | 362 | | } |
| 355 | 363 | | |
| ⋯ |
| 541 | 549 | | for repo in repos { |
| 542 | 550 | | let landed = if repo["landed"] == true { format!(" {}", out.green("landed")) } else { String::new() }; |
| 543 | 551 | | let legacy = repo["legacy_number"].as_u64().map(|n| out.dim(&format!(" (was #{n} there)"))).unwrap_or_default(); |
| 544 | | − | println!(" {} {}{landed}{legacy}", s(repo, "path"), out.dim(s(repo, "role"))); |
| 552 | + | let path = repo_path(repo); |
| 553 | + | let path = if path == DELETED_REPO { out.dim(path) } else { path.to_owned() }; |
| 554 | + | println!(" {path} {}{landed}{legacy}", out.dim(s(repo, "role"))); |
| 545 | 555 | | for pull in list_of(repo, "pulls") { |
| 546 | 556 | | println!(" {}", pull_line(out, pull)); |
| 547 | 557 | | for overlap in list_of(pull, "overlaps") { |
| ⋯ |
| 1203 | 1213 | | } |
| 1204 | 1214 | | |
| 1205 | 1215 | | #[test] |
| 1216 | + | fn a_repository_deleted_before_work_items_is_named_so() { |
| 1217 | + | // Its link kept no path: the column says what it was, not nothing. |
| 1218 | + | let item = json!({ "repos": [{ "path": "", "role": "origin" }] }); |
| 1219 | + | assert_eq!(repos_cell(&item), "deleted repository"); |
| 1220 | + | let item = json!({ "repos": [{ "path": null, "role": "origin" }, { "path": "acme/api", "role": "target" }] }); |
| 1221 | + | assert_eq!(repos_cell(&item), "deleted repository +1"); |
| 1222 | + | assert_eq!(repo_path(&json!({ "path": "acme/web" })), "acme/web"); |
| 1223 | + | } |
| 1224 | + | |
| 1225 | + | #[test] |
| 1206 | 1226 | | fn blocking_adds_and_unblocking_removes() { |
| 1207 | 1227 | | assert_eq!(blocked_set(12, &[9], &[10, 9], true).unwrap(), vec![9, 10]); |
| 1208 | 1228 | | assert_eq!(blocked_set(12, &[9, 10], &[9], false).unwrap(), vec![10]); |