Mercurial > public > mercurial-scm > hg
view rust/rhg/src/commands/annotate.rs @ 53042:cdd7bf612c7b stable tip
bundle-spec: properly format boolean parameter (issue6960)
This was breaking automatic clone bundle generation. This changeset fixes it and
add a test to catch it in the future.
author | Pierre-Yves David <pierre-yves.david@octobus.net> |
---|---|
date | Tue, 11 Mar 2025 02:29:42 +0100 |
parents | 874c64e041b5 |
children |
line wrap: on
line source
use core::str; use std::{collections::hash_map::Entry, ffi::OsString}; use format_bytes::format_bytes; use hg::{ encoding::Encoder, operations::{ annotate, AnnotateOptions, AnnotateOutput, ChangesetAnnotation, }, revlog::changelog::Changelog, utils::strings::CleanWhitespace, FastHashMap, Revision, }; use crate::{error::CommandError, utils::path_utils::resolve_file_args}; pub const HELP_TEXT: &str = " show changeset information by line for each file "; pub fn args() -> clap::Command { clap::command!("annotate") .alias("blame") .arg( clap::Arg::new("files") .help("files to annotate") .required(true) .num_args(1..) .value_name("FILE") .value_parser(clap::value_parser!(OsString)), ) .arg( clap::Arg::new("rev") .help("annotate the specified revision") .short('r') .long("rev") .value_name("REV") .default_value("."), ) .arg( clap::Arg::new("no-follow") .help("don't follow copies and renames") .long("no-follow") .action(clap::ArgAction::SetTrue), ) .arg( clap::Arg::new("text") .help("treat all files as text") .short('a') .long("text") .action(clap::ArgAction::SetTrue), ) .arg( clap::Arg::new("user") .help("list the author (long with -v)") .short('u') .long("user") .action(clap::ArgAction::SetTrue), ) .arg( clap::Arg::new("number") .help("list the revision number (default)") .short('n') .long("number") .action(clap::ArgAction::SetTrue), ) .arg( clap::Arg::new("changeset") .help("list the changeset") .short('c') .long("changeset") .action(clap::ArgAction::SetTrue), ) .arg( clap::Arg::new("date") .help("list the date (short with -q)") .short('d') .long("date") .action(clap::ArgAction::SetTrue), ) .arg( clap::Arg::new("file") .help("list the filename") .short('f') .long("file") .action(clap::ArgAction::SetTrue), ) .arg( clap::Arg::new("line-number") .help("show the line number at the first appearance") .short('l') .long("line-number") .action(clap::ArgAction::SetTrue), ) .arg( clap::Arg::new("quiet") .help("show short date for -d") .short('q') .long("quiet") .action(clap::ArgAction::SetTrue), ) .arg( clap::Arg::new("verbose") .help("show full username for -u") .short('v') .long("verbose") .action(clap::ArgAction::SetTrue) .conflicts_with("quiet"), ) .arg( clap::Arg::new("ignore-all-space") .help("ignore white space when comparing lines") .short('w') .long("ignore-all-space") .action(clap::ArgAction::SetTrue), ) .arg( clap::Arg::new("ignore-space-change") .help("ignore changes in the amount of white space") .short('b') .long("ignore-space-change") .action(clap::ArgAction::SetTrue), ) .arg( clap::Arg::new("ignore-blank-lines") .help("ignore changes whose lines are all blank") .short('B') .long("ignore-blank-lines") .action(clap::ArgAction::SetTrue), ) .arg( clap::Arg::new("ignore-space-at-eol") .help("ignore changes in whitespace at EOL") .short('Z') .long("ignore-space-at-eol") .action(clap::ArgAction::SetTrue), ) .about(HELP_TEXT) } pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> { let config = invocation.config; if config.has_non_empty_section(b"annotate") { return Err(CommandError::unsupported( "rhg annotate does not support any [annotate] configs", )); } let repo = invocation.repo?; let args = invocation.subcommand_args; let rev = args.get_one::<String>("rev").expect("rev has a default"); let rev = hg::revset::resolve_single(rev, repo)?; let files = match args.get_many::<OsString>("files") { None => vec![], Some(files) => resolve_file_args(repo, files)?, }; let options = AnnotateOptions { treat_binary_as_text: args.get_flag("text"), follow_copies: !args.get_flag("no-follow"), whitespace: if args.get_flag("ignore-all-space") { CleanWhitespace::All } else if args.get_flag("ignore-space-change") { CleanWhitespace::Collapse } else if args.get_flag("ignore-space-at-eol") { CleanWhitespace::AtEol } else { // We ignore the --ignore-blank-lines flag (present for consistency // with other commands) since it has no effect on annotate. CleanWhitespace::None }, }; let mut include = Include { user: args.get_flag("user"), number: args.get_flag("number"), changeset: args.get_flag("changeset"), date: args.get_flag("date"), file: args.get_flag("file"), line_number: args.get_flag("line-number"), }; if !(include.user || include.file || include.date || include.changeset) { include.number = true; } if include.line_number && !(include.number || include.changeset) { return Err(CommandError::abort( "at least one of -n/-c is required for -l", )); } let verbosity = match (args.get_flag("quiet"), args.get_flag("verbose")) { (false, false) => Verbosity::Default, (true, false) => Verbosity::Quiet, (false, true) => Verbosity::Verbose, (true, true) => unreachable!(), }; let changelog = repo.changelog()?; let mut formatter = Formatter::new( &changelog, invocation.ui.encoder(), include, verbosity, ); let mut stdout = invocation.ui.stdout_buffer(); for path in files { match annotate(repo, &path, rev, options)? { AnnotateOutput::Text(text) => { let annotations = formatter.format(text.annotations)?; for (annotation, line) in annotations.iter().zip(&text.lines) { stdout.write_all(&format_bytes!( b"{}: {}", annotation, line ))?; } if let Some(line) = text.lines.last() { if !line.ends_with(b"\n") { stdout.write_all(b"\n")?; } } } AnnotateOutput::Binary => { stdout.write_all(&format_bytes!( b"{}: binary file\n", path.as_bytes() ))?; } AnnotateOutput::NotFound => { let short = changelog.node_from_rev(rev).short(); return Err(CommandError::abort(format!( "{path}: no such file in rev {short:x}", ))); } } } stdout.flush()?; Ok(()) } struct Formatter<'a> { changelog: &'a Changelog, encoder: &'a Encoder, include: Include, verbosity: Verbosity, cache: FastHashMap<Revision, ChangesetData>, } #[derive(Copy, Clone)] struct Include { user: bool, number: bool, changeset: bool, date: bool, file: bool, line_number: bool, } impl Include { fn count(&self) -> usize { // Rust guarantees false is 0 and true is 1. self.user as usize + self.number as usize + self.changeset as usize + self.date as usize + self.file as usize + self.line_number as usize } } #[derive(Copy, Clone)] enum Verbosity { Quiet, Default, Verbose, } #[derive(Default)] struct ChangesetData { user: Option<Vec<u8>>, changeset: Option<Vec<u8>>, date: Option<Vec<u8>>, } impl ChangesetData { fn create( revision: Revision, changelog: &Changelog, include: Include, verbosity: Verbosity, ) -> Result<ChangesetData, CommandError> { let mut result = ChangesetData::default(); if !(include.user || include.changeset || include.date) { return Ok(result); } let entry = changelog.entry(revision)?; let data = entry.data()?; if include.user { let user = match verbosity { Verbosity::Verbose => data.user(), _ => hg::utils::strings::short_user(data.user()), }; result.user = Some(user.to_vec()); } if include.changeset { let changeset = entry.as_revlog_entry().node().short(); result.changeset = Some(format!("{:x}", changeset).into_bytes()); } if include.date { let date = data.timestamp()?.format(match verbosity { Verbosity::Quiet => "%Y-%m-%d", _ => "%a %b %d %H:%M:%S %Y %z", }); result.date = Some(format!("{}", date).into_bytes()); } Ok(result) } } impl<'a> Formatter<'a> { fn new( changelog: &'a Changelog, encoder: &'a Encoder, include: Include, verbosity: Verbosity, ) -> Self { let cache = FastHashMap::default(); Self { changelog, encoder, include, verbosity, cache, } } fn format( &mut self, annotations: Vec<ChangesetAnnotation>, ) -> Result<Vec<Vec<u8>>, CommandError> { let mut lines: Vec<Vec<Vec<u8>>> = Vec::with_capacity(annotations.len()); let num_fields = self.include.count(); let mut widths = vec![0usize; num_fields]; for annotation in annotations { let revision = annotation.revision; let data = match self.cache.entry(revision) { Entry::Occupied(occupied) => occupied.into_mut(), Entry::Vacant(vacant) => vacant.insert(ChangesetData::create( revision, self.changelog, self.include, self.verbosity, )?), }; let mut fields = Vec::with_capacity(num_fields); if let Some(user) = &data.user { fields.push(user.clone()); } if self.include.number { fields.push(format_bytes!(b"{}", revision)); } if let Some(changeset) = &data.changeset { fields.push(changeset.clone()); } if let Some(date) = &data.date { fields.push(date.clone()); } if self.include.file { fields.push(annotation.path.into_vec()); } if self.include.line_number { fields.push(format_bytes!(b"{}", annotation.line_number)); } for (field, width) in fields.iter().zip(widths.iter_mut()) { *width = std::cmp::max( *width, self.encoder.column_width_bytes(field), ); } lines.push(fields); } let total_width = widths.iter().sum::<usize>() + num_fields - 1; Ok(lines .iter() .map(|fields| { let mut bytes = Vec::with_capacity(total_width); for (i, (field, width)) in fields.iter().zip(widths.iter()).enumerate() { if i > 0 { let colon = self.include.line_number && i == num_fields - 1; bytes.push(if colon { b':' } else { b' ' }); } let padding = width - self.encoder.column_width_bytes(field); bytes.resize(bytes.len() + padding, b' '); bytes.extend_from_slice(field); } bytes }) .collect()) } }