1pub use octocrab;
40
41use crate::prelude::*;
42use octocrab::Octocrab;
43
44pub struct Hub {
46 host: String,
47 oc: Octocrab,
48 tok: tokio::runtime::Runtime,
49}
50impl Debug for Hub {
51 #[throws(fmt::Error)]
52 fn fmt(&self, f: &mut fmt::Formatter) {
53 f.debug_struct("Hub")
54 .field("oc", &self.oc)
55 .field("tok", &format_args!("{{..tokio..}}"))
56 .finish()?
57 }
58}
59
60impl TryFrom<Octocrab> for Hub {
61 type Error = FE;
62 #[throws(FE)]
63 fn try_from(oc: Octocrab) -> Self {
64 let host = (||{
65 let url = oc.absolute_url("/").context("get base url")?;
66 let domain = url.domain()
67 .ok_or_else(|| anyhow!("no domain (IP address?"))
68 .with_context(|| format!("{:?}", &url))?;
69 let host = domain.strip_prefix("api.").unwrap_or(domain);
70 Ok::<_,AE>(host.to_owned())
71 })()
72 .context("determine canonical hostname")
73 .map_err(FE::ClientCreationFailed)?;
74
75 let tok = tokio::runtime::Runtime::new()
76 .context("create").map_err(FE::Async)?;
77 Hub { host, oc, tok }
78 }
79}
80
81impl Hub {
82 #[throws(FE)]
87 pub fn new(config: &forge::Config) -> Box<dyn Forge> {
88 let mut oc = Octocrab::builder();
89 if let Some(Token(token)) = config.get_token()? {
90 oc = oc.personal_token(token.into());
91 }
92 if config.host != "github.com" {
93 oc = oc.base_url(format!("https://{}", config.host))
94 .context("set url")
95 .map_err(FE::ClientCreationFailed)?;
96 }
97 let oc = oc.build()
98 .context("build Octocrab client")
99 .map_err(FE::ClientCreationFailed)?;
100
101 let oc: Hub = oc.try_into()?;
102 Box::new(oc) as _
103 }
104}
105
106#[throws(FE)]
107fn repo_parse(frepo: &str) -> (&str , &str ) {
108 frepo.split_once('/')
109 .ok_or_else(|| FE::InvalidObjectSyntax(
110 RemoteObjectKind::Repo,
111 "github repo must be <owner>/<repo>".into(),
112 frepo.into()
113 ))?
114}
115
116impl ForgeMethods for Hub {
117 fn clear_id_caches(&mut self) { }
118
119 fn host(&self) -> &str { &self.host }
120 fn kind(&self) -> Kind { Kind::GitHub }
121
122 #[throws(FE)]
123 fn request(&mut self, req: &Req) -> Resp {
124 macro_rules! make_request {
125 ($future:expr) => {
126 self.tok.block_on($future)
127 .with_context(|| format!("{:?}", req))
128 .map_err(FE::UncleassifiedOperationError)
129 }
130 }
131
132 match req {
133
134 Req::MergeRequests(q) => {
135 let (t_owner, t_repo) = repo_parse(&q.target_repo)?;
136
137 debug!("MergeRequests t_owner={:?} t_repo={:?} query {:?}",
138 &t_owner, &t_repo, &q);
139
140 let d = self.oc.pulls(t_owner, t_repo);
141
142 let d = if let Some(number) = &q.number {
143
144 let number = number.parse().map_err(
145 |e: ParseIntError| FE::InvalidIdSyntax(
146 RemoteObjectKind::MergeReq,
147 e.to_string(),
148 number.clone(),
149 ))?;
150
151 let d = make_request!( d.get(number) )?;
152
153 debug!("MergeRequests reply {:?}", &d);
154
155 vec![d]
156
157 } else {
158
159 let mut d = d.list();
160
161 d = d.state({
162 use IssueMrStatus as F;
163 use octocrab::params::State as G;
164 let some_want = |m: &dyn Fn(_) -> bool| match &q.statuses {
165 None => true,
166 Some(states) => states.iter().cloned().any(m),
167 };
168 let want_open = some_want(
169 &|st| matches!(st, F::Open)
170 );
171 let want_closed = some_want(
172 &|st| matches!(st, F::Closed | F::Merged)
173 );
174 match (want_open, want_closed) {
175 (false,false) => return Resp::MergeRequests { mrs: vec![] },
176 (false,true ) => G::Closed,
177 (true, false) => G::Open,
178 (true, true ) => G::All,
179 }
180 });
181
182 if let (Some(source_repo), Some(source_branch)) =
183 (&q.source_repo, &q.source_branch)
184 {
185 d = d.head(format!("{}:{}",
186 repo_parse(source_repo)?.0,
187 source_branch));
188 }
189
190 if let Some(target_branch) = &q.target_branch {
191 d = d.base(target_branch);
192 }
193
194 let d = make_request!( d.send() )?;
195
196 debug!("MergeRequests reply {:?}", &d);
197
198 if d.incomplete_results == Some(true) ||
199 d.next.is_some() { throw!(FE::TooManyResults); }
200
201 d.items
202
203 };
204
205 let mrs = d.into_iter().map(|g| {
206 let number = &g.number;
207
208 macro_rules! some_repo { { $what_repo:ident = $field:ident } => {
209 let $what_repo = g.$field.repo.ok_or_else(
210 || anyhow!("missing {} for #{}", stringify!($field), number)
211 )?.full_name;
212 } }
213 some_repo!{ target_repo = base }
214 some_repo!{ source_repo = head }
215
216 Ok::<_,AE>(Resp_MergeRequest {
217 number: number.to_string(),
218 author: g.user.login,
219 state: IssueMrState {
220 locked: if g.locked { IssueMrLocked::Locked }
221 else { IssueMrLocked::Unlocked },
222 status: {
223 use IssueMrStatus as F;
224 use octocrab::models::IssueState as G;
225
226 match (g.state, &g.merged_at) {
227 (G::Closed, None, ) => F::Closed,
228 (G::Closed, Some(_),) => F::Merged,
229 (G::Open , None, ) => F::Open,
230 (G::Open , Some(_),) => {
231 info!("mapping MR {:?} {:?} \
232 open + merged to Unrepresentable",
233 &target_repo, &g.number);
234 F::Unrepresentable
235 },
236 (gstate, _) => {
237 info!("mapping MR {:?} {:?} \
238 unknown state={:?} Unrepresentable",
239 &target_repo, &g.number, gstate);
240 F::Unrepresentable
241 },
242 }
243 },
244 },
245 target: RepoBranch {
246 repo: target_repo,
247 branch: g.base.ref_field,
248 },
249 source: RepoBranch {
250 repo: source_repo,
251 branch: g.head.ref_field,
252 },
253 })
254 })
255 .filter_ok(filter_mergerequests(&q))
256 .collect::<Result<Vec<_>,_>>()
257 .map_err(FE::ResultsProcessingFailed)?;
258
259 Resp::MergeRequests { mrs }
260 },
261
262 Req::CreateMergeRequest(Req_CreateMergeRequest {
263 title, description,
264 target: RepoBranch { repo: target_repo, branch: target_branch },
265 source: RepoBranch { repo: source_repo, branch: source_branch },
266 _non_exhaustive, }) => {
268 let (t_owner, t_repo) = repo_parse(target_repo)?;
269 let (s_owner, s_repo) = repo_parse(source_repo)?;
270
271 debug!("MergeRequests query {:?} t_owner={:?} t_repo={:?} \
272 s_owner={:?} s_repo={:?}",
273 &t_owner, &t_repo,
274 &s_owner, &s_repo, &req);
275
276 if &s_repo != &t_repo { throw!(FE::UnsupportedOperation(anyhow!(
280 "github octocrab cannot make an MR when \
281 source and target repos have different names :-( \
282 source={:?} != target={:?}",
283 &s_repo, &t_repo
284 ))) }
285
286 let d = self.oc.pulls(t_owner, t_repo);
287 let d = d.create(
288 title,
289 format!("{}:{}", &s_owner, source_branch),
290 target_branch,
291 );
292 let d = d.body(description);
293
294 let d = make_request!( d.send() )?;
295
296 Resp::CreateMergeRequest {
297 number: d.number.to_string(),
298 }
299 },
300
301 q@ Req::_NonExhaustive() => panic!("bad request {:?}", q),
302 }
303 }
304}