Unified · Split
| @@ -83,16 +83,16 @@ is usable to some extent. · 82 unmodified lines | ||
| 83 | 83 | * [gix-command](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-command) |
| 84 | 84 | * [gix-prompt](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-prompt) |
| 85 | 85 | * [gix-refspec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-refspec) |
| 86 | − * `gitoxide-core` | |
| 87 | −* **very early** _(possibly without any documentation and many rough edges)_ | |
| 88 | − * [gix-utils](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-utils) | |
| 89 | 86 | * [gix-fs](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-fs) |
| 87 | + * [gix-utils](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-utils) | |
| 88 | + * [gix-hashtable](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-hashtable) | |
| 90 | 89 | * [gix-worktree](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-worktree) |
| 91 | 90 | * [gix-bitmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-bitmap) |
| 91 | + * `gitoxide-core` | |
| 92 | +* **very early** _(possibly without any documentation and many rough edges)_ | |
| 92 | 93 | * [gix-date](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-date) |
| 93 | − * [gix-hashtable](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-hashtable) | |
| 94 | −* **idea** _(just a name placeholder)_ | |
| 95 | 94 | * [gix-archive](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-archive) |
| 95 | +* **idea** _(just a name placeholder)_ | |
| 96 | 96 | * [gix-note](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-note) |
| 97 | 97 | * [gix-fetchhead](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-fetchhead) |
| 98 | 98 | * [gix-filter](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-filter) |
| @@ -690,7 +690,7 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/gix-lock/README. · 689 unmodified lines | ||
| 690 | 690 | * [x] proper handling of worktree related refs |
| 691 | 691 | * [ ] create, move, remove, and repair |
| 692 | 692 | * [x] access exclude information |
| 693 | − * [ ] access attribute information | |
| 693 | + * [x] access attribute information | |
| 694 | 694 | * [x] respect `core.worktree` configuration |
| 695 | 695 | - **deviation** |
| 696 | 696 | * The delicate interplay between `GIT_COMMON_DIR` and `GIT_WORK_TREE` isn't implemented. |
| @@ -1,94 +0,0 @@ | ||
| 1 | −use std::path::Path; | |
| 2 | − | |
| 3 | −use crate::index::{parse_file, Options}; | |
| 4 | − | |
| 5 | −pub fn entries( | |
| 6 | − index_path: impl AsRef<Path>, | |
| 7 | − mut out: impl std::io::Write, | |
| 8 | − Options { object_hash, format }: Options, | |
| 9 | −) -> anyhow::Result<()> { | |
| 10 | − use crate::OutputFormat::*; | |
| 11 | − let file = parse_file(index_path, object_hash)?; | |
| 12 | − | |
| 13 | − #[cfg(feature = "serde")] | |
| 14 | − if let Json = format { | |
| 15 | − out.write_all(b"[\n")?; | |
| 16 | − } | |
| 17 | − | |
| 18 | − let mut entries = file.entries().iter().peekable(); | |
| 19 | − while let Some(entry) = entries.next() { | |
| 20 | − match format { | |
| 21 | − Human => to_human(&mut out, &file, entry)?, | |
| 22 | − #[cfg(feature = "serde")] | |
| 23 | − Json => to_json(&mut out, &file, entry, entries.peek().is_none())?, | |
| 24 | − } | |
| 25 | − } | |
| 26 | − | |
| 27 | − #[cfg(feature = "serde")] | |
| 28 | − if let Json = format { | |
| 29 | − out.write_all(b"]\n")?; | |
| 30 | − } | |
| 31 | − Ok(()) | |
| 32 | −} | |
| 33 | − | |
| 34 | −#[cfg(feature = "serde")] | |
| 35 | −pub(crate) fn to_json( | |
| 36 | − mut out: &mut impl std::io::Write, | |
| 37 | − file: &gix::index::File, | |
| 38 | − entry: &gix::index::Entry, | |
| 39 | − is_last: bool, | |
| 40 | −) -> anyhow::Result<()> { | |
| 41 | − use gix::bstr::ByteSlice; | |
| 42 | − | |
| 43 | − #[cfg_attr(feature = "serde", derive(serde::Serialize))] | |
| 44 | − struct Entry<'a> { | |
| 45 | − stat: &'a gix::index::entry::Stat, | |
| 46 | − hex_id: String, | |
| 47 | − flags: u32, | |
| 48 | − mode: u32, | |
| 49 | − path: std::borrow::Cow<'a, str>, | |
| 50 | − } | |
| 51 | − | |
| 52 | − serde_json::to_writer( | |
| 53 | − &mut out, | |
| 54 | − &Entry { | |
| 55 | − stat: &entry.stat, | |
| 56 | − hex_id: entry.id.to_hex().to_string(), | |
| 57 | − flags: entry.flags.bits(), | |
| 58 | − mode: entry.mode.bits(), | |
| 59 | − path: entry.path(file).to_str_lossy(), | |
| 60 | − }, | |
| 61 | − )?; | |
| 62 | − | |
| 63 | − if is_last { | |
| 64 | − out.write_all(b"\n")?; | |
| 65 | − } else { | |
| 66 | − out.write_all(b",\n")?; | |
| 67 | − } | |
| 68 | − Ok(()) | |
| 69 | −} | |
| 70 | − | |
| 71 | −pub(crate) fn to_human( | |
| 72 | − out: &mut impl std::io::Write, | |
| 73 | − file: &gix::index::File, | |
| 74 | − entry: &gix::index::Entry, | |
| 75 | −) -> std::io::Result<()> { | |
| 76 | − writeln!( | |
| 77 | − out, | |
| 78 | − "{} {}{:?} {} {}", | |
| 79 | − match entry.flags.stage() { | |
| 80 | − 0 => "BASE ", | |
| 81 | − 1 => "OURS ", | |
| 82 | − 2 => "THEIRS ", | |
| 83 | − _ => "UNKNOWN", | |
| 84 | − }, | |
| 85 | − if entry.flags.is_empty() { | |
| 86 | − "".to_string() | |
| 87 | − } else { | |
| 88 | − format!("{:?} ", entry.flags) | |
| 89 | − }, | |
| 90 | − entry.mode, | |
| 91 | − entry.id, | |
| 92 | − entry.path(file) | |
| 93 | − ) | |
| 94 | −} | |
| @@ -5,9 +5,6 @@ pub struct Options { · 4 unmodified lines | ||
| 5 | 5 | pub format: crate::OutputFormat, |
| 6 | 6 | } |
| 7 | 7 | |
| 8 | −mod entries; | |
| 9 | −pub use entries::entries; | |
| 10 | − | |
| 11 | 8 | pub mod information; |
| 12 | 9 | |
| 13 | 10 | fn parse_file(index_path: impl AsRef<Path>, object_hash: gix::hash::Kind) -> anyhow::Result<gix::index::File> { |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | use std::io; |
| 2 | 2 | |
| 3 | −use anyhow::{bail, Context}; | |
| 3 | +use anyhow::bail; | |
| 4 | 4 | use gix::prelude::FindExt; |
| 5 | 5 | |
| 6 | 6 | use crate::OutputFormat; |
| @@ -31,11 +31,12 @@ pub fn query( · 24 unmodified lines | ||
| 31 | 31 | bail!("JSON output isn't implemented yet"); |
| 32 | 32 | } |
| 33 | 33 | |
| 34 | − let worktree = repo | |
| 35 | − .worktree() | |
| 36 | − .with_context(|| "Cannot check excludes without a current worktree")?; | |
| 37 | − let index = worktree.index()?; | |
| 38 | − let mut cache = worktree.excludes(&index, Some(gix::ignore::Search::from_overrides(overrides)))?; | |
| 34 | + let index = repo.index()?; | |
| 35 | + let mut cache = repo.excludes( | |
| 36 | + &index, | |
| 37 | + Some(gix::ignore::Search::from_overrides(overrides)), | |
| 38 | + Default::default(), | |
| 39 | + )?; | |
| 39 | 40 | |
| 40 | 41 | let prefix = repo.prefix().expect("worktree - we have an index by now")?; |
| 41 | 42 | |
| @@ -0,0 +1,228 @@ | ||
| 1 | +#[derive(Debug)] | |
| 2 | +pub struct Options { | |
| 3 | + pub format: crate::OutputFormat, | |
| 4 | + /// If true, also show attributes | |
| 5 | + pub attributes: Option<Attributes>, | |
| 6 | + pub statistics: bool, | |
| 7 | +} | |
| 8 | + | |
| 9 | +#[derive(Debug)] | |
| 10 | +pub enum Attributes { | |
| 11 | + /// Look at worktree attributes and index as fallback. | |
| 12 | + WorktreeAndIndex, | |
| 13 | + /// Look at attributes from index files only. | |
| 14 | + Index, | |
| 15 | +} | |
| 16 | + | |
| 17 | +pub(crate) mod function { | |
| 18 | + use crate::repository::index::entries::{Attributes, Options}; | |
| 19 | + use gix::attrs::State; | |
| 20 | + use gix::bstr::ByteSlice; | |
| 21 | + use gix::odb::FindExt; | |
| 22 | + use std::borrow::Cow; | |
| 23 | + use std::io::{BufWriter, Write}; | |
| 24 | + | |
| 25 | + pub fn entries( | |
| 26 | + repo: gix::Repository, | |
| 27 | + out: impl std::io::Write, | |
| 28 | + mut err: impl std::io::Write, | |
| 29 | + Options { | |
| 30 | + format, | |
| 31 | + attributes, | |
| 32 | + statistics, | |
| 33 | + }: Options, | |
| 34 | + ) -> anyhow::Result<()> { | |
| 35 | + use crate::OutputFormat::*; | |
| 36 | + let index = repo.index()?; | |
| 37 | + let mut cache = attributes | |
| 38 | + .map(|attrs| { | |
| 39 | + repo.attributes( | |
| 40 | + &index, | |
| 41 | + match attrs { | |
| 42 | + Attributes::WorktreeAndIndex => { | |
| 43 | + gix::worktree::cache::state::attributes::Source::WorktreeThenIdMapping | |
| 44 | + } | |
| 45 | + Attributes::Index => gix::worktree::cache::state::attributes::Source::IdMapping, | |
| 46 | + }, | |
| 47 | + match attrs { | |
| 48 | + Attributes::WorktreeAndIndex => { | |
| 49 | + gix::worktree::cache::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped | |
| 50 | + } | |
| 51 | + Attributes::Index => gix::worktree::cache::state::ignore::Source::IdMapping, | |
| 52 | + }, | |
| 53 | + None, | |
| 54 | + ) | |
| 55 | + .map(|cache| (cache.attribute_matches(), cache)) | |
| 56 | + }) | |
| 57 | + .transpose()?; | |
| 58 | + let mut stats = Statistics { | |
| 59 | + entries: index.entries().len(), | |
| 60 | + ..Default::default() | |
| 61 | + }; | |
| 62 | + | |
| 63 | + let mut out = BufWriter::new(out); | |
| 64 | + #[cfg(feature = "serde")] | |
| 65 | + if let Json = format { | |
| 66 | + out.write_all(b"[\n")?; | |
| 67 | + } | |
| 68 | + let mut entries = index.entries().iter().peekable(); | |
| 69 | + while let Some(entry) = entries.next() { | |
| 70 | + let attrs = cache | |
| 71 | + .as_mut() | |
| 72 | + .map(|(attrs, cache)| { | |
| 73 | + cache | |
| 74 | + .at_entry(entry.path(&index), None, |id, buf| repo.objects.find_blob(id, buf)) | |
| 75 | + .map(|entry| { | |
| 76 | + let is_excluded = entry.is_excluded(); | |
| 77 | + stats.excluded += usize::from(is_excluded); | |
| 78 | + let attributes: Vec<_> = { | |
| 79 | + entry.matching_attributes(attrs); | |
| 80 | + attrs.iter().map(|m| m.assignment.to_owned()).collect() | |
| 81 | + }; | |
| 82 | + stats.with_attributes += usize::from(!attributes.is_empty()); | |
| 83 | + Attrs { | |
| 84 | + is_excluded, | |
| 85 | + attributes, | |
| 86 | + } | |
| 87 | + }) | |
| 88 | + }) | |
| 89 | + .transpose()?; | |
| 90 | + match format { | |
| 91 | + Human => to_human(&mut out, &index, entry, attrs)?, | |
| 92 | + #[cfg(feature = "serde")] | |
| 93 | + Json => to_json(&mut out, &index, entry, attrs, entries.peek().is_none())?, | |
| 94 | + } | |
| 95 | + } | |
| 96 | + | |
| 97 | + #[cfg(feature = "serde")] | |
| 98 | + if format == Json { | |
| 99 | + out.write_all(b"]\n")?; | |
| 100 | + out.flush()?; | |
| 101 | + if statistics { | |
| 102 | + serde_json::to_writer_pretty(&mut err, &stats)?; | |
| 103 | + } | |
| 104 | + } | |
| 105 | + if format == Human && statistics { | |
| 106 | + out.flush()?; | |
| 107 | + stats.cache = cache.map(|c| *c.1.statistics()); | |
| 108 | + writeln!(err, "{:#?}", stats)?; | |
| 109 | + } | |
| 110 | + Ok(()) | |
| 111 | + } | |
| 112 | + | |
| 113 | + #[cfg_attr(feature = "serde", derive(serde::Serialize))] | |
| 114 | + struct Attrs { | |
| 115 | + is_excluded: bool, | |
| 116 | + attributes: Vec<gix::attrs::Assignment>, | |
| 117 | + } | |
| 118 | + | |
| 119 | + #[cfg_attr(feature = "serde", derive(serde::Serialize))] | |
| 120 | + #[derive(Default, Debug)] | |
| 121 | + struct Statistics { | |
| 122 | + #[allow(dead_code)] // Not really dead, but Debug doesn't count for it even though it's crucial. | |
| 123 | + pub entries: usize, | |
| 124 | + pub excluded: usize, | |
| 125 | + pub with_attributes: usize, | |
| 126 | + pub cache: Option<gix::worktree::cache::Statistics>, | |
| 127 | + } | |
| 128 | + | |
| 129 | + #[cfg(feature = "serde")] | |
| 130 | + fn to_json( | |
| 131 | + mut out: &mut impl std::io::Write, | |
| 132 | + index: &gix::index::File, | |
| 133 | + entry: &gix::index::Entry, | |
| 134 | + attrs: Option<Attrs>, | |
| 135 | + is_last: bool, | |
| 136 | + ) -> anyhow::Result<()> { | |
| 137 | + #[cfg_attr(feature = "serde", derive(serde::Serialize))] | |
| 138 | + struct Entry<'a> { | |
| 139 | + stat: &'a gix::index::entry::Stat, | |
| 140 | + hex_id: String, | |
| 141 | + flags: u32, | |
| 142 | + mode: u32, | |
| 143 | + path: std::borrow::Cow<'a, str>, | |
| 144 | + meta: Option<Attrs>, | |
| 145 | + } | |
| 146 | + | |
| 147 | + serde_json::to_writer( | |
| 148 | + &mut out, | |
| 149 | + &Entry { | |
| 150 | + stat: &entry.stat, | |
| 151 | + hex_id: entry.id.to_hex().to_string(), | |
| 152 | + flags: entry.flags.bits(), | |
| 153 | + mode: entry.mode.bits(), | |
| 154 | + path: entry.path(index).to_str_lossy(), | |
| 155 | + meta: attrs, | |
| 156 | + }, | |
| 157 | + )?; | |
| 158 | + | |
| 159 | + if is_last { | |
| 160 | + out.write_all(b"\n")?; | |
| 161 | + } else { | |
| 162 | + out.write_all(b",\n")?; | |
| 163 | + } | |
| 164 | + Ok(()) | |
| 165 | + } | |
| 166 | + | |
| 167 | + fn to_human( | |
| 168 | + out: &mut impl std::io::Write, | |
| 169 | + file: &gix::index::File, | |
| 170 | + entry: &gix::index::Entry, | |
| 171 | + attrs: Option<Attrs>, | |
| 172 | + ) -> std::io::Result<()> { | |
| 173 | + writeln!( | |
| 174 | + out, | |
| 175 | + "{} {}{:?} {} {}{}", | |
| 176 | + match entry.flags.stage() { | |
| 177 | + 0 => "BASE ", | |
| 178 | + 1 => "OURS ", | |
| 179 | + 2 => "THEIRS ", | |
| 180 | + _ => "UNKNOWN", | |
| 181 | + }, | |
| 182 | + if entry.flags.is_empty() { | |
| 183 | + "".to_string() | |
| 184 | + } else { | |
| 185 | + format!("{:?} ", entry.flags) | |
| 186 | + }, | |
| 187 | + entry.mode, | |
| 188 | + entry.id, | |
| 189 | + entry.path(file), | |
| 190 | + attrs | |
| 191 | + .map(|a| { | |
| 192 | + let mut buf = String::new(); | |
| 193 | + if a.is_excluded { | |
| 194 | + buf.push_str(" ❌"); | |
| 195 | + } | |
| 196 | + if !a.attributes.is_empty() { | |
| 197 | + buf.push_str(" ("); | |
| 198 | + for assignment in a.attributes { | |
| 199 | + match assignment.state { | |
| 200 | + State::Set => { | |
| 201 | + buf.push_str(assignment.name.as_str()); | |
| 202 | + } | |
| 203 | + State::Unset => { | |
| 204 | + buf.push('-'); | |
| 205 | + buf.push_str(assignment.name.as_str()); | |
| 206 | + } | |
| 207 | + State::Value(v) => { | |
| 208 | + buf.push_str(assignment.name.as_str()); | |
| 209 | + buf.push('='); | |
| 210 | + buf.push_str(v.as_ref().as_bstr().to_str_lossy().as_ref()); | |
| 211 | + } | |
| 212 | + State::Unspecified => { | |
| 213 | + buf.push('!'); | |
| 214 | + buf.push_str(assignment.name.as_str()); | |
| 215 | + } | |
| 216 | + } | |
| 217 | + buf.push_str(", "); | |
| 218 | + } | |
| 219 | + buf.pop(); | |
| 220 | + buf.pop(); | |
| 221 | + buf.push(')'); | |
| 222 | + } | |
| 223 | + buf.into() | |
| 224 | + }) | |
| 225 | + .unwrap_or(Cow::Borrowed("")) | |
| 226 | + ) | |
| 227 | + } | |
| 228 | +} | |
| @@ -34,3 +34,6 @@ pub fn from_tree( · 33 unmodified lines | ||
| 34 | 34 | |
| 35 | 35 | Ok(()) |
| 36 | 36 | } |
| 37 | + | |
| 38 | +pub mod entries; | |
| 39 | +pub use entries::function::entries; | |
| @@ -1,13 +1,17 @@ | ||
| 1 | 1 | use std::convert::TryInto; |
| 2 | 2 | |
| 3 | +/// | |
| 3 | 4 | pub mod decode { |
| 5 | + /// The error returned by [`decode()`][super::decode()]. | |
| 4 | 6 | #[derive(Debug, thiserror::Error)] |
| 7 | + #[allow(missing_docs)] | |
| 5 | 8 | pub enum Error { |
| 6 | 9 | #[error("{}", message)] |
| 7 | 10 | Corrupt { message: &'static str }, |
| 8 | 11 | } |
| 9 | 12 | } |
| 10 | 13 | |
| 14 | +/// Decode `data` as EWAH bitmap. | |
| 11 | 15 | pub fn decode(data: &[u8]) -> Result<(Vec, &[u8]), decode::Error> { |
| 12 | 16 | use self::decode::Error; |
| 13 | 17 | use crate::decode; |
| @@ -1,8 +1,7 @@ | ||
| 1 | 1 | //! An implementation of the shared parts of git bitmaps used in `gix-pack`, `gix-index` and `gix-worktree`. |
| 2 | 2 | //! |
| 3 | 3 | //! Note that many tests are performed indirectly by tests in the aforementioned consumer crates. |
| 4 | −#![deny(rust_2018_idioms, unsafe_code)] | |
| 5 | −#![allow(missing_docs)] | |
| 4 | +#![deny(rust_2018_idioms, unsafe_code, missing_docs)] | |
| 6 | 5 | |
| 7 | 6 | /// Bitmap utilities for the advanced word-aligned hybrid bitmap |
| 8 | 7 | pub mod ewah; |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | //! A crate with file-system specific utilities. |
| 2 | −#![deny(rust_2018_idioms)] | |
| 2 | +#![deny(rust_2018_idioms, missing_docs)] | |
| 3 | 3 | #![forbid(unsafe_code)] |
| 4 | 4 | |
| 5 | 5 | /// Common knowledge about the worktree that is needed across most interactions with the work tree |
| @@ -51,4 +51,5 @@ pub struct Stack { · 45 unmodified lines | ||
| 51 | 51 | current_is_directory: bool, |
| 52 | 52 | } |
| 53 | 53 | |
| 54 | +/// | |
| 54 | 55 | pub mod stack; |
| @@ -1,16 +1,21 @@ | ||
| 1 | 1 | use std::{io, io::ErrorKind::AlreadyExists, path::Path}; |
| 2 | 2 | |
| 3 | 3 | #[cfg(not(windows))] |
| 4 | +/// Create a new symlink at `link` which points to `original`. | |
| 4 | 5 | pub fn create(original: &Path, link: &Path) -> io::Result<()> { |
| 5 | 6 | std::os::unix::fs::symlink(original, link) |
| 6 | 7 | } |
| 7 | 8 | |
| 8 | 9 | #[cfg(not(windows))] |
| 10 | +/// Remove a symlink. | |
| 11 | +/// | |
| 12 | +/// Note that on only on windows this is special. | |
| 9 | 13 | pub fn remove(path: &Path) -> io::Result<()> { |
| 10 | 14 | std::fs::remove_file(path) |
| 11 | 15 | } |
| 12 | 16 | |
| 13 | 17 | // TODO: use the `symlink` crate once it can delete directory symlinks |
| 18 | +/// Remove a symlink. | |
| 14 | 19 | #[cfg(windows)] |
| 15 | 20 | pub fn remove(path: &Path) -> io::Result<()> { |
| 16 | 21 | if let Ok(meta) = std::fs::metadata(path) { |
| @@ -25,6 +30,7 @@ pub fn remove(path: &Path) -> io::Result<()> { · 8 unmodified lines | ||
| 25 | 30 | } |
| 26 | 31 | |
| 27 | 32 | #[cfg(windows)] |
| 33 | +/// Create a new symlink at `link` which points to `original`. | |
| 28 | 34 | pub fn create(original: &Path, link: &Path) -> io::Result<()> { |
| 29 | 35 | use std::os::windows::fs::{symlink_dir, symlink_file}; |
| 30 | 36 | // TODO: figure out if links to links count as files or whatever they point at |
| @@ -36,6 +42,8 @@ pub fn create(original: &Path, link: &Path) -> io::Result<()> { · 5 unmodified lines | ||
| 36 | 42 | } |
| 37 | 43 | |
| 38 | 44 | #[cfg(not(windows))] |
| 45 | +/// Return true if `err` indicates that a file collision happened, i.e. a symlink couldn't be created as the `link` | |
| 46 | +/// already exists as filesystem object. | |
| 39 | 47 | pub fn is_collision_error(err: &std::io::Error) -> bool { |
| 40 | 48 | // TODO: use ::IsDirectory as well when stabilized instead of raw_os_error(), and ::FileSystemLoop respectively |
| 41 | 49 | err.kind() == AlreadyExists |
| @@ -45,6 +53,8 @@ pub fn is_collision_error(err: &std::io::Error) -> bool { · 3 unmodified lines | ||
| 45 | 53 | } |
| 46 | 54 | |
| 47 | 55 | #[cfg(windows)] |
| 56 | +/// Return true if `err` indicates that a file collision happened, i.e. a symlink couldn't be created as the `link` | |
| 57 | +/// already exists as filesystem object. | |
| 48 | 58 | pub fn is_collision_error(err: &std::io::Error) -> bool { |
| 49 | 59 | err.kind() == AlreadyExists || err.kind() == std::io::ErrorKind::PermissionDenied |
| 50 | 60 | } |
| @@ -85,7 +85,7 @@ pub fn into_bstr<'a>(path: impl Into<Cow<'a, Path>>) -> Cow<'a, BStr> { · 84 unmodified lines | ||
| 85 | 85 | /// Join `path` to `base` such that they are separated with a `/`, i.e. `base/path`. |
| 86 | 86 | pub fn join_bstr_unix_pathsep<'a, 'b>(base: impl Into<Cow<'a, BStr>>, path: impl Into<&'b BStr>) -> Cow<'a, BStr> { |
| 87 | 87 | let mut base = base.into(); |
| 88 | − if base.last() != Some(&b'/') { | |
| 88 | + if !base.is_empty() && base.last() != Some(&b'/') { | |
| 89 | 89 | base.to_mut().push(b'/'); |
| 90 | 90 | } |
| 91 | 91 | base.to_mut().extend_from_slice(path.into()); |
| @@ -56,9 +56,9 @@ mod join_bstr_unix_pathsep { · 55 unmodified lines | ||
| 56 | 56 | assert_eq!(join_bstr_unix_pathsep(b("base/"), ""), b("base/")); |
| 57 | 57 | } |
| 58 | 58 | #[test] |
| 59 | − fn empty_base_creates_absolute_paths() { | |
| 60 | − assert_eq!(join_bstr_unix_pathsep(b(""), ""), b("/")); | |
| 61 | − assert_eq!(join_bstr_unix_pathsep(b(""), "hi"), b("/hi")); | |
| 62 | − assert_eq!(join_bstr_unix_pathsep(b(""), "/hi"), b("//hi")); | |
| 59 | + fn empty_base_leaves_everything_untouched() { | |
| 60 | + assert_eq!(join_bstr_unix_pathsep(b(""), ""), b("")); | |
| 61 | + assert_eq!(join_bstr_unix_pathsep(b(""), "hi"), b("hi")); | |
| 62 | + assert_eq!(join_bstr_unix_pathsep(b(""), "/hi"), b("/hi")); | |
| 63 | 63 | } |
| 64 | 64 | } |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | //! A crate with utilities that don't need feature toggles. |
| 2 | 2 | //! |
| 3 | 3 | //! If they would need feature toggles, they should be in `gix-features` instead. |
| 4 | −#![deny(rust_2018_idioms)] | |
| 4 | +#![deny(rust_2018_idioms, missing_docs)] | |
| 5 | 5 | #![forbid(unsafe_code)] |
| 6 | 6 | |
| 7 | 7 | /// |
| @@ -1,12 +1,30 @@ | ||
| 1 | 1 | use crate::cache::State; |
| 2 | 2 | use crate::PathIdMapping; |
| 3 | 3 | |
| 4 | −pub struct StackDelegate<'a, Find> { | |
| 4 | +/// Various aggregate numbers related to the stack delegate itself. | |
| 5 | +#[derive(Default, Clone, Copy, Debug)] | |
| 6 | +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] | |
| 7 | +pub struct Statistics { | |
| 8 | + /// The amount of `std::fs::create_dir` calls. | |
| 9 | + /// | |
| 10 | + /// This only happens if we are in the respective mode to create leading directories efficiently. | |
| 11 | + pub num_mkdir_calls: usize, | |
| 12 | + /// Amount of calls to push a path element. | |
| 13 | + pub push_element: usize, | |
| 14 | + /// Amount of calls to push a directory. | |
| 15 | + pub push_directory: usize, | |
| 16 | + /// Amount of calls to pop a directory. | |
| 17 | + pub pop_directory: usize, | |
| 18 | +} | |
| 19 | + | |
| 20 | +pub(crate) struct StackDelegate<'a, Find> { | |
| 5 | 21 | pub state: &'a mut State, |
| 6 | 22 | pub buf: &'a mut Vec<u8>, |
| 7 | 23 | pub is_dir: bool, |
| 8 | 24 | pub id_mappings: &'a Vec<PathIdMapping>, |
| 9 | 25 | pub find: Find, |
| 26 | + pub case: gix_glob::pattern::Case, | |
| 27 | + pub statistics: &'a mut super::Statistics, | |
| 10 | 28 | } |
| 11 | 29 | |
| 12 | 30 | impl<'a, Find, E> gix_fs::stack::Delegate for StackDelegate<'a, Find> |
| @@ -15,72 +33,86 @@ where · 2 unmodified lines | ||
| 15 | 33 | E: std::error::Error + Send + Sync + 'static, |
| 16 | 34 | { |
| 17 | 35 | fn push_directory(&mut self, stack: &gix_fs::Stack) -> std::io::Result<()> { |
| 36 | + self.statistics.delegate.push_directory += 1; | |
| 37 | + let dir_bstr = gix_path::into_bstr(stack.current()); | |
| 38 | + let mut rela_dir = gix_glob::search::pattern::strip_base_handle_recompute_basename_pos( | |
| 39 | + gix_path::into_bstr(stack.root()).as_ref(), | |
| 40 | + dir_bstr.as_ref(), | |
| 41 | + None, | |
| 42 | + self.case, | |
| 43 | + ) | |
| 44 | + .expect("dir in root") | |
| 45 | + .0; | |
| 46 | + if rela_dir.starts_with(b"/") { | |
| 47 | + rela_dir = &rela_dir[1..]; | |
| 48 | + } | |
| 18 | 49 | match &mut self.state { |
| 19 | 50 | State::CreateDirectoryAndAttributesStack { attributes, .. } => { |
| 20 | 51 | attributes.push_directory( |
| 21 | 52 | stack.root(), |
| 22 | 53 | stack.current(), |
| 54 | + rela_dir, | |
| 23 | 55 | self.buf, |
| 24 | 56 | self.id_mappings, |
| 25 | 57 | &mut self.find, |
| 58 | + &mut self.statistics.attributes, | |
| 26 | 59 | )?; |
| 27 | 60 | } |
| 28 | 61 | State::AttributesAndIgnoreStack { ignore, attributes } => { |
| 29 | 62 | attributes.push_directory( |
| 30 | 63 | stack.root(), |
| 31 | 64 | stack.current(), |
| 65 | + rela_dir, | |
| 32 | 66 | self.buf, |
| 33 | 67 | self.id_mappings, |
| 34 | 68 | &mut self.find, |
| 69 | + &mut self.statistics.attributes, | |
| 35 | 70 | )?; |
| 36 | 71 | ignore.push_directory( |
| 37 | 72 | stack.root(), |
| 38 | 73 | stack.current(), |
| 74 | + rela_dir, | |
| 39 | 75 | self.buf, |
| 40 | 76 | self.id_mappings, |
| 41 | 77 | &mut self.find, |
| 78 | + self.case, | |
| 79 | + &mut self.statistics.ignore, | |
| 42 | 80 | )? |
| 43 | 81 | } |
| 44 | 82 | State::IgnoreStack(ignore) => ignore.push_directory( |
| 45 | 83 | stack.root(), |
| 46 | 84 | stack.current(), |
| 85 | + rela_dir, | |
| 47 | 86 | self.buf, |
| 48 | 87 | self.id_mappings, |
| 49 | 88 | &mut self.find, |
| 89 | + self.case, | |
| 90 | + &mut self.statistics.ignore, | |
| 50 | 91 | )?, |
| 51 | 92 | } |
| 52 | 93 | Ok(()) |
| 53 | 94 | } |
| 54 | 95 | |
| 55 | 96 | fn push(&mut self, is_last_component: bool, stack: &gix_fs::Stack) -> std::io::Result<()> { |
| 97 | + self.statistics.delegate.push_element += 1; | |
| 56 | 98 | match &mut self.state { |
| 57 | 99 | State::CreateDirectoryAndAttributesStack { |
| 58 | − #[cfg(debug_assertions)] | |
| 59 | − test_mkdir_calls, | |
| 60 | 100 | unlink_on_collision, |
| 61 | 101 | attributes: _, |
| 62 | − } => { | |
| 63 | − #[cfg(debug_assertions)] | |
| 64 | − { | |
| 65 | − create_leading_directory( | |
| 66 | − is_last_component, | |
| 67 | − stack, | |
| 68 | − self.is_dir, | |
| 69 | − test_mkdir_calls, | |
| 70 | − *unlink_on_collision, | |
| 71 | − )? | |
| 72 | − } | |
| 73 | − #[cfg(not(debug_assertions))] | |
| 74 | − { | |
| 75 | − create_leading_directory(is_last_component, stack, self.is_dir, *unlink_on_collision)? | |
| 76 | − } | |
| 77 | − } | |
| 102 | + } => create_leading_directory( | |
| 103 | + is_last_component, | |
| 104 | + stack, | |
| 105 | + self.is_dir, | |
| 106 | + &mut self.statistics.delegate.num_mkdir_calls, | |
| 107 | + *unlink_on_collision, | |
| 108 | + )?, | |
| 78 | 109 | State::AttributesAndIgnoreStack { .. } | State::IgnoreStack(_) => {} |
| 79 | 110 | } |
| 80 | 111 | Ok(()) |
| 81 | 112 | } |
| 82 | 113 | |
| 83 | 114 | fn pop_directory(&mut self) { |
| 115 | + self.statistics.delegate.pop_directory += 1; | |
| 84 | 116 | match &mut self.state { |
| 85 | 117 | State::CreateDirectoryAndAttributesStack { attributes, .. } => { |
| 86 | 118 | attributes.pop_directory(); |
| @@ -100,16 +132,13 @@ fn create_leading_directory( · 13 unmodified lines | ||
| 100 | 132 | is_last_component: bool, |
| 101 | 133 | stack: &gix_fs::Stack, |
| 102 | 134 | is_dir: bool, |
| 103 | − #[cfg(debug_assertions)] mkdir_calls: &mut usize, | |
| 135 | + mkdir_calls: &mut usize, | |
| 104 | 136 | unlink_on_collision: bool, |
| 105 | 137 | ) -> std::io::Result<()> { |
| 106 | 138 | if is_last_component && !is_dir { |
| 107 | 139 | return Ok(()); |
| 108 | 140 | } |
| 109 | − #[cfg(debug_assertions)] | |
| 110 | − { | |
| 111 | − *mkdir_calls += 1; | |
| 112 | − } | |
| 141 | + *mkdir_calls += 1; | |
| 113 | 142 | match std::fs::create_dir(stack.current()) { |
| 114 | 143 | Ok(()) => Ok(()), |
| 115 | 144 | Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { |
| @@ -122,10 +151,7 @@ fn create_leading_directory( · 6 unmodified lines | ||
| 122 | 151 | } else { |
| 123 | 152 | std::fs::remove_file(stack.current())?; |
| 124 | 153 | } |
| 125 | − #[cfg(debug_assertions)] | |
| 126 | − { | |
| 127 | − *mkdir_calls += 1; | |
| 128 | − } | |
| 154 | + *mkdir_calls += 1; | |
| 129 | 155 | std::fs::create_dir(stack.current()) |
| 130 | 156 | } else { |
| 131 | 157 | Err(err) |
| @@ -7,16 +7,26 @@ use gix_hash::oid; · 6 unmodified lines | ||
| 7 | 7 | use super::Cache; |
| 8 | 8 | use crate::PathIdMapping; |
| 9 | 9 | |
| 10 | +/// Various aggregate numbers collected from when the corresponding [`Cache`] was instantiated. | |
| 11 | +#[derive(Default, Clone, Copy, Debug)] | |
| 12 | +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] | |
| 13 | +pub struct Statistics { | |
| 14 | + /// The amount of platforms created to do further matching. | |
| 15 | + pub platforms: usize, | |
| 16 | + /// Information about the stack delegate. | |
| 17 | + pub delegate: delegate::Statistics, | |
| 18 | + /// Information about attributes | |
| 19 | + pub attributes: state::attributes::Statistics, | |
| 20 | + /// Information about the ignore stack | |
| 21 | + pub ignore: state::ignore::Statistics, | |
| 22 | +} | |
| 23 | + | |
| 10 | 24 | #[derive(Clone)] |
| 11 | 25 | pub enum State { |
| 12 | 26 | /// Useful for checkout where directories need creation, but we need to access attributes as well. |
| 13 | 27 | CreateDirectoryAndAttributesStack { |
| 14 | 28 | /// If there is a symlink or a file in our path, try to unlink it before creating the directory. |
| 15 | 29 | unlink_on_collision: bool, |
| 16 | − | |
| 17 | − /// just for testing | |
| 18 | − #[cfg(debug_assertions)] | |
| 19 | − test_mkdir_calls: usize, | |
| 20 | 30 | /// State to handle attribute information |
| 21 | 31 | attributes: state::Attributes, |
| 22 | 32 | }, |
| @@ -31,35 +41,6 @@ pub enum State { · 8 unmodified lines | ||
| 31 | 41 | IgnoreStack(state::Ignore), |
| 32 | 42 | } |
| 33 | 43 | |
| 34 | −#[cfg(debug_assertions)] | |
| 35 | −/// debug builds only for use in tests. | |
| 36 | −impl Cache { | |
| 37 | − pub fn set_case(&mut self, case: gix_glob::pattern::Case) { | |
| 38 | − self.case = case; | |
| 39 | − } | |
| 40 | − pub fn num_mkdir_calls(&self) -> usize { | |
| 41 | − match self.state { | |
| 42 | − State::CreateDirectoryAndAttributesStack { test_mkdir_calls, .. } => test_mkdir_calls, | |
| 43 | − _ => 0, | |
| 44 | − } | |
| 45 | − } | |
| 46 | − | |
| 47 | − pub fn reset_mkdir_calls(&mut self) { | |
| 48 | − if let State::CreateDirectoryAndAttributesStack { test_mkdir_calls, .. } = &mut self.state { | |
| 49 | − *test_mkdir_calls = 0; | |
| 50 | − } | |
| 51 | − } | |
| 52 | − | |
| 53 | − pub fn unlink_on_collision(&mut self, value: bool) { | |
| 54 | − if let State::CreateDirectoryAndAttributesStack { | |
| 55 | − unlink_on_collision, .. | |
| 56 | − } = &mut self.state | |
| 57 | − { | |
| 58 | − *unlink_on_collision = value; | |
| 59 | − } | |
| 60 | − } | |
| 61 | −} | |
| 62 | − | |
| 63 | 44 | #[must_use] |
| 64 | 45 | pub struct Platform<'a> { |
| 65 | 46 | parent: &'a Cache, |
| @@ -87,11 +68,12 @@ impl Cache { · 21 unmodified lines | ||
| 87 | 68 | case, |
| 88 | 69 | buf, |
| 89 | 70 | id_mappings, |
| 71 | + statistics: Statistics::default(), | |
| 90 | 72 | } |
| 91 | 73 | } |
| 92 | 74 | } |
| 93 | 75 | |
| 94 | −/// Mutable Access | |
| 76 | +/// Entry points for attribute query | |
| 95 | 77 | impl Cache { |
| 96 | 78 | /// Append the `relative` path to the root directory of the cache and efficiently create leading directories, while assuring that no |
| 97 | 79 | /// symlinks are in that path. |
| @@ -110,12 +92,15 @@ impl Cache { · 12 unmodified lines | ||
| 110 | 92 | Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E>, |
| 111 | 93 | E: std::error::Error + Send + Sync + 'static, |
| 112 | 94 | { |
| 95 | + self.statistics.platforms += 1; | |
| 113 | 96 | let mut delegate = StackDelegate { |
| 114 | 97 | state: &mut self.state, |
| 115 | 98 | buf: &mut self.buf, |
| 116 | 99 | is_dir: is_dir.unwrap_or(false), |
| 117 | 100 | id_mappings: &self.id_mappings, |
| 118 | 101 | find, |
| 102 | + case: self.case, | |
| 103 | + statistics: &mut self.statistics, | |
| 119 | 104 | }; |
| 120 | 105 | self.stack.make_relative_path_current(relative, &mut delegate)?; |
| 121 | 106 | Ok(Platform { parent: self, is_dir }) |
| @@ -152,8 +137,25 @@ impl Cache { · 30 unmodified lines | ||
| 152 | 137 | } |
| 153 | 138 | } |
| 154 | 139 | |
| 140 | +/// Mutation | |
| 141 | +impl Cache { | |
| 142 | + /// Reset the statistics after returning them. | |
| 143 | + pub fn take_statistics(&mut self) -> Statistics { | |
| 144 | + std::mem::take(&mut self.statistics) | |
| 145 | + } | |
| 146 | + | |
| 147 | + /// Return our state for applying changes. | |
| 148 | + pub fn state_mut(&mut self) -> &mut State { | |
| 149 | + &mut self.state | |
| 150 | + } | |
| 151 | +} | |
| 152 | + | |
| 155 | 153 | /// Access |
| 156 | 154 | impl Cache { |
| 155 | + /// Return the statistics we gathered thus far. | |
| 156 | + pub fn statistics(&self) -> &Statistics { | |
| 157 | + &self.statistics | |
| 158 | + } | |
| 157 | 159 | /// Return the state for introspection. |
| 158 | 160 | pub fn state(&self) -> &State { |
| 159 | 161 | &self.state |
| @@ -167,7 +169,8 @@ impl Cache { · 7 unmodified lines | ||
| 167 | 169 | } |
| 168 | 170 | } |
| 169 | 171 | |
| 170 | −mod delegate; | |
| 172 | +/// | |
| 173 | +pub mod delegate; | |
| 171 | 174 | use delegate::StackDelegate; |
| 172 | 175 | |
| 173 | 176 | mod platform; |
| @@ -6,6 +6,18 @@ use gix_glob::pattern::Case; · 5 unmodified lines | ||
| 6 | 6 | use crate::cache::state::{AttributeMatchGroup, Attributes}; |
| 7 | 7 | use bstr::{BStr, ByteSlice}; |
| 8 | 8 | |
| 9 | +/// Various aggregate numbers related [`Attributes`]. | |
| 10 | +#[derive(Default, Clone, Copy, Debug)] | |
| 11 | +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] | |
| 12 | +pub struct Statistics { | |
| 13 | + /// Amount of patterns buffers read from the index. | |
| 14 | + pub patterns_buffers: usize, | |
| 15 | + /// Amount of pattern files read from disk. | |
| 16 | + pub pattern_files: usize, | |
| 17 | + /// Amount of pattern files we tried to find on disk. | |
| 18 | + pub tried_pattern_files: usize, | |
| 19 | +} | |
| 20 | + | |
| 9 | 21 | /// Decide where to read `.gitattributes` files from. |
| 10 | 22 | #[derive(Default, Debug, Clone, Copy)] |
| 11 | 23 | pub enum Source { |
| @@ -42,7 +54,6 @@ impl Attributes { · 30 unmodified lines | ||
| 42 | 54 | pub fn new( |
| 43 | 55 | globals: AttributeMatchGroup, |
| 44 | 56 | info_attributes: Option<PathBuf>, |
| 45 | − case: Case, | |
| 46 | 57 | source: Source, |
| 47 | 58 | collection: gix_attributes::search::MetadataCollection, |
| 48 | 59 | ) -> Self { |
| @@ -50,7 +61,6 @@ impl Attributes { · 1 unmodified line | ||
| 50 | 61 | globals, |
| 51 | 62 | stack: Default::default(), |
| 52 | 63 | info_attributes, |
| 53 | − case, | |
| 54 | 64 | source, |
| 55 | 65 | collection, |
| 56 | 66 | } |
| @@ -62,31 +72,21 @@ impl Attributes { · 5 unmodified lines | ||
| 62 | 72 | self.stack.pop_pattern_list().expect("something to pop"); |
| 63 | 73 | } |
| 64 | 74 | |
| 75 | + #[allow(clippy::too_many_arguments)] | |
| 65 | 76 | pub(crate) fn push_directory<Find, E>( |
| 66 | 77 | &mut self, |
| 67 | 78 | root: &Path, |
| 68 | 79 | dir: &Path, |
| 80 | + rela_dir: &BStr, | |
| 69 | 81 | buf: &mut Vec<u8>, |
| 70 | 82 | id_mappings: &[PathIdMapping], |
| 71 | 83 | mut find: Find, |
| 84 | + stats: &mut Statistics, | |
| 72 | 85 | ) -> std::io::Result<()> |
| 73 | 86 | where |
| 74 | 87 | Find: for<'b> FnMut(&gix_hash::oid, &'b mut Vec<u8>) -> Result<gix_object::BlobRef<'b>, E>, |
| 75 | 88 | E: std::error::Error + Send + Sync + 'static, |
| 76 | 89 | { |
| 77 | − let dir_bstr = gix_path::into_bstr(dir); | |
| 78 | − let mut rela_dir = gix_glob::search::pattern::strip_base_handle_recompute_basename_pos( | |
| 79 | − gix_path::into_bstr(root).as_ref(), | |
| 80 | − dir_bstr.as_ref(), | |
| 81 | − None, | |
| 82 | − self.case, | |
| 83 | − ) | |
| 84 | − .expect("dir in root") | |
| 85 | − .0; | |
| 86 | − if rela_dir.starts_with(b"/") { | |
| 87 | − rela_dir = &rela_dir[1..]; | |
| 88 | − } | |
| 89 | − | |
| 90 | 90 | let attr_path_relative = |
| 91 | 91 | gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitattributes")); |
| 92 | 92 | let attr_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(attr_path_relative.as_ref())); |
| @@ -103,6 +103,7 @@ impl Attributes { · 10 unmodified lines | ||
| 103 | 103 | self.stack |
| 104 | 104 | .add_patterns_buffer(blob.data, attr_path, Some(Path::new("")), &mut self.collection); |
| 105 | 105 | added = true; |
| 106 | + stats.patterns_buffers += 1; | |
| 106 | 107 | } |
| 107 | 108 | if !added && matches!(self.source, Source::IdMappingThenWorktree) { |
| 108 | 109 | added = self.stack.add_patterns_file( |
| @@ -112,6 +113,8 @@ impl Attributes { · 3 unmodified lines | ||
| 112 | 113 | buf, |
| 113 | 114 | &mut self.collection, |
| 114 | 115 | )?; |
| 116 | + stats.pattern_files += usize::from(added); | |
| 117 | + stats.tried_pattern_files += 1; | |
| 115 | 118 | } |
| 116 | 119 | } |
| 117 | 120 | Source::WorktreeThenIdMapping => { |
| @@ -122,6 +125,8 @@ impl Attributes { · 4 unmodified lines | ||
| 122 | 125 | buf, |
| 123 | 126 | &mut self.collection, |
| 124 | 127 | )?; |
| 128 | + stats.pattern_files += usize::from(added); | |
| 129 | + stats.tried_pattern_files += 1; | |
| 125 | 130 | if let Some(idx) = attr_file_in_index.ok().filter(|_| !added) { |
| 126 | 131 | let blob = find(&id_mappings[idx].1, buf) |
| 127 | 132 | .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; |
| @@ -129,6 +134,7 @@ impl Attributes { · 1 unmodified line | ||
| 129 | 134 | self.stack |
| 130 | 135 | .add_patterns_buffer(blob.data, attr_path, Some(Path::new("")), &mut self.collection); |
| 131 | 136 | added = true; |
| 137 | + stats.patterns_buffers += 1; | |
| 132 | 138 | } |
| 133 | 139 | } |
| 134 | 140 | } |
| @@ -142,8 +148,11 @@ impl Attributes { · 7 unmodified lines | ||
| 142 | 148 | // When reading the root, always the first call, we can try to also read the `.git/info/attributes` file which is |
| 143 | 149 | // by nature never popped, and follows the root, as global. |
| 144 | 150 | if let Some(info_attr) = self.info_attributes.take() { |
| 145 | − self.stack | |
| 151 | + let added = self | |
| 152 | + .stack | |
| 146 | 153 | .add_patterns_file(info_attr, true, None, buf, &mut self.collection)?; |
| 154 | + stats.pattern_files += usize::from(added); | |
| 155 | + stats.tried_pattern_files += 1; | |
| 147 | 156 | } |
| 148 | 157 | |
| 149 | 158 | Ok(()) |
| @@ -168,15 +177,6 @@ impl Attributes { · 18 unmodified lines | ||
| 168 | 177 | } |
| 169 | 178 | } |
| 170 | 179 | |
| 171 | −/// Builder | |
| 172 | −impl Attributes { | |
| 173 | − /// Set the case to use when matching attributes to paths. | |
| 174 | − pub fn with_case(mut self, case: gix_glob::pattern::Case) -> Self { | |
| 175 | − self.case = case; | |
| 176 | − self | |
| 177 | − } | |
| 178 | −} | |
| 179 | − | |
| 180 | 180 | /// Attribute matching specific methods |
| 181 | 181 | impl Cache { |
| 182 | 182 | /// Creates a new container to store match outcomes for all attribute matches. |
| @@ -1,45 +1,52 @@ | ||
| 1 | 1 | use std::path::Path; |
| 2 | 2 | |
| 3 | +use crate::cache::state::Ignore; | |
| 3 | 4 | use crate::{cache::state::IgnoreMatchGroup, PathIdMapping}; |
| 4 | −use bstr::{BStr, BString, ByteSlice}; | |
| 5 | +use bstr::{BStr, ByteSlice}; | |
| 5 | 6 | use gix_glob::pattern::Case; |
| 6 | 7 | |
| 7 | −/// State related to the exclusion of files. | |
| 8 | −#[derive(Default, Clone)] | |
| 9 | −#[allow(unused)] | |
| 10 | −pub struct Ignore { | |
| 11 | − /// Ignore patterns passed as overrides to everything else, typically passed on the command-line and the first patterns to | |
| 12 | − /// be consulted. | |
| 13 | − overrides: IgnoreMatchGroup, | |
| 14 | − /// Ignore patterns that match the currently set director (in the stack), which is pushed and popped as needed. | |
| 15 | − stack: IgnoreMatchGroup, | |
| 16 | − /// Ignore patterns which aren't tied to the repository root, hence are global. They are consulted last. | |
| 17 | − globals: IgnoreMatchGroup, | |
| 18 | − /// A matching stack of pattern indices which is empty if we have just been initialized to indicate that the | |
| 19 | − /// currently set directory had a pattern matched. Note that this one could be negated. | |
| 20 | − /// (index into match groups, index into list of pattern lists, index into pattern list) | |
| 21 | − matched_directory_patterns_stack: Vec<Option<(usize, usize, usize)>>, | |
| 22 | − /// The name of the file to look for in directories. | |
| 23 | − pub(crate) exclude_file_name_for_directories: BString, | |
| 24 | − /// The case to use when matching directories as they are pushed onto the stack. We run them against the exclude engine | |
| 25 | − /// to know if an entire path can be ignored as a parent directory is ignored. | |
| 26 | − pub(crate) case: Case, | |
| 8 | +/// Decide where to read `.gitignore` files from. | |
| 9 | +#[derive(Default, Debug, Clone, Copy)] | |
| 10 | +pub enum Source { | |
| 11 | + /// Retrieve ignore files from id mappings, see | |
| 12 | + /// [State::id_mappings_from_index()][crate::cache::State::id_mappings_from_index()]. | |
| 13 | + /// | |
| 14 | + /// These mappings are typically produced from an index. | |
| 15 | + /// If a tree should be the source, build an attribute list from a tree instead, or convert a tree to an index. | |
| 16 | + /// | |
| 17 | + /// Use this when no worktree checkout is available, like in bare repositories or when accessing blobs from other parts | |
| 18 | + /// of the history which aren't checked out. | |
| 19 | + IdMapping, | |
| 20 | + /// Read from the worktree and if not present, read them from the id mappings *if* these don't have the skip-worktree bit set. | |
| 21 | + #[default] | |
| 22 | + WorktreeThenIdMappingIfNotSkipped, | |
| 23 | +} | |
| 24 | + | |
| 25 | +/// Various aggregate numbers related [`Ignore`]. | |
| 26 | +#[derive(Default, Clone, Copy, Debug)] | |
| 27 | +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] | |
| 28 | +pub struct Statistics { | |
| 29 | + /// Amount of patterns buffers read from the index. | |
| 30 | + pub patterns_buffers: usize, | |
| 31 | + /// Amount of pattern files read from disk. | |
| 32 | + pub pattern_files: usize, | |
| 33 | + /// Amount of pattern files we tried to find on disk. | |
| 34 | + pub tried_pattern_files: usize, | |
| 27 | 35 | } |
| 28 | 36 | |
| 29 | 37 | impl Ignore { |
| 38 | + /// Configure gitignore file matching by providing the immutable groups being `overrides` and `globals`, while letting the directory | |
| 39 | + /// stack be dynamic. | |
| 40 | + /// | |
| 30 | 41 | /// The `exclude_file_name_for_directories` is an optional override for the filename to use when checking per-directory |
| 31 | 42 | /// ignore files within the repository, defaults to`.gitignore`. |
| 32 | − /// | |
| 33 | − // This is what it should be able represent: https://github.com/git/git/blob/140b9478dad5d19543c1cb4fd293ccec228f1240/dir.c#L3354 | |
| 34 | − // TODO: more docs | |
| 35 | 43 | pub fn new( |
| 36 | 44 | overrides: IgnoreMatchGroup, |
| 37 | 45 | globals: IgnoreMatchGroup, |
| 38 | 46 | exclude_file_name_for_directories: Option<&BStr>, |
| 39 | − case: Case, | |
| 47 | + source: Source, | |
| 40 | 48 | ) -> Self { |
| 41 | 49 | Ignore { |
| 42 | − case, | |
| 43 | 50 | overrides, |
| 44 | 51 | globals, |
| 45 | 52 | stack: Default::default(), |
| @@ -47,6 +54,7 @@ impl Ignore { · 1 unmodified line | ||
| 47 | 54 | exclude_file_name_for_directories: exclude_file_name_for_directories |
| 48 | 55 | .map(ToOwned::to_owned) |
| 49 | 56 | .unwrap_or_else(|| ".gitignore".into()), |
| 57 | + source, | |
| 50 | 58 | } |
| 51 | 59 | } |
| 52 | 60 | } |
| @@ -135,55 +143,71 @@ impl Ignore { · 82 unmodified lines | ||
| 135 | 143 | }) |
| 136 | 144 | } |
| 137 | 145 | |
| 146 | + #[allow(clippy::too_many_arguments)] | |
| 138 | 147 | pub(crate) fn push_directory<Find, E>( |
| 139 | 148 | &mut self, |
| 140 | 149 | root: &Path, |
| 141 | 150 | dir: &Path, |
| 151 | + rela_dir: &BStr, | |
| 142 | 152 | buf: &mut Vec<u8>, |
| 143 | 153 | id_mappings: &[PathIdMapping], |
| 144 | 154 | mut find: Find, |
| 155 | + case: Case, | |
| 156 | + stats: &mut Statistics, | |
| 145 | 157 | ) -> std::io::Result<()> |
| 146 | 158 | where |
| 147 | 159 | Find: for<'b> FnMut(&gix_hash::oid, &'b mut Vec<u8>) -> Result<gix_object::BlobRef<'b>, E>, |
| 148 | 160 | E: std::error::Error + Send + Sync + 'static, |
| 149 | 161 | { |
| 150 | − let dir_bstr = gix_path::into_bstr(dir); | |
| 151 | − let mut rela_dir = gix_glob::search::pattern::strip_base_handle_recompute_basename_pos( | |
| 152 | − gix_path::into_bstr(root).as_ref(), | |
| 153 | − dir_bstr.as_ref(), | |
| 154 | − None, | |
| 155 | − self.case, | |
| 156 | − ) | |
| 157 | − .expect("dir in root") | |
| 158 | − .0; | |
| 159 | − if rela_dir.starts_with(b"/") { | |
| 160 | − rela_dir = &rela_dir[1..]; | |
| 161 | − } | |
| 162 | 162 | self.matched_directory_patterns_stack |
| 163 | − .push(self.matching_exclude_pattern_no_dir(rela_dir, Some(true), self.case)); | |
| 163 | + .push(self.matching_exclude_pattern_no_dir(rela_dir, Some(true), case)); | |
| 164 | 164 | |
| 165 | 165 | let ignore_path_relative = |
| 166 | 166 | gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitignore")); |
| 167 | 167 | let ignore_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(ignore_path_relative.as_ref())); |
| 168 | − let follow_symlinks = ignore_file_in_index.is_err(); | |
| 169 | − if !gix_glob::search::add_patterns_file( | |
| 170 | − &mut self.stack.patterns, | |
| 171 | − dir.join(".gitignore"), | |
| 172 | − follow_symlinks, | |
| 173 | − Some(root), | |
| 174 | − buf, | |
| 175 | − )? { | |
| 176 | − match ignore_file_in_index { | |
| 177 | − Ok(idx) => { | |
| 178 | − let ignore_blob = find(&id_mappings[idx].1, buf) | |
| 179 | − .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; | |
| 180 | − let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned()); | |
| 181 | − self.stack | |
| 182 | − .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new(""))); | |
| 168 | + match self.source { | |
| 169 | + Source::IdMapping => { | |
| 170 | + match ignore_file_in_index { | |
| 171 | + Ok(idx) => { | |
| 172 | + let ignore_blob = find(&id_mappings[idx].1, buf) | |
| 173 | + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; | |
| 174 | + let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned()); | |
| 175 | + self.stack | |
| 176 | + .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new(""))); | |
| 177 | + stats.patterns_buffers += 1; | |
| 178 | + } | |
| 179 | + Err(_) => { | |
| 180 | + // Need one stack level per component so push and pop matches. | |
| 181 | + self.stack.patterns.push(Default::default()) | |
| 182 | + } | |
| 183 | 183 | } |
| 184 | − Err(_) => { | |
| 185 | − // Need one stack level per component so push and pop matches. | |
| 186 | − self.stack.patterns.push(Default::default()) | |
| 184 | + } | |
| 185 | + Source::WorktreeThenIdMappingIfNotSkipped => { | |
| 186 | + let follow_symlinks = ignore_file_in_index.is_err(); | |
| 187 | + let added = gix_glob::search::add_patterns_file( | |
| 188 | + &mut self.stack.patterns, | |
| 189 | + dir.join(".gitignore"), | |
| 190 | + follow_symlinks, | |
| 191 | + Some(root), | |
| 192 | + buf, | |
| 193 | + )?; | |
| 194 | + stats.pattern_files += usize::from(added); | |
| 195 | + stats.tried_pattern_files += 1; | |
| 196 | + if !added { | |
| 197 | + match ignore_file_in_index { | |
| 198 | + Ok(idx) => { | |
| 199 | + let ignore_blob = find(&id_mappings[idx].1, buf) | |
| 200 | + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; | |
| 201 | + let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned()); | |
| 202 | + self.stack | |
| 203 | + .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new(""))); | |
| 204 | + stats.patterns_buffers += 1; | |
| 205 | + } | |
| 206 | + Err(_) => { | |
| 207 | + // Need one stack level per component so push and pop matches. | |
| 208 | + self.stack.patterns.push(Default::default()) | |
| 209 | + } | |
| 210 | + } | |
| 187 | 211 | } |
| 188 | 212 | } |
| 189 | 213 | } |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | use std::path::PathBuf; |
| 2 | 2 | |
| 3 | −use bstr::ByteSlice; | |
| 3 | +use bstr::{BString, ByteSlice}; | |
| 4 | 4 | use gix_glob::pattern::Case; |
| 5 | 5 | |
| 6 | 6 | use crate::{cache::State, PathIdMapping}; |
| @@ -23,17 +23,36 @@ pub struct Attributes { · 16 unmodified lines | ||
| 23 | 23 | info_attributes: Option<PathBuf>, |
| 24 | 24 | /// A lookup table to accelerate searches. |
| 25 | 25 | collection: gix_attributes::search::MetadataCollection, |
| 26 | − /// The case to use when matching directories as they are pushed onto the stack. We run them against the exclude engine | |
| 27 | − /// to know if an entire path can be ignored as a parent directory is ignored. | |
| 28 | − case: Case, | |
| 29 | 26 | /// Where to read `.gitattributes` data from. |
| 30 | 27 | source: attributes::Source, |
| 31 | 28 | } |
| 32 | 29 | |
| 30 | +/// State related to the exclusion of files, supporting static overrides and globals, along with a stack of dynamically read | |
| 31 | +/// ignore files from disk or from the index each time the directory changes. | |
| 32 | +#[derive(Default, Clone)] | |
| 33 | +#[allow(unused)] | |
| 34 | +pub struct Ignore { | |
| 35 | + /// Ignore patterns passed as overrides to everything else, typically passed on the command-line and the first patterns to | |
| 36 | + /// be consulted. | |
| 37 | + overrides: IgnoreMatchGroup, | |
| 38 | + /// Ignore patterns that match the currently set director (in the stack), which is pushed and popped as needed. | |
| 39 | + stack: IgnoreMatchGroup, | |
| 40 | + /// Ignore patterns which aren't tied to the repository root, hence are global. They are consulted last. | |
| 41 | + globals: IgnoreMatchGroup, | |
| 42 | + /// A matching stack of pattern indices which is empty if we have just been initialized to indicate that the | |
| 43 | + /// currently set directory had a pattern matched. Note that this one could be negated. | |
| 44 | + /// (index into match groups, index into list of pattern lists, index into pattern list) | |
| 45 | + matched_directory_patterns_stack: Vec<Option<(usize, usize, usize)>>, | |
| 46 | + /// The name of the file to look for in directories. | |
| 47 | + pub(crate) exclude_file_name_for_directories: BString, | |
| 48 | + /// Where to read ignore files from | |
| 49 | + source: ignore::Source, | |
| 50 | +} | |
| 51 | + | |
| 33 | 52 | /// |
| 34 | 53 | pub mod attributes; |
| 35 | −mod ignore; | |
| 36 | −pub use ignore::Ignore; | |
| 54 | +/// | |
| 55 | +pub mod ignore; | |
| 37 | 56 | |
| 38 | 57 | /// Initialization |
| 39 | 58 | impl State { |
| @@ -41,8 +60,6 @@ impl State { · 1 unmodified line | ||
| 41 | 60 | pub fn for_checkout(unlink_on_collision: bool, attributes: Attributes) -> Self { |
| 42 | 61 | State::CreateDirectoryAndAttributesStack { |
| 43 | 62 | unlink_on_collision, |
| 44 | − #[cfg(debug_assertions)] | |
| 45 | − test_mkdir_calls: 0, | |
| 46 | 63 | attributes, |
| 47 | 64 | } |
| 48 | 65 | } |
| @@ -76,6 +93,7 @@ impl State { · 27 unmodified lines | ||
| 76 | 93 | &self, |
| 77 | 94 | index: &gix_index::State, |
| 78 | 95 | paths: &gix_index::PathStorageRef, |
| 96 | + ignore_source: ignore::Source, | |
| 79 | 97 | case: Case, |
| 80 | 98 | ) -> Vec<PathIdMapping> { |
| 81 | 99 | let a1_backing; |
| @@ -118,9 +136,16 @@ impl State { · 36 unmodified lines | ||
| 118 | 136 | } |
| 119 | 137 | .then_some(t.1) |
| 120 | 138 | })?; |
| 121 | − // See https://github.com/git/git/blob/master/dir.c#L912:L912 | |
| 122 | − if is_ignore && !entry.flags.contains(gix_index::entry::Flags::SKIP_WORKTREE) { | |
| 123 | − return None; | |
| 139 | + if is_ignore { | |
| 140 | + match ignore_source { | |
| 141 | + ignore::Source::IdMapping => {} | |
| 142 | + ignore::Source::WorktreeThenIdMappingIfNotSkipped => { | |
| 143 | + // See https://github.com/git/git/blob/master/dir.c#L912:L912 | |
| 144 | + if !entry.flags.contains(gix_index::entry::Flags::SKIP_WORKTREE) { | |
| 145 | + return None; | |
| 146 | + } | |
| 147 | + } | |
| 148 | + }; | |
| 124 | 149 | } |
| 125 | 150 | Some((path.to_owned(), entry.id)) |
| 126 | 151 | } else { |
| @@ -57,8 +57,8 @@ where · 56 unmodified lines | ||
| 57 | 57 | None, |
| 58 | 58 | ); |
| 59 | 59 | |
| 60 | − let state = cache::State::for_checkout(options.overwrite_existing, options.attributes.clone().with_case(case)); | |
| 61 | − let attribute_files = state.id_mappings_from_index(index, paths, case); | |
| 60 | + let state = cache::State::for_checkout(options.overwrite_existing, options.attributes.clone()); | |
| 61 | + let attribute_files = state.id_mappings_from_index(index, paths, Default::default(), case); | |
| 62 | 62 | let mut ctx = chunk::Context { |
| 63 | 63 | buf: Vec::new(), |
| 64 | 64 | path_cache: Cache::new(dir, state, case, Vec::with_capacity(512), attribute_files), |
| @@ -45,6 +45,7 @@ pub struct Cache { · 44 unmodified lines | ||
| 45 | 45 | case: gix_glob::pattern::Case, |
| 46 | 46 | /// A lookup table for object ids to read from in some situations when looking up attributes or exclusions. |
| 47 | 47 | id_mappings: Vec<PathIdMapping>, |
| 48 | + statistics: cache::Statistics, | |
| 48 | 49 | } |
| 49 | 50 | |
| 50 | 51 | pub(crate) type PathIdMapping = (BString, gix_hash::ObjectId); |
| @@ -1,3 +0,0 @@ | ||
| 1 | −version https://git-lfs.github.com/spec/v1 | |
| 2 | −oid sha256:38df6900cdb0ad620e158a3cc19325053658b9cfcb8987779af634813d957f6c | |
| 3 | −size 11088 | |
| @@ -111,6 +111,8 @@ other-dir-with-ignore/other-sub-level-dir-anywhere/hello · 110 unmodified lines | ||
| 111 | 111 | other-dir-with-ignore/other-sub-level-dir-anywhere/ |
| 112 | 112 | dir-with-ignore/negated |
| 113 | 113 | dir-with-ignore/negated-dir/hello |
| 114 | +User-file-ANYWHERE | |
| 115 | +User-Dir-ANYWHERE | |
| 114 | 116 | a/b/C |
| 115 | 117 | a/B/c |
| 116 | 118 | A/B/C |
| @@ -24,7 +24,6 @@ fn baseline() -> crate::Result { · 23 unmodified lines | ||
| 24 | 24 | state::Attributes::new( |
| 25 | 25 | gix_attributes::Search::new_globals([base.join("user.attributes")], &mut buf, &mut collection)?, |
| 26 | 26 | Some(git_dir.join("info").join("attributes")), |
| 27 | − case, | |
| 28 | 27 | gix_worktree::cache::state::attributes::Source::WorktreeThenIdMapping, |
| 29 | 28 | collection, |
| 30 | 29 | ), |
| @@ -17,11 +17,11 @@ fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() -> crate · 16 unmodified lines | ||
| 17 | 17 | Vec::new(), |
| 18 | 18 | Default::default(), |
| 19 | 19 | ); |
| 20 | − assert_eq!(cache.num_mkdir_calls(), 0); | |
| 20 | + assert_eq!(cache.statistics().delegate.num_mkdir_calls, 0); | |
| 21 | 21 | |
| 22 | 22 | let path = cache.at_path("hello", Some(false), panic_on_find)?.path(); |
| 23 | 23 | assert!(!path.parent().unwrap().exists(), "prefix itself is never created"); |
| 24 | − assert_eq!(cache.num_mkdir_calls(), 0); | |
| 24 | + assert_eq!(cache.statistics().delegate.num_mkdir_calls, 0); | |
| 25 | 25 | Ok(()) |
| 26 | 26 | } |
| 27 | 27 | |
| @@ -43,7 +43,7 @@ fn directory_paths_are_created_in_full() { · 15 unmodified lines | ||
| 43 | 43 | assert!(path.parent().unwrap().is_dir(), "dir exists"); |
| 44 | 44 | } |
| 45 | 45 | |
| 46 | − assert_eq!(cache.num_mkdir_calls(), 3); | |
| 46 | + assert_eq!(cache.statistics().delegate.num_mkdir_calls, 3); | |
| 47 | 47 | } |
| 48 | 48 | |
| 49 | 49 | #[test] |
| @@ -54,7 +54,7 @@ fn existing_directories_are_fine() -> crate::Result { · 4 unmodified lines | ||
| 54 | 54 | let path = cache.at_path("dir/file", Some(false), panic_on_find)?.path(); |
| 55 | 55 | assert!(path.parent().unwrap().is_dir(), "directory is still present"); |
| 56 | 56 | assert!(!path.exists(), "it won't create the file"); |
| 57 | − assert_eq!(cache.num_mkdir_calls(), 1); | |
| 57 | + assert_eq!(cache.statistics().delegate.num_mkdir_calls, 1); | |
| 58 | 58 | Ok(()) |
| 59 | 59 | } |
| 60 | 60 | |
| @@ -67,7 +67,12 @@ fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> crate::R · 6 unmodified lines | ||
| 67 | 67 | std::fs::write(tmp.path().join("file-in-dir"), &[])?; |
| 68 | 68 | |
| 69 | 69 | for dirname in &["file-in-dir", "link-to-dir"] { |
| 70 | − cache.unlink_on_collision(false); | |
| 70 | + if let cache::State::CreateDirectoryAndAttributesStack { | |
| 71 | + unlink_on_collision, .. | |
| 72 | + } = cache.state_mut() | |
| 73 | + { | |
| 74 | + *unlink_on_collision = false; | |
| 75 | + } | |
| 71 | 76 | let relative_path = format!("{}/file", dirname); |
| 72 | 77 | assert_eq!( |
| 73 | 78 | cache |
| @@ -78,20 +83,25 @@ fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> crate::R · 4 unmodified lines | ||
| 78 | 83 | ); |
| 79 | 84 | } |
| 80 | 85 | assert_eq!( |
| 81 | − cache.num_mkdir_calls(), | |
| 86 | + cache.statistics().delegate.num_mkdir_calls, | |
| 82 | 87 | 2, |
| 83 | 88 | "it tries to create each directory once, but it's a file" |
| 84 | 89 | ); |
| 85 | − cache.reset_mkdir_calls(); | |
| 90 | + cache.take_statistics(); | |
| 86 | 91 | for dirname in &["link-to-dir", "file-in-dir"] { |
| 87 | − cache.unlink_on_collision(true); | |
| 92 | + if let cache::State::CreateDirectoryAndAttributesStack { | |
| 93 | + unlink_on_collision, .. | |
| 94 | + } = cache.state_mut() | |
| 95 | + { | |
| 96 | + *unlink_on_collision = true; | |
| 97 | + } | |
| 88 | 98 | let relative_path = format!("{}/file", dirname); |
| 89 | 99 | let path = cache.at_path(&relative_path, Some(false), panic_on_find)?.path(); |
| 90 | 100 | assert!(path.parent().unwrap().is_dir(), "directory was forcefully created"); |
| 91 | 101 | assert!(!path.exists()); |
| 92 | 102 | } |
| 93 | 103 | assert_eq!( |
| 94 | − cache.num_mkdir_calls(), | |
| 104 | + cache.statistics().delegate.num_mkdir_calls, | |
| 95 | 105 | 4, |
| 96 | 106 | "like before, but it unlinks what's there and tries again" |
| 97 | 107 | ); |
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | use bstr::{BStr, ByteSlice}; |
| 2 | 2 | use gix_glob::pattern::Case; |
| 3 | 3 | use gix_odb::FindExt; |
| 4 | +use gix_worktree::cache::state::ignore::Source; | |
| 4 | 5 | use gix_worktree::Cache; |
| 5 | 6 | |
| 6 | 7 | use crate::hex_to_id; |
| @@ -43,7 +44,7 @@ fn special_exclude_cases_we_handle_differently() { · 36 unmodified lines | ||
| 43 | 44 | Default::default(), |
| 44 | 45 | gix_ignore::Search::from_git_dir(&git_dir, None, &mut buf).unwrap(), |
| 45 | 46 | None, |
| 46 | − case, | |
| 47 | + Source::WorktreeThenIdMappingIfNotSkipped, | |
| 47 | 48 | ), |
| 48 | 49 | ); |
| 49 | 50 | let mut cache = Cache::new(&dir, state, case, buf, Default::default()); |
| @@ -104,11 +105,11 @@ fn check_against_baseline() -> crate::Result { · 54 unmodified lines | ||
| 104 | 105 | gix_ignore::Search::from_overrides(vec!["!force-include"]), |
| 105 | 106 | gix_ignore::Search::from_git_dir(&git_dir, Some(user_exclude_path), &mut buf)?, |
| 106 | 107 | None, |
| 107 | − case, | |
| 108 | + Source::WorktreeThenIdMappingIfNotSkipped, | |
| 108 | 109 | ), |
| 109 | 110 | ); |
| 110 | 111 | let paths_storage = index.take_path_backing(); |
| 111 | − let attribute_files_in_index = state.id_mappings_from_index(&index, &paths_storage, case); | |
| 112 | + let attribute_files_in_index = state.id_mappings_from_index(&index, &paths_storage, Default::default(), case); | |
| 112 | 113 | assert_eq!( |
| 113 | 114 | attribute_files_in_index, |
| 114 | 115 | vec![( |
| @@ -156,15 +157,5 @@ fn check_against_baseline() -> crate::Result { · 41 unmodified lines | ||
| 156 | 157 | } |
| 157 | 158 | } |
| 158 | 159 | } |
| 159 | − | |
| 160 | − cache.set_case(Case::Fold); | |
| 161 | − let platform = cache.at_entry("User-file-ANYWHERE", Some(false), |oid, buf| odb.find_blob(oid, buf))?; | |
| 162 | − let m = platform.matching_exclude_pattern().expect("match"); | |
| 163 | − assert_eq!(m.pattern.text, "user-file-anywhere"); | |
| 164 | − | |
| 165 | − cache.set_case(Case::Fold); | |
| 166 | − let platform = cache.at_entry("User-Dir-ANYWHERE", Some(true), |oid, buf| odb.find_blob(oid, buf))?; | |
| 167 | − let m = platform.matching_exclude_pattern().expect("match"); | |
| 168 | − assert_eq!(m.pattern.text, "user-Dir-anywhere"); | |
| 169 | 160 | Ok(()) |
| 170 | 161 | } |
| @@ -67,6 +67,7 @@ serde = [ "dep:serde", · 66 unmodified lines | ||
| 67 | 67 | "gix-attributes/serde", |
| 68 | 68 | "gix-ignore/serde", |
| 69 | 69 | "gix-revision/serde", |
| 70 | + "gix-worktree/serde", | |
| 70 | 71 | "gix-credentials/serde"] |
| 71 | 72 | |
| 72 | 73 | ## Re-export the progress tree root which allows to obtain progress from various functions which take `impl gix::Progress`. |
| @@ -0,0 +1,9 @@ | ||
| 1 | +/// The error returned by [`Repository::attributes()`][crate::Repository::attributes()]. | |
| 2 | +#[derive(Debug, thiserror::Error)] | |
| 3 | +#[allow(missing_docs)] | |
| 4 | +pub enum Error { | |
| 5 | + #[error(transparent)] | |
| 6 | + ConfigureAttributes(#[from] crate::config::attribute_stack::Error), | |
| 7 | + #[error(transparent)] | |
| 8 | + ConfigureExcludes(#[from] crate::config::exclude_stack::Error), | |
| 9 | +} | |
| @@ -2,7 +2,6 @@ · 1 unmodified line | ||
| 2 | 2 | use std::{borrow::Cow, path::PathBuf, time::Duration}; |
| 3 | 3 | |
| 4 | 4 | use gix_attributes::Source; |
| 5 | −use gix_glob::pattern::Case; | |
| 6 | 5 | use gix_lock::acquire::Fail; |
| 7 | 6 | |
| 8 | 7 | use crate::{ |
| @@ -167,18 +166,14 @@ impl Cache { · 158 unmodified lines | ||
| 167 | 166 | executable_bit: boolean(self, "core.fileMode", &Core::FILE_MODE, true)?, |
| 168 | 167 | symlink: boolean(self, "core.symlinks", &Core::SYMLINKS, true)?, |
| 169 | 168 | }; |
| 170 | − let case = if capabilities.ignore_case { | |
| 171 | − Case::Fold | |
| 172 | − } else { | |
| 173 | − Case::Sensitive | |
| 174 | − }; | |
| 175 | 169 | Ok(gix_worktree::checkout::Options { |
| 176 | − attributes: self.assemble_attribute_globals( | |
| 177 | − git_dir, | |
| 178 | − case, | |
| 179 | − gix_worktree::cache::state::attributes::Source::IdMappingThenWorktree, | |
| 180 | − self.attributes, | |
| 181 | − )?, | |
| 170 | + attributes: self | |
| 171 | + .assemble_attribute_globals( | |
| 172 | + git_dir, | |
| 173 | + gix_worktree::cache::state::attributes::Source::IdMappingThenWorktree, | |
| 174 | + self.attributes, | |
| 175 | + )? | |
| 176 | + .0, | |
| 182 | 177 | fs: capabilities, |
| 183 | 178 | thread_limit, |
| 184 | 179 | destination_is_initially_empty: false, |
| @@ -199,14 +194,31 @@ impl Cache { · 14 unmodified lines | ||
| 199 | 194 | }) |
| 200 | 195 | } |
| 201 | 196 | |
| 197 | + pub(crate) fn assemble_exclude_globals( | |
| 198 | + &self, | |
| 199 | + git_dir: &std::path::Path, | |
| 200 | + overrides: Option<gix_ignore::Search>, | |
| 201 | + source: gix_worktree::cache::state::ignore::Source, | |
| 202 | + buf: &mut Vec<u8>, | |
| 203 | + ) -> Result<gix_worktree::cache::state::Ignore, config::exclude_stack::Error> { | |
| 204 | + let excludes_file = match self.excludes_file().transpose()? { | |
| 205 | + Some(user_path) => Some(user_path), | |
| 206 | + None => self.xdg_config_path("ignore")?, | |
| 207 | + }; | |
| 208 | + Ok(gix_worktree::cache::state::Ignore::new( | |
| 209 | + overrides.unwrap_or_default(), | |
| 210 | + gix_ignore::Search::from_git_dir(git_dir, excludes_file, buf)?, | |
| 211 | + None, | |
| 212 | + source, | |
| 213 | + )) | |
| 214 | + } | |
| 202 | 215 | // TODO: at least one test, maybe related to core.attributesFile configuration. |
| 203 | − fn assemble_attribute_globals( | |
| 216 | + pub(crate) fn assemble_attribute_globals( | |
| 204 | 217 | &self, |
| 205 | 218 | git_dir: &std::path::Path, |
| 206 | − case: gix_glob::pattern::Case, | |
| 207 | 219 | source: gix_worktree::cache::state::attributes::Source, |
| 208 | − attributes: crate::permissions::Attributes, | |
| 209 | − ) -> Result<gix_worktree::cache::state::Attributes, config::attribute_stack::Error> { | |
| 220 | + attributes: crate::open::permissions::Attributes, | |
| 221 | + ) -> Result<(gix_worktree::cache::state::Attributes, Vec<u8>), config::attribute_stack::Error> { | |
| 210 | 222 | let configured_or_user_attributes = match self |
| 211 | 223 | .trusted_file_path("core", None, Core::ATTRIBUTES_FILE.name) |
| 212 | 224 | .transpose()? |
| @@ -232,13 +244,13 @@ impl Cache { · 19 unmodified lines | ||
| 232 | 244 | let info_attributes_path = git_dir.join("info").join("attributes"); |
| 233 | 245 | let mut buf = Vec::new(); |
| 234 | 246 | let mut collection = gix_attributes::search::MetadataCollection::default(); |
| 235 | − Ok(gix_worktree::cache::state::Attributes::new( | |
| 247 | + let res = gix_worktree::cache::state::Attributes::new( | |
| 236 | 248 | gix_attributes::Search::new_globals(attribute_files, &mut buf, &mut collection)?, |
| 237 | 249 | Some(info_attributes_path), |
| 238 | − case, | |
| 239 | 250 | source, |
| 240 | 251 | collection, |
| 241 | − )) | |
| 252 | + ); | |
| 253 | + Ok((res, buf)) | |
| 242 | 254 | } |
| 243 | 255 | |
| 244 | 256 | pub(crate) fn xdg_config_path( |
| @@ -12,7 +12,7 @@ use crate::{ · 11 unmodified lines | ||
| 12 | 12 | tree::{gitoxide, Core, Http}, |
| 13 | 13 | Cache, |
| 14 | 14 | }, |
| 15 | − repository, | |
| 15 | + open, | |
| 16 | 16 | }; |
| 17 | 17 | |
| 18 | 18 | /// Initialization |
| @@ -32,7 +32,7 @@ impl Cache { · 13 unmodified lines | ||
| 32 | 32 | filter_config_section: fn(&gix_config::file::Metadata) -> bool, |
| 33 | 33 | git_install_dir: Option<&std::path::Path>, |
| 34 | 34 | home: Option<&std::path::Path>, |
| 35 | − environment @ repository::permissions::Environment { | |
| 35 | + environment @ open::permissions::Environment { | |
| 36 | 36 | git_prefix, |
| 37 | 37 | ssh_prefix: _, |
| 38 | 38 | xdg_config_home: _, |
| @@ -40,16 +40,16 @@ impl Cache { · 1 unmodified line | ||
| 40 | 40 | http_transport, |
| 41 | 41 | identity, |
| 42 | 42 | objects, |
| 43 | − }: repository::permissions::Environment, | |
| 44 | − attributes: repository::permissions::Attributes, | |
| 45 | − repository::permissions::Config { | |
| 43 | + }: open::permissions::Environment, | |
| 44 | + attributes: open::permissions::Attributes, | |
| 45 | + open::permissions::Config { | |
| 46 | 46 | git_binary: use_installation, |
| 47 | 47 | system: use_system, |
| 48 | 48 | git: use_git, |
| 49 | 49 | user: use_user, |
| 50 | 50 | env: use_env, |
| 51 | 51 | includes: use_includes, |
| 52 | − }: repository::permissions::Config, | |
| 52 | + }: open::permissions::Config, | |
| 53 | 53 | lenient_config: bool, |
| 54 | 54 | api_config_overrides: &[BString], |
| 55 | 55 | cli_config_overrides: &[BString], |
| @@ -233,12 +233,12 @@ impl Cache { · 177 unmodified lines | ||
| 233 | 233 | } |
| 234 | 234 | |
| 235 | 235 | pub(crate) fn make_source_env( |
| 236 | − crate::permissions::Environment { | |
| 236 | + crate::open::permissions::Environment { | |
| 237 | 237 | xdg_config_home, |
| 238 | 238 | git_prefix, |
| 239 | 239 | home, |
| 240 | 240 | .. |
| 241 | − }: crate::permissions::Environment, | |
| 241 | + }: open::permissions::Environment, | |
| 242 | 242 | ) -> impl FnMut(&str) -> Option<OsString> { |
| 243 | 243 | move |name| { |
| 244 | 244 | match name { |
| @@ -118,6 +118,23 @@ pub mod checkout_options { · 117 unmodified lines | ||
| 118 | 118 | } |
| 119 | 119 | } |
| 120 | 120 | |
| 121 | +/// | |
| 122 | +pub mod exclude_stack { | |
| 123 | + use std::path::PathBuf; | |
| 124 | + | |
| 125 | + /// The error produced when setting up a stack to query `gitignore` information. | |
| 126 | + #[derive(Debug, thiserror::Error)] | |
| 127 | + #[allow(missing_docs)] | |
| 128 | + pub enum Error { | |
| 129 | + #[error("Could not read repository exclude")] | |
| 130 | + Io(#[from] std::io::Error), | |
| 131 | + #[error(transparent)] | |
| 132 | + EnvironmentPermission(#[from] gix_sec::permission::Error<PathBuf>), | |
| 133 | + #[error("The value for `core.excludesFile` could not be read from configuration")] | |
| 134 | + ExcludesFilePathInterpolation(#[from] gix_config::path::interpolate::Error), | |
| 135 | + } | |
| 136 | +} | |
| 137 | + | |
| 121 | 138 | /// |
| 122 | 139 | pub mod attribute_stack { |
| 123 | 140 | /// The error produced when setting up the attribute stack to query `gitattributes`. |
| @@ -462,7 +479,7 @@ pub(crate) struct Cache { · 338 unmodified lines | ||
| 462 | 479 | /// If true, we should default what's possible if something is misconfigured, on case by case basis, to be more resilient. |
| 463 | 480 | /// Also available in options! Keep in sync! |
| 464 | 481 | pub lenient_config: bool, |
| 465 | − attributes: crate::permissions::Attributes, | |
| 466 | − environment: crate::permissions::Environment, | |
| 482 | + attributes: crate::open::permissions::Attributes, | |
| 483 | + environment: crate::open::permissions::Environment, | |
| 467 | 484 | // TODO: make core.precomposeUnicode available as well. |
| 468 | 485 | } |
| @@ -38,6 +38,8 @@ pub(crate) mod root { · 37 unmodified lines | ||
| 38 | 38 | pub const GITOXIDE: sections::Gitoxide = sections::Gitoxide; |
| 39 | 39 | /// The `http` section. |
| 40 | 40 | pub const HTTP: sections::Http = sections::Http; |
| 41 | + /// The `index` section. | |
| 42 | + pub const INDEX: sections::Index = sections::Index; | |
| 41 | 43 | /// The `init` section. |
| 42 | 44 | pub const INIT: sections::Init = sections::Init; |
| 43 | 45 | /// The `pack` section. |
| @@ -69,6 +71,7 @@ pub(crate) mod root { · 25 unmodified lines | ||
| 69 | 71 | &Self::EXTENSIONS, |
| 70 | 72 | &Self::GITOXIDE, |
| 71 | 73 | &Self::HTTP, |
| 74 | + &Self::INDEX, | |
| 72 | 75 | &Self::INIT, |
| 73 | 76 | &Self::PACK, |
| 74 | 77 | &Self::PROTOCOL, |
| @@ -84,9 +87,9 @@ pub(crate) mod root { · 9 unmodified lines | ||
| 84 | 87 | |
| 85 | 88 | mod sections; |
| 86 | 89 | pub use sections::{ |
| 87 | − branch, checkout, core, credential, diff, extensions, gitoxide, http, protocol, remote, ssh, Author, Branch, | |
| 88 | − Checkout, Clone, Committer, Core, Credential, Diff, Extensions, Gitoxide, Http, Init, Pack, Protocol, Remote, Safe, | |
| 89 | − Ssh, Url, User, | |
| 90 | + branch, checkout, core, credential, diff, extensions, gitoxide, http, index, protocol, remote, ssh, Author, Branch, | |
| 91 | + Checkout, Clone, Committer, Core, Credential, Diff, Extensions, Gitoxide, Http, Index, Init, Pack, Protocol, | |
| 92 | + Remote, Safe, Ssh, Url, User, | |
| 90 | 93 | }; |
| 91 | 94 | |
| 92 | 95 | /// Generic value implementations for static instantiation. |
| @@ -0,0 +1,62 @@ | ||
| 1 | +use crate::{ | |
| 2 | + config, | |
| 3 | + config::tree::{keys, Index, Key, Section}, | |
| 4 | +}; | |
| 5 | + | |
| 6 | +impl Index { | |
| 7 | + /// The `index.threads` key. | |
| 8 | + pub const THREADS: IndexThreads = | |
| 9 | + IndexThreads::new_with_validate("threads", &config::Tree::INDEX, validate::IndexThreads); | |
| 10 | +} | |
| 11 | + | |
| 12 | +/// The `index.threads` key. | |
| 13 | +pub type IndexThreads = keys::Any<validate::IndexThreads>; | |
| 14 | + | |
| 15 | +mod index_threads { | |
| 16 | + use crate::bstr::BStr; | |
| 17 | + use crate::config; | |
| 18 | + use crate::config::key::GenericErrorWithValue; | |
| 19 | + use crate::config::tree::index::IndexThreads; | |
| 20 | + use std::borrow::Cow; | |
| 21 | + | |
| 22 | + impl IndexThreads { | |
| 23 | + /// Parse `value` into the amount of threads to use, with `1` being single-threaded, or `0` indicating | |
| 24 | + /// to select the amount of threads, with any other number being the specific amount of threads to use. | |
| 25 | + pub fn try_into_index_threads( | |
| 26 | + &'static self, | |
| 27 | + value: Cow<'_, BStr>, | |
| 28 | + ) -> Result<usize, config::key::GenericErrorWithValue> { | |
| 29 | + gix_config::Integer::try_from(value.as_ref()) | |
| 30 | + .ok() | |
| 31 | + .and_then(|i| i.to_decimal().and_then(|i| i.try_into().ok())) | |
| 32 | + .or_else(|| { | |
| 33 | + gix_config::Boolean::try_from(value.as_ref()) | |
| 34 | + .ok() | |
| 35 | + .map(|b| if b.0 { 0 } else { 1 }) | |
| 36 | + }) | |
| 37 | + .ok_or_else(|| GenericErrorWithValue::from_value(self, value.into_owned())) | |
| 38 | + } | |
| 39 | + } | |
| 40 | +} | |
| 41 | + | |
| 42 | +impl Section for Index { | |
| 43 | + fn name(&self) -> &str { | |
| 44 | + "index" | |
| 45 | + } | |
| 46 | + | |
| 47 | + fn keys(&self) -> &[&dyn Key] { | |
| 48 | + &[&Self::THREADS] | |
| 49 | + } | |
| 50 | +} | |
| 51 | + | |
| 52 | +mod validate { | |
| 53 | + use crate::{bstr::BStr, config::tree::keys}; | |
| 54 | + | |
| 55 | + pub struct IndexThreads; | |
| 56 | + impl keys::Validate for IndexThreads { | |
| 57 | + fn validate(&self, value: &BStr) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { | |
| 58 | + super::Index::THREADS.try_into_index_threads(value.into())?; | |
| 59 | + Ok(()) | |
| 60 | + } | |
| 61 | + } | |
| 62 | +} | |
| @@ -55,6 +55,11 @@ pub mod gitoxide; · 54 unmodified lines | ||
| 55 | 55 | pub struct Http; |
| 56 | 56 | pub mod http; |
| 57 | 57 | |
| 58 | +/// The `index` top-level section. | |
| 59 | +#[derive(Copy, Clone, Default)] | |
| 60 | +pub struct Index; | |
| 61 | +pub mod index; | |
| 62 | + | |
| 58 | 63 | /// The `init` top-level section. |
| 59 | 64 | #[derive(Copy, Clone, Default)] |
| 60 | 65 | pub struct Init; |
| @@ -99,6 +99,9 @@ pub use hash::{oid, ObjectId}; · 98 unmodified lines | ||
| 99 | 99 | |
| 100 | 100 | pub mod interrupt; |
| 101 | 101 | |
| 102 | +/// | |
| 103 | +pub mod attributes; | |
| 104 | + | |
| 102 | 105 | mod ext; |
| 103 | 106 | /// |
| 104 | 107 | pub mod prelude; |
| @@ -223,13 +226,6 @@ pub fn open_opts(directory: impl Into<std::path::PathBuf>, options: open::Option · 118 unmodified lines | ||
| 223 | 226 | ThreadSafeRepository::open_opts(directory, options).map(Into::into) |
| 224 | 227 | } |
| 225 | 228 | |
| 226 | −/// | |
| 227 | −pub mod permission; | |
| 228 | − | |
| 229 | −/// | |
| 230 | −pub mod permissions; | |
| 231 | −pub use repository::permissions::Permissions; | |
| 232 | − | |
| 233 | 229 | /// |
| 234 | 230 | pub mod create; |
| 235 | 231 | |
| @@ -1,6 +1,17 @@ | ||
| 1 | 1 | use std::path::PathBuf; |
| 2 | 2 | |
| 3 | −use crate::{bstr::BString, config, permission, Permissions}; | |
| 3 | +use crate::{bstr::BString, config}; | |
| 4 | + | |
| 5 | +/// Permissions associated with various resources of a git repository | |
| 6 | +#[derive(Debug, Clone)] | |
| 7 | +pub struct Permissions { | |
| 8 | + /// Control which environment variables may be accessed. | |
| 9 | + pub env: permissions::Environment, | |
| 10 | + /// Permissions related where git configuration should be loaded from. | |
| 11 | + pub config: permissions::Config, | |
| 12 | + /// Permissions related to where `gitattributes` should be loaded from. | |
| 13 | + pub attributes: permissions::Attributes, | |
| 14 | +} | |
| 4 | 15 | |
| 5 | 16 | /// The options used in [`ThreadSafeRepository::open_opts()`][crate::ThreadSafeRepository::open_opts()]. |
| 6 | 17 | /// |
| @@ -44,11 +55,11 @@ pub enum Error { · 37 unmodified lines | ||
| 44 | 55 | #[error("The git directory at '{}' is considered unsafe as it's not owned by the current user.", .path.display())] |
| 45 | 56 | UnsafeGitDir { path: PathBuf }, |
| 46 | 57 | #[error(transparent)] |
| 47 | − EnvironmentAccessDenied(#[from] permission::env_var::resource::Error), | |
| 58 | + EnvironmentAccessDenied(#[from] gix_sec::permission::Error<std::path::PathBuf>), | |
| 48 | 59 | } |
| 49 | 60 | |
| 50 | 61 | mod options; |
| 51 | − | |
| 62 | +pub mod permissions; | |
| 52 | 63 | mod repository; |
| 53 | 64 | |
| 54 | 65 | #[cfg(test)] |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | use std::path::PathBuf; |
| 2 | 2 | |
| 3 | 3 | use super::{Error, Options}; |
| 4 | −use crate::{bstr::BString, config, Permissions, ThreadSafeRepository}; | |
| 4 | +use crate::{bstr::BString, config, open::Permissions, ThreadSafeRepository}; | |
| 5 | 5 | |
| 6 | 6 | impl Default for Options { |
| 7 | 7 | fn default() -> Self { |
| @@ -0,0 +1,215 @@ | ||
| 1 | +//! Various permissions to define what can be done when operating a [`Repository`][crate::Repository]. | |
| 2 | +use crate::open::Permissions; | |
| 3 | +use gix_sec::Trust; | |
| 4 | + | |
| 5 | +/// Configure from which sources git configuration may be loaded. | |
| 6 | +/// | |
| 7 | +/// Note that configuration from inside of the repository is always loaded as it's definitely required for correctness. | |
| 8 | +#[derive(Copy, Clone, Ord, PartialOrd, PartialEq, Eq, Debug, Hash)] | |
| 9 | +pub struct Config { | |
| 10 | + /// The git binary may come with configuration as part of its configuration, and if this is true (default false) | |
| 11 | + /// we will load the configuration of the git binary, if present and not a duplicate of the ones below. | |
| 12 | + /// | |
| 13 | + /// It's disabled by default as it may involve executing the git binary once per execution of the application. | |
| 14 | + pub git_binary: bool, | |
| 15 | + /// Whether to use the system configuration. | |
| 16 | + /// This is defined as `$(prefix)/etc/gitconfig` on unix. | |
| 17 | + pub system: bool, | |
| 18 | + /// Whether to use the git application configuration. | |
| 19 | + /// | |
| 20 | + /// A platform defined location for where a user's git application configuration should be located. | |
| 21 | + /// If `$XDG_CONFIG_HOME` is not set or empty, `$HOME/.config/git/config` will be used | |
| 22 | + /// on unix. | |
| 23 | + pub git: bool, | |
| 24 | + /// Whether to use the user configuration. | |
| 25 | + /// This is usually `~/.gitconfig` on unix. | |
| 26 | + pub user: bool, | |
| 27 | + /// Whether to use the configuration from environment variables. | |
| 28 | + pub env: bool, | |
| 29 | + /// Whether to follow include files are encountered in loaded configuration, | |
| 30 | + /// via `include` and `includeIf` sections. | |
| 31 | + pub includes: bool, | |
| 32 | +} | |
| 33 | + | |
| 34 | +impl Config { | |
| 35 | + /// Allow everything which usually relates to a fully trusted environment | |
| 36 | + pub fn all() -> Self { | |
| 37 | + Config { | |
| 38 | + git_binary: false, | |
| 39 | + system: true, | |
| 40 | + git: true, | |
| 41 | + user: true, | |
| 42 | + env: true, | |
| 43 | + includes: true, | |
| 44 | + } | |
| 45 | + } | |
| 46 | + | |
| 47 | + /// Load only configuration local to the git repository. | |
| 48 | + pub fn isolated() -> Self { | |
| 49 | + Config { | |
| 50 | + git_binary: false, | |
| 51 | + system: false, | |
| 52 | + git: false, | |
| 53 | + user: false, | |
| 54 | + env: false, | |
| 55 | + includes: false, | |
| 56 | + } | |
| 57 | + } | |
| 58 | +} | |
| 59 | + | |
| 60 | +impl Default for Config { | |
| 61 | + fn default() -> Self { | |
| 62 | + Self::all() | |
| 63 | + } | |
| 64 | +} | |
| 65 | + | |
| 66 | +/// Configure from which `gitattribute` files may be loaded. | |
| 67 | +/// | |
| 68 | +/// Note that `.gitattribute` files from within the repository are always loaded. | |
| 69 | +#[derive(Copy, Clone, Ord, PartialOrd, PartialEq, Eq, Debug, Hash)] | |
| 70 | +pub struct Attributes { | |
| 71 | + /// The git binary may come with attribute configuration in its installation directory, and if this is true (default false) | |
| 72 | + /// we will load the configuration of the git binary. | |
| 73 | + /// | |
| 74 | + /// It's disabled by default as it involves executing the git binary once per execution of the application. | |
| 75 | + pub git_binary: bool, | |
| 76 | + /// Whether to use the system configuration. | |
| 77 | + /// This is typically defined as `$(prefix)/etc/gitconfig`. | |
| 78 | + pub system: bool, | |
| 79 | + /// Whether to use the git application configuration. | |
| 80 | + /// | |
| 81 | + /// A platform defined location for where a user's git application configuration should be located. | |
| 82 | + /// If `$XDG_CONFIG_HOME` is not set or empty, `$HOME/.config/git/attributes` will be used | |
| 83 | + /// on unix. | |
| 84 | + pub git: bool, | |
| 85 | +} | |
| 86 | + | |
| 87 | +impl Attributes { | |
| 88 | + /// Allow everything which usually relates to a fully trusted environment | |
| 89 | + pub fn all() -> Self { | |
| 90 | + Attributes { | |
| 91 | + git_binary: false, | |
| 92 | + system: true, | |
| 93 | + git: true, | |
| 94 | + } | |
| 95 | + } | |
| 96 | + | |
| 97 | + /// Allow loading attributes that are local to the git repository. | |
| 98 | + pub fn isolated() -> Self { | |
| 99 | + Attributes { | |
| 100 | + git_binary: false, | |
| 101 | + system: false, | |
| 102 | + git: false, | |
| 103 | + } | |
| 104 | + } | |
| 105 | +} | |
| 106 | + | |
| 107 | +impl Default for Attributes { | |
| 108 | + fn default() -> Self { | |
| 109 | + Self::all() | |
| 110 | + } | |
| 111 | +} | |
| 112 | + | |
| 113 | +/// Permissions related to the usage of environment variables | |
| 114 | +#[derive(Debug, Clone, Copy)] | |
| 115 | +pub struct Environment { | |
| 116 | + /// Control whether resources pointed to by `XDG_CONFIG_HOME` can be used when looking up common configuration values. | |
| 117 | + /// | |
| 118 | + /// Note that [`gix_sec::Permission::Forbid`] will cause the operation to abort if a resource is set via the XDG config environment. | |
| 119 | + pub xdg_config_home: gix_sec::Permission, | |
| 120 | + /// Control the way resources pointed to by the home directory (similar to `xdg_config_home`) may be used. | |
| 121 | + pub home: gix_sec::Permission, | |
| 122 | + /// Control if environment variables to configure the HTTP transport, like `http_proxy` may be used. | |
| 123 | + /// | |
| 124 | + /// Note that http-transport related environment variables prefixed with `GIT_` may also be included here | |
| 125 | + /// if they match this category like `GIT_HTTP_USER_AGENT`. | |
| 126 | + pub http_transport: gix_sec::Permission, | |
| 127 | + /// Control if the `EMAIL` environment variables may be read. | |
| 128 | + /// | |
| 129 | + /// Note that identity related environment variables prefixed with `GIT_` may also be included here | |
| 130 | + /// if they match this category. | |
| 131 | + pub identity: gix_sec::Permission, | |
| 132 | + /// Control if environment variables related to the object database are handled. This includes features and performance | |
| 133 | + /// options alike. | |
| 134 | + pub objects: gix_sec::Permission, | |
| 135 | + /// Control if resources pointed to by `GIT_*` prefixed environment variables can be used, **but only** if they | |
| 136 | + /// are not contained in any other category. This is a catch-all section. | |
| 137 | + pub git_prefix: gix_sec::Permission, | |
| 138 | + /// Control if resources pointed to by `SSH_*` prefixed environment variables can be used (like `SSH_ASKPASS`) | |
| 139 | + pub ssh_prefix: gix_sec::Permission, | |
| 140 | +} | |
| 141 | + | |
| 142 | +impl Environment { | |
| 143 | + /// Allow access to the entire environment. | |
| 144 | + pub fn all() -> Self { | |
| 145 | + let allow = gix_sec::Permission::Allow; | |
| 146 | + Environment { | |
| 147 | + xdg_config_home: allow, | |
| 148 | + home: allow, | |
| 149 | + git_prefix: allow, | |
| 150 | + ssh_prefix: allow, | |
| 151 | + http_transport: allow, | |
| 152 | + identity: allow, | |
| 153 | + objects: allow, | |
| 154 | + } | |
| 155 | + } | |
| 156 | + | |
| 157 | + /// Don't allow loading any environment variables. | |
| 158 | + pub fn isolated() -> Self { | |
| 159 | + let deny = gix_sec::Permission::Deny; | |
| 160 | + Environment { | |
| 161 | + xdg_config_home: deny, | |
| 162 | + home: deny, | |
| 163 | + ssh_prefix: deny, | |
| 164 | + git_prefix: deny, | |
| 165 | + http_transport: deny, | |
| 166 | + identity: deny, | |
| 167 | + objects: deny, | |
| 168 | + } | |
| 169 | + } | |
| 170 | +} | |
| 171 | + | |
| 172 | +impl Permissions { | |
| 173 | + /// Secure permissions are similar to `all()` | |
| 174 | + pub fn secure() -> Self { | |
| 175 | + Permissions { | |
| 176 | + env: Environment::all(), | |
| 177 | + config: Config::all(), | |
| 178 | + attributes: Attributes::all(), | |
| 179 | + } | |
| 180 | + } | |
| 181 | + | |
| 182 | + /// Everything is allowed with this set of permissions, thus we read all configuration and do what git typically | |
| 183 | + /// does with owned repositories. | |
| 184 | + pub fn all() -> Self { | |
| 185 | + Permissions { | |
| 186 | + env: Environment::all(), | |
| 187 | + config: Config::all(), | |
| 188 | + attributes: Attributes::all(), | |
| 189 | + } | |
| 190 | + } | |
| 191 | + | |
| 192 | + /// Don't read any but the local git configuration and deny reading any environment variables. | |
| 193 | + pub fn isolated() -> Self { | |
| 194 | + Permissions { | |
| 195 | + config: Config::isolated(), | |
| 196 | + attributes: Attributes::isolated(), | |
| 197 | + env: Environment::isolated(), | |
| 198 | + } | |
| 199 | + } | |
| 200 | +} | |
| 201 | + | |
| 202 | +impl gix_sec::trust::DefaultForLevel for Permissions { | |
| 203 | + fn default_for_level(level: Trust) -> Self { | |
| 204 | + match level { | |
| 205 | + Trust::Full => Permissions::all(), | |
| 206 | + Trust::Reduced => Permissions::secure(), | |
| 207 | + } | |
| 208 | + } | |
| 209 | +} | |
| 210 | + | |
| 211 | +impl Default for Permissions { | |
| 212 | + fn default() -> Self { | |
| 213 | + Permissions::secure() | |
| 214 | + } | |
| 215 | +} | |
| @@ -10,7 +10,8 @@ use crate::{ · 9 unmodified lines | ||
| 10 | 10 | cache::{interpolate_context, util::ApplyLeniency}, |
| 11 | 11 | tree::{gitoxide, Core, Key, Safe}, |
| 12 | 12 | }, |
| 13 | − permission, Permissions, ThreadSafeRepository, | |
| 13 | + open::Permissions, | |
| 14 | + ThreadSafeRepository, | |
| 14 | 15 | }; |
| 15 | 16 | |
| 16 | 17 | #[derive(Default, Clone)] |
| @@ -26,7 +27,7 @@ pub(crate) struct EnvironmentOverrides { · 9 unmodified lines | ||
| 26 | 27 | } |
| 27 | 28 | |
| 28 | 29 | impl EnvironmentOverrides { |
| 29 | − fn from_env() -> Result<Self, permission::env_var::resource::Error> { | |
| 30 | + fn from_env() -> Result<Self, gix_sec::permission::Error<std::path::PathBuf>> { | |
| 30 | 31 | let mut worktree_dir = None; |
| 31 | 32 | if let Some(path) = std::env::var_os(Core::WORKTREE.the_environment_override()) { |
| 32 | 33 | worktree_dir = PathBuf::from(path).into(); |
| @@ -1,8 +0,0 @@ | ||
| 1 | −/// | |
| 2 | −pub mod env_var { | |
| 3 | − /// | |
| 4 | − pub mod resource { | |
| 5 | − /// | |
| 6 | − pub type Error = gix_sec::permission::Error<std::path::PathBuf>; | |
| 7 | − } | |
| 8 | −} | |
| @@ -1 +0,0 @@ | ||
| 1 | −pub use crate::repository::permissions::{Attributes, Config, Environment}; | |
| @@ -0,0 +1,50 @@ | ||
| 1 | +//! exclude information | |
| 2 | +use crate::Repository; | |
| 3 | + | |
| 4 | +impl Repository { | |
| 5 | + /// Configure a file-system cache for accessing git attributes *and* excludes on a per-path basis. | |
| 6 | + /// | |
| 7 | + /// Use `attribute_source` to specify where to read attributes from. Also note that exclude information will | |
| 8 | + /// always try to read `.gitignore` files from disk before trying to read it from the `index`. | |
| 9 | + /// | |
| 10 | + /// Note that no worktree is required for this to work, even though access to in-tree `.gitattributes` and `.gitignore` files | |
| 11 | + /// would require a non-empty `index` that represents a git tree. | |
| 12 | + /// | |
| 13 | + /// This takes into consideration all the usual repository configuration, namely: | |
| 14 | + /// | |
| 15 | + /// * `$XDG_CONFIG_HOME/…/ignore|attributes` if `core.excludesFile|attributesFile` is *not* set, otherwise use the configured file. | |
| 16 | + /// * `$GIT_DIR/info/exclude|attributes` if present. | |
| 17 | + // TODO: test, provide higher-level custom Cache wrapper that is much easier to use and doesn't panic when accessing entries | |
| 18 | + // by non-relative path. | |
| 19 | + pub fn attributes( | |
| 20 | + &self, | |
| 21 | + index: &gix_index::State, | |
| 22 | + attributes_source: gix_worktree::cache::state::attributes::Source, | |
| 23 | + ignore_source: gix_worktree::cache::state::ignore::Source, | |
| 24 | + exclude_overrides: Option<gix_ignore::Search>, | |
| 25 | + ) -> Result<gix_worktree::Cache, crate::attributes::Error> { | |
| 26 | + let case = if self.config.ignore_case { | |
| 27 | + gix_glob::pattern::Case::Fold | |
| 28 | + } else { | |
| 29 | + gix_glob::pattern::Case::Sensitive | |
| 30 | + }; | |
| 31 | + let (attributes, mut buf) = self.config.assemble_attribute_globals( | |
| 32 | + self.git_dir(), | |
| 33 | + attributes_source, | |
| 34 | + self.options.permissions.attributes, | |
| 35 | + )?; | |
| 36 | + let ignore = | |
| 37 | + self.config | |
| 38 | + .assemble_exclude_globals(self.git_dir(), exclude_overrides, ignore_source, &mut buf)?; | |
| 39 | + let state = gix_worktree::cache::State::AttributesAndIgnoreStack { attributes, ignore }; | |
| 40 | + let attribute_list = state.id_mappings_from_index(index, index.path_backing(), ignore_source, case); | |
| 41 | + Ok(gix_worktree::Cache::new( | |
| 42 | + // this is alright as we don't cause mutation of that directory, it's virtual. | |
| 43 | + self.work_dir().unwrap_or(self.git_dir()), | |
| 44 | + state, | |
| 45 | + case, | |
| 46 | + buf, | |
| 47 | + attribute_list, | |
| 48 | + )) | |
| 49 | + } | |
| 50 | +} | |
| @@ -0,0 +1,45 @@ | ||
| 1 | +//! exclude information | |
| 2 | +use crate::{config, Repository}; | |
| 3 | +impl Repository { | |
| 4 | + /// Configure a file-system cache checking if files below the repository are excluded, reading `.gitignore` files from | |
| 5 | + /// the specified `source`. | |
| 6 | + /// | |
| 7 | + /// Note that no worktree is required for this to work, even though access to in-tree `.gitignore` files would require | |
| 8 | + /// a non-empty `index` that represents a tree with `.gitignore` files. | |
| 9 | + /// | |
| 10 | + /// This takes into consideration all the usual repository configuration, namely: | |
| 11 | + /// | |
| 12 | + /// * `$XDG_CONFIG_HOME/…/ignore` if `core.excludesFile` is *not* set, otherwise use the configured file. | |
| 13 | + /// * `$GIT_DIR/info/exclude` if present. | |
| 14 | + /// | |
| 15 | + /// When only excludes are desired, this is the most efficient way to obtain them. Otherwise use | |
| 16 | + /// [`Repository::attributes()`] for accessing both attributes and excludes. | |
| 17 | + // TODO: test, provide higher-level custom Cache wrapper that is much easier to use and doesn't panic when accessing entries | |
| 18 | + // by non-relative path. | |
| 19 | + pub fn excludes( | |
| 20 | + &self, | |
| 21 | + index: &gix_index::State, | |
| 22 | + overrides: Option<gix_ignore::Search>, | |
| 23 | + source: gix_worktree::cache::state::ignore::Source, | |
| 24 | + ) -> Result<gix_worktree::Cache, config::exclude_stack::Error> { | |
| 25 | + let case = if self.config.ignore_case { | |
| 26 | + gix_glob::pattern::Case::Fold | |
| 27 | + } else { | |
| 28 | + gix_glob::pattern::Case::Sensitive | |
| 29 | + }; | |
| 30 | + let mut buf = Vec::with_capacity(512); | |
| 31 | + let ignore = self | |
| 32 | + .config | |
| 33 | + .assemble_exclude_globals(self.git_dir(), overrides, source, &mut buf)?; | |
| 34 | + let state = gix_worktree::cache::State::IgnoreStack(ignore); | |
| 35 | + let attribute_list = state.id_mappings_from_index(index, index.path_backing(), source, case); | |
| 36 | + Ok(gix_worktree::Cache::new( | |
| 37 | + // this is alright as we don't cause mutation of that directory, it's virtual. | |
| 38 | + self.work_dir().unwrap_or(self.git_dir()), | |
| 39 | + state, | |
| 40 | + case, | |
| 41 | + buf, | |
| 42 | + attribute_list, | |
| 43 | + )) | |
| 44 | + } | |
| 45 | +} | |
| @@ -19,14 +19,15 @@ impl crate::Repository { · 18 unmodified lines | ||
| 19 | 19 | } |
| 20 | 20 | } |
| 21 | 21 | |
| 22 | +mod attributes; | |
| 22 | 23 | mod cache; |
| 23 | 24 | mod config; |
| 25 | +mod excludes; | |
| 24 | 26 | pub(crate) mod identity; |
| 25 | 27 | mod impls; |
| 26 | 28 | mod init; |
| 27 | 29 | mod location; |
| 28 | 30 | mod object; |
| 29 | −pub(crate) mod permissions; | |
| 30 | 31 | mod reference; |
| 31 | 32 | mod remote; |
| 32 | 33 | mod revision; |
| @@ -1,16 +1,7 @@ | ||
| 1 | +//! Various permissions to define what can be done when operating a [`Repository`][crate::Repository]. | |
| 2 | +use crate::open::Permissions; | |
| 1 | 3 | use gix_sec::Trust; |
| 2 | 4 | |
| 3 | −/// Permissions associated with various resources of a git repository | |
| 4 | −#[derive(Debug, Clone)] | |
| 5 | −pub struct Permissions { | |
| 6 | − /// Control which environment variables may be accessed. | |
| 7 | − pub env: Environment, | |
| 8 | − /// Permissions related where git configuration should be loaded from. | |
| 9 | − pub config: Config, | |
| 10 | − /// Permissions related to where `gitattributes` should be loaded from. | |
| 11 | − pub attributes: Attributes, | |
| 12 | −} | |
| 13 | − | |
| 14 | 5 | /// Configure from which sources git configuration may be loaded. |
| 15 | 6 | /// |
| 16 | 7 | /// Note that configuration from inside of the repository is always loaded as it's definitely required for correctness. |
| @@ -1,6 +1,7 @@ | ||
| 1 | +use crate::config::cache::util::ApplyLeniencyDefault; | |
| 1 | 2 | use crate::{worktree, Worktree}; |
| 2 | 3 | |
| 3 | −/// Worktree iteration | |
| 4 | +/// Interact with individual worktrees and their information. | |
| 4 | 5 | impl crate::Repository { |
| 5 | 6 | /// Return a list of all _linked_ worktrees sorted by private git dir path as a lightweight proxy. |
| 6 | 7 | /// |
| @@ -25,10 +26,6 @@ impl crate::Repository { · 18 unmodified lines | ||
| 25 | 26 | res.sort_by(|a, b| a.git_dir.cmp(&b.git_dir)); |
| 26 | 27 | Ok(res) |
| 27 | 28 | } |
| 28 | −} | |
| 29 | − | |
| 30 | −/// Interact with individual worktrees and their information. | |
| 31 | −impl crate::Repository { | |
| 32 | 29 | /// Return the repository owning the main worktree, typically from a linked worktree. |
| 33 | 30 | /// |
| 34 | 31 | /// Note that it might be the one that is currently open if this repository doesn't point to a linked worktree. |
| @@ -58,23 +55,14 @@ impl crate::Repository { · 23 unmodified lines | ||
| 58 | 55 | /// |
| 59 | 56 | /// It will use the `index.threads` configuration key to learn how many threads to use. |
| 60 | 57 | /// Note that it may fail if there is no index. |
| 61 | − // TODO: test | |
| 62 | 58 | pub fn open_index(&self) -> Result<gix_index::File, worktree::open_index::Error> { |
| 63 | 59 | let thread_limit = self |
| 64 | 60 | .config |
| 65 | 61 | .resolved |
| 66 | − .boolean("index", None, "threads") | |
| 67 | − .map(|res| { | |
| 68 | − res.map(|value| usize::from(!value)).or_else(|err| { | |
| 69 | − gix_config::Integer::try_from(err.input.as_ref()) | |
| 70 | − .map_err(|err| worktree::open_index::Error::ConfigIndexThreads { | |
| 71 | − value: err.input.clone(), | |
| 72 | − err, | |
| 73 | − }) | |
| 74 | − .map(|value| value.to_decimal().and_then(|v| v.try_into().ok()).unwrap_or(1)) | |
| 75 | − }) | |
| 76 | − }) | |
| 77 | − .transpose()?; | |
| 62 | + .string("index", None, "threads") | |
| 63 | + .map(|value| crate::config::tree::Index::THREADS.try_into_index_threads(value)) | |
| 64 | + .transpose() | |
| 65 | + .with_lenient_default(self.config.lenient_config)?; | |
| 78 | 66 | gix_index::File::at( |
| 79 | 67 | self.index_path(), |
| 80 | 68 | self.object_hash(), |
| @@ -71,18 +71,12 @@ pub mod proxy; · 70 unmodified lines | ||
| 71 | 71 | |
| 72 | 72 | /// |
| 73 | 73 | pub mod open_index { |
| 74 | − use crate::bstr::BString; | |
| 75 | − | |
| 76 | 74 | /// The error returned by [`Worktree::open_index()`][crate::Worktree::open_index()]. |
| 77 | 75 | #[derive(Debug, thiserror::Error)] |
| 78 | 76 | #[allow(missing_docs)] |
| 79 | 77 | pub enum Error { |
| 80 | − #[error("Could not interpret value '{}' as 'index.threads'", .value)] | |
| 81 | − ConfigIndexThreads { | |
| 82 | − value: BString, | |
| 83 | − #[source] | |
| 84 | − err: gix_config::value::Error, | |
| 85 | − }, | |
| 78 | + #[error(transparent)] | |
| 79 | + ConfigIndexThreads(#[from] crate::config::key::GenericErrorWithValue), | |
| 86 | 80 | #[error(transparent)] |
| 87 | 81 | IndexFile(#[from] gix_index::file::init::Error), |
| 88 | 82 | } |
| @@ -102,18 +96,14 @@ pub mod open_index { · 13 unmodified lines | ||
| 102 | 96 | |
| 103 | 97 | /// |
| 104 | 98 | pub mod excludes { |
| 105 | − use std::path::PathBuf; | |
| 106 | − | |
| 107 | 99 | /// The error returned by [`Worktree::excludes()`][crate::Worktree::excludes()]. |
| 108 | 100 | #[derive(Debug, thiserror::Error)] |
| 109 | 101 | #[allow(missing_docs)] |
| 110 | 102 | pub enum Error { |
| 111 | − #[error("Could not read repository exclude.")] | |
| 112 | − Io(#[from] std::io::Error), | |
| 113 | 103 | #[error(transparent)] |
| 114 | − EnvironmentPermission(#[from] gix_sec::permission::Error<PathBuf>), | |
| 115 | − #[error("The value for `core.excludesFile` could not be read from configuration")] | |
| 116 | − ExcludesFilePathInterpolation(#[from] gix_config::path::interpolate::Error), | |
| 104 | + OpenIndex(#[from] crate::worktree::open_index::Error), | |
| 105 | + #[error(transparent)] | |
| 106 | + CreateCache(#[from] crate::config::exclude_stack::Error), | |
| 117 | 107 | } |
| 118 | 108 | |
| 119 | 109 | impl<'repo> crate::Worktree<'repo> { |
| @@ -124,35 +114,46 @@ pub mod excludes { · 4 unmodified lines | ||
| 124 | 114 | /// * `$XDG_CONFIG_HOME/…/ignore` if `core.excludesFile` is *not* set, otherwise use the configured file. |
| 125 | 115 | /// * `$GIT_DIR/info/exclude` if present. |
| 126 | 116 | /// |
| 127 | − /// `index` may be used to obtain `.gitignore` files directly from the index under certain conditions. | |
| 128 | − // TODO: test, provide higher-level interface that is much easier to use and doesn't panic when accessing entries | |
| 129 | − // by non-relative path. | |
| 130 | − // TODO: `index` might be so special (given the conditions we are talking about) that it's better obtained internally | |
| 131 | − // so the caller won't have to care. | |
| 132 | − pub fn excludes( | |
| 133 | − &self, | |
| 134 | − index: &gix_index::State, | |
| 135 | − overrides: Option<gix_ignore::Search>, | |
| 136 | − ) -> Result<gix_worktree::Cache, Error> { | |
| 137 | − let repo = self.parent; | |
| 138 | − let case = if repo.config.ignore_case { | |
| 139 | − gix_glob::pattern::Case::Fold | |
| 140 | − } else { | |
| 141 | − gix_glob::pattern::Case::Sensitive | |
| 142 | − }; | |
| 143 | − let mut buf = Vec::with_capacity(512); | |
| 144 | − let excludes_file = match repo.config.excludes_file().transpose()? { | |
| 145 | − Some(user_path) => Some(user_path), | |
| 146 | − None => repo.config.xdg_config_path("ignore")?, | |
| 147 | − }; | |
| 148 | − let state = gix_worktree::cache::State::IgnoreStack(gix_worktree::cache::state::Ignore::new( | |
| 149 | − overrides.unwrap_or_default(), | |
| 150 | − gix_ignore::Search::from_git_dir(repo.git_dir(), excludes_file, &mut buf)?, | |
| 151 | − None, | |
| 152 | − case, | |
| 153 | − )); | |
| 154 | − let attribute_list = state.id_mappings_from_index(index, index.path_backing(), case); | |
| 155 | − Ok(gix_worktree::Cache::new(self.path, state, case, buf, attribute_list)) | |
| 117 | + /// When only excludes are desired, this is the most efficient way to obtain them. Otherwise use | |
| 118 | + /// [`Worktree::attributes()`][crate::Worktree::attributes()] for accessing both attributes and excludes. | |
| 119 | + pub fn excludes(&self, overrides: Option<gix_ignore::Search>) -> Result<gix_worktree::Cache, Error> { | |
| 120 | + let index = self.index()?; | |
| 121 | + Ok(self.parent.excludes( | |
| 122 | + &index, | |
| 123 | + overrides, | |
| 124 | + gix_worktree::cache::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, | |
| 125 | + )?) | |
| 126 | + } | |
| 127 | + } | |
| 128 | +} | |
| 129 | + | |
| 130 | +/// | |
| 131 | +pub mod attributes { | |
| 132 | + /// The error returned by [`Worktree::attributes()`][crate::Worktree::attributes()]. | |
| 133 | + #[derive(Debug, thiserror::Error)] | |
| 134 | + #[allow(missing_docs)] | |
| 135 | + pub enum Error { | |
| 136 | + #[error(transparent)] | |
| 137 | + OpenIndex(#[from] crate::worktree::open_index::Error), | |
| 138 | + #[error(transparent)] | |
| 139 | + CreateCache(#[from] crate::attributes::Error), | |
| 140 | + } | |
| 141 | + | |
| 142 | + impl<'repo> crate::Worktree<'repo> { | |
| 143 | + /// Configure a file-system cache checking if files below the repository are excluded or for querying their attributes. | |
| 144 | + /// | |
| 145 | + /// This takes into consideration all the usual repository configuration, namely: | |
| 146 | + /// | |
| 147 | + /// * `$XDG_CONFIG_HOME/…/ignore|attributes` if `core.excludesFile|attributesFile` is *not* set, otherwise use the configured file. | |
| 148 | + /// * `$GIT_DIR/info/exclude|attributes` if present. | |
| 149 | + pub fn attributes(&self, overrides: Option<gix_ignore::Search>) -> Result<gix_worktree::Cache, Error> { | |
| 150 | + let index = self.index()?; | |
| 151 | + Ok(self.parent.attributes( | |
| 152 | + &index, | |
| 153 | + gix_worktree::cache::state::attributes::Source::WorktreeThenIdMapping, | |
| 154 | + gix_worktree::cache::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, | |
| 155 | + overrides, | |
| 156 | + )?) | |
| 156 | 157 | } |
| 157 | 158 | } |
| 158 | 159 | } |
| @@ -351,6 +351,30 @@ mod core { · 350 unmodified lines | ||
| 351 | 351 | } |
| 352 | 352 | } |
| 353 | 353 | |
| 354 | +mod index { | |
| 355 | + use crate::config::tree::bcow; | |
| 356 | + use gix::config::tree::{Index, Key}; | |
| 357 | + | |
| 358 | + #[test] | |
| 359 | + fn threads() { | |
| 360 | + for (value, expected) in [("false", 1), ("true", 0), ("0", 0), ("1", 1), ("2", 2), ("12", 12)] { | |
| 361 | + assert_eq!( | |
| 362 | + Index::THREADS.try_into_index_threads(bcow(value)).unwrap(), | |
| 363 | + expected, | |
| 364 | + "{value}" | |
| 365 | + ); | |
| 366 | + assert!(Index::THREADS.validate(value.into()).is_ok()); | |
| 367 | + } | |
| 368 | + assert_eq!( | |
| 369 | + Index::THREADS | |
| 370 | + .try_into_index_threads(bcow("nothing")) | |
| 371 | + .unwrap_err() | |
| 372 | + .to_string(), | |
| 373 | + "The key \"index.threads=nothing\" was invalid" | |
| 374 | + ); | |
| 375 | + } | |
| 376 | +} | |
| 377 | + | |
| 354 | 378 | mod extensions { |
| 355 | 379 | use gix::config::tree::{Extensions, Key}; |
| 356 | 380 | |
| @@ -28,12 +28,12 @@ mod blocking_io { · 27 unmodified lines | ||
| 28 | 28 | let _env = env_value.map(|value| gix_testtools::Env::new().set("GIT_PROTOCOL_FROM_USER", value)); |
| 29 | 29 | let repo = gix::open_opts( |
| 30 | 30 | remote::repo("protocol_file_user").git_dir(), |
| 31 | − gix::open::Options::isolated().permissions(gix::Permissions { | |
| 32 | − env: gix::permissions::Environment { | |
| 31 | + gix::open::Options::isolated().permissions(gix::open::Permissions { | |
| 32 | + env: gix::open::permissions::Environment { | |
| 33 | 33 | git_prefix: gix_sec::Permission::Allow, |
| 34 | − ..gix::permissions::Environment::all() | |
| 34 | + ..gix::open::permissions::Environment::all() | |
| 35 | 35 | }, |
| 36 | − ..gix::Permissions::isolated() | |
| 36 | + ..gix::open::Permissions::isolated() | |
| 37 | 37 | }), |
| 38 | 38 | )?; |
| 39 | 39 | let remote = repo.find_remote("origin")?; |
| @@ -29,14 +29,17 @@ fn author_and_committer_and_fallback() -> crate::Result { · 28 unmodified lines | ||
| 29 | 29 | .set("GIT_CONFIG_VALUE_0", work_dir.join("c.config").display().to_string()); |
| 30 | 30 | let repo = gix::open_opts( |
| 31 | 31 | repo.git_dir(), |
| 32 | − repo.open_options().clone().with(trust).permissions(gix::Permissions { | |
| 33 | − env: gix::permissions::Environment { | |
| 34 | − xdg_config_home: Permission::Deny, | |
| 35 | − home: Permission::Deny, | |
| 36 | − ..gix::permissions::Environment::all() | |
| 37 | − }, | |
| 38 | − ..Default::default() | |
| 39 | − }), | |
| 32 | + repo.open_options() | |
| 33 | + .clone() | |
| 34 | + .with(trust) | |
| 35 | + .permissions(gix::open::Permissions { | |
| 36 | + env: gix::open::permissions::Environment { | |
| 37 | + xdg_config_home: Permission::Deny, | |
| 38 | + home: Permission::Deny, | |
| 39 | + ..gix::open::permissions::Environment::all() | |
| 40 | + }, | |
| 41 | + ..Default::default() | |
| 42 | + }), | |
| 40 | 43 | )?; |
| 41 | 44 | |
| 42 | 45 | assert_eq!( |
| @@ -148,11 +151,11 @@ fn author_from_different_config_sections() -> crate::Result { · 105 unmodified lines | ||
| 148 | 151 | .clone() |
| 149 | 152 | .config_overrides(None::<&str>) |
| 150 | 153 | .with(gix_sec::Trust::Full) |
| 151 | − .permissions(gix::Permissions { | |
| 152 | − env: gix::permissions::Environment { | |
| 154 | + .permissions(gix::open::Permissions { | |
| 155 | + env: gix::open::permissions::Environment { | |
| 153 | 156 | xdg_config_home: Permission::Deny, |
| 154 | 157 | home: Permission::Deny, |
| 155 | − ..gix::permissions::Environment::all() | |
| 158 | + ..gix::open::permissions::Environment::all() | |
| 156 | 159 | }, |
| 157 | 160 | ..Default::default() |
| 158 | 161 | }), |
| @@ -339,16 +339,6 @@ pub fn main() -> Result<()> { · 338 unmodified lines | ||
| 339 | 339 | ) |
| 340 | 340 | }, |
| 341 | 341 | ), |
| 342 | − free::index::Subcommands::Entries => prepare_and_run( | |
| 343 | − "index-entries", | |
| 344 | − verbose, | |
| 345 | − progress, | |
| 346 | − progress_keep_open, | |
| 347 | − None, | |
| 348 | − move |_progress, out, _err| { | |
| 349 | − core::index::entries(index_path, out, core::index::Options { object_hash, format }) | |
| 350 | − }, | |
| 351 | − ), | |
| 352 | 342 | free::index::Subcommands::Verify => prepare_and_run( |
| 353 | 343 | "index-verify", |
| 354 | 344 | auto_verbose, |
| @@ -865,6 +855,37 @@ pub fn main() -> Result<()> { · 510 unmodified lines | ||
| 865 | 855 | ), |
| 866 | 856 | }, |
| 867 | 857 | Subcommands::Index(cmd) => match cmd { |
| 858 | + index::Subcommands::Entries { | |
| 859 | + no_attributes, | |
| 860 | + attributes_from_index, | |
| 861 | + statistics, | |
| 862 | + } => prepare_and_run( | |
| 863 | + "index-entries", | |
| 864 | + verbose, | |
| 865 | + progress, | |
| 866 | + progress_keep_open, | |
| 867 | + None, | |
| 868 | + move |_progress, out, err| { | |
| 869 | + core::repository::index::entries( | |
| 870 | + repository(Mode::LenientWithGitInstallConfig)?, | |
| 871 | + out, | |
| 872 | + err, | |
| 873 | + core::repository::index::entries::Options { | |
| 874 | + format, | |
| 875 | + attributes: if no_attributes { | |
| 876 | + None | |
| 877 | + } else { | |
| 878 | + Some(if attributes_from_index { | |
| 879 | + core::repository::index::entries::Attributes::Index | |
| 880 | + } else { | |
| 881 | + core::repository::index::entries::Attributes::WorktreeAndIndex | |
| 882 | + }) | |
| 883 | + }, | |
| 884 | + statistics, | |
| 885 | + }, | |
| 886 | + ) | |
| 887 | + }, | |
| 888 | + ), | |
| 868 | 889 | index::Subcommands::FromTree { |
| 869 | 890 | force, |
| 870 | 891 | index_output_path, |
| @@ -899,3 +920,14 @@ fn verify_mode(decode: bool, re_encode: bool) -> verify::Mode { · 28 unmodified lines | ||
| 899 | 920 | (false, false) => verify::Mode::HashCrc32, |
| 900 | 921 | } |
| 901 | 922 | } |
| 923 | + | |
| 924 | +#[cfg(test)] | |
| 925 | +mod tests { | |
| 926 | + use super::*; | |
| 927 | + | |
| 928 | + #[test] | |
| 929 | + fn clap() { | |
| 930 | + use clap::CommandFactory; | |
| 931 | + Args::command().debug_assert(); | |
| 932 | + } | |
| 933 | +} | |
| @@ -55,8 +55,6 @@ pub mod index { · 54 unmodified lines | ||
| 55 | 55 | pub enum Subcommands { |
| 56 | 56 | /// Validate constraints and assumptions of an index along with its integrity. |
| 57 | 57 | Verify, |
| 58 | − /// Print all entries to standard output | |
| 59 | − Entries, | |
| 60 | 58 | /// Print information about the index structure |
| 61 | 59 | Info { |
| 62 | 60 | /// Do not extract specific extension information to gain only a superficial idea of the index's composition. |
| @@ -107,7 +105,7 @@ pub mod pack { · 44 unmodified lines | ||
| 107 | 105 | /// Possible values are "none" and "tree-traversal". Default is "none". |
| 108 | 106 | expansion: Option<core::pack::create::ObjectExpansion>, |
| 109 | 107 | |
| 110 | − #[clap(long, default_value_t = 3, requires = "nondeterministic-count")] | |
| 108 | + #[clap(long, default_value_t = 3, requires = "nondeterministic_count")] | |
| 111 | 109 | /// The amount of threads to use when counting and the `--nondeterminisitc-count` flag is set, defaulting |
| 112 | 110 | /// to the globally configured threads. |
| 113 | 111 | /// |
| @@ -361,11 +361,11 @@ pub mod commit { · 360 unmodified lines | ||
| 361 | 361 | /// Describe the current commit or the given one using the name of the closest annotated tag in its ancestry. |
| 362 | 362 | Describe { |
| 363 | 363 | /// Use annotated tag references only, not all tags. |
| 364 | − #[clap(long, short = 't', conflicts_with("all-refs"))] | |
| 364 | + #[clap(long, short = 't', conflicts_with("all_refs"))] | |
| 365 | 365 | annotated_tags: bool, |
| 366 | 366 | |
| 367 | 367 | /// Use all references under the `ref/` namespaces, which includes tag references, local and remote branches. |
| 368 | − #[clap(long, short = 'a', conflicts_with("annotated-tags"))] | |
| 368 | + #[clap(long, short = 'a', conflicts_with("annotated_tags"))] | |
| 369 | 369 | all_refs: bool, |
| 370 | 370 | |
| 371 | 371 | /// Only follow the first parent when traversing the commit graph. |
| @@ -471,6 +471,20 @@ pub mod index { · 99 unmodified lines | ||
| 471 | 471 | |
| 472 | 472 | #[derive(Debug, clap::Subcommand)] |
| 473 | 473 | pub enum Subcommands { |
| 474 | + /// Print all entries to standard output | |
| 475 | + Entries { | |
| 476 | + /// Do not visualize excluded entries or attributes per path. | |
| 477 | + #[clap(long)] | |
| 478 | + no_attributes: bool, | |
| 479 | + /// Load attribute and ignore files from the index, don't look at the worktree. | |
| 480 | + /// | |
| 481 | + /// This is to see what IO for probing attribute/ignore files does to performance. | |
| 482 | + #[clap(long, short = 'i', conflicts_with = "no_attributes")] | |
| 483 | + attributes_from_index: bool, | |
| 484 | + /// Print various statistics to stderr | |
| 485 | + #[clap(long, short = 's')] | |
| 486 | + statistics: bool, | |
| 487 | + }, | |
| 474 | 488 | /// Create an index from a tree-ish. |
| 475 | 489 | #[clap(visible_alias = "read-tree")] |
| 476 | 490 | FromTree { |
| @@ -113,7 +113,11 @@ pub mod pretty { · 112 unmodified lines | ||
| 113 | 113 | crate::shared::init_env_logger(); |
| 114 | 114 | |
| 115 | 115 | match (verbose, progress) { |
| 116 | − (false, false) => run(progress::DoOrDiscard::from(None), &mut stdout(), &mut stderr()), | |
| 116 | + (false, false) => { | |
| 117 | + let stdout = stdout(); | |
| 118 | + let mut stdout_lock = stdout.lock(); | |
| 119 | + run(progress::DoOrDiscard::from(None), &mut stdout_lock, &mut stderr()) | |
| 120 | + } | |
| 117 | 121 | (true, false) => { |
| 118 | 122 | use crate::shared::{self, STANDARD_RANGE}; |
| 119 | 123 | let progress = shared::progress_tree(); |