From 223278d31dbad6308b1c49d583fbfcd02846820e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 5 Dec 2023 11:28:38 +0100 Subject: [PATCH 1/4] fix: assure binary-detection also works if ODB files are below the big-file threshold. --- gix-diff/src/blob/pipeline.rs | 20 ++++++++++-- gix-diff/tests/blob/pipeline.rs | 55 +++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/gix-diff/src/blob/pipeline.rs b/gix-diff/src/blob/pipeline.rs index f6d3fbcadfc..58dddd90b22 100644 --- a/gix-diff/src/blob/pipeline.rs +++ b/gix-diff/src/blob/pipeline.rs @@ -377,8 +377,11 @@ impl Pipeline { .try_header(id) .map_err(gix_object::find::existing_object::Error::Find)? .ok_or_else(|| gix_object::find::existing_object::Error::NotFound { oid: id.to_owned() })?; - if is_binary.is_none() && self.options.large_file_threshold_bytes > 0 { - is_binary = Some(header.size > self.options.large_file_threshold_bytes); + if is_binary.is_none() + && self.options.large_file_threshold_bytes > 0 + && header.size > self.options.large_file_threshold_bytes + { + is_binary = Some(true); }; let data = if is_binary == Some(true) { Data::Binary { size: header.size } @@ -425,7 +428,18 @@ impl Pipeline { })?; match cmd_and_file { Some((cmd, mut tmp_file)) => { - tmp_file.write_all(out).map_err(|err| { + match res { + ToWorktreeOutcome::Unchanged(buf) | ToWorktreeOutcome::Buffer(buf) => { + tmp_file.write_all(buf) + } + ToWorktreeOutcome::Process(MaybeDelayed::Immediate(mut stream)) => { + std::io::copy(&mut stream, &mut tmp_file).map(|_| ()) + } + ToWorktreeOutcome::Process(MaybeDelayed::Delayed(_)) => { + unreachable!("we prohibit this") + } + } + .map_err(|err| { convert_to_diffable::Error::CreateTempfile { source: err, rela_path: rela_path.to_owned(), diff --git a/gix-diff/tests/blob/pipeline.rs b/gix-diff/tests/blob/pipeline.rs index 32bbb8b045b..fb1ef355715 100644 --- a/gix-diff/tests/blob/pipeline.rs +++ b/gix-diff/tests/blob/pipeline.rs @@ -93,6 +93,61 @@ pub(crate) mod convert_to_diffable { Ok(()) } + #[test] + fn binary_below_large_file_threshold() -> crate::Result { + let tmp = gix_testtools::tempfile::TempDir::new()?; + let mut filter = gix_diff::blob::Pipeline::new( + WorktreeRoots { + old_root: None, + new_root: Some(tmp.path().to_owned()), + }, + gix_filter::Pipeline::default(), + vec![], + gix_diff::blob::pipeline::Options { + large_file_threshold_bytes: 5, + ..default_options() + }, + ); + + let does_not_matter = gix_hash::Kind::Sha1.null(); + let mut buf = Vec::new(); + let a_name = "a"; + let large_content = "a\0b"; + std::fs::write(tmp.path().join(a_name), large_content.as_bytes())?; + let out = filter.convert_to_diffable( + &does_not_matter, + EntryKind::BlobExecutable, + a_name.into(), + ResourceKind::NewOrDestination, + &mut |_, _| {}, + &gix_object::find::Never, + pipeline::Mode::default(), + &mut buf, + )?; + assert!(out.driver_index.is_none(), "there was no driver"); + assert_eq!(out.data, Some(pipeline::Data::Binary { size: 3 }), "detected in buffer"); + assert_eq!(buf.len(), 0, "it should avoid querying that data in the first place"); + + let mut db = ObjectDb::default(); + let id = db.insert(large_content); + let out = filter.convert_to_diffable( + &id, + EntryKind::Blob, + a_name.into(), + ResourceKind::OldOrSource, + &mut |_, _| {}, + &db, + pipeline::Mode::default(), + &mut buf, + )?; + + assert!(out.driver_index.is_none(), "there was no driver"); + assert_eq!(out.data, Some(pipeline::Data::Binary { size: 3 })); + assert_eq!(buf.len(), 0, "it should avoid querying that data in the first place"); + + Ok(()) + } + #[test] fn above_large_file_threshold() -> crate::Result { let tmp = gix_testtools::tempfile::TempDir::new()?; From 21bea0f5496a0db607356c803a545358e766fa92 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 5 Dec 2023 10:51:14 +0100 Subject: [PATCH 2/4] feat: impl `From for gix_object::tree::EntryMode`. --- gix-index/src/entry/mod.rs | 6 +++--- gix-index/src/entry/mode.rs | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/gix-index/src/entry/mod.rs b/gix-index/src/entry/mod.rs index 2f257ef633d..b7fb13f7baf 100644 --- a/gix-index/src/entry/mod.rs +++ b/gix-index/src/entry/mod.rs @@ -18,9 +18,9 @@ mod write; use bitflags::bitflags; -// TODO: we essentially treat this as an enum withj the only exception being -// that `FILE_EXECUTABLE.contains(FILE)` works might want to turn this into an -// enum proper +// TODO: we essentially treat this as an enum with the only exception being +// that `FILE_EXECUTABLE.contains(FILE)` works might want to turn this into an +// enum proper bitflags! { /// The kind of file of an entry. #[derive(Copy, Clone, Debug, PartialEq, Eq)] diff --git a/gix-index/src/entry/mode.rs b/gix-index/src/entry/mode.rs index 583c295bc7b..fca861d2b18 100644 --- a/gix-index/src/entry/mode.rs +++ b/gix-index/src/entry/mode.rs @@ -11,6 +11,12 @@ impl Mode { *self == Self::DIR | Self::SYMLINK } + /// Convert this instance to a tree's entry mode, or return `None` if for some + /// and unexpected reason the bitflags don't resemble any known entry-mode. + pub fn to_tree_entry_mode(&self) -> Option { + gix_object::tree::EntryMode::try_from(self.bits()).ok() + } + /// Compares this mode to the file system version ([`std::fs::symlink_metadata`]) /// and returns the change needed to update this mode to match the file. /// From 77686db3f91e16fa6657dbae2182ec72e88d3fd0 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 5 Dec 2023 10:31:24 +0100 Subject: [PATCH 3/4] feat: `revision::Spec::path_and_mode()` Provide additional information about revspecs for use with worktree filters. --- gix/src/ext/rev_spec.rs | 1 + gix/src/revision/mod.rs | 3 +++ gix/src/revision/spec/mod.rs | 9 +++++++++ gix/src/revision/spec/parse/delegate/mod.rs | 2 ++ .../revision/spec/parse/delegate/navigate.rs | 20 ++++++++++++++++--- gix/src/revision/spec/parse/mod.rs | 4 ++++ .../revision/spec/from_bytes/ambiguous.rs | 7 ++++++- gix/tests/revision/spec/from_bytes/mod.rs | 16 +++++++++++++-- gix/tests/revision/spec/from_bytes/peel.rs | 4 +++- 9 files changed, 59 insertions(+), 7 deletions(-) diff --git a/gix/src/ext/rev_spec.rs b/gix/src/ext/rev_spec.rs index ed7dc0460c7..caa58e2c77f 100644 --- a/gix/src/ext/rev_spec.rs +++ b/gix/src/ext/rev_spec.rs @@ -12,6 +12,7 @@ impl RevSpecExt for gix_revision::Spec { fn attach(self, repo: &crate::Repository) -> crate::revision::Spec<'_> { crate::revision::Spec { inner: self, + path: None, first_ref: None, second_ref: None, repo, diff --git a/gix/src/revision/mod.rs b/gix/src/revision/mod.rs index 3de528ecde9..62fe72dd3f6 100644 --- a/gix/src/revision/mod.rs +++ b/gix/src/revision/mod.rs @@ -7,6 +7,7 @@ pub use gix_revision as plumbing; /// pub mod walk; +use crate::bstr::BString; pub use walk::iter::Walk; /// @@ -22,6 +23,8 @@ pub mod spec; #[cfg(feature = "revision")] pub struct Spec<'repo> { pub(crate) inner: gix_revision::Spec, + /// The path we encountered in the revspec, like `@:` or `@..@~1:`. + pub(crate) path: Option<(BString, gix_object::tree::EntryMode)>, /// The first name of a reference as seen while parsing a `RevSpec`, for completeness. pub(crate) first_ref: Option, /// The second name of a reference as seen while parsing a `RevSpec`, for completeness. diff --git a/gix/src/revision/spec/mod.rs b/gix/src/revision/spec/mod.rs index a6a6eb73994..af58ecdff51 100644 --- a/gix/src/revision/spec/mod.rs +++ b/gix/src/revision/spec/mod.rs @@ -1,3 +1,4 @@ +use crate::bstr::BStr; use crate::{ext::ReferenceExt, revision::Spec, Id, Reference}; /// @@ -37,6 +38,7 @@ impl<'repo> Spec<'repo> { pub fn from_id(id: Id<'repo>) -> Self { Spec { inner: gix_revision::Spec::Include(id.inner), + path: None, repo: id.repo, first_ref: None, second_ref: None, @@ -62,6 +64,13 @@ impl<'repo> Spec<'repo> { ) } + /// Return the path encountered in specs like `@:` or `:`, along with the kind of object it represents. + /// + /// Note that there can only be one as paths always terminates further revspec parsing. + pub fn path_and_mode(&self) -> Option<(&BStr, gix_object::tree::EntryMode)> { + self.path.as_ref().map(|(p, mode)| (p.as_ref(), *mode)) + } + /// Return the name of the first reference we encountered while resolving the rev-spec, or `None` if a short hash /// was used. For example, `@` might yield `Some(HEAD)`, but `abcd` yields `None`. pub fn first_reference(&self) -> Option<&gix_ref::Reference> { diff --git a/gix/src/revision/spec/parse/delegate/mod.rs b/gix/src/revision/spec/parse/delegate/mod.rs index eaf7f5fd6e3..374906eaf6b 100644 --- a/gix/src/revision/spec/parse/delegate/mod.rs +++ b/gix/src/revision/spec/parse/delegate/mod.rs @@ -17,6 +17,7 @@ impl<'repo> Delegate<'repo> { Delegate { refs: Default::default(), objs: Default::default(), + paths: Default::default(), ambiguous_objects: Default::default(), idx: 0, kind: None, @@ -100,6 +101,7 @@ impl<'repo> Delegate<'repo> { let range = zero_or_one_objects_or_ambiguity_err(self.objs, self.prefix, self.err, self.repo)?; Ok(crate::revision::Spec { + path: self.paths[0].take().or(self.paths[1].take()), first_ref: self.refs[0].take(), second_ref: self.refs[1].take(), inner: kind_to_spec(self.kind, range)?, diff --git a/gix/src/revision/spec/parse/delegate/navigate.rs b/gix/src/revision/spec/parse/delegate/navigate.rs index 51feb1d76f0..731a24136d3 100644 --- a/gix/src/revision/spec/parse/delegate/navigate.rs +++ b/gix/src/revision/spec/parse/delegate/navigate.rs @@ -121,7 +121,7 @@ impl<'repo> delegate::Navigate for Delegate<'repo> { let lookup_path = |obj: &ObjectId| { let tree_id = peel(repo, obj, gix_object::Kind::Tree)?; if path.is_empty() { - return Ok(tree_id); + return Ok((tree_id, gix_object::tree::EntryKind::Tree.into())); } let mut tree = repo.find_object(tree_id)?.into_tree(); let entry = @@ -131,11 +131,17 @@ impl<'repo> delegate::Navigate for Delegate<'repo> { object: obj.attach(repo).shorten_or_id(), tree: tree_id.attach(repo).shorten_or_id(), })?; - Ok(entry.object_id()) + Ok((entry.object_id(), entry.mode())) }; for obj in objs.iter() { match lookup_path(obj) { - Ok(replace) => replacements.push((*obj, replace)), + Ok((replace, mode)) => { + if !path.is_empty() { + // Technically this is letting the last one win, but so be it. + self.paths[self.idx] = Some((path.to_owned(), mode)); + } + replacements.push((*obj, replace)) + } Err(err) => errors.push((*obj, err)), } } @@ -306,6 +312,14 @@ impl<'repo> delegate::Navigate for Delegate<'repo> { self.objs[self.idx] .get_or_insert_with(HashSet::default) .insert(entry.id); + + self.paths[self.idx] = Some(( + path.to_owned(), + entry + .mode + .to_tree_entry_mode() + .unwrap_or(gix_object::tree::EntryKind::Blob.into()), + )); Some(()) } None => { diff --git a/gix/src/revision/spec/parse/mod.rs b/gix/src/revision/spec/parse/mod.rs index 950dfa0040b..e4584776363 100644 --- a/gix/src/revision/spec/parse/mod.rs +++ b/gix/src/revision/spec/parse/mod.rs @@ -7,6 +7,7 @@ use gix_revision::spec::parse; use crate::{bstr::BStr, revision::Spec, Repository}; mod types; +use crate::bstr::BString; pub use types::{Error, ObjectKindHint, Options, RefsHint}; /// @@ -45,6 +46,9 @@ impl<'repo> Spec<'repo> { struct Delegate<'repo> { refs: [Option; 2], objs: [Option>; 2], + /// Path specified like `@:` or `:` for later use when looking up specs. + /// Note that it terminates spec parsing, so it's either `0` or `1`, never both. + paths: [Option<(BString, gix_object::tree::EntryMode)>; 2], /// The originally encountered ambiguous objects for potential later use in errors. ambiguous_objects: [Option>; 2], idx: usize, diff --git a/gix/tests/revision/spec/from_bytes/ambiguous.rs b/gix/tests/revision/spec/from_bytes/ambiguous.rs index 261575858e8..68ed163745e 100644 --- a/gix/tests/revision/spec/from_bytes/ambiguous.rs +++ b/gix/tests/revision/spec/from_bytes/ambiguous.rs @@ -106,11 +106,16 @@ fn blob_and_tree_can_be_disambiguated_by_type() { #[test] fn trees_can_be_disambiguated_by_blob_access() { let repo = repo("ambiguous_blob_tree_commit").unwrap(); + let actual = parse_spec_better_than_baseline("0000000000:a0blgqsjc", &repo).unwrap(); assert_eq!( - parse_spec_better_than_baseline("0000000000:a0blgqsjc", &repo).unwrap(), + actual, Spec::from_id(hex_to_id("0000000000b36b6aa7ea4b75318ed078f55505c3").attach(&repo)), "we can disambiguate by providing a path, but git cannot" ); + assert_eq!( + actual.path_and_mode().expect("set"), + ("a0blgqsjc".into(), gix_object::tree::EntryKind::Blob.into()) + ); } #[test] diff --git a/gix/tests/revision/spec/from_bytes/mod.rs b/gix/tests/revision/spec/from_bytes/mod.rs index 8fde68aa957..a8eeb690cef 100644 --- a/gix/tests/revision/spec/from_bytes/mod.rs +++ b/gix/tests/revision/spec/from_bytes/mod.rs @@ -43,10 +43,16 @@ mod index { #[test] fn at_stage() { let repo = repo("complex_graph").unwrap(); + let actual = parse_spec(":file", &repo).unwrap(); assert_eq!( - parse_spec(":file", &repo).unwrap(), + actual, Spec::from_id(hex_to_id("fe27474251f7f8368742f01fbd3bd5666b630a82").attach(&repo)) ); + assert_eq!( + actual.path_and_mode().expect("set"), + ("file".into(), gix_object::tree::EntryKind::Blob.into()), + "index paths (that are present) are captured" + ); assert_eq!( parse_spec(":1:file", &repo).unwrap_err().to_string(), @@ -116,10 +122,16 @@ fn bad_objects_are_valid_until_they_are_actually_read_from_the_odb() { #[test] fn access_blob_through_tree() { let repo = repo("ambiguous_blob_tree_commit").unwrap(); + let actual = parse_spec("0000000000cdc:a0blgqsjc", &repo).unwrap(); assert_eq!( - parse_spec("0000000000cdc:a0blgqsjc", &repo).unwrap(), + actual, Spec::from_id(hex_to_id("0000000000b36b6aa7ea4b75318ed078f55505c3").attach(&repo)) ); + assert_eq!( + actual.path_and_mode().expect("set"), + ("a0blgqsjc".into(), gix_object::tree::EntryKind::Blob.into()), + "we capture tree-paths" + ); assert_eq!( parse_spec("0000000000cdc:missing", &repo).unwrap_err().to_string(), diff --git a/gix/tests/revision/spec/from_bytes/peel.rs b/gix/tests/revision/spec/from_bytes/peel.rs index 415ba39017c..a77afbf570c 100644 --- a/gix/tests/revision/spec/from_bytes/peel.rs +++ b/gix/tests/revision/spec/from_bytes/peel.rs @@ -21,5 +21,7 @@ fn peel_to_object() { #[test] fn trailing_colon_is_equivalent_to_peel_to_tree() { let repo = &repo("complex_graph").unwrap(); - assert_eq!(parse_spec("@^{tree}", repo).unwrap(), parse_spec("@:", repo).unwrap()); + let empty_path = parse_spec("@:", repo).unwrap(); + assert_eq!(parse_spec("@^{tree}", repo).unwrap(), empty_path); + assert_eq!(empty_path.path_and_mode(), None, "empty tree paths are ignored"); } From cf51a4de2d06eb28435ef8e386131710003b6928 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 5 Dec 2023 09:04:14 +0100 Subject: [PATCH 4/4] feat: `gix rev parse --format` to provide different versions of the same content. This only applies to blobs, but allows to obtain different versions of the same blob like: * what's stored in Git * what would be checked out to the worktree * what would be diffed --- crate-status.md | 1 + .../src/repository/revision/resolve.rs | 78 +++++++++++++++++-- src/plumbing/main.rs | 17 +++- src/plumbing/options/mod.rs | 21 ++++- 4 files changed, 107 insertions(+), 10 deletions(-) diff --git a/crate-status.md b/crate-status.md index 406d594f110..fa247e189fa 100644 --- a/crate-status.md +++ b/crate-status.md @@ -43,6 +43,7 @@ The top-level crate that acts as hub to all functionality provided by the `gix-* * [x] support for `GIT_CEILING_DIRECTORIES` environment variable * [ ] handle other non-discovery modes and provide control over environment variable usage required in applications * [x] rev-parse + - [ ] handle relative paths as relative to working directory * [x] rev-walk * [x] include tips * [ ] exclude commits diff --git a/gitoxide-core/src/repository/revision/resolve.rs b/gitoxide-core/src/repository/revision/resolve.rs index 9b2bda61f44..b0844c81a20 100644 --- a/gitoxide-core/src/repository/revision/resolve.rs +++ b/gitoxide-core/src/repository/revision/resolve.rs @@ -5,6 +5,7 @@ pub struct Options { pub explain: bool, pub cat_file: bool, pub tree_mode: TreeMode, + pub blob_format: BlobFormat, } pub enum TreeMode { @@ -12,13 +13,24 @@ pub enum TreeMode { Pretty, } +#[derive(Copy, Clone)] +pub enum BlobFormat { + Git, + Worktree, + Diff, + DiffOrGit, +} + pub(crate) mod function { use std::ffi::OsString; - use anyhow::Context; + use anyhow::{anyhow, Context}; + use gix::diff::blob::ResourceKind; + use gix::filter::plumbing::driver::apply::Delay; use gix::revision::Spec; use super::Options; + use crate::repository::revision::resolve::BlobFormat; use crate::{ repository::{revision, revision::resolve::TreeMode}, OutputFormat, @@ -33,9 +45,26 @@ pub(crate) mod function { explain, cat_file, tree_mode, + blob_format, }: Options, ) -> anyhow::Result<()> { repo.object_cache_size_if_unset(1024 * 1024); + let mut cache = (!matches!(blob_format, BlobFormat::Git)) + .then(|| { + repo.diff_resource_cache( + match blob_format { + BlobFormat::Git => { + unreachable!("checked before") + } + BlobFormat::Worktree | BlobFormat::Diff => { + gix::diff::blob::pipeline::Mode::ToWorktreeAndBinaryToText + } + BlobFormat::DiffOrGit => gix::diff::blob::pipeline::Mode::ToGitUnlessBinaryToTextIsPresent, + }, + Default::default(), + ) + }) + .transpose()?; match format { OutputFormat::Human => { @@ -46,7 +75,7 @@ pub(crate) mod function { let spec = gix::path::os_str_into_bstr(&spec)?; let spec = repo.rev_parse(spec)?; if cat_file { - return display_object(spec, tree_mode, out); + return display_object(&repo, spec, tree_mode, cache.as_mut().map(|c| (blob_format, c)), out); } writeln!(out, "{spec}", spec = spec.detach())?; } @@ -73,16 +102,51 @@ pub(crate) mod function { Ok(()) } - fn display_object(spec: Spec<'_>, tree_mode: TreeMode, mut out: impl std::io::Write) -> anyhow::Result<()> { + fn display_object( + repo: &gix::Repository, + spec: Spec<'_>, + tree_mode: TreeMode, + cache: Option<(BlobFormat, &mut gix::diff::blob::Platform)>, + mut out: impl std::io::Write, + ) -> anyhow::Result<()> { let id = spec.single().context("rev-spec must resolve to a single object")?; - let object = id.object()?; - match object.kind { + let header = id.header()?; + match header.kind() { gix::object::Kind::Tree if matches!(tree_mode, TreeMode::Pretty) => { - for entry in object.into_tree().iter() { + for entry in id.object()?.into_tree().iter() { writeln!(out, "{}", entry?)?; } } - _ => out.write_all(&object.data)?, + gix::object::Kind::Blob if cache.is_some() && spec.path_and_mode().is_some() => { + let (path, mode) = spec.path_and_mode().expect("is present"); + let is_dir = Some(mode.is_tree()); + match cache.expect("is some") { + (BlobFormat::Git, _) => unreachable!("no need for a cache when querying object db"), + (BlobFormat::Worktree, cache) => { + let platform = cache.attr_stack.at_entry(path, is_dir, &repo.objects)?; + let object = id.object()?; + let mut converted = cache.filter.worktree_filter.convert_to_worktree( + &object.data, + path, + &mut |_path, attrs| { + let _ = platform.matching_attributes(attrs); + }, + Delay::Forbid, + )?; + std::io::copy(&mut converted, &mut out)?; + } + (BlobFormat::Diff | BlobFormat::DiffOrGit, cache) => { + cache.set_resource(id.detach(), mode.kind(), path, ResourceKind::OldOrSource, &repo.objects)?; + let resource = cache.resource(ResourceKind::OldOrSource).expect("just set"); + let data = resource + .data + .as_slice() + .ok_or_else(|| anyhow!("Binary data at {} cannot be diffed", path))?; + out.write_all(data)?; + } + } + } + _ => out.write_all(&id.object()?.data)?, } Ok(()) } diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index 77b41a5dd37..6b49746b624 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -926,6 +926,7 @@ pub fn main() -> Result<()> { explain, cat_file, tree_mode, + blob_format, } => prepare_and_run( "revision-parse", trace, @@ -942,12 +943,26 @@ pub fn main() -> Result<()> { format, explain, cat_file, - tree_mode: match tree_mode.unwrap_or_default() { + tree_mode: match tree_mode { revision::resolve::TreeMode::Raw => core::repository::revision::resolve::TreeMode::Raw, revision::resolve::TreeMode::Pretty => { core::repository::revision::resolve::TreeMode::Pretty } }, + blob_format: match blob_format { + revision::resolve::BlobFormat::Git => { + core::repository::revision::resolve::BlobFormat::Git + } + revision::resolve::BlobFormat::Worktree => { + core::repository::revision::resolve::BlobFormat::Worktree + } + revision::resolve::BlobFormat::Diff => { + core::repository::revision::resolve::BlobFormat::Diff + } + revision::resolve::BlobFormat::DiffOrGit => { + core::repository::revision::resolve::BlobFormat::DiffOrGit + } + }, }, ) }, diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs index a5a14989057..60d3c02c2b1 100644 --- a/src/plumbing/options/mod.rs +++ b/src/plumbing/options/mod.rs @@ -614,6 +614,19 @@ pub mod revision { #[default] Pretty, } + + #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] + pub enum BlobFormat { + /// The version stored in the Git Object Database. + #[default] + Git, + /// The version that would be checked out into the worktree, including filters. + Worktree, + /// The version that would be diffed (Worktree + Text-Conversion) + Diff, + /// The version that would be diffed if there is a text-conversion, or the one stored in Git otherwise. + DiffOrGit, + } } #[derive(Debug, clap::Subcommand)] #[clap(visible_alias = "rev", visible_alias = "r")] @@ -645,8 +658,12 @@ pub mod revision { /// Show the first resulting object similar to how `git cat-file` would, but don't show the resolved spec. #[clap(short = 'c', long, conflicts_with = "explain")] cat_file: bool, - #[clap(short = 't', long)] - tree_mode: Option, + /// How to display blobs. + #[clap(short = 'b', long, default_value = "git")] + blob_format: resolve::BlobFormat, + /// How to display trees as obtained with `@:dirname` or `@^{tree}`. + #[clap(short = 't', long, default_value = "pretty")] + tree_mode: resolve::TreeMode, /// rev-specs like `@`, `@~1` or `HEAD^2`. #[clap(required = true)] specs: Vec,