|
|
… |
17 unchanged lines |
| 18 |
18 |
|
let (owner, name) = (parts.next().unwrap_or(""), parts.next().unwrap_or("")); |
| 19 |
19 |
|
return Ok((url.to_string(), format!("{base}/{owner}/{name}"))); |
| 20 |
20 |
|
} |
| 21 |
|
- |
let (repo_url, number) = url.rsplit_once("/proposal/").ok_or_else(|| anyhow!("{url} is not a proposal URL (…/OWNER/NAME/proposal/N)"))?; |
| 22 |
|
- |
let (base, slug) = repo_url.rsplit_once('/').and_then(|(b, n)| b.rsplit_once('/').map(|(bb, o)| (bb, format!("{o}/{n}")))).ok_or_else(|| anyhow!("{url} is not a proposal URL"))?; |
| 23 |
|
- |
Ok((format!("{base}/api/v1/repos/{slug}/proposals/{number}"), repo_url.to_string())) |
|
21 |
+ |
let (repo_url, number) = url |
|
22 |
+ |
.rsplit_once("/proposal/") |
|
23 |
+ |
.ok_or_else(|| anyhow!("{url} is not a proposal URL (…/OWNER/NAME/proposal/N)"))?; |
|
24 |
+ |
let (base, slug) = repo_url |
|
25 |
+ |
.rsplit_once('/') |
|
26 |
+ |
.and_then(|(b, n)| b.rsplit_once('/').map(|(bb, o)| (bb, format!("{o}/{n}")))) |
|
27 |
+ |
.ok_or_else(|| anyhow!("{url} is not a proposal URL"))?; |
|
28 |
+ |
Ok(( |
|
29 |
+ |
format!("{base}/api/v1/repos/{slug}/proposals/{number}"), |
|
30 |
+ |
repo_url.to_string(), |
|
31 |
+ |
)) |
| 24 |
32 |
|
} |
| 25 |
33 |
|
|
| 26 |
34 |
|
pub fn fetch(repo: &Repo, url: &str) -> Result<(Value, String)> { |
|
|
… |
5 unchanged lines |
| 32 |
40 |
|
} |
| 33 |
41 |
|
|
| 34 |
42 |
|
fn hashes(v: &Value, key: &str) -> Vec<ChangeHash> { |
| 35 |
|
- |
v.get(key).and_then(Value::as_array).map(|a| a.iter().filter_map(|x| x.as_str().and_then(ChangeHash::from_hex)).collect()).unwrap_or_default() |
|
43 |
+ |
v.get(key) |
|
44 |
+ |
.and_then(Value::as_array) |
|
45 |
+ |
.map(|a| { |
|
46 |
+ |
a.iter() |
|
47 |
+ |
.filter_map(|x| x.as_str().and_then(ChangeHash::from_hex)) |
|
48 |
+ |
.collect() |
|
49 |
+ |
}) |
|
50 |
+ |
.unwrap_or_default() |
| 36 |
51 |
|
} |
| 37 |
52 |
|
|
| 38 |
53 |
|
fn change_hashes(v: &Value) -> Vec<ChangeHash> { |
| 39 |
|
- |
v.get("changes").and_then(Value::as_array).map(|a| a.iter().filter_map(|c| c.get("hash").and_then(Value::as_str).and_then(ChangeHash::from_hex)).collect()).unwrap_or_default() |
|
54 |
+ |
v.get("changes") |
|
55 |
+ |
.and_then(Value::as_array) |
|
56 |
+ |
.map(|a| { |
|
57 |
+ |
a.iter() |
|
58 |
+ |
.filter_map(|c| { |
|
59 |
+ |
c.get("hash") |
|
60 |
+ |
.and_then(Value::as_str) |
|
61 |
+ |
.and_then(ChangeHash::from_hex) |
|
62 |
+ |
}) |
|
63 |
+ |
.collect() |
|
64 |
+ |
}) |
|
65 |
+ |
.unwrap_or_default() |
| 40 |
66 |
|
} |
| 41 |
67 |
|
|
| 42 |
68 |
|
fn text<'a>(v: &'a Value, key: &str) -> &'a str { |
|
|
… |
7 unchanged lines |
| 50 |
76 |
|
println!("{v}"); |
| 51 |
77 |
|
return Ok(0); |
| 52 |
78 |
|
} |
| 53 |
|
- |
println!("{} #{} {}", ui::done("Proposal"), v.get("number").and_then(Value::as_i64).unwrap_or(0), ui::bookmark(text(&v, "title"))); |
|
79 |
+ |
println!( |
|
80 |
+ |
"{} #{} {}", |
|
81 |
+ |
ui::done("Proposal"), |
|
82 |
+ |
v.get("number").and_then(Value::as_i64).unwrap_or(0), |
|
83 |
+ |
ui::bookmark(text(&v, "title")) |
|
84 |
+ |
); |
| 54 |
85 |
|
println!("{}", ui::kv("state", text(&v, "state"))); |
| 55 |
86 |
|
println!("{}", ui::kv("target", ui::bookmark(text(&v, "target")))); |
| 56 |
87 |
|
println!("{}", ui::kv("author", ui::author(text(&v, "author")))); |
| 57 |
|
- |
let changes = v.get("changes").and_then(Value::as_array).cloned().unwrap_or_default(); |
|
88 |
+ |
let changes = v |
|
89 |
+ |
.get("changes") |
|
90 |
+ |
.and_then(Value::as_array) |
|
91 |
+ |
.cloned() |
|
92 |
+ |
.unwrap_or_default(); |
| 58 |
93 |
|
println!("{}", ui::kv("changes", changes.len().to_string())); |
| 59 |
94 |
|
for c in &changes { |
| 60 |
|
- |
println!(" {} {}", ui::hash(&text(c, "hash")[..12.min(text(c, "hash").len())]), text(c, "message").lines().next().unwrap_or("")); |
|
95 |
+ |
println!( |
|
96 |
+ |
" {} {}", |
|
97 |
+ |
ui::hash(&text(c, "hash")[..12.min(text(c, "hash").len())]), |
|
98 |
+ |
text(c, "message").lines().next().unwrap_or("") |
|
99 |
+ |
); |
| 61 |
100 |
|
} |
| 62 |
|
- |
if v.get("conflicted").and_then(Value::as_bool).unwrap_or(false) { |
|
101 |
+ |
if v.get("conflicted") |
|
102 |
+ |
.and_then(Value::as_bool) |
|
103 |
+ |
.unwrap_or(false) |
|
104 |
+ |
{ |
| 63 |
105 |
|
println!("{}", ui::warn("landing would carry conflicts")); |
| 64 |
106 |
|
} |
| 65 |
107 |
|
if let Some(blockers) = v.get("blockers").and_then(Value::as_array) { |
|
|
… |
1 unchanged lines |
| 67 |
109 |
|
println!(" {} {}", ui::warn("blocked:"), b.as_str().unwrap_or("")); |
| 68 |
110 |
|
} |
| 69 |
111 |
|
} |
| 70 |
|
- |
let approvals = v.get("approvals").and_then(Value::as_array).cloned().unwrap_or_default(); |
|
112 |
+ |
let approvals = v |
|
113 |
+ |
.get("approvals") |
|
114 |
+ |
.and_then(Value::as_array) |
|
115 |
+ |
.cloned() |
|
116 |
+ |
.unwrap_or_default(); |
| 71 |
117 |
|
for a in &approvals { |
| 72 |
|
- |
let mark = if a.get("sealed").and_then(Value::as_bool).unwrap_or(false) { ui::done("approved") } else { ui::warn("pending seal") }; |
|
118 |
+ |
let mark = if a.get("sealed").and_then(Value::as_bool).unwrap_or(false) { |
|
119 |
+ |
ui::done("approved") |
|
120 |
+ |
} else { |
|
121 |
+ |
ui::warn("pending seal") |
|
122 |
+ |
}; |
| 73 |
123 |
|
println!(" {mark} {}", text(a, "account")); |
| 74 |
124 |
|
} |
| 75 |
125 |
|
Ok(0) |
|
|
… |
13 unchanged lines |
| 89 |
139 |
|
for chunk in hashes.chunks(oxy_core::attest::MAX_SEAL_HASHES) { |
| 90 |
140 |
|
refs.push(repo.seal(chunk)?); |
| 91 |
141 |
|
} |
| 92 |
|
- |
let remote = repo.remote_for_url(&repo_url).ok_or_else(|| anyhow!("no remote is configured for {repo_url}; add one with `oxy remote add`"))?; |
|
142 |
+ |
let remote = repo.remote_for_url(&repo_url).ok_or_else(|| { |
|
143 |
+ |
anyhow!("no remote is configured for {repo_url}; add one with `oxy remote add`") |
|
144 |
+ |
})?; |
| 93 |
145 |
|
repo.push_seals(&remote, &refs)?; |
| 94 |
146 |
|
if as_json { |
| 95 |
|
- |
let list: Vec<String> = hashes.iter().map(|h| format!("\"{}\"", h.to_hex())).collect(); |
|
147 |
+ |
let list: Vec<String> = hashes |
|
148 |
+ |
.iter() |
|
149 |
+ |
.map(|h| format!("\"{}\"", h.to_hex())) |
|
150 |
+ |
.collect(); |
| 96 |
151 |
|
println!("{{\"sealed\":[{}],\"pushed\":true}}", list.join(",")); |
| 97 |
152 |
|
} else { |
| 98 |
|
- |
println!("{} {} change{} of the proposal and pushed the seal to {remote}", ui::done("Sealed"), hashes.len(), if hashes.len() == 1 { "" } else { "s" }); |
|
153 |
+ |
println!( |
|
154 |
+ |
"{} {} change{} of the proposal and pushed the seal to {remote}", |
|
155 |
+ |
ui::done("Sealed"), |
|
156 |
+ |
hashes.len(), |
|
157 |
+ |
if hashes.len() == 1 { "" } else { "s" } |
|
158 |
+ |
); |
| 99 |
159 |
|
} |
| 100 |
160 |
|
Ok(0) |
| 101 |
161 |
|
} |
| 102 |
162 |
|
|
| 103 |
163 |
|
/// Fetch the proposal's changes (and anything they need) from the hub |
| 104 |
164 |
|
/// into the local store. |
| 105 |
|
- |
fn ensure_changes(repo: &Repo, repo_url: &str, want: &[ChangeHash], _changes: &[ChangeHash]) -> Result<()> { |
| 106 |
|
- |
let missing: Vec<ChangeHash> = want.iter().copied().filter(|h| !repo.store().has(h)).collect(); |
|
165 |
+ |
fn ensure_changes( |
|
166 |
+ |
repo: &Repo, |
|
167 |
+ |
repo_url: &str, |
|
168 |
+ |
want: &[ChangeHash], |
|
169 |
+ |
_changes: &[ChangeHash], |
|
170 |
+ |
) -> Result<()> { |
|
171 |
+ |
let missing: Vec<ChangeHash> = want |
|
172 |
+ |
.iter() |
|
173 |
+ |
.copied() |
|
174 |
+ |
.filter(|h| !repo.store().has(h)) |
|
175 |
+ |
.collect(); |
| 107 |
176 |
|
if missing.is_empty() { |
| 108 |
177 |
|
return Ok(()); |
| 109 |
178 |
|
} |
|
|
… |
17 unchanged lines |
| 127 |
196 |
|
anyhow::bail!("the proposal is {state}, not open (use --force to land anyway)"); |
| 128 |
197 |
|
} |
| 129 |
198 |
|
if !v.get("landable").and_then(Value::as_bool).unwrap_or(false) && !force { |
| 130 |
|
- |
let blockers: Vec<String> = v.get("blockers").and_then(Value::as_array).map(|a| a.iter().filter_map(|b| b.as_str().map(String::from)).collect()).unwrap_or_default(); |
| 131 |
|
- |
anyhow::bail!("the hub says the proposal cannot land yet: {} (use --force to land anyway)", blockers.join("; ")); |
|
199 |
+ |
let blockers: Vec<String> = v |
|
200 |
+ |
.get("blockers") |
|
201 |
+ |
.and_then(Value::as_array) |
|
202 |
+ |
.map(|a| { |
|
203 |
+ |
a.iter() |
|
204 |
+ |
.filter_map(|b| b.as_str().map(String::from)) |
|
205 |
+ |
.collect() |
|
206 |
+ |
}) |
|
207 |
+ |
.unwrap_or_default(); |
|
208 |
+ |
anyhow::bail!( |
|
209 |
+ |
"the hub says the proposal cannot land yet: {} (use --force to land anyway)", |
|
210 |
+ |
blockers.join("; ") |
|
211 |
+ |
); |
| 132 |
212 |
|
} |
| 133 |
213 |
|
let target = text(&v, "target").to_string(); |
| 134 |
214 |
|
let heads = hashes(&v, "projected_heads"); |
|
|
… |
1 unchanged lines |
| 136 |
216 |
|
anyhow::bail!("the hub has not computed the projected state yet; try again shortly"); |
| 137 |
217 |
|
} |
| 138 |
218 |
|
ensure_changes(&repo, &repo_url, &heads, &change_hashes(&v))?; |
| 139 |
|
- |
let remote = repo.remote_for_url(&repo_url).ok_or_else(|| anyhow!("no remote is configured for {repo_url}; add one with `oxy remote add`"))?; |
|
219 |
+ |
let remote = repo.remote_for_url(&repo_url).ok_or_else(|| { |
|
220 |
+ |
anyhow!("no remote is configured for {repo_url}; add one with `oxy remote add`") |
|
221 |
+ |
})?; |
| 140 |
222 |
|
// Bring the target bookmark up to date first, so the push is a fast-forward. |
| 141 |
223 |
|
repo.fetch(&remote)?; |
| 142 |
224 |
|
repo.bookmark_set(&target, &heads)?; |
| 143 |
225 |
|
let results = repo.push_bookmark(&remote, &target, false)?; |
| 144 |
226 |
|
for r in &results { |
| 145 |
227 |
|
if r.ok { |
| 146 |
|
- |
println!("{} {}/{} - landed proposal", ui::done("Pushed"), remote, r.name); |
|
228 |
+ |
println!( |
|
229 |
+ |
"{} {}/{} - landed proposal", |
|
230 |
+ |
ui::done("Pushed"), |
|
231 |
+ |
remote, |
|
232 |
+ |
r.name |
|
233 |
+ |
); |
| 147 |
234 |
|
} else { |
| 148 |
|
- |
println!("{} {}/{} - {}", ui::warn("Rejected"), remote, r.name, r.reason); |
|
235 |
+ |
println!( |
|
236 |
+ |
"{} {}/{} - {}", |
|
237 |
+ |
ui::warn("Rejected"), |
|
238 |
+ |
remote, |
|
239 |
+ |
r.name, |
|
240 |
+ |
r.reason |
|
241 |
+ |
); |
| 149 |
242 |
|
} |
| 150 |
243 |
|
} |
| 151 |
244 |
|
Ok(if results.iter().all(|r| r.ok) { 0 } else { 1 }) |
|
|
… |
1 unchanged lines |