Skip to content

Commit bfdcb14

Browse files
committed
feat: gix attribute query as something similar to git check-attrs.
1 parent 450212e commit bfdcb14

File tree

4 files changed

+116
-0
lines changed

4 files changed

+116
-0
lines changed

Diff for: gitoxide-core/src/repository/attributes.rs

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use std::io;
2+
3+
use anyhow::bail;
4+
use gix::prelude::FindExt;
5+
6+
use crate::OutputFormat;
7+
8+
pub mod query {
9+
use crate::OutputFormat;
10+
11+
pub struct Options {
12+
pub format: OutputFormat,
13+
pub statistics: bool,
14+
}
15+
}
16+
17+
pub fn query(
18+
repo: gix::Repository,
19+
pathspecs: impl Iterator<Item = gix::path::Spec>,
20+
mut out: impl io::Write,
21+
mut err: impl io::Write,
22+
query::Options { format, statistics }: query::Options,
23+
) -> anyhow::Result<()> {
24+
if format != OutputFormat::Human {
25+
bail!("JSON output isn't implemented yet");
26+
}
27+
28+
let index = repo.index()?;
29+
let mut cache = repo.attributes(
30+
&index,
31+
gix::worktree::cache::state::attributes::Source::WorktreeThenIdMapping,
32+
gix::worktree::cache::state::ignore::Source::IdMapping,
33+
None,
34+
)?;
35+
36+
let prefix = repo.prefix().expect("worktree - we have an index by now")?;
37+
let mut matches = cache.attribute_matches();
38+
39+
for mut spec in pathspecs {
40+
for path in spec.apply_prefix(&prefix).items() {
41+
let is_dir = gix::path::from_bstr(path).metadata().ok().map(|m| m.is_dir());
42+
let entry = cache.at_entry(path, is_dir, |oid, buf| repo.objects.find_blob(oid, buf))?;
43+
44+
if !entry.matching_attributes(&mut matches) {
45+
continue;
46+
}
47+
for m in matches.iter() {
48+
writeln!(
49+
out,
50+
"{}:{}:{}\t{}\t{}",
51+
m.location.source.map(|p| p.to_string_lossy()).unwrap_or_default(),
52+
m.location.sequence_number,
53+
m.pattern,
54+
path,
55+
m.assignment
56+
)?;
57+
}
58+
}
59+
}
60+
61+
if let Some(stats) = statistics.then(|| cache.take_statistics()) {
62+
out.flush()?;
63+
writeln!(err, "{:#?}", stats).ok();
64+
}
65+
Ok(())
66+
}

Diff for: gitoxide-core/src/repository/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub mod commit;
1515
pub mod config;
1616
mod credential;
1717
pub use credential::function as credential;
18+
pub mod attributes;
1819
#[cfg(feature = "blocking-client")]
1920
pub mod clone;
2021
pub mod exclude;

Diff for: src/plumbing/main.rs

+29
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use gitoxide_core as core;
1313
use gitoxide_core::pack::verify;
1414
use gix::bstr::io::BufReadExt;
1515

16+
use crate::plumbing::options::attributes;
1617
use crate::{
1718
plumbing::{
1819
options::{commit, config, credential, exclude, free, index, mailmap, odb, revision, tree, Args, Subcommands},
@@ -831,6 +832,34 @@ pub fn main() -> Result<()> {
831832
},
832833
),
833834
},
835+
Subcommands::Attributes(cmd) => match cmd {
836+
attributes::Subcommands::Query { statistics, pathspecs } => prepare_and_run(
837+
"attributes-query",
838+
verbose,
839+
progress,
840+
progress_keep_open,
841+
None,
842+
move |_progress, out, err| {
843+
use gix::bstr::ByteSlice;
844+
core::repository::attributes::query(
845+
repository(Mode::Strict)?,
846+
if pathspecs.is_empty() {
847+
Box::new(
848+
stdin_or_bail()?
849+
.byte_lines()
850+
.filter_map(Result::ok)
851+
.filter_map(|line| gix::path::Spec::from_bytes(line.as_bstr())),
852+
) as Box<dyn Iterator<Item = gix::path::Spec>>
853+
} else {
854+
Box::new(pathspecs.into_iter())
855+
},
856+
out,
857+
err,
858+
core::repository::attributes::query::Options { format, statistics },
859+
)
860+
},
861+
),
862+
},
834863
Subcommands::Exclude(cmd) => match cmd {
835864
exclude::Subcommands::Query {
836865
patterns,

Diff for: src/plumbing/options/mod.rs

+20
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ pub enum Subcommands {
103103
/// Interact with the remote hosts.
104104
#[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))]
105105
Remote(remote::Platform),
106+
/// Interact with the attribute files like .gitattributes.
107+
#[clap(subcommand, visible_alias = "attrs")]
108+
Attributes(attributes::Subcommands),
106109
/// Interact with the exclude files like .gitignore.
107110
#[clap(subcommand)]
108111
Exclude(exclude::Subcommands),
@@ -440,6 +443,23 @@ pub mod revision {
440443
}
441444
}
442445

446+
pub mod attributes {
447+
use crate::shared::AsPathSpec;
448+
449+
#[derive(Debug, clap::Subcommand)]
450+
pub enum Subcommands {
451+
/// List all attributes of the given path-specs and display the result similar to `git check-attr`.
452+
Query {
453+
/// Print various statistics to stderr
454+
#[clap(long, short = 's')]
455+
statistics: bool,
456+
/// The git path specifications to list attributes for, or unset to read from stdin one per line.
457+
#[clap(value_parser = AsPathSpec)]
458+
pathspecs: Vec<gix::path::Spec>,
459+
},
460+
}
461+
}
462+
443463
pub mod exclude {
444464
use std::ffi::OsString;
445465

0 commit comments

Comments
 (0)