Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,26 @@ pub fn update_plan(root: &Path, plan: &Plan) -> Result<()> {
write_json(&path, plan)
}

/// A suggested follow-up command for a plan in a given state.
#[derive(Clone, Debug, Serialize)]
pub struct NextStep {
pub command: String,
}

/// The follow-up commands for a plan in `state`; shared by `rep status` and
/// `rep show` so both always suggest the same next action.
pub fn next_steps(state: &str, plan_id: &str) -> Vec<NextStep> {
match state {
STATE_PLANNED => vec![NextStep {
command: format!("rep apply --plan {plan_id} --json"),
}],
STATE_APPLIED => vec![NextStep {
command: format!("rep residual --plan {plan_id} --json"),
}],
_ => vec![],
}
}

/// Resolve the most recent plan id from the active-state pointer — the plan
/// `rep status` reports. Backs `--last` so callers need not copy plan ids.
pub fn last_plan_id(root: &Path) -> Result<String> {
Expand Down
33 changes: 33 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,39 @@ if anything is left.")]
tracked_only: bool,
},

/// Inspect a saved plan (summary; add --files/--skipped/--preview detail)
#[command(long_about = "\
Inspect a saved plan without reading .rep/plans/<plan-id>/ files by hand.

Prints the plan's mappings, counts, state, and suggested next command. \
--files, --skipped, and --preview add the corresponding detail sections. \
With no --plan, shows the most recent plan (the one 'rep status' shows).")]
#[command(
after_help = "Example:\n rep show # most recent plan\n rep show --plan <plan-id> --skipped"
)]
Show {
/// The <plan-id> to inspect (defaults to the most recent plan)
#[arg(long, conflicts_with = "last")]
plan: Option<String>,

/// Inspect the most recent plan (the default; accepted for symmetry
/// with `rep apply --last`)
#[arg(long)]
last: bool,

/// Also list planned content files and path renames
#[arg(long)]
files: bool,

/// Also list skipped paths with reasons
#[arg(long)]
skipped: bool,

/// Also print the line-level content preview
#[arg(long)]
preview: bool,
},

/// Show rep's current state for this repository (last plan, etc.)
Status,
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod residual;
pub mod scanner;
pub mod schema;
pub mod scope;
pub mod show;
pub mod status;
pub mod text;

Expand Down
22 changes: 21 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ use rep::error::{RepError, Result};
use rep::planner::PlanOpts;
use rep::residual::ResidualOpts;
use rep::scope::ScopeOpts;
use rep::show::ShowOpts;
use rep::text;
use rep::{applier, output, planner, residual, scanner, status};
use rep::{applier, output, planner, residual, scanner, show, status};

fn main() -> ExitCode {
let cli = match Cli::try_parse() {
Expand Down Expand Up @@ -87,6 +88,7 @@ fn suggested_command() -> &'static str {
Some("plan") => "rep plan --map old_name=new_name",
Some("apply") => "rep apply --last",
Some("residual") => "rep residual old_name",
Some("show") => "rep show",
Some("status") => "rep status",
_ => "rep scan old_name (list every command with 'rep --help')",
}
Expand Down Expand Up @@ -197,6 +199,24 @@ fn dispatch(cli: Cli) -> Result<i32> {
json,
),

// `last` only exists for CLI symmetry; `plan: None` already means the
// most recent plan.
Commands::Show {
plan,
last: _,
files,
skipped,
preview,
} => show::run(
ShowOpts {
plan,
files,
skipped,
preview,
},
json,
),

Commands::Status => status::run(json),
}
}
2 changes: 2 additions & 0 deletions src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub const APPLY: &str = "rep.apply.v1";
pub const RESIDUAL: &str = "rep.residual.v1";
/// `rep status` output schema version.
pub const STATUS: &str = "rep.status.v1";
/// `rep show` output schema version.
pub const SHOW: &str = "rep.show.v1";
/// Machine-readable error output schema version (emitted on `--json` failures).
pub const ERROR: &str = "rep.error.v1";

Expand Down
167 changes: 167 additions & 0 deletions src/show.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//! `rep show` — inspect a saved plan without reading `.rep/plans/<plan-id>/`
//! files by hand.
//!
//! The summary (mappings, counts, state, next step) always prints; `--files`,
//! `--skipped`, and `--preview` add the corresponding detail sections.

use serde::Serialize;

use crate::artifacts::{self, Artifacts, ContentFile, NextStep, RepoInfo};
use crate::error::Result;
use crate::output;
use crate::path_rename::Rename;
use crate::scope::{Scope, Skip};
use crate::text::Mapping;
use crate::{git, schema};

#[derive(Serialize)]
struct ContentSummary {
enabled: bool,
matched_files: usize,
changed_files: usize,
replacements: usize,
}

#[derive(Serialize)]
struct PathsSummary {
enabled: bool,
matched_paths: usize,
renames: usize,
}

#[derive(Serialize)]
struct FilesSection {
content: Vec<ContentFile>,
renames: Vec<Rename>,
}

#[derive(Serialize)]
struct ShowOutput {
schema_version: String,
plan_id: String,
created_at: String,
state: String,
repo: RepoInfo,
scope: Scope,
mappings: Vec<Mapping>,
content: ContentSummary,
paths: PathsSummary,
skipped_count: usize,
artifacts: Artifacts,
next: Vec<NextStep>,
#[serde(skip_serializing_if = "Option::is_none")]
files: Option<FilesSection>,
#[serde(skip_serializing_if = "Option::is_none")]
skipped: Option<Vec<Skip>>,
#[serde(skip_serializing_if = "Option::is_none")]
preview: Option<String>,
}

/// Options for `rep show`. `plan: None` means the most recent plan (the one
/// the state pointer names), which is also what bare `rep show` does.
pub struct ShowOpts {
pub plan: Option<String>,
pub files: bool,
pub skipped: bool,
pub preview: bool,
}

/// Execute `rep show`.
pub fn run(opts: ShowOpts, json: bool) -> Result<i32> {
let root = git::discover_root()?;
let plan_id = match opts.plan {
Some(id) => id,
None => artifacts::last_plan_id(&root)?,
};
let plan = artifacts::read_plan(&root, &plan_id)?;

let preview = if opts.preview {
Some(std::fs::read_to_string(
root.join(&plan.artifacts.content_preview),
)?)
} else {
None
};

let out = ShowOutput {
schema_version: schema::SHOW.to_string(),
plan_id: plan.plan_id.clone(),
created_at: plan.created_at,
state: plan.state.clone(),
repo: plan.repo,
scope: plan.scope,
mappings: plan.mappings,
content: ContentSummary {
enabled: plan.content.enabled,
matched_files: plan.content.matched_files,
changed_files: plan.content.changed_files,
replacements: plan.content.replacements,
},
paths: PathsSummary {
enabled: plan.paths.enabled,
matched_paths: plan.paths.matched_paths,
renames: plan.paths.renames.len(),
},
skipped_count: plan.skipped.len(),
artifacts: plan.artifacts,
next: artifacts::next_steps(&plan.state, &plan.plan_id),
files: opts.files.then_some(FilesSection {
content: plan.content.files,
renames: plan.paths.renames,
}),
skipped: opts.skipped.then_some(plan.skipped),
preview,
};

if json {
output::print_json(&out)?;
} else {
print_human(&out);
}

Ok(0)
}

fn print_human(out: &ShowOutput) {
output::info(&format!(
"plan {} ({})",
output::bold(&out.plan_id),
out.state
));
let short_head: String = out.repo.git_head.chars().take(12).collect();
println!(" created: {} head: {}", out.created_at, short_head);
println!(" mappings:");
for m in &out.mappings {
println!(" {} -> {}", m.from, m.to);
}
println!(
" content: {} replacements in {} files",
out.content.replacements, out.content.changed_files
);
println!(" paths: {} renames", out.paths.renames);
println!(" skipped: {}", out.skipped_count);
if let Some(files) = &out.files {
println!(" files:");
for f in &files.content {
println!(" {}", f.path);
}
for r in &files.renames {
println!(" {} -> {}", r.from, r.to);
}
}
if let Some(skipped) = &out.skipped {
println!(" skipped paths:");
for s in skipped {
match &s.matched_rule {
Some(rule) => println!(" {} ({}: {})", s.path, s.reason, rule),
None => println!(" {} ({})", s.path, s.reason),
}
}
}
if let Some(preview) = &out.preview {
print!("{preview}");
}
for step in &out.next {
output::action(&step.command);
}
}
19 changes: 1 addition & 18 deletions src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use serde::Serialize;

use crate::artifacts::{self, STATE_NONE};
use crate::artifacts::{self, NextStep, STATE_NONE, next_steps};
use crate::error::Result;
use crate::output;
use crate::text::Mapping;
Expand All @@ -20,11 +20,6 @@ struct RepoInfo {
tracked_tree_clean: bool,
}

#[derive(Serialize)]
struct NextStep {
command: String,
}

#[derive(Serialize)]
struct StatusReport {
schema_version: String,
Expand Down Expand Up @@ -80,18 +75,6 @@ pub fn run(json: bool) -> Result<i32> {
Ok(0)
}

fn next_steps(state: &str, plan_id: &str) -> Vec<NextStep> {
match state {
artifacts::STATE_PLANNED => vec![NextStep {
command: format!("rep apply --plan {plan_id} --json"),
}],
artifacts::STATE_APPLIED => vec![NextStep {
command: format!("rep residual --plan {plan_id} --json"),
}],
_ => vec![],
}
}

fn print_human(report: &StatusReport) {
output::info(&format!("state: {}", output::bold(&report.state)));
if let Some(id) = &report.active_plan_id {
Expand Down
Loading
Loading