use anyhow::{anyhow, bail, Context};
use log::{info, warn};
use std::{
collections::BTreeSet,
env as stdenv,
ffi::OsString,
fs,
io::Write,
path::{Path, PathBuf},
process,
};
use structopt::StructOpt;
mod app;
mod bootstrap;
mod cargo;
mod changelog;
mod config;
mod csproj;
mod env;
mod errors;
mod github;
mod gitutil;
mod graph;
mod logger;
mod npm;
mod project;
mod pypa;
mod repository;
mod rewriters;
mod version;
mod zenodo;
use errors::Result;
#[derive(Debug, PartialEq, StructOpt)]
#[structopt(about = "automate versioning and releasing")]
struct CrankoOptions {
#[structopt(subcommand)]
command: Commands,
}
trait Command {
fn execute(self) -> Result<i32>;
}
#[allow(clippy::enum_variant_names)]
#[derive(Debug, PartialEq, StructOpt)]
enum Commands {
#[structopt(name = "bootstrap")]
Bootstrap(bootstrap::BootstrapCommand),
#[structopt(name = "cargo")]
Cargo(cargo::CargoCommand),
#[structopt(name = "ci-util")]
CiUtil(CiUtilCommand),
#[structopt(name = "confirm")]
Confirm(ConfirmCommand),
#[structopt(name = "diff")]
Diff(DiffCommand),
#[structopt(name = "github")]
Github(github::GithubCommand),
#[structopt(name = "git-util")]
GitUtil(gitutil::GitUtilCommand),
#[structopt(name = "help")]
Help(HelpCommand),
#[structopt(name = "list-commands")]
ListCommands(ListCommandsCommand),
#[structopt(name = "log")]
Log(LogCommand),
#[structopt(name = "npm")]
Npm(npm::NpmCommand),
#[structopt(name = "python")]
Python(pypa::PythonCommand),
#[structopt(name = "release-workflow")]
ReleaseWorkflow(ReleaseWorkflowCommand),
#[structopt(name = "show")]
Show(ShowCommand),
#[structopt(name = "stage")]
Stage(StageCommand),
#[structopt(name = "status")]
Status(StatusCommand),
#[structopt(name = "zenodo")]
Zenodo(zenodo::ZenodoCommand),
#[structopt(external_subcommand)]
External(Vec<String>),
}
impl Command for Commands {
fn execute(self) -> Result<i32> {
match self {
Commands::Bootstrap(o) => o.execute(),
Commands::Cargo(o) => o.execute(),
Commands::CiUtil(o) => o.execute(),
Commands::Confirm(o) => o.execute(),
Commands::Diff(o) => o.execute(),
Commands::Github(o) => o.execute(),
Commands::GitUtil(o) => o.execute(),
Commands::Help(o) => o.execute(),
Commands::ListCommands(o) => o.execute(),
Commands::Log(o) => o.execute(),
Commands::Npm(o) => o.execute(),
Commands::Python(o) => o.execute(),
Commands::ReleaseWorkflow(o) => o.execute(),
Commands::Show(o) => o.execute(),
Commands::Stage(o) => o.execute(),
Commands::Status(o) => o.execute(),
Commands::Zenodo(o) => o.execute(),
Commands::External(args) => do_external(args),
}
}
}
fn main() {
let opts = CrankoOptions::from_args();
if let Err(e) = logger::Logger::init() {
eprintln!("error: cannot initialize logging backend: {}", e);
process::exit(1);
}
log::set_max_level(log::LevelFilter::Info);
process::exit(errors::report(opts.command.execute()));
}
#[derive(Debug, PartialEq, StructOpt)]
struct CiUtilCommand {
#[structopt(subcommand)]
command: CiUtilCommands,
}
#[derive(Debug, PartialEq, StructOpt)]
enum CiUtilCommands {
#[structopt(name = "env-to-file")]
EnvToFile(CiUtilEnvToFileCommand),
}
impl Command for CiUtilCommand {
fn execute(self) -> Result<i32> {
match self.command {
CiUtilCommands::EnvToFile(o) => o.execute(),
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum EnvDecodingMode {
Text,
Base64,
}
impl std::str::FromStr for EnvDecodingMode {
type Err = errors::Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"text" => Ok(EnvDecodingMode::Text),
"base64" => Ok(EnvDecodingMode::Base64),
_ => Err(anyhow!("unrecognized encoding mode `{}`", s)),
}
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct CiUtilEnvToFileCommand {
#[structopt(
long = "decode",
default_value = "text",
help = "How to decode the variable value into bytes"
)]
decode_mode: EnvDecodingMode,
#[structopt(help = "Name of the environment variable")]
var_name: OsString,
#[structopt(help = "The destination file name")]
file_name: PathBuf,
}
impl Command for CiUtilEnvToFileCommand {
fn execute(self) -> Result<i32> {
use std::fs::OpenOptions;
let value = stdenv::var_os(&self.var_name).ok_or_else(|| {
anyhow!(
"environment variable `{}` not available",
&self.var_name.to_string_lossy()
)
})?;
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
fn platform_options(o: &mut OpenOptions) {
use std::os::unix::fs::OpenOptionsExt;
o.mode(0o600);
}
#[cfg(not(unix))]
fn platform_options(_o: &mut OpenOptions) {}
platform_options(&mut options);
let mut file = options.open(&self.file_name).with_context(|| {
format!(
"cannot securely open `{}` for writing",
self.file_name.display()
)
})?;
let value = value.into_string().map_err(|_| {
anyhow!(
"cannot interpret value of environment variable `{}` as Unicode text",
self.var_name.to_string_lossy()
)
})?;
let b = match self.decode_mode {
EnvDecodingMode::Text => value.into_bytes(),
EnvDecodingMode::Base64 => base64::decode(&value).with_context(|| {
format!(
"failed to decode value of environment variable `{}` as BASE64",
self.var_name.to_string_lossy()
)
})?,
};
file.write_all(&b[..]).with_context(|| {
format!(
"failed trying to write data to file `{}`",
self.file_name.display()
)
})?;
Ok(0)
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct ConfirmCommand {
#[structopt(
short = "f",
long = "force",
help = "Force operation even in unexpected conditions"
)]
force: bool,
}
impl Command for ConfirmCommand {
fn execute(self) -> Result<i32> {
use project::DepRequirement;
let mut sess = app::AppSession::initialize_default()?;
sess.ensure_not_ci(self.force)?;
if let Err(e) = sess.ensure_changelog_clean() {
warn!(
"not recommended to confirm with a modified working tree ({})",
e
);
if !self.force {
bail!("refusing to proceed (use `--force` to override)");
}
}
let histories = sess.analyze_histories()?;
let mut changes = repository::ChangeList::default();
let mut rc_info = Vec::new();
sess.solve_internal_deps(|repo, graph, ident| {
let history = histories.lookup(ident);
let dirty_allowed = self.force;
let mut updated_version = false;
if let Some(info) =
repo.scan_rc_info(graph.lookup(ident), &mut changes, dirty_allowed)?
{
let (old_version_text, new_version) = {
let proj = graph.lookup_mut(ident);
let last_rel_info = history.release_info(repo)?;
let scheme = proj.version.parse_bump_scheme(&info.bump_spec)?;
if let Some(last_release) = last_rel_info.lookup_project(proj) {
proj.version = proj.version.parse_like(&last_release.version)?;
scheme.apply(&mut proj.version)?;
(last_release.version.clone(), proj.version.clone())
} else {
scheme.apply(&mut proj.version)?;
("[no previous releases]".to_owned(), proj.version.clone())
}
};
let proj = graph.lookup(ident);
if history.n_commits() == 0 {
warn!(
"project `{}` is being staged for release, but does not \
seem to have been modified since its last release",
proj.user_facing_name
);
}
info!(
"{}: {} (expected: {} => {})",
proj.user_facing_name, info.bump_spec, old_version_text, new_version
);
rc_info.push(info);
updated_version = true;
for dep in &proj.internal_deps[..] {
let dproj = graph.lookup(dep.ident);
let req_text = match &dep.cranko_requirement {
DepRequirement::Commit(_) => {
format!(">= {}", dep.resolved_version.as_ref().unwrap())
}
DepRequirement::Manual(t) => format!("{} (manual)", t),
DepRequirement::Unavailable => {
"** version requirement unavailable **".to_owned()
}
};
info!(" internal dep {}: {}", dproj.user_facing_name, req_text);
}
} else if history.n_commits() > 0 {
warn!(
"project `{}` has been changed since its last release, \
but is not part of the rc submission",
graph.lookup(ident).user_facing_name
);
}
Ok(updated_version)
})?;
if rc_info.is_empty() {
warn!("no releases seem to have been staged; use \"cranko stage\"?");
return Ok(0);
}
sess.make_rc_commit(rc_info, &changes)?;
info!(
"staged rc commit to `{}` branch",
sess.repo.upstream_rc_name()
);
sess.repo.hard_reset_changes(&changes)?;
Ok(0)
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct DiffCommand {
#[structopt(help = "Name of the project to query")]
proj_names: Vec<String>,
}
impl Command for DiffCommand {
fn execute(self) -> Result<i32> {
let sess = app::AppSession::initialize_default()?;
let mut q = graph::GraphQueryBuilder::default();
q.names(self.proj_names);
let idents = sess.graph().query(q)?;
if idents.len() != 1 {
bail!("must specify exactly one project to diff");
}
let ident = idents[0];
let dir = sess
.repo
.resolve_workdir(sess.graph().lookup(ident).prefix());
let histories = atry!(
sess.analyze_histories();
["failed to analyze the repository history"]
);
let history = histories.lookup(ident);
let commit = match history.main_branch_commit(&sess.repo)? {
Some(c) => c,
None => {
println!(
"no known last release commit to diff against for `{}`",
sess.graph().lookup(ident).user_facing_name
);
return Ok(0);
}
};
let mut cmd = process::Command::new("git");
cmd.arg("diff");
cmd.arg(&commit.to_string()[..8]);
cmd.arg("--");
cmd.arg(dir);
exec_or_spawn(&mut cmd)
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct HelpCommand {
command: Option<String>,
}
impl Command for HelpCommand {
fn execute(self) -> Result<i32> {
match self.command.as_deref() {
None => {
CrankoOptions::clap().print_long_help()?;
println!();
Ok(0)
}
Some(cmd) => {
CrankoOptions::from_iter(&[&stdenv::args().next().unwrap(), cmd, "--help"])
.command
.execute()
}
}
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct ListCommandsCommand {}
impl Command for ListCommandsCommand {
fn execute(self) -> Result<i32> {
println!("Currently available \"cranko\" subcommands:\n");
for command in list_commands() {
println!(" {}", command);
}
Ok(0)
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct LogCommand {
#[structopt(long = "stat", help = "Show a diffstat with each commit")]
stat: bool,
#[structopt(help = "Name of the project to query")]
proj_names: Vec<String>,
}
impl Command for LogCommand {
fn execute(self) -> Result<i32> {
let sess = app::AppSession::initialize_default()?;
let mut q = graph::GraphQueryBuilder::default();
q.names(self.proj_names);
let idents = sess.graph().query(q)?;
if idents.len() != 1 {
bail!("must specify exactly one project to log");
}
let ident = idents[0];
let histories = atry!(
sess.analyze_histories();
["failed to analyze the repository history"]
);
let history = histories.lookup(ident);
if history.n_commits() == 0 {
println!(
"no relevant commits to show for `{}`",
sess.graph().lookup(ident).user_facing_name
);
return Ok(0);
}
let mut cmd = process::Command::new("git");
cmd.arg("show");
if self.stat {
cmd.arg("--stat");
} else {
cmd.arg("--no-patch");
}
for cid in history.commits() {
cmd.arg(&cid.to_string()[..8]);
}
exec_or_spawn(&mut cmd)
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct ReleaseWorkflowCommand {
#[structopt(subcommand)]
command: ReleaseWorkflowCommands,
}
#[derive(Debug, PartialEq, StructOpt)]
enum ReleaseWorkflowCommands {
#[structopt(name = "apply-versions")]
ApplyVersions(ReleaseWorkflowApplyVersionsCommand),
#[structopt(name = "commit")]
Commit(ReleaseWorkflowCommitCommand),
#[structopt(name = "tag")]
Tag(ReleaseWorkflowTagCommand),
}
impl Command for ReleaseWorkflowCommand {
fn execute(self) -> Result<i32> {
match self.command {
ReleaseWorkflowCommands::ApplyVersions(o) => o.execute(),
ReleaseWorkflowCommands::Commit(o) => o.execute(),
ReleaseWorkflowCommands::Tag(o) => o.execute(),
}
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct ReleaseWorkflowApplyVersionsCommand {
#[structopt(
short = "f",
long = "force",
help = "Force operation even in unexpected conditions"
)]
force: bool,
}
impl Command for ReleaseWorkflowApplyVersionsCommand {
fn execute(self) -> Result<i32> {
let mut sess = app::AppSession::initialize_default()?;
sess.ensure_fully_clean()?;
let (dev_mode, rci) = sess.ensure_ci_rc_mode(self.force)?;
if dev_mode {
info!("computing new versions for \"development\" mode");
} else {
info!("computing new versions based on `rc` commit request data");
}
let rel_info = sess.repo.get_latest_release_info()?;
sess.apply_versions(&rci)?;
let mut changes = sess.rewrite()?;
if !dev_mode {
sess.apply_changelogs(rel_info.commit, &rci, &mut changes)?;
}
Ok(0)
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct ReleaseWorkflowCommitCommand {
#[structopt(
short = "f",
long = "force",
help = "Force operation even in unexpected conditions"
)]
force: bool,
}
impl Command for ReleaseWorkflowCommitCommand {
fn execute(self) -> Result<i32> {
let mut sess = app::AppSession::initialize_default()?;
let (_dev, rci) = sess.ensure_ci_rc_mode(self.force)?;
sess.make_release_commit(&rci)?;
Ok(0)
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct ReleaseWorkflowTagCommand {}
impl Command for ReleaseWorkflowTagCommand {
fn execute(self) -> Result<i32> {
let mut sess = app::AppSession::initialize_default()?;
let (dev_mode, rel_info) = sess.ensure_ci_release_mode()?;
if dev_mode {
bail!("refusing to create tags in dev mode");
}
sess.create_tags(&rel_info)?;
Ok(0)
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct ShowCommand {
#[structopt(subcommand)]
command: ShowCommands,
}
#[derive(Debug, PartialEq, StructOpt)]
enum ShowCommands {
#[structopt(name = "cranko-version-doi")]
CrankoVersionDoi(ShowCrankoVersionDoiCommand),
#[structopt(name = "cranko-concept-doi")]
CrankoConceptDoi(ShowCrankoConceptDoiCommand),
#[structopt(name = "if-released")]
IfReleased(ShowIfReleasedCommand),
#[structopt(name = "tctag")]
TcTag(ShowTcTagCommand),
#[structopt(name = "toposort")]
Toposort(ShowToposortCommand),
#[structopt(name = "version")]
Version(ShowVersionCommand),
}
impl Command for ShowCommand {
fn execute(self) -> Result<i32> {
match self.command {
ShowCommands::CrankoVersionDoi(o) => o.execute(),
ShowCommands::CrankoConceptDoi(o) => o.execute(),
ShowCommands::IfReleased(o) => o.execute(),
ShowCommands::TcTag(o) => o.execute(),
ShowCommands::Toposort(o) => o.execute(),
ShowCommands::Version(o) => o.execute(),
}
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct ShowCrankoVersionDoiCommand {}
impl Command for ShowCrankoVersionDoiCommand {
fn execute(self) -> Result<i32> {
let doi = "10.5281/zenodo.7383887";
if doi.starts_with("xx.") {
warn!("you are running a development build; the printed value is not a real DOI");
}
println!("{}", doi);
Ok(0)
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct ShowCrankoConceptDoiCommand {}
impl Command for ShowCrankoConceptDoiCommand {
fn execute(self) -> Result<i32> {
let doi = "10.5281/zenodo.6981679";
if doi.starts_with("xx.") {
warn!("you are running a development build; the printed value is not a real DOI");
}
println!("{}", doi);
Ok(0)
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct ShowIfReleasedCommand {
#[structopt(
long = "exit-code",
help = "Exit the program with success if released, failure if not"
)]
exit_code: bool,
#[structopt(long = "tf", help = "Print \"true\" if released, \"false\" if not")]
true_false: bool,
#[structopt(help = "Name of the project to query")]
proj_names: Vec<String>,
}
impl Command for ShowIfReleasedCommand {
fn execute(self) -> Result<i32> {
let sess = app::AppSession::initialize_default()?;
if !(self.exit_code || self.true_false) {
bail!("must specify at least one output mechanism");
}
let mut q = graph::GraphQueryBuilder::default();
q.names(self.proj_names);
let idents = sess.graph().query(q)?;
if idents.len() != 1 {
bail!("must specify exactly one project to show");
}
let (_dev_mode, rel_info) = sess.ensure_ci_release_mode()?;
let proj = sess.graph().lookup(idents[0]);
let was_released = rel_info.lookup_if_released(proj).is_some();
if self.true_false {
println!("{}", if was_released { "true" } else { "false" });
}
Ok(if self.exit_code {
if was_released {
0
} else {
1
}
} else {
0
})
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct ShowTcTagCommand {}
impl Command for ShowTcTagCommand {
fn execute(self) -> Result<i32> {
use chrono::prelude::*;
use rand::{distributions::Alphanumeric, Rng};
let utc: DateTime<Utc> = Utc::now();
let mut rng = rand::thread_rng();
let chars: String = std::iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.map(char::from)
.take(7)
.collect();
println!(
"thiscommit:{:>04}-{:>02}-{:>02}:{}",
utc.year(),
utc.month(),
utc.day(),
chars
);
Ok(0)
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct ShowToposortCommand {}
impl Command for ShowToposortCommand {
fn execute(self) -> Result<i32> {
let sess = app::AppSession::initialize_default()?;
let graph = sess.graph();
for ident in graph.toposorted() {
let proj = graph.lookup(ident);
println!("{}", proj.user_facing_name);
}
Ok(0)
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct ShowVersionCommand {
#[structopt(help = "Name of the project to query")]
proj_names: Vec<String>,
}
impl Command for ShowVersionCommand {
fn execute(self) -> Result<i32> {
let sess = app::AppSession::initialize_default()?;
let mut q = graph::GraphQueryBuilder::default();
q.names(self.proj_names);
let idents = sess.graph().query(q)?;
if idents.len() != 1 {
bail!("must specify exactly one project to show");
}
let proj = sess.graph().lookup(idents[0]);
println!("{}", proj.version);
Ok(0)
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct StageCommand {
#[structopt(
short = "f",
long = "force",
help = "Force staging even in unexpected conditions"
)]
force: bool,
#[structopt(help = "Name(s) of the project(s) to stage for release")]
proj_names: Vec<String>,
}
impl Command for StageCommand {
fn execute(self) -> Result<i32> {
let sess = app::AppSession::initialize_default()?;
if let Err(e) = sess.ensure_changelog_clean() {
warn!(
"not recommended to stage with a modified working tree ({})",
e
);
if !self.force {
bail!("refusing to proceed (use `--force` to override)");
}
}
sess.ensure_not_ci(self.force)?;
let mut q = graph::GraphQueryBuilder::default();
q.names(self.proj_names);
let no_names = q.no_names();
let idents = sess
.graph()
.query(q)
.context("could not select projects for staging")?;
if idents.is_empty() {
info!("no projects selected");
return Ok(0);
}
let histories = sess.analyze_histories()?;
let mut n_staged = 0;
let rel_info = sess.repo.get_latest_release_info()?;
let mut changes = repository::ChangeList::default();
for ident in &idents {
let proj = sess.graph().lookup(*ident);
let history = histories.lookup(*ident);
let dirty_allowed = self.force;
if sess
.repo
.scan_rc_info(proj, &mut changes, dirty_allowed)?
.is_some()
{
if !no_names {
warn!(
"skipping {}: it appears to have already been staged",
proj.user_facing_name
);
}
continue;
}
if (no_names || !self.force) && history.n_commits() == 0 {
if !no_names {
warn!("no changes detected for project {}", proj.user_facing_name);
}
} else {
println!(
"{}: {} relevant commits",
proj.user_facing_name,
history.n_commits()
);
let commits: Vec<repository::CommitId> =
history.commits().into_iter().copied().collect();
proj.changelog
.draft_release_update(proj, &sess, &commits[..], rel_info.commit)?;
n_staged += 1;
}
}
if no_names && n_staged == 0 {
info!("nothing further to stage at this time");
} else if no_names && n_staged != 1 {
info!("{} of {} projects staged", n_staged, idents.len());
} else if n_staged != idents.len() {
info!("{} of {} selected projects staged", n_staged, idents.len());
}
Ok(0)
}
}
#[derive(Debug, PartialEq, StructOpt)]
struct StatusCommand {
#[structopt(help = "Name(s) of the project(s) to query (default: all)")]
proj_names: Vec<String>,
}
impl Command for StatusCommand {
fn execute(self) -> Result<i32> {
let sess = app::AppSession::initialize_default()?;
let mut q = graph::GraphQueryBuilder::default();
q.names(self.proj_names);
let idents = sess
.graph()
.query(q)
.context("cannot get requested statuses")?;
let histories = sess.analyze_histories()?;
for ident in idents {
let proj = sess.graph().lookup(ident);
let history = histories.lookup(ident);
let n = history.n_commits();
let rel_info = history.release_info(&sess.repo)?;
if let Some(this_info) = rel_info.lookup_project(proj) {
if this_info.age == 0 {
if n == 0 {
println!(
"{}: no relevant commits since {}",
proj.user_facing_name, this_info.version
);
} else {
logger::Logger::println_highlighted(
format!("{}: ", proj.user_facing_name),
n,
format!(" relevant commit(s) since {}", this_info.version),
);
}
} else {
logger::Logger::println_highlighted(
format!("{}: no more than ", proj.user_facing_name),
n,
format!(
" relevant commit(s) since {} (unable to track in detail)",
this_info.version
),
);
}
} else {
println!(
"{}: {} relevant commit(s) since start of history (no releases on record)",
proj.user_facing_name, n
);
}
}
Ok(0)
}
}
#[allow(clippy::redundant_closure)]
fn do_external(all_args: Vec<String>) -> Result<i32> {
let (cmd, args) = all_args.split_first().unwrap();
let command_exe = format!("cranko-{}{}", cmd, stdenv::consts::EXE_SUFFIX);
let path = search_directories()
.iter()
.map(|dir| dir.join(&command_exe))
.find(|file| is_executable(file));
let command = path.ok_or_else(|| {
anyhow!(
"no internal or external subcommand `{0}` is available (install `cranko-{0}`?)",
cmd
)
})?;
exec_or_spawn(process::Command::new(command).args(args))
}
#[cfg(unix)]
fn exec_or_spawn(cmd: &mut process::Command) -> Result<i32> {
use std::os::unix::process::CommandExt;
Err(cmd.exec().into())
}
#[cfg(not(unix))]
fn exec_or_spawn(cmd: &mut process::Command) -> Result<i32> {
Ok(cmd.status()?.code().unwrap())
}
fn list_commands() -> BTreeSet<String> {
let prefix = "cranko-";
let suffix = stdenv::consts::EXE_SUFFIX;
let mut commands = BTreeSet::new();
for dir in search_directories() {
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
_ => continue,
};
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
let filename = match path.file_name().and_then(|s| s.to_str()) {
Some(filename) => filename,
_ => continue,
};
if !filename.starts_with(prefix) || !filename.ends_with(suffix) {
continue;
}
if is_executable(entry.path()) {
let end = filename.len() - suffix.len();
commands.insert(filename[prefix.len()..end].to_string());
}
}
}
commands.insert("bootstrap".to_owned());
commands.insert("cargo".to_owned());
commands.insert("ci-util".to_owned());
commands.insert("confirm".to_owned());
commands.insert("git-util".to_owned());
commands.insert("github".to_owned());
commands.insert("help".to_owned());
commands.insert("list-commands".to_owned());
commands.insert("log".to_owned());
commands.insert("npm".to_owned());
commands.insert("python".to_owned());
commands.insert("release-workflow".to_owned());
commands.insert("show".to_owned());
commands.insert("stage".to_owned());
commands.insert("status".to_owned());
commands
}
#[cfg(unix)]
fn is_executable<P: AsRef<Path>>(path: P) -> bool {
use std::os::unix::prelude::*;
fs::metadata(path)
.map(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(windows)]
fn is_executable<P: AsRef<Path>>(path: P) -> bool {
fs::metadata(path)
.map(|metadata| metadata.is_file())
.unwrap_or(false)
}
fn search_directories() -> Vec<PathBuf> {
let mut dirs = Vec::new();
if let Some(val) = stdenv::var_os("PATH") {
dirs.extend(stdenv::split_paths(&val));
}
dirs
}
#[cfg(not(windows))]
#[macro_export]
macro_rules! write_crlf {
($stream:expr, $format:literal $($rest:tt)*) => {{
use std::io::Write;
write!($stream, $format $($rest)*).and_then(|_x| write!($stream, "\n"))
}}
}
#[cfg(windows)]
#[macro_export]
macro_rules! write_crlf {
($stream:expr, $format:literal $($rest:tt)*) => {{
use std::io::Write;
write!($stream, $format $($rest)*).and_then(|_x| write!($stream, "\r\n"))
}}
}