use crate::prelude::*;
#[derive(Error,Debug)]
#[non_exhaustive]
pub enum Error {
#[error("error conducting forge operation (unclassified): {0}")]
UncleassifiedOperationError(anyhow::Error),
#[error("forge operation build failed: {0}")]
OperationBuildFailed(anyhow::Error),
#[error("forge results processing failed: {0}")]
ResultsProcessingFailed(anyhow::Error),
#[error("forge ancillary operation failed: {0}")]
AncillaryOperationFailed(anyhow::Error),
#[error("forge client creation failed (bad config?): {0}")]
ClientCreationFailed(anyhow::Error),
#[error("forge kind must be specified")]
KindMustBeSpecified,
#[error("forge kind {0:?} disnabled (cargo feature)")]
KindDisabled(Kind),
#[error("async runtime error: {0}")] Async(anyhow::Error),
#[error("token always required for {0}")]
TokenAlwaysRequired(Kind),
#[error("request returned too many results (API limit)")]
TooManyResults,
#[error("unsupporrted operation: {0}")]
UnsupportedOperation(anyhow::Error),
#[error("{0}: state not supported (in this context): {1:?}")]
UnsupportedState(RemoteObjectKind, String),
#[error("{0}: name not found: {1:?}")]
NameNotFound(RemoteObjectKind,String),
#[error("{0}: id not found: {1}")]
IdNotFound(RemoteObjectKind,String),
#[error("invalid {0} syntax: {2:?}: {1}")]
InvalidObjectSyntax(RemoteObjectKind,String,String),
#[error("{0}: invalid id syntax: {2:?}: {1}")]
InvalidIdSyntax(RemoteObjectKind,String,String),
#[error("unsupported remote URL format: {1:?}: {0}")]
UnsupportedRemoteUrlSyntax(String,String),
#[error("remote URL host {url:?} does not match forge host {forge:?}")]
UrlHostMismatch { url: String, forge: String },
}
#[derive(Debug,Error,Clone,Copy,Eq,PartialEq,Hash)]
pub enum RemoteObjectKind {
#[error("user")] User,
#[error("repo")] Repo,
#[error("merge req")] MergeReq,
#[error("issue")] Issue,
}
#[derive(Debug,Error,Clone,Copy,Eq,PartialEq,Hash)]
#[derive(Serialize,Deserialize)]
#[serde(rename_all="snake_case")]
#[derive(Display,EnumString)]
#[strum(serialize_all = "lowercase")]
pub enum Kind {
GitLab,
GitHub,
}
#[derive(Clone,From,FromStr,Serialize,Deserialize)]
#[serde(transparent)]
pub struct Token(pub String);
impl Debug for Token {
#[throws(fmt::Error)]
fn fmt(&self, f: &mut fmt::Formatter) { write!(f,"forge::Token(..)")? }
}
pub(crate) type Constructor = fn(&Config) -> Result<
Box<dyn Forge + 'static>,
FE,
>;
pub(crate) type ForgeEntry = (Kind, Constructor);
pub(crate) const FORGES: &[ForgeEntry] = &[
#[cfg(feature="gitlab")] (Kind::GitLab, crate::lab::Lab::new),
#[cfg(feature="github")] (Kind::GitHub, crate::hub::Hub::new),
];
#[derive(Default,Clone,Debug,Serialize,Deserialize)]
pub struct Config {
#[serde(default)]
pub token: Option<TokenConfig>,
#[serde(default)]
pub kind: Option<Kind>,
#[serde(default)]
pub host: String,
#[serde(skip)]
pub _non_exhaustive: (),
}
#[derive(Clone,Debug,Serialize,Deserialize)]
pub enum TokenConfig {
Anonymous,
Path(PathBuf),
Value(Token),
}
impl Config {
#[throws(FE)]
pub fn forge(&self) -> Box<dyn Forge + 'static> {
let kind = self.kind.ok_or_else(|| FE::KindMustBeSpecified)?;
let entry = FORGES.iter().find(|c| c.0 == kind)
.ok_or_else(|| FE::KindDisabled(kind))?;
entry.1(&self)?
}
}
impl TokenConfig {
#[throws(AE)]
fn get_token(&self) -> Option<Token> {
match self {
TokenConfig::Anonymous => None,
TokenConfig::Value(v) => Some(v).cloned(),
TokenConfig::Path(path) => {
let token = Token::load(path)?
.ok_or_else(|| anyhow!("specified token file does not exist"))
.with_context(|| format!("{:?}", &path))?;
Some(token)
}
}
}
}
impl Token {
#[throws(anyhow::Error)]
pub fn load(path: &Path) -> Option<Token> { (|| {
let mut f = match File::open(&path) {
Err(e) if e.kind() == ErrorKind::NotFound => {
info!("forge token file {:?} does not exist", path);
return Ok(None);
},
Err(e) => throw!(anyhow::Error::from(e).context("open")),
Ok(f) => f,
};
#[cfg(unix)] {
use std::os::unix::fs::MetadataExt;
let m = f.metadata().context("stat")?;
if m.mode() & 0o004 != 0 {
throw!(anyhow!("token file is world-readable! refusing to use"))
}
}
let mut buf = String::new();
f.read_to_string(&mut buf).context("read")?;
let token = Token(buf.trim().into());
info!("forge token file {:?}", path);
Ok::<_,anyhow::Error>(Some(token))
})()
.with_context(|| format!("{:?}", path))
.context("git forge auth token file")?
}
}
impl Config {
#[throws(anyhow::Error)]
pub fn default_token_path(&self) -> PathBuf {
let chk = |s: &str, what| if {
s.chars().all(|c| c=='-' || c=='.' || c.is_ascii_alphanumeric()) &&
s.chars().next().map(|c| c.is_ascii_alphanumeric()) == Some(true)
} { Ok(()) } else { Err(anyhow!(
"{} contains, or starts with, bad character(s)", what
)) };
let kind = self.kind.ok_or_else(|| FE::KindMustBeSpecified)?;
let host: &str = &self.host;
chk(host, "hostname")?;
let mut path =
directories::ProjectDirs::from("","","GitForge")
.ok_or_else(|| anyhow!("could not find home directory"))?
.config_dir().to_owned();
path.push(format!(
"{}.{}-token",
host.replace('.',"_"),
kind,
));
path
}
#[throws(AE)]
pub(crate) fn get_token_or_default(&self) -> Option<Token> {
match &self.token {
Some(c) => {
c.get_token()?
},
None => {
let path = self.default_token_path()?;
let token = Token::load(&path)?;
token
},
}
}
#[throws(anyhow::Error)]
pub fn load_default_token(&mut self) -> &mut Self {
(||{
self.token = Some(match self.get_token_or_default()? {
None => TokenConfig::Anonymous,
Some(v) => TokenConfig::Value(v),
});
Ok::<_,AE>(())
})().context("load default token")?;
self
}
#[throws(FE)]
pub(crate) fn get_token(&self) -> Option<Token> {
self.get_token_or_default()
.map_err(FE::ClientCreationFailed)?
}
}
pub trait ForgeMethods {
fn request(&mut self, req: &Req) -> Result<Resp, forge::Error>;
fn clear_id_caches(&mut self);
#[throws(FE)]
fn remote_url_to_repo(&self, url: &str) -> String {
parse_git_remote_url(url, Some(self.host()))?.path.to_owned()
}
fn host(&self) -> &str;
fn kind(&self) -> Kind;
}
pub trait Forge: ForgeMethods + Debug + Send + Sync + 'static { }
impl<T> Forge for T where T: ForgeMethods + Debug + Send + Sync + 'static { }
#[derive(Debug,Default,Clone,Eq,PartialEq,Serialize,Deserialize)]
#[allow(non_camel_case_types)]
pub struct Req_MergeRequests {
pub target_repo: String,
pub number: Option<String>,
pub author: Option<String>,
pub source_repo: Option<String>,
pub target_branch: Option<String>,
pub source_branch: Option<String>,
pub statuses: Option<HashSet<IssueMrStatus>>,
#[serde(skip)] pub _non_exhaustive: (),
}
#[derive(Debug,Default,Clone,Eq,PartialEq,Serialize,Deserialize)]
#[allow(non_camel_case_types)]
pub struct Req_CreateMergeRequest {
pub target: RepoBranch,
pub source: RepoBranch,
pub title: String,
pub description: String,
#[serde(skip)] pub _non_exhaustive: (),
}
#[derive(Debug,Clone,Copy,Hash,Serialize,Deserialize)]
#[derive(Eq,PartialEq,Ord,PartialOrd)]
pub enum IssueMrStatus {
Closed, Merged, Open, Unrepresentable,
}
#[derive(Debug,Clone,Copy,Hash,Serialize,Deserialize)]
#[derive(Eq,PartialEq,Ord,PartialOrd)] pub enum IssueMrLocked {
Unlocked, Locked,
}
#[derive(Debug,Clone,Copy,Hash,Serialize,Deserialize)]
#[derive(Eq,PartialEq,Ord,PartialOrd)] #[non_exhaustive]
pub struct IssueMrState {
pub status: IssueMrStatus,
pub locked: IssueMrLocked,
}
#[derive(Debug,Clone,Eq,PartialEq,Serialize,Deserialize)]
pub enum Req {
MergeRequests(Req_MergeRequests),
CreateMergeRequest(Req_CreateMergeRequest),
#[allow(non_camel_case_types)] _NonExhaustive(),
}
#[derive(Debug,Clone,Hash,Eq,PartialEq,Serialize,Deserialize)]
pub enum Resp {
#[non_exhaustive] MergeRequests { mrs: Vec<Resp_MergeRequest>, },
#[non_exhaustive] CreateMergeRequest { number: String, },
#[allow(non_camel_case_types)] _NonExhaustive(),
}
#[derive(Debug,Clone,Default,Hash,Eq,PartialEq,Serialize,Deserialize)]
pub struct RepoBranch {
pub repo: String,
pub branch: String,
}
#[derive(Debug,Clone,Hash,Eq,PartialEq,Serialize,Deserialize)]
#[allow(non_camel_case_types)]
#[non_exhaustive]
pub struct Resp_MergeRequest {
pub number: String,
pub author: String,
pub state: IssueMrState,
pub source: RepoBranch,
pub target: RepoBranch,
}