use std::io::Write;
use std::path::{Path, PathBuf};
use clap::{Args, ValueEnum};
use pointbreak::documents::revision_list_document;
use pointbreak::session::{
QueryDiagnosticCode, QuerySurface, RefFilterMode, RevisionListOptions,
RevisionOverviewsOptions, RevisionRecordInputs, SupersessionView, UnreachableVisibility,
build_revision_search_record, list_revisions, matches_query, parse_search_query_for,
read_events_for_display, revision_supersession_classification, show_revision_overviews,
};
use crate::cli::common::{count_label, endpoint_label};
use crate::cli::output;
#[derive(Debug, Args)]
pub(super) struct RevisionListArgs {
#[arg(long, default_value = ".")]
repo: PathBuf,
#[arg(long)]
object: Option<String>,
#[arg(long)]
filter: Option<String>,
#[arg(long = "ref", alias = "branch")]
ref_name: Option<String>,
#[arg(long, value_enum, default_value = "label")]
by: RefFilterByArg,
#[arg(long = "all")]
_all: bool,
#[arg(long, alias = "orphans")]
unreachable: bool,
#[arg(long = "integration-ref")]
integration_ref: Option<String>,
#[arg(long)]
worktree: Option<PathBuf>,
#[command(flatten)]
format_args: output::FormatArgs,
}
#[derive(Clone, Copy, Debug, Default, ValueEnum)]
#[value(rename_all = "kebab-case")]
enum RefFilterByArg {
#[default]
Label,
Liveness,
}
impl From<RefFilterByArg> for RefFilterMode {
fn from(by: RefFilterByArg) -> Self {
match by {
RefFilterByArg::Label => RefFilterMode::Label,
RefFilterByArg::Liveness => RefFilterMode::Liveness,
}
}
}
pub(super) fn run(
args: RevisionListArgs,
stdout: &mut dyn Write,
) -> Result<(), Box<dyn std::error::Error>> {
let span = tracing::info_span!("shore.revision.list");
let _entered = span.enter();
tracing::debug!(command = "revision.list", "command_start");
let ids = crate::cli::id_resolver::IdResolver::new(&args.repo);
let object = match &args.object {
Some(object) => Some(ids.object(object)?),
None => None,
};
let mut options = RevisionListOptions::new(&args.repo).with_read_for_display(true);
if let Some(ref_name) = args.ref_name {
options = options.with_ref_filter(ref_name, args.by.into());
}
let visibility = if args.unreachable {
UnreachableVisibility::UnreachableOnly
} else {
UnreachableVisibility::All
};
options = options.with_unreachable_visibility(visibility);
if let Some(integration_ref) = args.integration_ref {
options = options.with_integration_ref(integration_ref);
}
if let Some(worktree) = args.worktree {
options = options.with_worktree_scope(worktree);
}
let mut result = list_revisions(options)?;
if let Some(object) = object.as_deref() {
result
.entries
.retain(|entry| entry.object_id.as_str() == object);
result.revision_count = result.entries.len();
}
apply_revision_filter(&args.repo, args.filter.as_deref(), &mut result)?;
let format = output::resolve_format(args.format_args.explicit(), output::OutputFormat::Json)?;
let text = matches!(format.format, output::OutputFormat::Text)
.then(|| render_revision_list_text(&result));
let document = revision_list_document(result);
output::write_document(stdout, format, &document, || {
text.expect("text lane resolves the digest source")
})
}
fn render_revision_list_text(result: &pointbreak::session::RevisionListResult) -> String {
if result.entries.is_empty() {
return "no revisions".to_owned();
}
let mut lines = vec![format!(
"{}:",
count_label(result.revision_count, "revision", "revisions")
)];
for entry in &result.entries {
let mut line = format!(
" {}{} · base {} → {} · {} · {}",
output::short_ref(entry.revision_id.as_str()),
entry
.summary
.as_deref()
.map(crate::cli::common::clamp_title)
.map(|summary| format!(" · \"{summary}\""))
.unwrap_or_default(),
endpoint_label(&entry.base),
endpoint_label(&entry.target),
entry.merge_status,
entry.captured_at,
);
let members = entry.grouped_revision_ids.len();
if members > 1 {
line.push_str(&format!(
" · stands for {}",
count_label(members, "revision", "revisions")
));
}
lines.push(line);
}
lines.join("\n")
}
fn apply_revision_filter(
repo: &Path,
filter: Option<&str>,
result: &mut pointbreak::session::RevisionListResult,
) -> Result<(), Box<dyn std::error::Error>> {
let Some(filter) = filter else {
return Ok(());
};
let overviews = show_revision_overviews(
RevisionOverviewsOptions::new(repo)
.with_revisions(result.entries.iter().map(|e| e.revision_id.clone()))
.with_trust_set(crate::cli::common::discover_trust_set(repo))
.with_read_for_display(true),
)?;
let (events, _) = read_events_for_display(repo)?;
let classification =
revision_supersession_classification(&SupersessionView::from_events(&events)?);
let parsed = parse_search_query_for(filter, QuerySurface::Revision);
for diagnostic in &parsed.diagnostics {
match diagnostic.code {
QueryDiagnosticCode::UnsupportedQualifier | QueryDiagnosticCode::UnsupportedValue => {
return Err(diagnostic.message.clone().into());
}
QueryDiagnosticCode::DeprecatedQualifier => eprintln!("hint: {}", diagnostic.message),
}
}
result.entries.retain(|entry| {
let Some(overview) = overviews.get(&entry.revision_id) else {
return false;
};
let facet = classification.get(&entry.revision_id);
let record = build_revision_search_record(RevisionRecordInputs {
entry,
overview,
classification_state: facet.map(|f| f.state).unwrap_or("isolated"),
competing: facet.is_some_and(|f| f.competing),
});
matches_query(&record.0, &parsed.clauses)
});
result.revision_count = result.entries.len();
Ok(())
}