use std::io::Write;
use std::path::Path;
use crate::cli::{
ArtifactCommand, Command, EventsArgs, ImportArgs, IntegrateArgs, PublishArgs,
PublishBranchArgs, RecoverArgs, RecoverableArgs, RegisterArgs, ReleaseAcknowledgeArgs,
ReleaseCommand, ReleaseLatestArgs, ReleaseStatusArgs, ReleaseTargetsArgs, ReposArgs,
ResolveArgs, RulesCheckArgs, RulesCommand, SessionCommand, SessionHoldersArgs, SessionOpenArgs,
SessionTokenArgs, StatusArgs, SweepArgs, SyncArgs,
};
use crate::error::{self, Error, Result};
use crate::event::EventFilter;
use crate::landed::Landed;
use crate::providers::Providers;
use crate::publish::{PublishOutcome, PublishRequest, Retention, Subject};
use crate::registry::{Registry, RepoType, Workflow};
use crate::releases::{
Acknowledgement, Baseline, Probe, ReleaseAnswer, ReleaseMethod, ReleaseStatus, ReleaseTarget,
TargetName,
};
use crate::session::{Lifecycle, Provenance, Scope, SessionRequest, SessionToken};
use crate::store::{self, Resolution};
use crate::stream::Stream;
use crate::{
git, guidance, import, integrate, lock, policy, provenance, publish, publish_branch, recover,
status, stream, sweep, workspace,
};
pub fn run(command: &Command, providers: &Providers<'_>) -> u8 {
match dispatch(command, providers) {
Ok(code) => code,
Err(error) => {
eprintln!("onevcs: {error}");
publish::exit_code(&error)
}
}
}
fn dispatch(command: &Command, providers: &Providers<'_>) -> Result<u8> {
git::check_bounds()?;
lock::timeout_seconds()?;
match command {
Command::Register(args) => register(args),
Command::Repos(args) => repos(args),
Command::Resolve(args) => resolve(args, providers),
Command::Session { command } => match command {
SessionCommand::Open(args) => session_open(args, providers),
SessionCommand::Adopt(args) => session_adopt(args, providers),
SessionCommand::Close(args) => session_close(args, providers),
SessionCommand::Holders(args) => session_holders(args),
},
Command::Publish(args) => publish_session(args, providers),
Command::PublishBranch(args) => publish_branch(args, providers),
Command::Recover(args) => recover_branch(args, providers),
Command::Recoverable(args) => recoverable(args, providers),
Command::Status(args) => report_status(args, providers),
Command::Import(args) => import_branch(args),
Command::Integrate(args) => integrate_branches(args),
Command::Sync(args) => sync(args),
Command::Sweep(args) => sweep_workspaces(args),
Command::Events(args) => events(args, providers),
Command::Artifact { command } => match command {
ArtifactCommand::Cat(args) => artifact(&args.id),
},
Command::Rules { command } => match command {
RulesCommand::Check(args) => rules_check(args),
},
Command::Release { command } => match command {
ReleaseCommand::Targets(args) => release_targets(args),
ReleaseCommand::Latest(args) => release_latest(args),
ReleaseCommand::Status(args) => release_status(args),
ReleaseCommand::Acknowledge(args) => release_acknowledge(args),
},
}
}
fn register(args: &RegisterArgs) -> Result<u8> {
let origin = args.origin.as_ref().map(url::Url::to_string);
let resolution = store::register(&args.path, origin.as_deref())?;
println!("{}", resolution.key);
println!(" alias: {}", resolution.alias);
println!(
" workflow: {}",
spell_workflow(resolution.identity.workflow)
);
println!(
" repo_type: {}",
spell_repo_type(resolution.identity.repo_type)
);
println!(" gate: {}", resolution.identity.gate);
let coverage = store::merge_path_coverage(&resolution, &resolution.publication);
println!(" merge-path coverage: {}", coverage.describe());
if coverage == store::Coverage::None {
eprintln!(
"onevcs: warning: nothing on this identity's merge path runs a gate, so a \
publication is unproven. Install an executable pre-push hook."
);
}
Ok(0)
}
fn repos(args: &ReposArgs) -> Result<u8> {
let registry = store::load()?;
if registry.identities.is_empty() {
println!("no repositories registered");
return Ok(0);
}
for (key, identity) in ®istry.identities {
println!(
"{key}\t{}\t{}\t{}",
spell_workflow(identity.workflow),
spell_repo_type(identity.repo_type),
identity.gate
);
for (alias, checkout) in ®istry.checkouts {
if checkout.identity != *key {
continue;
}
println!(" {alias}\t{}", checkout.path.display());
if args.audit_gates {
let resolution = Resolution {
key: key.clone(),
identity: identity.clone(),
alias: alias.clone(),
publication: checkout.path.clone(),
};
let coverage = store::merge_path_coverage(&resolution, &checkout.path);
println!(" merge-path coverage: {}", coverage.describe());
}
}
}
Ok(0)
}
fn resolve(args: &ResolveArgs, providers: &Providers<'_>) -> Result<u8> {
let registry = store::load()?;
let resolution = store::resolve(®istry, &args.repo)?;
let identity = providers.vcs.resolve_identity(&args.repo)?;
debug_assert_eq!(identity, resolution.identity);
println!(
"{}",
serde_json::json!({
"identity": resolution.key,
"alias": resolution.alias,
"origin": resolution.identity.origin,
"workflow": spell_workflow(resolution.identity.workflow),
"repo_type": spell_repo_type(resolution.identity.repo_type),
"gate": resolution.identity.gate,
"publication_checkout": resolution.publication.display().to_string(),
})
);
Ok(0)
}
fn session_open(args: &SessionOpenArgs, providers: &Providers<'_>) -> Result<u8> {
let registry = store::load()?;
let request = SessionRequest {
repo: args.repo.clone(),
branch: args.branch.clone(),
base: args.base.clone(),
execution_checkout: args.execution_checkout.clone(),
};
let _ = ®istry;
let session = providers.vcs.open_session(request)?;
println!(
"{}",
serde_json::to_string(&session).map_err(serialization)?
);
Ok(0)
}
fn session_adopt(args: &SessionTokenArgs, providers: &Providers<'_>) -> Result<u8> {
let token = SessionToken(args.token.clone());
let session = providers.vcs.adopt_session(token.clone())?;
println!(
"{}",
serde_json::to_string(&session).map_err(serialization)?
);
if providers.vcs.session(&token)?.provenance == Provenance::IncompleteStep {
eprintln!(
"onevcs: this branch carries incomplete-step provenance, so it must pass its \
merge path through `onevcs recover` before it may be published."
);
}
Ok(0)
}
fn session_close(args: &SessionTokenArgs, providers: &Providers<'_>) -> Result<u8> {
let session = crate::close_session(providers, &SessionToken(args.token.clone()))?;
println!("{} closed", session.token.0);
Ok(0)
}
fn session_holders(args: &SessionHoldersArgs) -> Result<u8> {
let holders = crate::session_holders(&args.repo)?;
if args.json {
println!(
"{}",
serde_json::to_string(&holders).map_err(serialization)?
);
} else {
for holder in holders {
println!(
"{}\t{}\t{}\tpid={}\t{}\t{}",
holder.token.0,
match holder.state {
Lifecycle::Open => "open",
Lifecycle::Closed => "closed",
},
holder.liveness.as_str(),
holder.owner_pid,
holder.branch,
holder.worktree.display()
);
}
}
Ok(0)
}
fn publish_session(args: &PublishArgs, providers: &Providers<'_>) -> Result<u8> {
let title = explicit_title(args.title.as_ref())?;
let body = explicit_body(
&["onevcs", "publish", &args.token],
args.body.as_ref(),
args.body_file.as_deref(),
)?;
let publication = crate::publish(
providers,
&SessionToken(args.token.clone()),
&PublishRequest {
policy: args.policy,
title,
body,
},
)?;
let PublishOutcome::Failed {
kind,
reason,
retained,
} = &publication.outcome
else {
println!("{}", publication.outcome.describe());
return Ok(0);
};
eprintln!("onevcs: {reason}");
match retained {
Some(Retention::HandedBack(checkout)) => eprintln!(
"onevcs: branch {:?} is preserved in {}",
publication.branch,
checkout.display()
),
Some(Retention::Refused(checkout)) => eprintln!(
"onevcs: warning: {} refused branch {:?}, so nothing outside this session carries it",
checkout.display(),
publication.branch
),
None => {}
}
Ok(kind.exit_code())
}
fn report_publication(outcome: Result<PublishOutcome>) -> Result<u8> {
match outcome {
Ok(outcome) => {
println!("{}", outcome.describe());
Ok(0)
}
Err(error) => {
eprintln!("onevcs: {error}");
Ok(publish::exit_code(&error))
}
}
}
fn explicit_title(title: Option<&String>) -> Result<Option<Subject>> {
title
.cloned()
.map(Subject::try_from)
.transpose()
.map_err(error::invalid)
}
fn explicit_body(
command_prefix: &[&str],
body: Option<&String>,
body_file: Option<&Path>,
) -> Result<Option<String>> {
match (body, body_file) {
(Some(_), Some(path)) => {
let named = path.to_string_lossy();
let keeping = |option: &str, value: &str| {
let mut argv = command_prefix.to_vec();
argv.extend([option, value]);
guidance::command(argv)
};
Err(error::invalid(format!(
"--body and --body-file both name the body of the change request, and it is \
opened with one body. Keep the one that holds it: `{}` for the body in {}, or \
`{}` for the text as typed",
keeping("--body-file", &named),
path.display(),
keeping("--body", "TEXT"),
)))
}
(Some(body), None) => Ok(Some(body.clone())),
(None, Some(path)) => std::fs::read_to_string(path)
.map(Some)
.map_err(error::at("read the change request's body from", path)),
(None, None) => Ok(None),
}
}
fn recover_branch(args: &RecoverArgs, providers: &Providers<'_>) -> Result<u8> {
let registry = store::load()?;
let title = explicit_title(args.title.as_ref())?;
let body = explicit_body(
&[
"onevcs",
"recover",
&args.branch,
"--repo",
&args.repo.to_string_lossy(),
],
args.body.as_ref(),
args.body_file.as_deref(),
)?;
let token = format!("recover-{}", policy::branch_slug(&args.branch));
let mut stream = Stream::open(&token)?;
report_publication(recover::run(
®istry,
&args.repo,
&args.branch,
title,
body,
providers.hosting,
&mut stream,
))
}
fn publish_branch(args: &PublishBranchArgs, providers: &Providers<'_>) -> Result<u8> {
let registry = store::load()?;
let title = explicit_title(args.title.as_ref())?;
let body = explicit_body(
&[
"onevcs",
"publish-branch",
&args.branch,
"--repo",
&args.repo.to_string_lossy(),
],
args.body.as_ref(),
args.body_file.as_deref(),
)?;
let token = format!("publish-branch-{}", policy::branch_slug(&args.branch));
let mut stream = Stream::open(&token)?;
report_publication(publish_branch::run(
®istry,
&args.repo,
&args.branch,
title,
body,
args.policy,
providers.hosting,
&mut stream,
))
}
fn recoverable(args: &RecoverableArgs, providers: &Providers<'_>) -> Result<u8> {
let registry = store::load()?;
let here = resolve_here(®istry).ok();
let scope = match &here {
Some(resolution) => Scope::Repo(resolution.alias.clone()),
None => Scope::All,
};
let rows = match args.all {
true => providers.vcs.preserved(scope)?,
false => providers.vcs.recoverable(scope)?,
};
let scoped = here.as_ref().map(|resolution| {
format!(
"{} — the identity of {}, the registered checkout this was run in",
resolution.key,
resolution.publication.display()
)
});
let widen = "Only that identity is covered: run `onevcs recoverable` from a directory \
outside every registered checkout to see them all.";
let withheld = "Branches whose work reached their base are not listed; \
`onevcs recoverable --all` lists every preserved branch, each saying what \
became of its work and what says so.";
if args.json {
if let Some(scoped) = &scoped {
eprintln!("onevcs: this answer covers {scoped}. {widen}");
}
if !args.all {
eprintln!("onevcs: {withheld}");
}
println!("{}", serde_json::to_string(&rows).map_err(serialization)?);
return Ok(0);
}
let what = match args.all {
true => "preserved branch(es), whatever became of the work",
false => "preserved unpublished branch(es)",
};
if rows.is_empty() {
let none = match args.all {
true => "No preserved branches",
false => "No preserved unpublished branches",
};
match &scoped {
Some(scoped) => {
println!("{none} in {scoped}. Every branch of it has reached its base or a remote.")
}
None => println!(
"{none}. Every branch across the registered identities has reached its base \
or a remote."
),
}
if !args.all {
println!("{withheld}");
}
if scoped.is_some() {
println!("{widen}");
}
return Ok(0);
}
match &scoped {
Some(scoped) => println!("{} {what} in {scoped}:", rows.len()),
None => println!("{} {what} across every registered identity:", rows.len()),
}
for row in rows {
let kind = match row.branch.provenance {
Provenance::IncompleteStep => "incomplete step (provenance marker)",
Provenance::Complete => "complete",
};
let mut marks: Vec<String> = Vec::new();
match &row.landed {
Landed::Yes { .. } => marks.push(format!("landed — {}", row.landed.tier())),
Landed::Unknown => marks.push("may have landed".to_owned()),
Landed::No => {}
}
if row.held_by.is_some() {
marks.push("held by a live session".to_owned());
}
if let Some(net) = row.net_negative {
marks.push(format!(
"net-negative: {added} added, {removed} removed",
added = net.added(),
removed = net.removed(),
));
}
let marked = match marks.is_empty() {
true => String::new(),
false => format!(" — {}", marks.join("; ")),
};
println!("{} [{}] {kind}{marked}", row.branch.branch, row.identity);
println!(" Found in: {}", row.checkout.display());
println!(" Stopped because: {}", row.stopped_because);
if let Some(net) = row.net_negative {
println!(
" Net-negative: it removes {removed} line(s) and adds {added} since it forked \
from {base}, so landing it unread would strip work. Read it first with \
`{diff}`",
removed = net.removed(),
added = net.added(),
base = row.branch.base,
diff = guidance::command([
"git",
"-C",
&row.checkout.to_string_lossy(),
"diff",
"--stat",
&format!("{}...{}", row.branch.base, row.branch.branch),
]),
);
}
let command = guidance::command(row.recover_command.iter().map(String::as_str));
if let Landed::Yes { evidence } = &row.landed {
println!(
" Landed: {tier} ({commit}) says this branch's work reached {base}. Nothing \
to resume — publishing it again would re-open a change request for work \
{base} already carries",
tier = row.landed.tier(),
commit = evidence.commit(),
base = row.branch.base,
);
continue;
}
if row.landed == Landed::Unknown {
println!(
" Not decided: nothing records that this branch's work reached {base} — no \
landing, no change request's number in {base}'s history, and no landing \
trailer — and {base} already carries everything it changed. Read it with \
`{diff}`; if it really has not landed, `{command}` lands it",
base = row.branch.base,
diff = guidance::command([
"git",
"-C",
&row.checkout.to_string_lossy(),
"diff",
"--stat",
&format!("{}...{}", row.branch.base, row.branch.branch),
]),
);
continue;
}
match &row.held_by {
Some(held) => println!(
" Not ready: session {token} still holds this branch and {because}, so \
running `{command}` now would publish a branch mid-flight. Its worktree is \
{worktree}; wait for it, or close it with `{close}`, and then run that command",
token = held.token.0,
because = held.holding.because(),
worktree = held.worktree.display(),
close = guidance::command(["onevcs", "session", "close", &held.token.0]),
),
None => println!(" Resume: {command}"),
}
}
if !args.all {
println!("{withheld}");
}
if scoped.is_some() {
println!("{widen}");
}
Ok(0)
}
fn report_status(args: &StatusArgs, providers: &Providers<'_>) -> Result<u8> {
let registry = store::load()?;
let report = status::run(®istry, &args.reference, providers.hosting)?;
if args.json {
println!("{}", serde_json::to_string(&report).map_err(serialization)?);
} else {
print!("{}", report.render());
}
Ok(0)
}
fn import_branch(args: &ImportArgs) -> Result<u8> {
let registry = store::load()?;
let imported = import::run(
®istry,
&args.repo,
&args.branch,
args.from.as_deref(),
args.r#as.as_deref(),
)?;
println!(
"{} {} in {} from {}, at {}",
match imported.wrote {
import::Wrote::Created => "imported",
import::Wrote::FastForwarded => "fast-forwarded",
import::Wrote::Unchanged => "already had",
},
imported.name,
imported.destination.display(),
imported.source.describe(),
imported.tip,
);
Ok(0)
}
fn integrate_branches(args: &IntegrateArgs) -> Result<u8> {
let registry = store::load()?;
let resolution = resolve_here(®istry)?;
let token = format!("integrate-{}", policy::branch_slug(&resolution.alias));
let mut stream = Stream::open(&token)?;
let outcome = integrate::run(&resolution, &args.branches, args.push, &mut stream)?;
println!("Integration train for {}:", outcome.base);
for branch in &outcome.branches {
println!(" {}: {}", branch.branch, branch.status.describe());
}
println!("Base advanced: {}", yes_or_no(outcome.ending.advanced()));
println!("Pushed: {}", yes_or_no(outcome.ending.pushed()));
Ok(0)
}
fn sync(args: &SyncArgs) -> Result<u8> {
let registry = store::load()?;
let resolution = resolve_here(®istry)?;
let checkout = &resolution.publication;
let branch = match args.branch.clone() {
Some(branch) => workspace::Ref::try_from(branch).map_err(|reason| Error::Invalid {
reason: format!("{reason}: it is not a valid branch name"),
})?,
None => workspace::Ref::from_git(git::default_branch(checkout, "origin")?),
};
if git::current_branch(checkout)? != *branch {
return Err(Error::Invalid {
reason: format!(
"{} does not have {branch:?} checked out; sync only ever fast-forwards the \
branch a checkout is already on",
checkout.display()
),
});
}
git::fetch(checkout, "origin")?;
git::merge_ff_only(checkout, &format!("origin/{branch}"))?;
println!("{branch} fast-forwarded to origin/{branch}");
Ok(0)
}
fn sweep_workspaces(args: &SweepArgs) -> Result<u8> {
println!("{}", sweep::run(args.dry_run, args.min_age_hours)?);
Ok(0)
}
fn events(args: &EventsArgs, providers: &Providers<'_>) -> Result<u8> {
let token = args.token.as_str();
let filter = args.filter.as_deref().map(load_filter).transpose()?;
let mut reader = stream::Reader::open(token)?;
let session = SessionToken(token.to_owned());
let mut line_number = 0usize;
loop {
let closed = args.follow
&& providers
.vcs
.session(&session)
.map(|record| record.lifecycle == Lifecycle::Closed)
.unwrap_or(true);
let mut out = std::io::stdout().lock();
for line in reader.lines()? {
line_number += 1;
if let Some(filter) = &filter {
if !filter.matches(&stream::attributed(&line, token, line_number)?) {
continue;
}
}
writeln!(out, "{line}").map_err(|e| {
error::invalid(format!("cannot write the event stream for {token:?}: {e}"))
})?;
}
drop(out);
if !args.follow || closed {
return Ok(0);
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
fn load_filter(spec: &str) -> Result<EventFilter> {
if spec.trim_start().starts_with('{') {
return EventFilter::parse(spec);
}
let path = Path::new(spec);
let raw = std::fs::read_to_string(path).map_err(error::at("read the event filter at", path))?;
EventFilter::parse(&raw)
}
fn artifact(id: &str) -> Result<u8> {
print!("{}", stream::read_artifact(id)?);
Ok(0)
}
fn rules_check(args: &RulesCheckArgs) -> Result<u8> {
let registry = store::load()?;
let resolution = store::resolve(®istry, &args.repo)?;
let (file, source) = policy::load(®istry)?;
let normalized = store::normalize(&resolution.identity.origin);
let resolved = policy::resolve(&file, &source, &normalized, &resolution.publication);
println!("repo: {}", args.repo);
println!("identity: {}", resolution.key);
println!("checkout: {}", resolution.publication.display());
println!("rules: {}", resolved.source);
match &resolved.matched {
Some(matched) => println!(
"matched: rule {} {}",
matched.index,
describe_match(&matched.criteria)
),
None => println!("matched: no rule; the default applies"),
}
println!(
"publication: {} (from {})",
policy::spell(resolved.policy.publication),
resolved.publication_from
);
println!(
"approvals: {} (from {})",
match resolved.policy.approvals {
crate::rules::Approvals::Required => "required",
crate::rules::Approvals::None => "none",
},
resolved.approvals_from
);
println!(
"trailer_prefix: {} (from {})",
provenance::from_rules(&file).prefix(),
if file.trailer_prefix.is_some() {
"the rules file"
} else {
"the default"
}
);
Ok(0)
}
fn release_targets(args: &ReleaseTargetsArgs) -> Result<u8> {
let releases = crate::release_targets(&args.repo)?;
if args.json {
return print_json(&releases);
}
println!("identity: {}", releases.identity);
println!("adoption: {}", releases.adoption);
println!(
"default target: {}",
releases
.default_target
.as_ref()
.map_or_else(|| "none".to_owned(), TargetName::to_string)
);
if releases.targets.is_empty() {
println!("targets: none");
return Ok(0);
}
println!("targets:");
for target in &releases.targets {
println!(
" {}\t{}\t{}",
target.name,
target.style(),
describe(target)
);
}
Ok(0)
}
fn describe(target: &ReleaseTarget) -> String {
match &target.release {
ReleaseMethod::Automated {
probe: Probe::Script { script, args, .. },
} => match args.is_empty() {
true => format!("script {}", script.display()),
false => format!("script {} {}", script.display(), args.join(" ")),
},
ReleaseMethod::Automated {
probe: Probe::Shell { shell, .. },
} => format!("shell {shell}"),
ReleaseMethod::HumanStep { action } => format!("action: {action}"),
}
}
fn release_latest(args: &ReleaseLatestArgs) -> Result<u8> {
let answer = crate::release_latest(&args.repo, args.target.as_ref())?;
if args.json {
return print_json(&answer);
}
match answer {
ReleaseAnswer::Released { version } => println!("released: {version}"),
ReleaseAnswer::NoRelease => println!("no release yet"),
ReleaseAnswer::NotAnswered { reason } => println!("not answered: {reason}"),
}
Ok(0)
}
fn release_status(args: &ReleaseStatusArgs) -> Result<u8> {
let status = crate::release_status(&args.reference, args.target.as_ref())?;
if args.json {
return print_json(&status);
}
match status {
ReleaseStatus::Released {
target,
style,
version,
} => println!("released: {target} {version} ({style})"),
ReleaseStatus::NotReleased { at_landing, now } => println!(
"not released: at landing {landing}, now {now}",
landing = spell_baseline(&at_landing),
now = spell_version(&now),
),
ReleaseStatus::AwaitingHumanStep {
target,
action,
since,
} => println!("awaiting human step: {target} since {since}\n action: {action}"),
ReleaseStatus::NotAnswered { reason } => println!("not answered: {reason}"),
ReleaseStatus::NotLanded => println!("not landed"),
}
Ok(0)
}
fn release_acknowledge(args: &ReleaseAcknowledgeArgs) -> Result<u8> {
let recorded =
crate::acknowledge_release(&args.reference, &args.target, &args.version, args.supersede)?;
if args.json {
return print_json(&recorded);
}
print!("{}", render_acknowledgement(&recorded));
Ok(0)
}
fn render_acknowledgement(recorded: &Acknowledgement) -> String {
let mut rendered = format!(
"acknowledged: {target} {version} for landing {commit}\n identity: {identity}\n \
recorded at: {at} by {actor}\n",
target = recorded.target,
version = recorded.version,
commit = recorded.landing_commit,
identity = recorded.identity,
at = recorded.recorded_at,
actor = recorded.actor,
);
for replaced in &recorded.superseded {
rendered.push_str(&format!(
" superseded: {version} recorded at {at} by {actor}\n",
version = replaced.version,
at = replaced.recorded_at,
actor = replaced.actor,
));
}
rendered
}
fn spell_baseline(baseline: &Baseline) -> String {
match baseline {
Baseline::At { version } => version.clone(),
Baseline::NoRelease => "no release at landing".to_owned(),
}
}
fn spell_version(version: &str) -> &str {
match version.is_empty() {
true => "no release",
false => version,
}
}
fn print_json<T: serde::Serialize>(value: &T) -> Result<u8> {
println!("{}", serde_json::to_string(value).map_err(serialization)?);
Ok(0)
}
fn describe_match(criteria: &crate::rules::RuleMatch) -> String {
let mut parts = Vec::new();
if let Some(host) = &criteria.host {
parts.push(format!("host: {host}"));
}
if let Some(owner) = &criteria.owner {
parts.push(format!("owner: {owner}"));
}
if let Some(name) = &criteria.name {
parts.push(format!("name: {name}"));
}
if let Some(path) = &criteria.path {
parts.push(format!("path: {path}"));
}
format!("{{{}}}", parts.join(", "))
}
fn resolve_here(registry: &Registry) -> Result<Resolution> {
let here = std::env::current_dir()
.map_err(|e| error::invalid(format!("cannot read the current directory: {e}")))?;
let canonical = std::fs::canonicalize(&here).unwrap_or(here);
let mut candidate: Option<&Path> = Some(canonical.as_path());
while let Some(path) = candidate {
for (alias, checkout) in ®istry.checkouts {
if checkout.path == path {
return store::resolve(registry, alias);
}
}
candidate = path.parent();
}
Err(Error::Invalid {
reason: format!(
"{} is not inside a registered checkout; register it with `onevcs register PATH`",
canonical.display()
),
})
}
fn yes_or_no(answer: bool) -> &'static str {
if answer {
"yes"
} else {
"no"
}
}
fn serialization(failure: serde_json::Error) -> Error {
error::invalid(format!("cannot serialize the result: {failure}"))
}
fn spell_workflow(workflow: Workflow) -> &'static str {
match workflow {
Workflow::Local => "local",
Workflow::Remote => "remote",
}
}
fn spell_repo_type(repo_type: RepoType) -> &'static str {
match repo_type {
RepoType::SingleOwner => "single-owner",
RepoType::Team => "team",
}
}