d82e4409 Andy McCall 1 month ago ↶
1
2
3
4
5
use super :: { open, ui} ;
6
use anyhow:: { anyhow, Context, Result } ;
7
use oxy_core:: hash:: ChangeHash;
8
use oxy_core:: repo:: Repo;
9
use serde_json:: Value;
10
11
12
13
pub fn api_url ( url : & str ) -> Result< ( String, String) > {
14
let url = url.trim_end_matches ( ' /' ) ;
15
if url.contains ( " /api/v1/repos/" ) {
16
let ( base, rest) = url.split_once ( " /api/v1/repos/" ) .unwrap ( ) ;
17
let mut parts = rest.split ( ' /' ) ;
18
let ( owner, name) = ( parts.next ( ) .unwrap_or ( " " ) , parts.next ( ) .unwrap_or ( " " ) ) ;
19
return Ok ( ( url.to_string ( ) , format! ( " {base} /{owner} /{name} " ) ) ) ;
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 ( ) ) )
24
}
25
26
pub fn fetch ( repo : & Repo, url : & str ) -> Result< ( Value, String) > {
27
let ( api, repo_url) = api_url ( url) ? ;
28
let options = repo.options_for_url ( & repo_url) ;
29
let body = oxy_core:: protocol:: http_get( & api, & options) .map_err ( | e | anyhow! ( " {e}" ) ) ? ;
30
let v: Value = serde_json:: from_slice( & body) .context ( " the hub did not answer with JSON" ) ? ;
31
Ok ( ( v, repo_url) )
32
}
33
34
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 ( )
36
}
37
38
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 ( )
40
}
41
42
fn text < 'a > ( v : & 'a Value, key : & str ) -> & 'a str {
43
v.get ( key) .and_then ( Value:: as_str) .unwrap_or ( " " )
44
}
45
46
pub fn show ( url : & str , as_json : bool ) -> Result< i32 > {
47
let repo = open ( ) ? ;
48
let ( v, _ ) = fetch ( & repo, url) ? ;
49
if as_json {
50
println! ( " {v} " ) ;
51
return Ok ( 0 ) ;
52
}
53
println! ( " {} #{} {} " , ui:: done( " Proposal" ) , v.get ( " number" ) .and_then ( Value:: as_i64) .unwrap_or ( 0 ) , ui:: bookmark( text ( & v, " title" ) ) ) ;
54
println! ( " {} " , ui:: kv( " state" , text ( & v, " state" ) ) ) ;
55
println! ( " {} " , ui:: kv( " target" , ui:: bookmark( text ( & v, " target" ) ) ) ) ;
56
println! ( " {} " , ui:: kv( " author" , ui:: author( text ( & v, " author" ) ) ) ) ;
57
let changes = v.get ( " changes" ) .and_then ( Value:: as_array) .cloned ( ) .unwrap_or_default ( ) ;
58
println! ( " {} " , ui:: kv( " changes" , changes.len ( ) .to_string ( ) ) ) ;
59
for c in & changes {
60
println! ( " {} {} " , ui:: hash( & text ( c, " hash" ) [ .. 12. min ( text ( c, " hash" ) .len ( ) ) ] ) , text ( c, " message" ) .lines ( ) .next ( ) .unwrap_or ( " " ) ) ;
61
}
62
if v.get ( " conflicted" ) .and_then ( Value:: as_bool) .unwrap_or ( false ) {
63
println! ( " {} " , ui:: warn( " landing would carry conflicts" ) ) ;
64
}
65
if let Some ( blockers) = v.get ( " blockers" ) .and_then ( Value:: as_array) {
66
for b in blockers {
67
println! ( " {} {} " , ui:: warn( " blocked:" ) , b.as_str ( ) .unwrap_or ( " " ) ) ;
68
}
69
}
70
let approvals = v.get ( " approvals" ) .and_then ( Value:: as_array) .cloned ( ) .unwrap_or_default ( ) ;
71
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" ) } ;
73
println! ( " {mark} {} " , text ( a, " account" ) ) ;
74
}
75
Ok ( 0 )
76
}
77
78
79
80
pub fn seal ( url : & str , as_json : bool ) -> Result< i32 > {
81
let repo = open ( ) ? ;
82
let ( v, repo_url) = fetch ( & repo, url) ? ;
83
let hashes = change_hashes ( & v) ;
84
if hashes.is_empty ( ) {
85
anyhow:: bail! ( " the proposal has no changes to seal" ) ;
86
}
87
ensure_changes ( & repo, & repo_url, & hashes, & hashes) ? ;
88
let mut refs = Vec :: new( ) ;
89
for chunk in hashes.chunks ( oxy_core:: attest:: MAX_SEAL_HASHES ) {
90
refs.push ( repo.seal ( chunk) ? ) ;
91
}
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`" ) ) ? ;
93
repo.push_seals ( & remote, & refs) ? ;
94
if as_json {
95
let list: Vec< String> = hashes.iter ( ) .map ( | h | format! ( " \" {} \" " , h.to_hex ( ) ) ) .collect ( ) ;
96
println! ( " {{ \" sealed\" :[{} ],\" pushed\" :true}} " , list.join ( " ," ) ) ;
97
} else {
98
println! ( " {} {} change{} of the proposal and pushed the seal to {remote} " , ui:: done( " Sealed" ) , hashes.len ( ) , if hashes.len ( ) = = 1 { " " } else { " s" } ) ;
99
}
100
Ok ( 0 )
101
}
102
103
104
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 ( ) ;
107
if missing.is_empty ( ) {
108
return Ok ( ( ) ) ;
109
}
110
let ( _ , remote) = repo.open_remote_at ( repo_url) ? ;
111
let view = repo.view ( ) ? ;
112
let have = repo.known_heads ( & view) ? ;
113
let needed = remote.negotiate ( & have, want) ? ;
114
let bundle = remote.fetch ( & needed, repo.config.max_blob) ? ;
115
repo.import_bundle ( & bundle, false ) ? ;
116
Ok ( ( ) )
117
}
118
119
120
121
122
pub fn land ( url : & str , force : bool ) -> Result< i32 > {
123
let repo = open ( ) ? ;
124
let ( v, repo_url) = fetch ( & repo, url) ? ;
125
let state = text ( & v, " state" ) ;
126
if state ! = " open" & & ! force {
127
anyhow:: bail! ( " the proposal is {state}, not open (use --force to land anyway)" ) ;
128
}
129
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 ( " ; " ) ) ;
132
}
133
let target = text ( & v, " target" ) .to_string ( ) ;
134
let heads = hashes ( & v, " projected_heads" ) ;
135
if heads.is_empty ( ) {
136
anyhow:: bail! ( " the hub has not computed the projected state yet; try again shortly" ) ;
137
}
138
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`" ) ) ? ;
140
141
repo.fetch ( & remote) ? ;
142
repo.bookmark_set ( & target, & heads) ? ;
143
let results = repo.push_bookmark ( & remote, & target, false ) ? ;
144
for r in & results {
145
if r.ok {
146
println! ( " {} {} /{} - landed proposal" , ui:: done( " Pushed" ) , remote, r.name) ;
147
} else {
148
println! ( " {} {} /{} - {} " , ui:: warn( " Rejected" ) , remote, r.name, r.reason) ;
149
}
150
}
151
Ok ( if results.iter ( ) .all ( | r | r.ok ) { 0 } else { 1 } )
152
}