pub use octocrab;
use crate::prelude::*;
use octocrab::Octocrab;
pub struct Hub {
host: String,
oc: Octocrab,
tok: tokio::runtime::Runtime,
}
impl Debug for Hub {
#[throws(fmt::Error)]
fn fmt(&self, f: &mut fmt::Formatter) {
f.debug_struct("Hub")
.field("oc", &self.oc)
.field("tok", &format_args!("{{..tokio..}}"))
.finish()?
}
}
impl TryFrom<Octocrab> for Hub {
type Error = FE;
#[throws(FE)]
fn try_from(oc: Octocrab) -> Self {
let host = (||{
let url = oc.absolute_url("/").context("get base url")?;
let domain = url.domain()
.ok_or_else(|| anyhow!("no domain (IP address?"))
.with_context(|| format!("{:?}", &url))?;
let host = domain.strip_prefix("api.").unwrap_or(domain);
Ok::<_,AE>(host.to_owned())
})()
.context("determine canonical hostname")
.map_err(FE::ClientCreationFailed)?;
let tok = tokio::runtime::Runtime::new()
.context("create").map_err(FE::Async)?;
Hub { host, oc, tok }
}
}
impl Hub {
#[throws(FE)]
pub fn new(config: &forge::Config) -> Box<dyn Forge> {
let mut oc = Octocrab::builder();
if let Some(Token(token)) = config.get_token()? {
oc = oc.personal_token(token.into());
}
if config.host != "github.com" {
oc = oc.base_url(format!("https://{}", config.host))
.context("set url")
.map_err(FE::ClientCreationFailed)?;
}
let oc = oc.build()
.context("build Octocrab client")
.map_err(FE::ClientCreationFailed)?;
let oc: Hub = oc.try_into()?;
Box::new(oc) as _
}
}
#[throws(FE)]
fn repo_parse(frepo: &str) -> (&str /*gh owner*/, &str /*gh owner */) {
frepo.split_once('/')
.ok_or_else(|| FE::InvalidObjectSyntax(
RemoteObjectKind::Repo,
"github repo must be <owner>/<repo>".into(),
frepo.into()
))?
}
impl ForgeMethods for Hub {
fn clear_id_caches(&mut self) { }
fn host(&self) -> &str { &self.host }
fn kind(&self) -> Kind { Kind::GitHub }
#[throws(FE)]
fn request(&mut self, req: &Req) -> Resp {
macro_rules! make_request {
($future:expr) => {
self.tok.block_on($future)
.with_context(|| format!("{:?}", req))
.map_err(FE::UncleassifiedOperationError)
}
}
match req {
Req::MergeRequests(q) => {
let (t_owner, t_repo) = repo_parse(&q.target_repo)?;
debug!("MergeRequests t_owner={:?} t_repo={:?} query {:?}",
&t_owner, &t_repo, &q);
let d = self.oc.pulls(t_owner, t_repo);
let d = if let Some(number) = &q.number {
let number = number.parse().map_err(
|e: ParseIntError| FE::InvalidIdSyntax(
RemoteObjectKind::MergeReq,
e.to_string(),
number.clone(),
))?;
let d = make_request!( d.get(number) )?;
debug!("MergeRequests reply {:?}", &d);
vec![d]
} else {
let mut d = d.list();
d = d.state({
use IssueMrStatus as F;
use octocrab::params::State as G;
let some_want = |m: &dyn Fn(_) -> bool| match &q.statuses {
None => true,
Some(states) => states.iter().cloned().any(m),
};
let want_open = some_want(
&|st| matches!(st, F::Open)
);
let want_closed = some_want(
&|st| matches!(st, F::Closed | F::Merged)
);
match (want_open, want_closed) {
(false,false) => return Resp::MergeRequests { mrs: vec![] },
(false,true ) => G::Closed,
(true, false) => G::Open,
(true, true ) => G::All,
}
});
if let (Some(source_repo), Some(source_branch)) =
(&q.source_repo, &q.source_branch)
{
d = d.head(format!("{}:{}",
repo_parse(source_repo)?.0,
source_branch));
}
if let Some(target_branch) = &q.target_branch {
d = d.base(target_branch);
}
let d = make_request!( d.send() )?;
debug!("MergeRequests reply {:?}", &d);
if d.incomplete_results == Some(true) ||
d.next.is_some() { throw!(FE::TooManyResults); }
d.items
};
let mrs = d.into_iter().map(|g| {
let number = &g.number;
macro_rules! some_repo { { $what_repo:ident = $field:ident } => {
let $what_repo = g.$field.repo.ok_or_else(
|| anyhow!("missing {} for #{}", stringify!($field), number)
)?.full_name;
} }
some_repo!{ target_repo = base }
some_repo!{ source_repo = head }
Ok::<_,AE>(Resp_MergeRequest {
number: number.to_string(),
author: g.user.login,
state: IssueMrState {
locked: if g.locked { IssueMrLocked::Locked }
else { IssueMrLocked::Unlocked },
status: {
use IssueMrStatus as F;
use octocrab::models::IssueState as G;
match (g.state, &g.merged_at) {
(G::Closed, None, ) => F::Closed,
(G::Closed, Some(_),) => F::Merged,
(G::Open , None, ) => F::Open,
(G::Open , Some(_),) => {
info!("mapping MR {:?} {:?} \
open + merged to Unrepresentable",
&target_repo, &g.number);
F::Unrepresentable
},
(gstate, _) => {
info!("mapping MR {:?} {:?} \
unknown state={:?} Unrepresentable",
&target_repo, &g.number, gstate);
F::Unrepresentable
},
}
},
},
target: RepoBranch {
repo: target_repo,
branch: g.base.ref_field,
},
source: RepoBranch {
repo: source_repo,
branch: g.head.ref_field,
},
})
})
.filter_ok(filter_mergerequests(&q))
.collect::<Result<Vec<_>,_>>()
.map_err(FE::ResultsProcessingFailed)?;
Resp::MergeRequests { mrs }
},
Req::CreateMergeRequest(Req_CreateMergeRequest {
title, description,
target: RepoBranch { repo: target_repo, branch: target_branch },
source: RepoBranch { repo: source_repo, branch: source_branch },
_non_exhaustive, }) => {
let (t_owner, t_repo) = repo_parse(target_repo)?;
let (s_owner, s_repo) = repo_parse(source_repo)?;
debug!("MergeRequests query {:?} t_owner={:?} t_repo={:?} \
s_owner={:?} s_repo={:?}",
&t_owner, &t_repo,
&s_owner, &s_repo, &req);
if &s_repo != &t_repo { throw!(FE::UnsupportedOperation(anyhow!(
"github octocrab cannot make an MR when \
source and target repos have different names :-( \
source={:?} != target={:?}",
&s_repo, &t_repo
))) }
let d = self.oc.pulls(t_owner, t_repo);
let d = d.create(
title,
format!("{}:{}", &s_owner, source_branch),
target_branch,
);
let d = d.body(description);
let d = make_request!( d.send() )?;
Resp::CreateMergeRequest {
number: d.number.to_string(),
}
},
q@ Req::_NonExhaustive() => panic!("bad request {:?}", q),
}
}
}