pub mod frontend;
pub mod supported;
use crate::{Outcome, Result};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use supported::{Designsync, Git};
#[derive(Clone, Default, Debug)]
pub struct Status {
pub added: Vec<PathBuf>,
pub removed: Vec<PathBuf>,
pub changed: Vec<PathBuf>,
pub conflicted: Vec<PathBuf>,
pub revision: String,
}
impl Status {
pub fn is_modified(&self) -> bool {
!self.added.is_empty()
|| !self.removed.is_empty()
|| !self.changed.is_empty()
|| !self.conflicted.is_empty()
|| !self.added.is_empty()
}
pub fn summarize(&self) {
displayln!("Workspace Status");
if !self.added.is_empty() {
displayln!(" ADDED: {} ITEMS", self.added.len());
for file in &self.added {
displayln!(" {}", file.display());
}
}
if !self.removed.is_empty() {
displayln!(" DELETED: {} ITEMS", self.removed.len());
for file in &self.removed {
displayln!(" {}", file.display());
}
}
if !self.changed.is_empty() {
displayln!(" CHANGED: {} ITEMS", self.changed.len());
for file in &self.changed {
displayln!(" {}", file.display());
}
}
if !self.conflicted.is_empty() {
displayln!(" CONFLICTED: {} ITEMS", self.conflicted.len());
for file in &self.conflicted {
display_redln!(" {}", file.display());
}
}
}
}
pub enum SupportedSystems {
Git,
Designsync,
}
impl SupportedSystems {
pub fn from_str(system: &str) -> Result<Self> {
let s = system.to_lowercase();
match s.as_str() {
"git" => Ok(Self::Git),
"design_sync" | "designsync" => Ok(Self::Designsync),
_ => bail!("Unsupported revision control system '{}'", system),
}
}
}
#[derive(Clone, Default)]
pub struct Credentials {
pub username: Option<String>,
pub password: Option<String>,
}
impl std::fmt::Debug for Credentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Credentials")
.field("username", &self.username)
.field("password", &"<-- Plaintext Password Withheld -->")
.finish()
}
}
#[derive(Debug)]
pub struct RevisionControl {
driver: Box<dyn RevisionControlAPI>,
}
impl RevisionControl {
pub fn new(
local: &Path,
remotes: Vec<&str>,
credentials: Option<Credentials>,
) -> RevisionControl {
if remotes.iter().any(|r| r.ends_with(".git")) {
RevisionControl {
driver: Box::new(RevisionControl::git(local, remotes, credentials)),
}
} else {
RevisionControl {
driver: Box::new(RevisionControl::designsync(local, remotes, credentials)),
}
}
}
pub fn from_config(config: &HashMap<String, String>) -> Result<Self> {
let driver: Box<dyn RevisionControlAPI>;
if let Some(c) = config.get("system") {
let _c = c.to_lowercase();
match _c.as_str() {
"git" => driver = Box::new(Self::git_from_config(config)?),
"designsync" | "design_sync" => {
driver = Box::new(Self::designsync_from_config(config)?)
}
_ => bail!("Unknown RC system '{}'", _c),
}
} else {
if config.contains_key("vault") {
if config.contains_key("remote") {
bail!("Both 'vault' and 'remote' cannot be used without specifying the 'system' parameter");
} else {
driver = Box::new(Self::designsync_from_config(config)?);
}
} else if config.contains_key("remote") {
driver = Box::new(Self::git_from_config(config)?);
} else {
bail!("Could not discern revision control system. None of 'remote', 'vault', or 'system' were given");
}
}
Ok(Self { driver: driver })
}
pub fn git(local: &Path, remotes: Vec<&str>, credentials: Option<Credentials>) -> Git {
Git::new(local, remotes, credentials)
}
pub fn git_from_config(config: &HashMap<String, String>) -> Result<Git> {
Ok(Self::git(
match config.get("local") {
Some(l) => &Path::new(l),
None => bail!("Git driver must be given a 'local' parameter"),
},
match config.get("remote") {
Some(r) => vec![r],
None => bail!("Git driver must be given a 'remote' parameter"),
},
None,
))
}
pub fn designsync(
local: &Path,
remotes: Vec<&str>,
credentials: Option<Credentials>,
) -> Designsync {
if remotes.len() > 1 {
log_warning!("Multiple remotes were given to the DesignSync driver, but only the first one is currently used");
}
Designsync::new(local, remotes[0], credentials)
}
pub fn designsync_from_config(config: &HashMap<String, String>) -> Result<Designsync> {
Ok(Self::designsync(
&Path::new(config.get("local").unwrap()),
match config.get("vault") {
Some(v) => vec![v],
None => bail!("DesignSync driver muust be given a 'vault' parameter"),
},
None,
))
}
}
pub trait RevisionControlAPI: std::fmt::Debug {
fn populate(&self, version: &str) -> Result<()>;
fn checkout(&self, force: bool, path: Option<&Path>, version: &str) -> Result<bool>;
fn revert(&self, path: Option<&Path>) -> Result<()>;
fn status(&self, path: Option<&Path>) -> Result<Status>;
fn tag(&self, tagname: &str, force: bool, message: Option<&str>) -> Result<()>;
fn init(&self) -> Result<Outcome>;
fn is_initialized(&self) -> Result<bool>;
fn checkin(
&self,
files_or_dirs: Option<Vec<&Path>>,
msg: &str,
dry_run: bool,
) -> Result<Outcome>;
fn system(&self) -> &str;
}
impl RevisionControlAPI for RevisionControl {
fn populate(&self, version: &str) -> Result<()> {
self.driver.populate(version)
}
fn checkout(&self, force: bool, path: Option<&Path>, version: &str) -> Result<bool> {
self.driver.checkout(force, path, version)
}
fn revert(&self, path: Option<&Path>) -> Result<()> {
self.driver.revert(path)
}
fn status(&self, path: Option<&Path>) -> Result<Status> {
self.driver.status(path)
}
fn tag(&self, tagname: &str, force: bool, message: Option<&str>) -> Result<()> {
self.driver.tag(tagname, force, message)
}
fn init(&self) -> Result<Outcome> {
self.driver.init()
}
fn is_initialized(&self) -> Result<bool> {
self.driver.is_initialized()
}
fn checkin(
&self,
files_or_dirs: Option<Vec<&Path>>,
msg: &str,
dry_run: bool,
) -> Result<Outcome> {
self.driver.checkin(files_or_dirs, msg, dry_run)
}
fn system(&self) -> &str {
self.driver.system()
}
}