use crate::model::{
cluster::{Cluster, DeploymentKind, NodeEntry, RootEntry, WorkDirAlias},
config_dir, data_dir, glob_match,
};
use anyhow::{anyhow, Context, Result};
use auth_git2::{GitAuthenticator, Prompter};
use beau_collector::BeauCollector as _;
use futures::{stream, StreamExt};
use git2::{
build::RepoBuilder, Config, FetchOptions, ObjectType, RemoteCallbacks, Repository,
RepositoryInitOptions,
};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use inquire::{Password, Text};
use std::{
collections::VecDeque,
ffi::{OsStr, OsString},
fmt::Write as FmtWrite,
fs::{remove_dir_all, File},
io::Write as IoWrite,
path::{Path, PathBuf},
process::Command,
sync::{Arc, Mutex},
time::{Duration, Instant},
};
use tracing::{debug, info, instrument, trace, warn};
#[derive(Debug)]
pub struct Root {
entry: RepoEntry,
deployer: RepoEntryDeployer,
}
impl Root {
#[instrument(skip(url), level = "debug")]
pub fn new_clone(url: impl AsRef<str>) -> Result<Self> {
trace!("Clone new root repository");
let bar = ProgressBar::no_length();
let entry = RepoEntry::builder("root")?
.url(url.as_ref())
.deployment_kind(DeploymentKind::BareAlias)
.work_dir_alias(WorkDirAlias::new(config_dir()?))
.authentication_prompter(ProgressBarAuthenticator::new(ProgressBarKind::SingleBar(
bar.clone(),
)))
.clone(&bar)?;
bar.finish_and_clear();
let deployer = RepoEntryDeployer::new(&entry);
let mut root = Self { entry, deployer };
let config = root.extract_root_config()?;
std::fs::create_dir_all(config_dir()?)?;
root.entry.set_deployment(DeploymentKind::BareAlias, config.settings.work_dir_alias);
root.deployer.add_excluded(config.settings.excluded.iter().flatten());
root.deployer.deploy_with(BareAliasDeployment, &root.entry, DeployAction::Deploy)?;
Ok(root)
}
pub fn new_open(entry: &RootEntry) -> Result<Self> {
let repo = RepoEntry::builder("root")?.open()?;
let deployer = RepoEntryDeployer::new(&repo);
let mut root = Self { entry: repo, deployer };
root.entry.set_deployment(DeploymentKind::BareAlias, entry.settings.work_dir_alias.clone());
root.deployer.add_excluded(entry.settings.excluded.iter().flatten());
root.deployer.deploy_with(RootDeployment, &root.entry, DeployAction::Deploy)?;
Ok(root)
}
#[instrument(skip(root), level = "debug")]
pub fn new_init(root: &RootEntry) -> Result<Self> {
info!("Initialize root repository");
let entry = RepoEntry::builder("root")?
.deployment_kind(DeploymentKind::BareAlias)
.work_dir_alias(root.settings.work_dir_alias.clone())
.init()?;
let mut deployer = RepoEntryDeployer::new(&entry);
deployer.add_excluded(root.settings.excluded.iter().flatten());
Ok(Self { entry, deployer })
}
pub fn deploy(&self, action: DeployAction) -> Result<()> {
self.deployer.deploy_with(RootDeployment, &self.entry, action)
}
pub fn is_deployed(&self, state: DeployState) -> Result<bool> {
is_deployed(&self.entry, &self.deployer.excluded, state)
}
#[instrument(skip(self), level = "debug")]
pub fn nuke(&self) -> Result<()> {
self.deployer.deploy_with(BareAliasDeployment, &self.entry, DeployAction::Undeploy)?;
remove_dir_all(self.path())?;
info!("Nuke {:?} from cluster", self.entry.name());
Ok(())
}
pub fn current_branch(&self) -> Result<String> {
self.entry.current_branch()
}
pub fn path(&self) -> &Path {
self.entry.path()
}
pub fn gitcall(&self, args: impl IntoIterator<Item = impl Into<OsString>>) -> Result<()> {
self.entry.gitcall_interactive(args)
}
pub(crate) fn extract_root_config(&self) -> Result<RootEntry> {
if self.entry.is_empty()? {
warn!("Root is empty, defer to default settings");
return RootEntry::try_default();
}
let commit = self.entry.repository.head()?.peel_to_commit()?;
let tree = commit.tree()?;
let blob = if let Some(entry) = tree.get_name("root.toml") {
entry.to_object(&self.entry.repository)?.peel_to_blob()?
} else {
let entry = tree
.get_path(PathBuf::from(".config/ocd/root.toml").as_path())
.map_err(|_| anyhow!("Cannot locate 'root.toml' file"))?;
entry.to_object(&self.entry.repository)?.peel_to_blob()?
};
let content = String::from_utf8_lossy(blob.content()).into_owned();
let root: RootEntry = toml::de::from_str(&content)?;
debug!("Extracted the following content from 'root.toml'\n{root:?}");
Ok(root)
}
}
#[derive(Debug)]
pub struct Node {
entry: RepoEntry,
deployer: RepoEntryDeployer,
}
impl Node {
#[instrument(skip(name, node), level = "debug")]
pub fn new_init(name: impl AsRef<str>, node: &NodeEntry) -> Result<Self> {
info!("Initialize node repository {:?}", name.as_ref());
let entry = RepoEntry::builder(name.as_ref())?
.deployment_kind(node.settings.deployment.kind.clone())
.work_dir_alias(node.settings.deployment.work_dir_alias.clone())
.init()?;
let mut deployer = RepoEntryDeployer::new(&entry);
deployer.add_excluded(node.settings.excluded.iter().flatten());
Ok(Self { entry, deployer })
}
pub fn new_open(name: impl AsRef<str>, node: &NodeEntry) -> Result<Self> {
let entry = if data_dir()?.join(name.as_ref()).exists() {
RepoEntry::builder(name.as_ref())?
.url(&node.settings.url)
.deployment_kind(node.settings.deployment.kind.clone())
.work_dir_alias(node.settings.deployment.work_dir_alias.clone())
.open()?
} else {
let bar = ProgressBar::no_length();
let entry = RepoEntry::builder(name.as_ref())?
.url(&node.settings.url)
.deployment_kind(node.settings.deployment.kind.clone())
.work_dir_alias(node.settings.deployment.work_dir_alias.clone())
.authentication_prompter(ProgressBarAuthenticator::new(ProgressBarKind::SingleBar(
bar.clone(),
)))
.clone(&bar)?;
bar.finish_and_clear();
entry
};
let mut deployer = RepoEntryDeployer::new(&entry);
deployer.add_excluded(node.settings.excluded.iter().flatten());
Ok(Self { entry, deployer })
}
#[instrument(skip(self), level = "debug")]
pub fn nuke(&self) -> Result<()> {
self.deploy(DeployAction::Undeploy)?;
remove_dir_all(self.path())?;
info!("Nuke node {:?} from cluster", self.entry.name());
Ok(())
}
pub fn path(&self) -> &Path {
self.entry.path()
}
pub fn name(&self) -> &str {
self.entry.name()
}
pub fn is_bare_alias(&self) -> bool {
self.entry.is_bare_alias()
}
pub fn is_deployed(&self, state: DeployState) -> Result<bool> {
is_deployed(&self.entry, &self.deployer.excluded, state)
}
pub fn current_branch(&self) -> Result<String> {
self.entry.current_branch()
}
pub fn deploy(&self, action: DeployAction) -> Result<()> {
match self.entry.deployment_kind {
DeploymentKind::Normal => {
self.deployer.deploy_with(NormalDeployment, &self.entry, action)
}
DeploymentKind::BareAlias => {
self.deployer.deploy_with(BareAliasDeployment, &self.entry, action)
}
}
}
pub fn gitcall(&self, args: impl IntoIterator<Item = impl Into<OsString>>) -> Result<()> {
self.entry.gitcall_interactive(args)
}
}
#[derive(Debug)]
pub struct MultiNodeClone {
nodes: Vec<RepoEntryBuilder>,
multi_bar: MultiProgress,
jobs: Option<usize>,
}
impl MultiNodeClone {
pub fn new(cluster: &Cluster, jobs: Option<usize>) -> Result<Self> {
let multi_bar = MultiProgress::new();
let mut nodes: Vec<RepoEntryBuilder> = Vec::new();
for (name, node) in &cluster.nodes {
let repo = RepoEntryBuilder::new(name)?
.url(&node.settings.url)
.deployment_kind(node.settings.deployment.kind.clone())
.work_dir_alias(node.settings.deployment.work_dir_alias.clone())
.authentication_prompter(ProgressBarAuthenticator::new(ProgressBarKind::MultiBar(
multi_bar.clone(),
)));
nodes.push(repo);
}
Ok(Self { nodes, multi_bar, jobs })
}
pub async fn clone_all(self) -> Result<()> {
let mut bars = Vec::new();
let results = Arc::new(Mutex::new(Vec::new()));
stream::iter(self.nodes)
.for_each_concurrent(self.jobs, |node| {
let results = results.clone();
let bar = self.multi_bar.add(ProgressBar::no_length());
bars.push(bar.clone());
async move {
let node_name = node.name.clone();
let result = tokio::spawn(async move { node.clone(&bar) }).await;
let mut guard = results.lock().unwrap();
guard.push(
result.map_err(|err| anyhow!("Failed to clone {node_name:?}: {err:?}")),
);
drop(guard);
}
})
.await;
for bar in bars {
bar.finish_and_clear();
}
let results = Arc::try_unwrap(results).unwrap().into_inner().unwrap();
let _ = results.into_iter().flatten().bcollect::<Vec<_>>()?;
Ok(())
}
}
#[derive(Debug)]
pub struct TablizeCluster<'cluster> {
root: &'cluster Root,
cluster: &'cluster Cluster,
}
impl<'cluster> TablizeCluster<'cluster> {
pub fn new(root: &'cluster Root, cluster: &'cluster Cluster) -> Self {
Self { root, cluster }
}
pub fn names_only(&self) -> Result<()> {
let mut builder = tabled::builder::Builder::new();
builder.push_record(["<root>"]);
let mut nodes: Vec<Node> = self
.cluster
.nodes
.iter()
.map(|(name, node)| Node::new_open(name, node))
.collect::<Result<Vec<_>>>()?;
nodes.sort_by(|a, b| a.name().cmp(b.name()));
for node in &nodes {
builder.push_record([node.name()]);
}
let mut table = builder.build();
table.with(tabled::settings::Style::ascii_rounded());
info!("Name only listing:\n{table}");
Ok(())
}
#[instrument(skip(self), level = "debug")]
pub fn fancy(&self) -> Result<()> {
let mut builder = tabled::builder::Builder::new();
let state = if is_deployed(
&self.root.entry,
&self.root.deployer.excluded,
DeployState::WithExcluded,
)? {
"deployed fully"
} else {
"deployed"
};
builder.push_record(["bare-alias", "<root>", state, self.root.current_branch()?.as_str()]);
let mut nodes: Vec<Node> = self
.cluster
.nodes
.iter()
.map(|(name, node)| Node::new_open(name, node))
.collect::<Result<Vec<_>>>()?;
nodes.sort_by(|a, b| a.name().cmp(b.name()));
for node in &nodes {
let (deploy, state) = if node.entry.is_bare_alias() {
if is_deployed(&node.entry, &node.deployer.excluded, DeployState::WithExcluded)? {
("bare-alias", "deployed fully")
} else if is_deployed(
&node.entry,
&node.deployer.excluded,
DeployState::WithoutExcluded,
)? {
("bare-alias", "deployed")
} else {
("bare-alias", "undeployed")
}
} else {
("[node:normal]", "undeployable")
};
builder.push_record([deploy, node.name(), state, node.current_branch()?.as_str()]);
}
let mut table = builder.build();
table.with(tabled::settings::Style::ascii_rounded());
info!("Fancy listing:\n{table}");
Ok(())
}
}
pub(crate) struct RepoEntry {
name: String,
repository: Repository,
deployment_kind: DeploymentKind,
work_dir_alias: WorkDirAlias,
authenticator: GitAuthenticator,
}
impl RepoEntry {
pub(crate) fn builder(name: impl Into<String>) -> Result<RepoEntryBuilder> {
RepoEntryBuilder::new(name)
}
pub(crate) fn set_deployment(
&mut self,
deployment_kind: DeploymentKind,
work_dir_alias: WorkDirAlias,
) {
self.deployment_kind = deployment_kind;
self.work_dir_alias = work_dir_alias;
}
pub(crate) fn is_empty(&self) -> Result<bool> {
match self.repository.head() {
Ok(_) => {
let mut revwalk = self.repository.revwalk()?;
revwalk.push_head()?;
let mut no_commits = true;
if revwalk.flatten().next().is_some() {
no_commits = false;
}
Ok(no_commits)
}
Err(_) => Ok(true),
}
}
pub(crate) fn is_bare_alias(&self) -> bool {
self.repository.is_bare() && self.deployment_kind.is_bare_alias()
}
pub(crate) fn name(&self) -> &str {
&self.name
}
pub(crate) fn path(&self) -> &Path {
self.repository.path()
}
pub(crate) fn current_branch(&self) -> Result<String> {
let shorthand = self.repository.head()?.shorthand_bytes().to_vec();
Ok(String::from_utf8_lossy(shorthand.as_slice()).into_owned())
}
#[instrument(skip(self, args), level = "debug")]
pub(crate) fn gitcall_non_interactive(
&self,
args: impl IntoIterator<Item = impl Into<OsString>>,
) -> Result<String> {
let args = self.expand_bin_args(args);
debug!("Run non interactive git with {args:?}");
syscall_non_interactive("git", args)
}
#[instrument(skip(self, args), level = "debug")]
pub(crate) fn gitcall_interactive(
&self,
args: impl IntoIterator<Item = impl Into<OsString>>,
) -> Result<()> {
info!("Interactive call to git for {:?}", self.name);
let args = self.expand_bin_args(args);
debug!("Run interactive git with {args:?}");
syscall_interactive("git", args)
}
fn expand_bin_args(
&self,
args: impl IntoIterator<Item = impl Into<OsString>>,
) -> Vec<OsString> {
let gitdir = self.repository.path().to_string_lossy().into_owned().into();
let path_args: Vec<OsString> = match &self.deployment_kind {
DeploymentKind::Normal => vec!["--git-dir".into(), gitdir],
DeploymentKind::BareAlias => {
vec![
"--git-dir".into(),
gitdir,
"--work-tree".into(),
self.work_dir_alias.to_os_string(),
]
}
};
let mut bin_args: Vec<OsString> = Vec::new();
bin_args.extend(path_args);
bin_args.extend(args.into_iter().map(Into::into));
bin_args
}
}
impl std::fmt::Debug for RepoEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "RepoEntry {{ name: {:?}, ", self.name)?;
write!(f, "repository: (git2 stuff), ")?;
write!(f, "deployment_kind: {:?} ", self.deployment_kind)?;
write!(f, "work_dir_alias: {:?} ", self.work_dir_alias)?;
writeln!(f, "authenticator: {:?} }}", self.authenticator)
}
}
#[derive(Debug)]
pub(crate) struct RepoEntryBuilder {
name: String,
path: PathBuf,
url: String,
deployment_kind: DeploymentKind,
work_dir_alias: WorkDirAlias,
authenticator: GitAuthenticator,
}
impl RepoEntryBuilder {
pub(crate) fn new(name: impl Into<String>) -> Result<Self> {
let name = name.into();
let path = data_dir()?.join(&name);
Ok(Self {
name,
path,
url: String::default(),
deployment_kind: DeploymentKind::BareAlias,
work_dir_alias: WorkDirAlias::try_default()?,
authenticator: GitAuthenticator::default(),
})
}
pub(crate) fn deployment_kind(mut self, kind: DeploymentKind) -> Self {
self.deployment_kind = kind;
self
}
pub(crate) fn work_dir_alias(mut self, path: WorkDirAlias) -> Self {
self.work_dir_alias = path;
self
}
pub(crate) fn url(mut self, url: impl Into<String>) -> Self {
self.url = url.into();
self
}
pub(crate) fn authentication_prompter(
mut self,
prompter: impl Prompter + Clone + 'static,
) -> Self {
self.authenticator = self.authenticator.set_prompter(prompter);
self
}
pub(crate) fn clone(self, bar: &ProgressBar) -> Result<RepoEntry> {
let style = ProgressStyle::with_template(
"{elapsed_precise:.green} {msg:<50} [{wide_bar:.yellow/blue}]",
)?
.progress_chars("-Cco.");
bar.set_style(style);
bar.set_message(format!("{} - {}", self.name, self.url));
bar.enable_steady_tick(std::time::Duration::from_millis(100));
let mut throttle = Instant::now();
let config = Config::open_default()?;
let mut rc = RemoteCallbacks::new();
rc.credentials(self.authenticator.credentials(&config));
rc.transfer_progress(|progress| {
let stats = progress.to_owned();
let bar_size = stats.total_objects() as u64;
let bar_pos = stats.received_objects() as u64;
if throttle.elapsed() > Duration::from_millis(50) {
throttle = Instant::now();
bar.set_length(bar_size);
bar.set_position(bar_pos);
}
true
});
let mut fo = FetchOptions::new();
fo.remote_callbacks(rc);
let repository = RepoBuilder::new()
.bare(self.deployment_kind.is_bare_alias())
.fetch_options(fo)
.clone(&self.url, &self.path)?;
if self.deployment_kind.is_bare_alias() {
let mut config = repository.config()?;
config.set_str("status.showUntrackedFiles", "no")?;
config.set_str("core.sparseCheckout", "true")?;
}
Ok(RepoEntry {
name: self.name,
repository,
deployment_kind: self.deployment_kind,
work_dir_alias: self.work_dir_alias,
authenticator: self.authenticator,
})
}
pub(crate) fn init(self) -> Result<RepoEntry> {
let mut opts = RepositoryInitOptions::new();
opts.bare(self.deployment_kind.is_bare_alias());
let repository = Repository::init_opts(&self.path, &opts)?;
if self.deployment_kind.is_bare_alias() {
let mut config = repository.config()?;
config.set_str("status.showUntrackedFiles", "no")?;
config.set_str("core.sparseCheckout", "true")?;
}
Ok(RepoEntry {
name: self.name,
repository,
deployment_kind: self.deployment_kind,
work_dir_alias: self.work_dir_alias,
authenticator: self.authenticator,
})
}
pub(crate) fn open(self) -> Result<RepoEntry> {
let repository = Repository::open(&self.path)?;
Ok(RepoEntry {
name: self.name,
repository,
deployment_kind: self.deployment_kind,
work_dir_alias: self.work_dir_alias,
authenticator: self.authenticator,
})
}
}
pub(crate) trait Deployment {
fn deploy_action(
&self,
entry: &RepoEntry,
excluded: &SparseCheckout,
action: DeployAction,
) -> Result<()>;
}
#[derive(Debug)]
pub(crate) struct RepoEntryDeployer {
excluded: SparseCheckout,
}
impl RepoEntryDeployer {
pub(crate) fn new(entry: &RepoEntry) -> Self {
let mut excluded = SparseCheckout::new();
excluded.set_sparse_path(entry.path());
Self { excluded }
}
pub(crate) fn add_excluded(&mut self, rules: impl IntoIterator<Item = impl Into<String>>) {
self.excluded.add_exclusions(rules);
}
pub(crate) fn deploy_with(
&self,
deployer: impl Deployment,
entry: &RepoEntry,
action: DeployAction,
) -> Result<()> {
deployer.deploy_action(entry, &self.excluded, action)
}
}
pub(crate) struct RootDeployment;
impl Deployment for RootDeployment {
fn deploy_action(
&self,
entry: &RepoEntry,
excluded: &SparseCheckout,
action: DeployAction,
) -> Result<()> {
if entry.is_empty()? {
warn!("Root repository is empty, nothing to deploy");
return Ok(());
}
if !entry.is_bare_alias() {
return Err(anyhow!(
"Root repository was somehow defined as normal when it should be bare-alias"
));
}
let msg = match action {
DeployAction::Deploy => {
if is_deployed(entry, excluded, DeployState::WithoutExcluded)? {
return Ok(());
}
warn!("Root repository not deployed");
excluded.write_rules(ExcludeAction::ExcludeUnwanted)?;
"Deploy root, because it must always be deployed".to_string()
}
DeployAction::DeployAll => {
if is_deployed(entry, excluded, DeployState::WithExcluded)? {
warn!("Root repository is already deployed fully");
return Ok(());
}
excluded.write_rules(ExcludeAction::IncludeAll)?;
"Deploy all of root repository".to_string()
}
DeployAction::Undeploy => {
warn!("Root repository cannot be undeployed");
return Ok(());
}
DeployAction::UndeployExcludes => {
if !is_deployed(entry, excluded, DeployState::WithExcluded)? {
warn!("Root repository excluded files are undeployed");
return Ok(());
}
excluded.write_rules(ExcludeAction::ExcludeUnwanted)?;
"Undeploy excluded files of root".to_string()
}
};
let output = entry.gitcall_non_interactive(["checkout"])?;
info!("{msg}\n{output}");
Ok(())
}
}
pub(crate) struct NormalDeployment;
impl Deployment for NormalDeployment {
fn deploy_action(
&self,
entry: &RepoEntry,
_excluded: &SparseCheckout,
_action: DeployAction,
) -> Result<()> {
if entry.is_bare_alias() {
return Err(anyhow!(
"Repository {:?} defined as normal, but is bare-alias",
entry.name
));
}
info!("Repository {:?} is normal, no deployment needed", entry.name());
Ok(())
}
}
pub(crate) struct BareAliasDeployment;
impl Deployment for BareAliasDeployment {
fn deploy_action(
&self,
entry: &RepoEntry,
excluded: &SparseCheckout,
action: DeployAction,
) -> Result<()> {
if entry.is_empty()? {
warn!("Repository {:?} is empty, nothing to deploy", entry.name());
return Ok(());
}
if !entry.is_bare_alias() {
return Err(anyhow!(
"Repository {:?} defined as bare-alias, but is normal",
entry.name
));
}
let msg = match action {
DeployAction::Deploy => {
if is_deployed(entry, excluded, DeployState::WithoutExcluded)? {
warn!("Repository {:?} is already deployed", entry.name);
return Ok(());
}
excluded.write_rules(ExcludeAction::ExcludeUnwanted)?;
format!("Deploy {:?}", entry.name)
}
DeployAction::DeployAll => {
if is_deployed(entry, excluded, DeployState::WithExcluded)? {
warn!("Repository {:?} is already deployed fully", entry.name);
return Ok(());
}
excluded.write_rules(ExcludeAction::IncludeAll)?;
format!("Deploy all of {:?}", entry.name)
}
DeployAction::Undeploy => {
if !is_deployed(entry, excluded, DeployState::WithoutExcluded)? {
warn!("Repository {:?} is already undeployed fully", entry.name);
return Ok(());
}
excluded.write_rules(ExcludeAction::ExcludeAll)?;
format!("Undeploy {:?}", entry.name)
}
DeployAction::UndeployExcludes => {
if !is_deployed(entry, excluded, DeployState::WithExcluded)? {
warn!("Repository {:?} excluded files are already undeployed", entry.name);
return Ok(());
}
excluded.write_rules(ExcludeAction::ExcludeUnwanted)?;
format!("Undeploy excluded files of {:?}", entry.name)
}
};
let output = entry.gitcall_non_interactive(["checkout"])?;
info!("{msg}\n{output}");
Ok(())
}
}
fn is_deployed(entry: &RepoEntry, excluded: &SparseCheckout, state: DeployState) -> Result<bool> {
if entry.is_empty()? {
return Ok(false);
}
let work_dir_alias = match &entry.deployment_kind {
DeploymentKind::Normal => return Ok(false),
DeploymentKind::BareAlias => &entry.work_dir_alias,
};
let mut entries: Vec<String> =
list_file_paths(entry)?.into_iter().map(|p| p.to_string_lossy().into_owned()).collect();
if state == DeployState::WithoutExcluded {
let result = glob_match(excluded.iter(), entries.iter());
entries.retain(|x| !result.contains(x));
}
for entry in entries {
let path = work_dir_alias.0.join(entry);
if !path.exists() {
return Ok(false);
}
}
Ok(true)
}
fn list_file_paths(entry: &RepoEntry) -> Result<Vec<PathBuf>> {
let mut entries = Vec::new();
let commit = entry.repository.head()?.peel_to_commit()?;
let tree = commit.tree()?;
let mut trees_and_paths = VecDeque::new();
trees_and_paths.push_front((tree, PathBuf::new()));
while let Some((tree, path)) = trees_and_paths.pop_front() {
for tree_entry in &tree {
match tree_entry.kind() {
Some(ObjectType::Tree) => {
let next_tree = entry.repository.find_tree(tree_entry.id())?;
let next_path = path.join(bytes_to_path(tree_entry.name_bytes()));
trees_and_paths.push_front((next_tree, next_path));
}
Some(ObjectType::Blob) => {
let full_path = path.join(bytes_to_path(tree_entry.name_bytes()));
entries.push(full_path);
}
_ => continue,
}
}
}
Ok(entries)
}
#[cfg(unix)]
fn bytes_to_path(bytes: &[u8]) -> &Path {
use std::os::unix::prelude::*;
Path::new(OsStr::from_bytes(bytes))
}
#[cfg(windows)]
fn bytes_to_path(bytes: &[u8]) -> PathBuf {
use std::str;
Path::new(str::from_utf8(bytes).unwrap())
}
#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
pub enum DeployState {
#[default]
WithoutExcluded,
WithExcluded,
}
#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
pub enum DeployAction {
#[default]
Deploy,
DeployAll,
Undeploy,
UndeployExcludes,
}
#[derive(Clone)]
pub(crate) struct ProgressBarAuthenticator {
bar_kind: ProgressBarKind,
}
impl ProgressBarAuthenticator {
pub(crate) fn new(bar_kind: ProgressBarKind) -> Self {
Self { bar_kind }
}
}
impl Prompter for ProgressBarAuthenticator {
#[instrument(skip(self, url, _git_config), level = "debug")]
fn prompt_username_password(
&mut self,
url: &str,
_git_config: &git2::Config,
) -> Option<(String, String)> {
let prompt = || -> Option<(String, String)> {
info!("Authentication required for {url}");
let username = Text::new("username").prompt().unwrap();
let password = Password::new("password").without_confirmation().prompt().unwrap();
Some((username, password))
};
match &self.bar_kind {
ProgressBarKind::MultiBar(bar) => bar.suspend(prompt),
ProgressBarKind::SingleBar(bar) => bar.suspend(prompt),
}
}
#[instrument(skip(self, username, url, _git_config), level = "debug")]
fn prompt_password(
&mut self,
username: &str,
url: &str,
_git_config: &git2::Config,
) -> Option<String> {
let prompt = || -> Option<String> {
info!("Authentication required for {url} for user {username}");
let password = Password::new("password").without_confirmation().prompt().unwrap();
Some(password)
};
match &self.bar_kind {
ProgressBarKind::MultiBar(bar) => bar.suspend(prompt),
ProgressBarKind::SingleBar(bar) => bar.suspend(prompt),
}
}
#[instrument(skip(self, private_key_path, _git_config), level = "debug")]
fn prompt_ssh_key_passphrase(
&mut self,
private_key_path: &Path,
_git_config: &git2::Config,
) -> Option<String> {
let prompt = || -> Option<String> {
info!("Authentication required for {}", private_key_path.display());
let password = Password::new("password").without_confirmation().prompt().unwrap();
Some(password)
};
match &self.bar_kind {
ProgressBarKind::MultiBar(bar) => bar.suspend(prompt),
ProgressBarKind::SingleBar(bar) => bar.suspend(prompt),
}
}
}
#[derive(Clone)]
pub(crate) enum ProgressBarKind {
SingleBar(ProgressBar),
MultiBar(MultiProgress),
}
#[derive(Debug, Default, Clone)]
pub(crate) struct SparseCheckout {
sparse_path: PathBuf,
exclusion_rules: Vec<String>,
}
impl SparseCheckout {
pub(crate) fn new() -> Self {
SparseCheckout::default()
}
pub(crate) fn set_sparse_path(&mut self, gitdir: &Path) {
self.sparse_path = gitdir.join("info/sparse-checkout");
}
pub(crate) fn add_exclusions(&mut self, rules: impl IntoIterator<Item = impl Into<String>>) {
let mut vec = Vec::new();
vec.extend(rules.into_iter().map(Into::into));
self.exclusion_rules = vec;
}
pub(crate) fn write_rules(&self, action: ExcludeAction) -> Result<()> {
let rules: String = match action {
ExcludeAction::ExcludeUnwanted => {
let mut excluded = self.exclusion_rules.iter().fold(String::new(), |mut acc, u| {
writeln!(&mut acc, "!{u}").unwrap();
acc
});
excluded.insert_str(0, "/*\n");
excluded
}
ExcludeAction::IncludeAll => "/*".into(),
ExcludeAction::ExcludeAll => String::default(),
};
let mut file = File::create(&self.sparse_path)
.with_context(|| "Failed to create sparse checkout file")?;
file.write_all(rules.as_bytes()).with_context(|| "Failed to write sparsity rules")?;
Ok(())
}
pub(crate) fn iter(&self) -> SparsityRuleIter<'_> {
SparsityRuleIter { exclusion_rules: &self.exclusion_rules, index: 0 }
}
}
#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
pub(crate) enum ExcludeAction {
#[default]
ExcludeUnwanted,
IncludeAll,
ExcludeAll,
}
#[derive(Debug)]
pub(crate) struct SparsityRuleIter<'rule> {
exclusion_rules: &'rule Vec<String>,
index: usize,
}
impl Iterator for SparsityRuleIter<'_> {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.exclusion_rules.len() {
return None;
}
let mut rule = self.exclusion_rules[self.index].clone();
self.index += 1;
if rule.ends_with('/') {
rule.push('*');
}
Some(rule)
}
}
fn syscall_non_interactive(
cmd: impl AsRef<OsStr>,
args: impl IntoIterator<Item = impl AsRef<OsStr>>,
) -> Result<String> {
let output = Command::new(cmd.as_ref()).args(args).output()?;
let stdout = String::from_utf8_lossy(output.stdout.as_slice()).into_owned();
let stderr = String::from_utf8_lossy(output.stderr.as_slice()).into_owned();
let mut message = String::new();
if !stdout.is_empty() {
message.push_str(format!("stdout: {stdout}").as_str());
}
if !stderr.is_empty() {
message.push_str(format!("stderr: {stderr}").as_str());
}
if !output.status.success() {
return Err(anyhow!("Command {:?} failed:\n{message}", cmd.as_ref()));
}
let message = message
.strip_suffix("\r\n")
.or(message.strip_suffix('\n'))
.map(ToString::to_string)
.unwrap_or(message);
Ok(message)
}
fn syscall_interactive(
cmd: impl AsRef<OsStr>,
args: impl IntoIterator<Item = impl AsRef<OsStr>>,
) -> Result<()> {
let status = Command::new(cmd.as_ref()).args(args).spawn()?.wait()?;
if !status.success() {
return Err(anyhow!("Command {:?} failed", cmd.as_ref()));
}
Ok(())
}