Unified · Split
| @@ -83,16 +83,16 @@ is usable to some extent. · 82 unmodified lines | |||
| 83 | * [gix-command](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-command) | 83 | * [gix-command](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-command) |
| 84 | * [gix-prompt](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-prompt) | 84 | * [gix-prompt](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-prompt) |
| 85 | * [gix-refspec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-refspec) | 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 | * [gix-fs](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-fs) | 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 | * [gix-worktree](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-worktree) | 89 | * [gix-worktree](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-worktree) |
| 91 | * [gix-bitmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-bitmap) | 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 | * [gix-date](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-date) | 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 | * [gix-archive](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-archive) | 94 | * [gix-archive](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-archive) |
| 95 | * **idea** _(just a name placeholder)_ | ||
| 96 | * [gix-note](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-note) | 96 | * [gix-note](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-note) |
| 97 | * [gix-fetchhead](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-fetchhead) | 97 | * [gix-fetchhead](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-fetchhead) |
| 98 | * [gix-filter](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-filter) | 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 | * [x] proper handling of worktree related refs | 690 | * [x] proper handling of worktree related refs |
| 691 | * [ ] create, move, remove, and repair | 691 | * [ ] create, move, remove, and repair |
| 692 | * [x] access exclude information | 692 | * [x] access exclude information |
| 693 | * [ ] access attribute information | 693 | * [x] access attribute information |
| 694 | * [x] respect `core.worktree` configuration | 694 | * [x] respect `core.worktree` configuration |
| 695 | - **deviation** | 695 | - **deviation** |
| 696 | * The delicate interplay between `GIT_COMMON_DIR` and `GIT_WORK_TREE` isn't implemented. | 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 | pub format: crate::OutputFormat, | 5 | pub format: crate::OutputFormat, |
| 6 | } | 6 | } |
| 7 | 7 | ||
| 8 | mod entries; | ||
| 9 | pub use entries::entries; | ||
| 10 | |||
| 11 | pub mod information; | 8 | pub mod information; |
| 12 | 9 | ||
| 13 | fn parse_file(index_path: impl AsRef<Path>, object_hash: gix::hash::Kind) -> anyhow::Result<gix::index::File> { | 10 | fn parse_file(index_path: impl AsRef<Path>, object_hash: gix::hash::Kind) -> anyhow::Result<gix::index::File> { |
| @@ -1,6 +1,6 @@ | |||
| 1 | use std::io; | 1 | use std::io; |
| 2 | 2 | ||
| 3 | use anyhow::{bail, Context}; | 3 | use anyhow::bail; |
| 4 | use gix::prelude::FindExt; | 4 | use gix::prelude::FindExt; |
| 5 | 5 | ||
| 6 | use crate::OutputFormat; | 6 | use crate::OutputFormat; |
| @@ -31,11 +31,12 @@ pub fn query( · 24 unmodified lines | |||
| 31 | bail!("JSON output isn't implemented yet"); | 31 | bail!("JSON output isn't implemented yet"); |
| 32 | } | 32 | } |
| 33 | 33 | ||
| 34 | let worktree = repo | 34 | let index = repo.index()?; |
| 35 | .worktree() | 35 | let mut cache = repo.excludes( |
| 36 | .with_context(|| "Cannot check excludes without a current worktree")?; | 36 | &index, |
| 37 | let index = worktree.index()?; | 37 | Some(gix::ignore::Search::from_overrides(overrides)), |
| 38 | let mut cache = worktree.excludes(&index, Some(gix::ignore::Search::from_overrides(overrides)))?; | 38 | Default::default(), |
| 39 | )?; | ||
| 39 | 40 | ||
| 40 | let prefix = repo.prefix().expect("worktree - we have an index by now")?; | 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 | Ok(()) | 35 | Ok(()) |
| 36 | } | 36 | } |
| 37 | |||
| 38 | pub mod entries; | ||
| 39 | pub use entries::function::entries; | ||
| @@ -1,13 +1,17 @@ | |||
| 1 | use std::convert::TryInto; | 1 | use std::convert::TryInto; |
| 2 | 2 | ||
| 3 | /// | ||
| 3 | pub mod decode { | 4 | pub mod decode { |
| 5 | /// The error returned by [`decode()`][super::decode()]. | ||
| 4 | #[derive(Debug, thiserror::Error)] | 6 | #[derive(Debug, thiserror::Error)] |
| 7 | #[allow(missing_docs)] | ||
| 5 | pub enum Error { | 8 | pub enum Error { |
| 6 | #[error("{}", message)] | 9 | #[error("{}", message)] |
| 7 | Corrupt { message: &'static str }, | 10 | Corrupt { message: &'static str }, |
| 8 | } | 11 | } |
| 9 | } | 12 | } |
| 10 | 13 | ||
| 14 | /// Decode `data` as EWAH bitmap. | ||
| 11 | pub fn decode(data: &[u8]) -> Result<(Vec, &[u8]), decode::Error> { | 15 | pub fn decode(data: &[u8]) -> Result<(Vec, &[u8]), decode::Error> { |
| 12 | use self::decode::Error; | 16 | use self::decode::Error; |
| 13 | use crate::decode; | 17 | use crate::decode; |
| @@ -1,8 +1,7 @@ | |||
| 1 | //! An implementation of the shared parts of git bitmaps used in `gix-pack`, `gix-index` and `gix-worktree`. | 1 | //! An implementation of the shared parts of git bitmaps used in `gix-pack`, `gix-index` and `gix-worktree`. |
| 2 | //! | 2 | //! |
| 3 | //! Note that many tests are performed indirectly by tests in the aforementioned consumer crates. | 3 | //! Note that many tests are performed indirectly by tests in the aforementioned consumer crates. |
| 4 | #![deny(rust_2018_idioms, unsafe_code)] | 4 | #![deny(rust_2018_idioms, unsafe_code, missing_docs)] |
| 5 | #![allow(missing_docs)] | ||
| 6 | 5 | ||
| 7 | /// Bitmap utilities for the advanced word-aligned hybrid bitmap | 6 | /// Bitmap utilities for the advanced word-aligned hybrid bitmap |
| 8 | pub mod ewah; | 7 | pub mod ewah; |
| @@ -1,5 +1,5 @@ | |||
| 1 | //! A crate with file-system specific utilities. | 1 | //! A crate with file-system specific utilities. |
| 2 | #![deny(rust_2018_idioms)] | 2 | #![deny(rust_2018_idioms, missing_docs)] |
| 3 | #![forbid(unsafe_code)] | 3 | #![forbid(unsafe_code)] |
| 4 | 4 | ||
| 5 | /// Common knowledge about the worktree that is needed across most interactions with the work tree | 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 | current_is_directory: bool, | 51 | current_is_directory: bool, |
| 52 | } | 52 | } |
| 53 | 53 | ||
| 54 | /// | ||
| 54 | pub mod stack; | 55 | pub mod stack; |
| @@ -1,16 +1,21 @@ | |||
| 1 | use std::{io, io::ErrorKind::AlreadyExists, path::Path}; | 1 | use std::{io, io::ErrorKind::AlreadyExists, path::Path}; |
| 2 | 2 | ||
| 3 | #[cfg(not(windows))] | 3 | #[cfg(not(windows))] |
| 4 | /// Create a new symlink at `link` which points to `original`. | ||
| 4 | pub fn create(original: &Path, link: &Path) -> io::Result<()> { | 5 | pub fn create(original: &Path, link: &Path) -> io::Result<()> { |
| 5 | std::os::unix::fs::symlink(original, link) | 6 | std::os::unix::fs::symlink(original, link) |
| 6 | } | 7 | } |
| 7 | 8 | ||
| 8 | #[cfg(not(windows))] | 9 | #[cfg(not(windows))] |
| 10 | /// Remove a symlink. | ||
| 11 | /// | ||
| 12 | /// Note that on only on windows this is special. | ||
| 9 | pub fn remove(path: &Path) -> io::Result<()> { | 13 | pub fn remove(path: &Path) -> io::Result<()> { |
| 10 | std::fs::remove_file(path) | 14 | std::fs::remove_file(path) |
| 11 | } | 15 | } |
| 12 | 16 | ||
| 13 | // TODO: use the `symlink` crate once it can delete directory symlinks | 17 | // TODO: use the `symlink` crate once it can delete directory symlinks |
| 18 | /// Remove a symlink. | ||
| 14 | #[cfg(windows)] | 19 | #[cfg(windows)] |
| 15 | pub fn remove(path: &Path) -> io::Result<()> { | 20 | pub fn remove(path: &Path) -> io::Result<()> { |
| 16 | if let Ok(meta) = std::fs::metadata(path) { | 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 | #[cfg(windows)] | 32 | #[cfg(windows)] |
| 33 | /// Create a new symlink at `link` which points to `original`. | ||
| 28 | pub fn create(original: &Path, link: &Path) -> io::Result<()> { | 34 | pub fn create(original: &Path, link: &Path) -> io::Result<()> { |
| 29 | use std::os::windows::fs::{symlink_dir, symlink_file}; | 35 | use std::os::windows::fs::{symlink_dir, symlink_file}; |
| 30 | // TODO: figure out if links to links count as files or whatever they point at | 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 | #[cfg(not(windows))] | 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 | pub fn is_collision_error(err: &std::io::Error) -> bool { | 47 | pub fn is_collision_error(err: &std::io::Error) -> bool { |
| 40 | // TODO: use ::IsDirectory as well when stabilized instead of raw_os_error(), and ::FileSystemLoop respectively | 48 | // TODO: use ::IsDirectory as well when stabilized instead of raw_os_error(), and ::FileSystemLoop respectively |
| 41 | err.kind() == AlreadyExists | 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 | #[cfg(windows)] | 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 | pub fn is_collision_error(err: &std::io::Error) -> bool { | 58 | pub fn is_collision_error(err: &std::io::Error) -> bool { |
| 49 | err.kind() == AlreadyExists || err.kind() == std::io::ErrorKind::PermissionDenied | 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 | /// Join `path` to `base` such that they are separated with a `/`, i.e. `base/path`. | 85 | /// Join `path` to `base` such that they are separated with a `/`, i.e. `base/path`. |
| 86 | pub fn join_bstr_unix_pathsep<'a, 'b>(base: impl Into<Cow<'a, BStr>>, path: impl Into<&'b BStr>) -> Cow<'a, BStr> { | 86 | pub fn join_bstr_unix_pathsep<'a, 'b>(base: impl Into<Cow<'a, BStr>>, path: impl Into<&'b BStr>) -> Cow<'a, BStr> { |
| 87 | let mut base = base.into(); | 87 | let mut base = base.into(); |
| 88 | if base.last() != Some(&b'/') { | 88 | if !base.is_empty() && base.last() != Some(&b'/') { |
| 89 | base.to_mut().push(b'/'); | 89 | base.to_mut().push(b'/'); |
| 90 | } | 90 | } |
| 91 | base.to_mut().extend_from_slice(path.into()); | 91 | base.to_mut().extend_from_slice(path.into()); |
| @@ -56,9 +56,9 @@ mod join_bstr_unix_pathsep { · 55 unmodified lines | |||
| 56 | assert_eq!(join_bstr_unix_pathsep(b("base/"), ""), b("base/")); | 56 | assert_eq!(join_bstr_unix_pathsep(b("base/"), ""), b("base/")); |
| 57 | } | 57 | } |
| 58 | #[test] | 58 | #[test] |
| 59 | fn empty_base_creates_absolute_paths() { | 59 | fn empty_base_leaves_everything_untouched() { |
| 60 | assert_eq!(join_bstr_unix_pathsep(b(""), ""), b("/")); | 60 | assert_eq!(join_bstr_unix_pathsep(b(""), ""), b("")); |
| 61 | assert_eq!(join_bstr_unix_pathsep(b(""), "hi"), b("/hi")); | 61 | assert_eq!(join_bstr_unix_pathsep(b(""), "hi"), b("hi")); |
| 62 | 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 | //! A crate with utilities that don't need feature toggles. | 1 | //! A crate with utilities that don't need feature toggles. |
| 2 | //! | 2 | //! |
| 3 | //! If they would need feature toggles, they should be in `gix-features` instead. | 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 | #![forbid(unsafe_code)] | 5 | #![forbid(unsafe_code)] |
| 6 | 6 | ||
| 7 | /// | 7 | /// |
| @@ -1,12 +1,30 @@ | |||
| 1 | use crate::cache::State; | 1 | use crate::cache::State; |
| 2 | use crate::PathIdMapping; | 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 | pub state: &'a mut State, | 21 | pub state: &'a mut State, |
| 6 | pub buf: &'a mut Vec<u8>, | 22 | pub buf: &'a mut Vec<u8>, |
| 7 | pub is_dir: bool, | 23 | pub is_dir: bool, |
| 8 | pub id_mappings: &'a Vec<PathIdMapping>, | 24 | pub id_mappings: &'a Vec<PathIdMapping>, |
| 9 | pub find: Find, | 25 | pub find: Find, |
| 26 | pub case: gix_glob::pattern::Case, | ||
| 27 | pub statistics: &'a mut super::Statistics, | ||
| 10 | } | 28 | } |
| 11 | 29 | ||
| 12 | impl<'a, Find, E> gix_fs::stack::Delegate for StackDelegate<'a, Find> | 30 | impl<'a, Find, E> gix_fs::stack::Delegate for StackDelegate<'a, Find> |
| @@ -15,72 +33,86 @@ where · 2 unmodified lines | |||
| 15 | E: std::error::Error + Send + Sync + 'static, | 33 | E: std::error::Error + Send + Sync + 'static, |
| 16 | { | 34 | { |
| 17 | fn push_directory(&mut self, stack: &gix_fs::Stack) -> std::io::Result<()> { | 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 | match &mut self.state { | 49 | match &mut self.state { |
| 19 | State::CreateDirectoryAndAttributesStack { attributes, .. } => { | 50 | State::CreateDirectoryAndAttributesStack { attributes, .. } => { |
| 20 | attributes.push_directory( | 51 | attributes.push_directory( |
| 21 | stack.root(), | 52 | stack.root(), |
| 22 | stack.current(), | 53 | stack.current(), |
| 54 | rela_dir, | ||
| 23 | self.buf, | 55 | self.buf, |
| 24 | self.id_mappings, | 56 | self.id_mappings, |
| 25 | &mut self.find, | 57 | &mut self.find, |
| 58 | &mut self.statistics.attributes, | ||
| 26 | )?; | 59 | )?; |
| 27 | } | 60 | } |
| 28 | State::AttributesAndIgnoreStack { ignore, attributes } => { | 61 | State::AttributesAndIgnoreStack { ignore, attributes } => { |
| 29 | attributes.push_directory( | 62 | attributes.push_directory( |
| 30 | stack.root(), | 63 | stack.root(), |
| 31 | stack.current(), | 64 | stack.current(), |
| 65 | rela_dir, | ||
| 32 | self.buf, | 66 | self.buf, |
| 33 | self.id_mappings, | 67 | self.id_mappings, |
| 34 | &mut self.find, | 68 | &mut self.find, |
| 69 | &mut self.statistics.attributes, | ||
| 35 | )?; | 70 | )?; |
| 36 | ignore.push_directory( | 71 | ignore.push_directory( |
| 37 | stack.root(), | 72 | stack.root(), |
| 38 | stack.current(), | 73 | stack.current(), |
| 74 | rela_dir, | ||
| 39 | self.buf, | 75 | self.buf, |
| 40 | self.id_mappings, | 76 | self.id_mappings, |
| 41 | &mut self.find, | 77 | &mut self.find, |
| 78 | self.case, | ||
| 79 | &mut self.statistics.ignore, | ||
| 42 | )? | 80 | )? |
| 43 | } | 81 | } |
| 44 | State::IgnoreStack(ignore) => ignore.push_directory( | 82 | State::IgnoreStack(ignore) => ignore.push_directory( |
| 45 | stack.root(), | 83 | stack.root(), |
| 46 | stack.current(), | 84 | stack.current(), |
| 85 | rela_dir, | ||
| 47 | self.buf, | 86 | self.buf, |
| 48 | self.id_mappings, | 87 | self.id_mappings, |
| 49 | &mut self.find, | 88 | &mut self.find, |
| 89 | self.case, | ||
| 90 | &mut self.statistics.ignore, | ||
| 50 | )?, | 91 | )?, |
| 51 | } | 92 | } |
| 52 | Ok(()) | 93 | Ok(()) |
| 53 | } | 94 | } |
| 54 | 95 | ||
| 55 | fn push(&mut self, is_last_component: bool, stack: &gix_fs::Stack) -> std::io::Result<()> { | 96 | fn push(&mut self, is_last_component: bool, stack: &gix_fs::Stack) -> std::io::Result<()> { |
| 97 | self.statistics.delegate.push_element += 1; | ||
| 56 | match &mut self.state { | 98 | match &mut self.state { |
| 57 | State::CreateDirectoryAndAttributesStack { | 99 | State::CreateDirectoryAndAttributesStack { |
| 58 | #[cfg(debug_assertions)] | ||
| 59 | test_mkdir_calls, | ||
| 60 | unlink_on_collision, | 100 | unlink_on_collision, |
| 61 | attributes: _, | 101 | attributes: _, |
| 62 | } => { | 102 | } => create_leading_directory( |
| 63 | #[cfg(debug_assertions)] | 103 | is_last_component, |
| 64 | { | 104 | stack, |
| 65 | create_leading_directory( | 105 | self.is_dir, |
| 66 | is_last_component, | 106 | &mut self.statistics.delegate.num_mkdir_calls, |
| 67 | stack, | 107 | *unlink_on_collision, |
| 68 | self.is_dir, | 108 | )?, |
| 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 | } | ||
| 78 | State::AttributesAndIgnoreStack { .. } | State::IgnoreStack(_) => {} | 109 | State::AttributesAndIgnoreStack { .. } | State::IgnoreStack(_) => {} |
| 79 | } | 110 | } |
| 80 | Ok(()) | 111 | Ok(()) |
| 81 | } | 112 | } |
| 82 | 113 | ||
| 83 | fn pop_directory(&mut self) { | 114 | fn pop_directory(&mut self) { |
| 115 | self.statistics.delegate.pop_directory += 1; | ||
| 84 | match &mut self.state { | 116 | match &mut self.state { |
| 85 | State::CreateDirectoryAndAttributesStack { attributes, .. } => { | 117 | State::CreateDirectoryAndAttributesStack { attributes, .. } => { |
| 86 | attributes.pop_directory(); | 118 | attributes.pop_directory(); |
| @@ -100,16 +132,13 @@ fn create_leading_directory( · 13 unmodified lines | |||
| 100 | is_last_component: bool, | 132 | is_last_component: bool, |
| 101 | stack: &gix_fs::Stack, | 133 | stack: &gix_fs::Stack, |
| 102 | is_dir: bool, | 134 | is_dir: bool, |
| 103 | #[cfg(debug_assertions)] mkdir_calls: &mut usize, | 135 | mkdir_calls: &mut usize, |
| 104 | unlink_on_collision: bool, | 136 | unlink_on_collision: bool, |
| 105 | ) -> std::io::Result<()> { | 137 | ) -> std::io::Result<()> { |
| 106 | if is_last_component && !is_dir { | 138 | if is_last_component && !is_dir { |
| 107 | return Ok(()); | 139 | return Ok(()); |
| 108 | } | 140 | } |
| 109 | #[cfg(debug_assertions)] | 141 | *mkdir_calls += 1; |
| 110 | { | ||
| 111 | *mkdir_calls += 1; | ||
| 112 | } | ||
| 113 | match std::fs::create_dir(stack.current()) { | 142 | match std::fs::create_dir(stack.current()) { |
| 114 | Ok(()) => Ok(()), | 143 | Ok(()) => Ok(()), |
| 115 | Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { | 144 | Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { |
| @@ -122,10 +151,7 @@ fn create_leading_directory( · 6 unmodified lines | |||
| 122 | } else { | 151 | } else { |
| 123 | std::fs::remove_file(stack.current())?; | 152 | std::fs::remove_file(stack.current())?; |
| 124 | } | 153 | } |
| 125 | #[cfg(debug_assertions)] | 154 | *mkdir_calls += 1; |
| 126 | { | ||
| 127 | *mkdir_calls += 1; | ||
| 128 | } | ||
| 129 | std::fs::create_dir(stack.current()) | 155 | std::fs::create_dir(stack.current()) |
| 130 | } else { | 156 | } else { |
| 131 | Err(err) | 157 | Err(err) |
| @@ -7,16 +7,26 @@ use gix_hash::oid; · 6 unmodified lines | |||
| 7 | use super::Cache; | 7 | use super::Cache; |
| 8 | use crate::PathIdMapping; | 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 | #[derive(Clone)] | 24 | #[derive(Clone)] |
| 11 | pub enum State { | 25 | pub enum State { |
| 12 | /// Useful for checkout where directories need creation, but we need to access attributes as well. | 26 | /// Useful for checkout where directories need creation, but we need to access attributes as well. |
| 13 | CreateDirectoryAndAttributesStack { | 27 | CreateDirectoryAndAttributesStack { |
| 14 | /// If there is a symlink or a file in our path, try to unlink it before creating the directory. | 28 | /// If there is a symlink or a file in our path, try to unlink it before creating the directory. |
| 15 | unlink_on_collision: bool, | 29 | unlink_on_collision: bool, |
| 16 | |||
| 17 | /// just for testing | ||
| 18 | #[cfg(debug_assertions)] | ||
| 19 | test_mkdir_calls: usize, | ||
| 20 | /// State to handle attribute information | 30 | /// State to handle attribute information |
| 21 | attributes: state::Attributes, | 31 | attributes: state::Attributes, |
| 22 | }, | 32 | }, |
| @@ -31,35 +41,6 @@ pub enum State { · 8 unmodified lines | |||
| 31 | IgnoreStack(state::Ignore), | 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 | #[must_use] | 44 | #[must_use] |
| 64 | pub struct Platform<'a> { | 45 | pub struct Platform<'a> { |
| 65 | parent: &'a Cache, | 46 | parent: &'a Cache, |
| @@ -87,11 +68,12 @@ impl Cache { · 21 unmodified lines | |||
| 87 | case, | 68 | case, |
| 88 | buf, | 69 | buf, |
| 89 | id_mappings, | 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 | impl Cache { | 77 | impl Cache { |
| 96 | /// Append the `relative` path to the root directory of the cache and efficiently create leading directories, while assuring that no | 78 | /// Append the `relative` path to the root directory of the cache and efficiently create leading directories, while assuring that no |
| 97 | /// symlinks are in that path. | 79 | /// symlinks are in that path. |
| @@ -110,12 +92,15 @@ impl Cache { · 12 unmodified lines | |||
| 110 | Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E>, | 92 | Find: for<'a> FnMut(&oid, &'a mut Vec<u8>) -> Result<gix_object::BlobRef<'a>, E>, |
| 111 | E: std::error::Error + Send + Sync + 'static, | 93 | E: std::error::Error + Send + Sync + 'static, |
| 112 | { | 94 | { |
| 95 | self.statistics.platforms += 1; | ||
| 113 | let mut delegate = StackDelegate { | 96 | let mut delegate = StackDelegate { |
| 114 | state: &mut self.state, | 97 | state: &mut self.state, |
| 115 | buf: &mut self.buf, | 98 | buf: &mut self.buf, |
| 116 | is_dir: is_dir.unwrap_or(false), | 99 | is_dir: is_dir.unwrap_or(false), |
| 117 | id_mappings: &self.id_mappings, | 100 | id_mappings: &self.id_mappings, |
| 118 | find, | 101 | find, |
| 102 | case: self.case, | ||
| 103 | statistics: &mut self.statistics, | ||
| 119 | }; | 104 | }; |
| 120 | self.stack.make_relative_path_current(relative, &mut delegate)?; | 105 | self.stack.make_relative_path_current(relative, &mut delegate)?; |
| 121 | Ok(Platform { parent: self, is_dir }) | 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 | /// Access | 153 | /// Access |
| 156 | impl Cache { | 154 | impl Cache { |
| 155 | /// Return the statistics we gathered thus far. | ||
| 156 | pub fn statistics(&self) -> &Statistics { | ||
| 157 | &self.statistics | ||
| 158 | } | ||
| 157 | /// Return the state for introspection. | 159 | /// Return the state for introspection. |
| 158 | pub fn state(&self) -> &State { | 160 | pub fn state(&self) -> &State { |
| 159 | &self.state | 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 | use delegate::StackDelegate; | 174 | use delegate::StackDelegate; |
| 172 | 175 | ||
| 173 | mod platform; | 176 | mod platform; |
| @@ -6,6 +6,18 @@ use gix_glob::pattern::Case; · 5 unmodified lines | |||
| 6 | use crate::cache::state::{AttributeMatchGroup, Attributes}; | 6 | use crate::cache::state::{AttributeMatchGroup, Attributes}; |
| 7 | use bstr::{BStr, ByteSlice}; | 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 | /// Decide where to read `.gitattributes` files from. | 21 | /// Decide where to read `.gitattributes` files from. |
| 10 | #[derive(Default, Debug, Clone, Copy)] | 22 | #[derive(Default, Debug, Clone, Copy)] |
| 11 | pub enum Source { | 23 | pub enum Source { |
| @@ -42,7 +54,6 @@ impl Attributes { · 30 unmodified lines | |||
| 42 | pub fn new( | 54 | pub fn new( |
| 43 | globals: AttributeMatchGroup, | 55 | globals: AttributeMatchGroup, |
| 44 | info_attributes: Option<PathBuf>, | 56 | info_attributes: Option<PathBuf>, |
| 45 | case: Case, | ||
| 46 | source: Source, | 57 | source: Source, |
| 47 | collection: gix_attributes::search::MetadataCollection, | 58 | collection: gix_attributes::search::MetadataCollection, |
| 48 | ) -> Self { | 59 | ) -> Self { |
| @@ -50,7 +61,6 @@ impl Attributes { · 1 unmodified line | |||
| 50 | globals, | 61 | globals, |
| 51 | stack: Default::default(), | 62 | stack: Default::default(), |
| 52 | info_attributes, | 63 | info_attributes, |
| 53 | case, | ||
| 54 | source, | 64 | source, |
| 55 | collection, | 65 | collection, |
| 56 | } | 66 | } |
| @@ -62,31 +72,21 @@ impl Attributes { · 5 unmodified lines | |||
| 62 | self.stack.pop_pattern_list().expect("something to pop"); | 72 | self.stack.pop_pattern_list().expect("something to pop"); |
| 63 | } | 73 | } |
| 64 | 74 | ||
| 75 | #[allow(clippy::too_many_arguments)] | ||
| 65 | pub(crate) fn push_directory<Find, E>( | 76 | pub(crate) fn push_directory<Find, E>( |
| 66 | &mut self, | 77 | &mut self, |
| 67 | root: &Path, | 78 | root: &Path, |
| 68 | dir: &Path, | 79 | dir: &Path, |
| 80 | rela_dir: &BStr, | ||
| 69 | buf: &mut Vec<u8>, | 81 | buf: &mut Vec<u8>, |
| 70 | id_mappings: &[PathIdMapping], | 82 | id_mappings: &[PathIdMapping], |
| 71 | mut find: Find, | 83 | mut find: Find, |
| 84 | stats: &mut Statistics, | ||
| 72 | ) -> std::io::Result<()> | 85 | ) -> std::io::Result<()> |
| 73 | where | 86 | where |
| 74 | Find: for<'b> FnMut(&gix_hash::oid, &'b mut Vec<u8>) -> Result<gix_object::BlobRef<'b>, E>, | 87 | Find: for<'b> FnMut(&gix_hash::oid, &'b mut Vec<u8>) -> Result<gix_object::BlobRef<'b>, E>, |
| 75 | E: std::error::Error + Send + Sync + 'static, | 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 | let attr_path_relative = | 90 | let attr_path_relative = |
| 91 | gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitattributes")); | 91 | gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitattributes")); |
| 92 | let attr_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(attr_path_relative.as_ref())); | 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 | self.stack | 103 | self.stack |
| 104 | .add_patterns_buffer(blob.data, attr_path, Some(Path::new("")), &mut self.collection); | 104 | .add_patterns_buffer(blob.data, attr_path, Some(Path::new("")), &mut self.collection); |
| 105 | added = true; | 105 | added = true; |
| 106 | stats.patterns_buffers += 1; | ||
| 106 | } | 107 | } |
| 107 | if !added && matches!(self.source, Source::IdMappingThenWorktree) { | 108 | if !added && matches!(self.source, Source::IdMappingThenWorktree) { |
| 108 | added = self.stack.add_patterns_file( | 109 | added = self.stack.add_patterns_file( |
| @@ -112,6 +113,8 @@ impl Attributes { · 3 unmodified lines | |||
| 112 | buf, | 113 | buf, |
| 113 | &mut self.collection, | 114 | &mut self.collection, |
| 114 | )?; | 115 | )?; |
| 116 | stats.pattern_files += usize::from(added); | ||
| 117 | stats.tried_pattern_files += 1; | ||
| 115 | } | 118 | } |
| 116 | } | 119 | } |
| 117 | Source::WorktreeThenIdMapping => { | 120 | Source::WorktreeThenIdMapping => { |
| @@ -122,6 +125,8 @@ impl Attributes { · 4 unmodified lines | |||
| 122 | buf, | 125 | buf, |
| 123 | &mut self.collection, | 126 | &mut self.collection, |
| 124 | )?; | 127 | )?; |
| 128 | stats.pattern_files += usize::from(added); | ||
| 129 | stats.tried_pattern_files += 1; | ||
| 125 | if let Some(idx) = attr_file_in_index.ok().filter(|_| !added) { | 130 | if let Some(idx) = attr_file_in_index.ok().filter(|_| !added) { |
| 126 | let blob = find(&id_mappings[idx].1, buf) | 131 | let blob = find(&id_mappings[idx].1, buf) |
| 127 | .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; | 132 | .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; |
| @@ -129,6 +134,7 @@ impl Attributes { · 1 unmodified line | |||
| 129 | self.stack | 134 | self.stack |
| 130 | .add_patterns_buffer(blob.data, attr_path, Some(Path::new("")), &mut self.collection); | 135 | .add_patterns_buffer(blob.data, attr_path, Some(Path::new("")), &mut self.collection); |
| 131 | added = true; | 136 | added = true; |
| 137 | stats.patterns_buffers += 1; | ||
| 132 | } | 138 | } |
| 133 | } | 139 | } |
| 134 | } | 140 | } |
| @@ -142,8 +148,11 @@ impl Attributes { · 7 unmodified lines | |||
| 142 | // When reading the root, always the first call, we can try to also read the `.git/info/attributes` file which is | 148 | // When reading the root, always the first call, we can try to also read the `.git/info/attributes` file which is |
| 143 | // by nature never popped, and follows the root, as global. | 149 | // by nature never popped, and follows the root, as global. |
| 144 | if let Some(info_attr) = self.info_attributes.take() { | 150 | if let Some(info_attr) = self.info_attributes.take() { |
| 145 | self.stack | 151 | let added = self |
| 152 | .stack | ||
| 146 | .add_patterns_file(info_attr, true, None, buf, &mut self.collection)?; | 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 | Ok(()) | 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 | /// Attribute matching specific methods | 180 | /// Attribute matching specific methods |
| 181 | impl Cache { | 181 | impl Cache { |
| 182 | /// Creates a new container to store match outcomes for all attribute matches. | 182 | /// Creates a new container to store match outcomes for all attribute matches. |
| @@ -1,45 +1,52 @@ | |||
| 1 | use std::path::Path; | 1 | use std::path::Path; |
| 2 | 2 | ||
| 3 | use crate::cache::state::Ignore; | ||
| 3 | use crate::{cache::state::IgnoreMatchGroup, PathIdMapping}; | 4 | use crate::{cache::state::IgnoreMatchGroup, PathIdMapping}; |
| 4 | use bstr::{BStr, BString, ByteSlice}; | 5 | use bstr::{BStr, ByteSlice}; |
| 5 | use gix_glob::pattern::Case; | 6 | use gix_glob::pattern::Case; |
| 6 | 7 | ||
| 7 | /// State related to the exclusion of files. | 8 | /// Decide where to read `.gitignore` files from. |
| 8 | #[derive(Default, Clone)] | 9 | #[derive(Default, Debug, Clone, Copy)] |
| 9 | #[allow(unused)] | 10 | pub enum Source { |
| 10 | pub struct Ignore { | 11 | /// Retrieve ignore files from id mappings, see |
| 11 | /// Ignore patterns passed as overrides to everything else, typically passed on the command-line and the first patterns to | 12 | /// [State::id_mappings_from_index()][crate::cache::State::id_mappings_from_index()]. |
| 12 | /// be consulted. | 13 | /// |
| 13 | overrides: IgnoreMatchGroup, | 14 | /// These mappings are typically produced from an index. |
| 14 | /// Ignore patterns that match the currently set director (in the stack), which is pushed and popped as needed. | 15 | /// If a tree should be the source, build an attribute list from a tree instead, or convert a tree to an index. |
| 15 | stack: IgnoreMatchGroup, | 16 | /// |
| 16 | /// Ignore patterns which aren't tied to the repository root, hence are global. They are consulted last. | 17 | /// Use this when no worktree checkout is available, like in bare repositories or when accessing blobs from other parts |
| 17 | globals: IgnoreMatchGroup, | 18 | /// of the history which aren't checked out. |
| 18 | /// A matching stack of pattern indices which is empty if we have just been initialized to indicate that the | 19 | IdMapping, |
| 19 | /// currently set directory had a pattern matched. Note that this one could be negated. | 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. |
| 20 | /// (index into match groups, index into list of pattern lists, index into pattern list) | 21 | #[default] |
| 21 | matched_directory_patterns_stack: Vec<Option<(usize, usize, usize)>>, | 22 | WorktreeThenIdMappingIfNotSkipped, |
| 22 | /// The name of the file to look for in directories. | 23 | } |
| 23 | pub(crate) exclude_file_name_for_directories: BString, | 24 | |
| 24 | /// The case to use when matching directories as they are pushed onto the stack. We run them against the exclude engine | 25 | /// Various aggregate numbers related [`Ignore`]. |
| 25 | /// to know if an entire path can be ignored as a parent directory is ignored. | 26 | #[derive(Default, Clone, Copy, Debug)] |
| 26 | pub(crate) case: Case, | 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 | impl Ignore { | 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 | /// The `exclude_file_name_for_directories` is an optional override for the filename to use when checking per-directory | 41 | /// The `exclude_file_name_for_directories` is an optional override for the filename to use when checking per-directory |
| 31 | /// ignore files within the repository, defaults to`.gitignore`. | 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 | pub fn new( | 43 | pub fn new( |
| 36 | overrides: IgnoreMatchGroup, | 44 | overrides: IgnoreMatchGroup, |
| 37 | globals: IgnoreMatchGroup, | 45 | globals: IgnoreMatchGroup, |
| 38 | exclude_file_name_for_directories: Option<&BStr>, | 46 | exclude_file_name_for_directories: Option<&BStr>, |
| 39 | case: Case, | 47 | source: Source, |
| 40 | ) -> Self { | 48 | ) -> Self { |
| 41 | Ignore { | 49 | Ignore { |
| 42 | case, | ||
| 43 | overrides, | 50 | overrides, |
| 44 | globals, | 51 | globals, |
| 45 | stack: Default::default(), | 52 | stack: Default::default(), |
| @@ -47,6 +54,7 @@ impl Ignore { · 1 unmodified line | |||
| 47 | exclude_file_name_for_directories: exclude_file_name_for_directories | 54 | exclude_file_name_for_directories: exclude_file_name_for_directories |
| 48 | .map(ToOwned::to_owned) | 55 | .map(ToOwned::to_owned) |
| 49 | .unwrap_or_else(|| ".gitignore".into()), | 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 | pub(crate) fn push_directory<Find, E>( | 147 | pub(crate) fn push_directory<Find, E>( |
| 139 | &mut self, | 148 | &mut self, |
| 140 | root: &Path, | 149 | root: &Path, |
| 141 | dir: &Path, | 150 | dir: &Path, |
| 151 | rela_dir: &BStr, | ||
| 142 | buf: &mut Vec<u8>, | 152 | buf: &mut Vec<u8>, |
| 143 | id_mappings: &[PathIdMapping], | 153 | id_mappings: &[PathIdMapping], |
| 144 | mut find: Find, | 154 | mut find: Find, |
| 155 | case: Case, | ||
| 156 | stats: &mut Statistics, | ||
| 145 | ) -> std::io::Result<()> | 157 | ) -> std::io::Result<()> |
| 146 | where | 158 | where |
| 147 | Find: for<'b> FnMut(&gix_hash::oid, &'b mut Vec<u8>) -> Result<gix_object::BlobRef<'b>, E>, | 159 | Find: for<'b> FnMut(&gix_hash::oid, &'b mut Vec<u8>) -> Result<gix_object::BlobRef<'b>, E>, |
| 148 | E: std::error::Error + Send + Sync + 'static, | 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 | self.matched_directory_patterns_stack | 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 | let ignore_path_relative = | 165 | let ignore_path_relative = |
| 166 | gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitignore")); | 166 | gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitignore")); |
| 167 | let ignore_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(ignore_path_relative.as_ref())); | 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(); | 168 | match self.source { |
| 169 | if !gix_glob::search::add_patterns_file( | 169 | Source::IdMapping => { |
| 170 | &mut self.stack.patterns, | 170 | match ignore_file_in_index { |
| 171 | dir.join(".gitignore"), | 171 | Ok(idx) => { |
| 172 | follow_symlinks, | 172 | let ignore_blob = find(&id_mappings[idx].1, buf) |
| 173 | Some(root), | 173 | .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; |
| 174 | buf, | 174 | let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned()); |
| 175 | )? { | 175 | self.stack |
| 176 | match ignore_file_in_index { | 176 | .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new(""))); |
| 177 | Ok(idx) => { | 177 | stats.patterns_buffers += 1; |
| 178 | let ignore_blob = find(&id_mappings[idx].1, buf) | 178 | } |
| 179 | .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; | 179 | Err(_) => { |
| 180 | let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned()); | 180 | // Need one stack level per component so push and pop matches. |
| 181 | self.stack | 181 | self.stack.patterns.push(Default::default()) |
| 182 | .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new(""))); | 182 | } |
| 183 | } | 183 | } |
| 184 | Err(_) => { | 184 | } |
| 185 | // Need one stack level per component so push and pop matches. | 185 | Source::WorktreeThenIdMappingIfNotSkipped => { |
| 186 | self.stack.patterns.push(Default::default()) | 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 | use std::path::PathBuf; | 1 | use std::path::PathBuf; |
| 2 | 2 | ||
| 3 | use bstr::ByteSlice; | 3 | use bstr::{BString, ByteSlice}; |
| 4 | use gix_glob::pattern::Case; | 4 | use gix_glob::pattern::Case; |
| 5 | 5 | ||
| 6 | use crate::{cache::State, PathIdMapping}; | 6 | use crate::{cache::State, PathIdMapping}; |
| @@ -23,17 +23,36 @@ pub struct Attributes { · 16 unmodified lines | |||
| 23 | info_attributes: Option<PathBuf>, | 23 | info_attributes: Option<PathBuf>, |
| 24 | /// A lookup table to accelerate searches. | 24 | /// A lookup table to accelerate searches. |
| 25 | collection: gix_attributes::search::MetadataCollection, | 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 | /// Where to read `.gitattributes` data from. | 26 | /// Where to read `.gitattributes` data from. |
| 30 | source: attributes::Source, | 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 | pub mod attributes; | 53 | pub mod attributes; |
| 35 | mod ignore; | 54 | /// |
| 36 | pub use ignore::Ignore; | 55 | pub mod ignore; |
| 37 | 56 | ||
| 38 | /// Initialization | 57 | /// Initialization |
| 39 | impl State { | 58 | impl State { |
| @@ -41,8 +60,6 @@ impl State { · 1 unmodified line | |||
| 41 | pub fn for_checkout(unlink_on_collision: bool, attributes: Attributes) -> Self { | 60 | pub fn for_checkout(unlink_on_collision: bool, attributes: Attributes) -> Self { |
| 42 | State::CreateDirectoryAndAttributesStack { | 61 | State::CreateDirectoryAndAttributesStack { |
| 43 | unlink_on_collision, | 62 | unlink_on_collision, |
| 44 | #[cfg(debug_assertions)] | ||
| 45 | test_mkdir_calls: 0, | ||
| 46 | attributes, | 63 | attributes, |
| 47 | } | 64 | } |
| 48 | } | 65 | } |
| @@ -76,6 +93,7 @@ impl State { · 27 unmodified lines | |||
| 76 | &self, | 93 | &self, |
| 77 | index: &gix_index::State, | 94 | index: &gix_index::State, |
| 78 | paths: &gix_index::PathStorageRef, | 95 | paths: &gix_index::PathStorageRef, |
| 96 | ignore_source: ignore::Source, | ||
| 79 | case: Case, | 97 | case: Case, |
| 80 | ) -> Vec<PathIdMapping> { | 98 | ) -> Vec<PathIdMapping> { |
| 81 | let a1_backing; | 99 | let a1_backing; |
| @@ -118,9 +136,16 @@ impl State { · 36 unmodified lines | |||
| 118 | } | 136 | } |
| 119 | .then_some(t.1) | 137 | .then_some(t.1) |
| 120 | })?; | 138 | })?; |
| 121 | // See https://github.com/git/git/blob/master/dir.c#L912:L912 | 139 | if is_ignore { |
| 122 | if is_ignore && !entry.flags.contains(gix_index::entry::Flags::SKIP_WORKTREE) { | 140 | match ignore_source { |
| 123 | return None; | 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 | Some((path.to_owned(), entry.id)) | 150 | Some((path.to_owned(), entry.id)) |
| 126 | } else { | 151 | } else { |
| @@ -57,8 +57,8 @@ where · 56 unmodified lines | |||
| 57 | None, | 57 | None, |
| 58 | ); | 58 | ); |
| 59 | 59 | ||
| 60 | let state = cache::State::for_checkout(options.overwrite_existing, options.attributes.clone().with_case(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, case); | 61 | let attribute_files = state.id_mappings_from_index(index, paths, Default::default(), case); |
| 62 | let mut ctx = chunk::Context { | 62 | let mut ctx = chunk::Context { |
| 63 | buf: Vec::new(), | 63 | buf: Vec::new(), |
| 64 | path_cache: Cache::new(dir, state, case, Vec::with_capacity(512), attribute_files), | 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 | case: gix_glob::pattern::Case, | 45 | case: gix_glob::pattern::Case, |
| 46 | /// A lookup table for object ids to read from in some situations when looking up attributes or exclusions. | 46 | /// A lookup table for object ids to read from in some situations when looking up attributes or exclusions. |
| 47 | id_mappings: Vec<PathIdMapping>, | 47 | id_mappings: Vec<PathIdMapping>, |
| 48 | statistics: cache::Statistics, | ||
| 48 | } | 49 | } |
| 49 | 50 | ||
| 50 | pub(crate) type PathIdMapping = (BString, gix_hash::ObjectId); | 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 | other-dir-with-ignore/other-sub-level-dir-anywhere/ | 111 | other-dir-with-ignore/other-sub-level-dir-anywhere/ |
| 112 | dir-with-ignore/negated | 112 | dir-with-ignore/negated |
| 113 | dir-with-ignore/negated-dir/hello | 113 | dir-with-ignore/negated-dir/hello |
| 114 | User-file-ANYWHERE | ||
| 115 | User-Dir-ANYWHERE | ||
| 114 | a/b/C | 116 | a/b/C |
| 115 | a/B/c | 117 | a/B/c |
| 116 | A/B/C | 118 | A/B/C |
| @@ -24,7 +24,6 @@ fn baseline() -> crate::Result { · 23 unmodified lines | |||
| 24 | state::Attributes::new( | 24 | state::Attributes::new( |
| 25 | gix_attributes::Search::new_globals([base.join("user.attributes")], &mut buf, &mut collection)?, | 25 | gix_attributes::Search::new_globals([base.join("user.attributes")], &mut buf, &mut collection)?, |
| 26 | Some(git_dir.join("info").join("attributes")), | 26 | Some(git_dir.join("info").join("attributes")), |
| 27 | case, | ||
| 28 | gix_worktree::cache::state::attributes::Source::WorktreeThenIdMapping, | 27 | gix_worktree::cache::state::attributes::Source::WorktreeThenIdMapping, |
| 29 | collection, | 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 | Vec::new(), | 17 | Vec::new(), |
| 18 | Default::default(), | 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 | let path = cache.at_path("hello", Some(false), panic_on_find)?.path(); | 22 | let path = cache.at_path("hello", Some(false), panic_on_find)?.path(); |
| 23 | assert!(!path.parent().unwrap().exists(), "prefix itself is never created"); | 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 | Ok(()) | 25 | Ok(()) |
| 26 | } | 26 | } |
| 27 | 27 | ||
| @@ -43,7 +43,7 @@ fn directory_paths_are_created_in_full() { · 15 unmodified lines | |||
| 43 | assert!(path.parent().unwrap().is_dir(), "dir exists"); | 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 | #[test] | 49 | #[test] |
| @@ -54,7 +54,7 @@ fn existing_directories_are_fine() -> crate::Result { · 4 unmodified lines | |||
| 54 | let path = cache.at_path("dir/file", Some(false), panic_on_find)?.path(); | 54 | let path = cache.at_path("dir/file", Some(false), panic_on_find)?.path(); |
| 55 | assert!(path.parent().unwrap().is_dir(), "directory is still present"); | 55 | assert!(path.parent().unwrap().is_dir(), "directory is still present"); |
| 56 | assert!(!path.exists(), "it won't create the file"); | 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 | Ok(()) | 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 | std::fs::write(tmp.path().join("file-in-dir"), &[])?; | 67 | std::fs::write(tmp.path().join("file-in-dir"), &[])?; |
| 68 | 68 | ||
| 69 | for dirname in &["file-in-dir", "link-to-dir"] { | 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 | let relative_path = format!("{}/file", dirname); | 76 | let relative_path = format!("{}/file", dirname); |
| 72 | assert_eq!( | 77 | assert_eq!( |
| 73 | cache | 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 | assert_eq!( | 85 | assert_eq!( |
| 81 | cache.num_mkdir_calls(), | 86 | cache.statistics().delegate.num_mkdir_calls, |
| 82 | 2, | 87 | 2, |
| 83 | "it tries to create each directory once, but it's a file" | 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 | for dirname in &["link-to-dir", "file-in-dir"] { | 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 | let relative_path = format!("{}/file", dirname); | 98 | let relative_path = format!("{}/file", dirname); |
| 89 | let path = cache.at_path(&relative_path, Some(false), panic_on_find)?.path(); | 99 | let path = cache.at_path(&relative_path, Some(false), panic_on_find)?.path(); |
| 90 | assert!(path.parent().unwrap().is_dir(), "directory was forcefully created"); | 100 | assert!(path.parent().unwrap().is_dir(), "directory was forcefully created"); |
| 91 | assert!(!path.exists()); | 101 | assert!(!path.exists()); |
| 92 | } | 102 | } |
| 93 | assert_eq!( | 103 | assert_eq!( |
| 94 | cache.num_mkdir_calls(), | 104 | cache.statistics().delegate.num_mkdir_calls, |
| 95 | 4, | 105 | 4, |
| 96 | "like before, but it unlinks what's there and tries again" | 106 | "like before, but it unlinks what's there and tries again" |
| 97 | ); | 107 | ); |
| @@ -1,6 +1,7 @@ | |||
| 1 | use bstr::{BStr, ByteSlice}; | 1 | use bstr::{BStr, ByteSlice}; |
| 2 | use gix_glob::pattern::Case; | 2 | use gix_glob::pattern::Case; |
| 3 | use gix_odb::FindExt; | 3 | use gix_odb::FindExt; |
| 4 | use gix_worktree::cache::state::ignore::Source; | ||
| 4 | use gix_worktree::Cache; | 5 | use gix_worktree::Cache; |
| 5 | 6 | ||
| 6 | use crate::hex_to_id; | 7 | use crate::hex_to_id; |
| @@ -43,7 +44,7 @@ fn special_exclude_cases_we_handle_differently() { · 36 unmodified lines | |||
| 43 | Default::default(), | 44 | Default::default(), |
| 44 | gix_ignore::Search::from_git_dir(&git_dir, None, &mut buf).unwrap(), | 45 | gix_ignore::Search::from_git_dir(&git_dir, None, &mut buf).unwrap(), |
| 45 | None, | 46 | None, |
| 46 | case, | 47 | Source::WorktreeThenIdMappingIfNotSkipped, |
| 47 | ), | 48 | ), |
| 48 | ); | 49 | ); |
| 49 | let mut cache = Cache::new(&dir, state, case, buf, Default::default()); | 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 | gix_ignore::Search::from_overrides(vec!["!force-include"]), | 105 | gix_ignore::Search::from_overrides(vec!["!force-include"]), |
| 105 | gix_ignore::Search::from_git_dir(&git_dir, Some(user_exclude_path), &mut buf)?, | 106 | gix_ignore::Search::from_git_dir(&git_dir, Some(user_exclude_path), &mut buf)?, |
| 106 | None, | 107 | None, |
| 107 | case, | 108 | Source::WorktreeThenIdMappingIfNotSkipped, |
| 108 | ), | 109 | ), |
| 109 | ); | 110 | ); |
| 110 | let paths_storage = index.take_path_backing(); | 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 | assert_eq!( | 113 | assert_eq!( |
| 113 | attribute_files_in_index, | 114 | attribute_files_in_index, |
| 114 | vec![( | 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 | Ok(()) | 160 | Ok(()) |
| 170 | } | 161 | } |
| @@ -67,6 +67,7 @@ serde = [ "dep:serde", · 66 unmodified lines | |||
| 67 | "gix-attributes/serde", | 67 | "gix-attributes/serde", |
| 68 | "gix-ignore/serde", | 68 | "gix-ignore/serde", |
| 69 | "gix-revision/serde", | 69 | "gix-revision/serde", |
| 70 | "gix-worktree/serde", | ||
| 70 | "gix-credentials/serde"] | 71 | "gix-credentials/serde"] |
| 71 | 72 | ||
| 72 | ## Re-export the progress tree root which allows to obtain progress from various functions which take `impl gix::Progress`. | 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 | use std::{borrow::Cow, path::PathBuf, time::Duration}; | 2 | use std::{borrow::Cow, path::PathBuf, time::Duration}; |
| 3 | 3 | ||
| 4 | use gix_attributes::Source; | 4 | use gix_attributes::Source; |
| 5 | use gix_glob::pattern::Case; | ||
| 6 | use gix_lock::acquire::Fail; | 5 | use gix_lock::acquire::Fail; |
| 7 | 6 | ||
| 8 | use crate::{ | 7 | use crate::{ |
| @@ -167,18 +166,14 @@ impl Cache { · 158 unmodified lines | |||
| 167 | executable_bit: boolean(self, "core.fileMode", &Core::FILE_MODE, true)?, | 166 | executable_bit: boolean(self, "core.fileMode", &Core::FILE_MODE, true)?, |
| 168 | symlink: boolean(self, "core.symlinks", &Core::SYMLINKS, true)?, | 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 | Ok(gix_worktree::checkout::Options { | 169 | Ok(gix_worktree::checkout::Options { |
| 176 | attributes: self.assemble_attribute_globals( | 170 | attributes: self |
| 177 | git_dir, | 171 | .assemble_attribute_globals( |
| 178 | case, | 172 | git_dir, |
| 179 | gix_worktree::cache::state::attributes::Source::IdMappingThenWorktree, | 173 | gix_worktree::cache::state::attributes::Source::IdMappingThenWorktree, |
| 180 | self.attributes, | 174 | self.attributes, |
| 181 | )?, | 175 | )? |
| 176 | .0, | ||
| 182 | fs: capabilities, | 177 | fs: capabilities, |
| 183 | thread_limit, | 178 | thread_limit, |
| 184 | destination_is_initially_empty: false, | 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 | // TODO: at least one test, maybe related to core.attributesFile configuration. | 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 | &self, | 217 | &self, |
| 205 | git_dir: &std::path::Path, | 218 | git_dir: &std::path::Path, |
| 206 | case: gix_glob::pattern::Case, | ||
| 207 | source: gix_worktree::cache::state::attributes::Source, | 219 | source: gix_worktree::cache::state::attributes::Source, |
| 208 | attributes: crate::permissions::Attributes, | 220 | attributes: crate::open::permissions::Attributes, |
| 209 | ) -> Result<gix_worktree::cache::state::Attributes, config::attribute_stack::Error> { | 221 | ) -> Result<(gix_worktree::cache::state::Attributes, Vec<u8>), config::attribute_stack::Error> { |
| 210 | let configured_or_user_attributes = match self | 222 | let configured_or_user_attributes = match self |
| 211 | .trusted_file_path("core", None, Core::ATTRIBUTES_FILE.name) | 223 | .trusted_file_path("core", None, Core::ATTRIBUTES_FILE.name) |
| 212 | .transpose()? | 224 | .transpose()? |
| @@ -232,13 +244,13 @@ impl Cache { · 19 unmodified lines | |||
| 232 | let info_attributes_path = git_dir.join("info").join("attributes"); | 244 | let info_attributes_path = git_dir.join("info").join("attributes"); |
| 233 | let mut buf = Vec::new(); | 245 | let mut buf = Vec::new(); |
| 234 | let mut collection = gix_attributes::search::MetadataCollection::default(); | 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 | gix_attributes::Search::new_globals(attribute_files, &mut buf, &mut collection)?, | 248 | gix_attributes::Search::new_globals(attribute_files, &mut buf, &mut collection)?, |
| 237 | Some(info_attributes_path), | 249 | Some(info_attributes_path), |
| 238 | case, | ||
| 239 | source, | 250 | source, |
| 240 | collection, | 251 | collection, |
| 241 | )) | 252 | ); |
| 253 | Ok((res, buf)) | ||
| 242 | } | 254 | } |
| 243 | 255 | ||
| 244 | pub(crate) fn xdg_config_path( | 256 | pub(crate) fn xdg_config_path( |
| @@ -12,7 +12,7 @@ use crate::{ · 11 unmodified lines | |||
| 12 | tree::{gitoxide, Core, Http}, | 12 | tree::{gitoxide, Core, Http}, |
| 13 | Cache, | 13 | Cache, |
| 14 | }, | 14 | }, |
| 15 | repository, | 15 | open, |
| 16 | }; | 16 | }; |
| 17 | 17 | ||
| 18 | /// Initialization | 18 | /// Initialization |
| @@ -32,7 +32,7 @@ impl Cache { · 13 unmodified lines | |||
| 32 | filter_config_section: fn(&gix_config::file::Metadata) -> bool, | 32 | filter_config_section: fn(&gix_config::file::Metadata) -> bool, |
| 33 | git_install_dir: Option<&std::path::Path>, | 33 | git_install_dir: Option<&std::path::Path>, |
| 34 | home: Option<&std::path::Path>, | 34 | home: Option<&std::path::Path>, |
| 35 | environment @ repository::permissions::Environment { | 35 | environment @ open::permissions::Environment { |
| 36 | git_prefix, | 36 | git_prefix, |
| 37 | ssh_prefix: _, | 37 | ssh_prefix: _, |
| 38 | xdg_config_home: _, | 38 | xdg_config_home: _, |
| @@ -40,16 +40,16 @@ impl Cache { · 1 unmodified line | |||
| 40 | http_transport, | 40 | http_transport, |
| 41 | identity, | 41 | identity, |
| 42 | objects, | 42 | objects, |
| 43 | }: repository::permissions::Environment, | 43 | }: open::permissions::Environment, |
| 44 | attributes: repository::permissions::Attributes, | 44 | attributes: open::permissions::Attributes, |
| 45 | repository::permissions::Config { | 45 | open::permissions::Config { |
| 46 | git_binary: use_installation, | 46 | git_binary: use_installation, |
| 47 | system: use_system, | 47 | system: use_system, |
| 48 | git: use_git, | 48 | git: use_git, |
| 49 | user: use_user, | 49 | user: use_user, |
| 50 | env: use_env, | 50 | env: use_env, |
| 51 | includes: use_includes, | 51 | includes: use_includes, |
| 52 | }: repository::permissions::Config, | 52 | }: open::permissions::Config, |
| 53 | lenient_config: bool, | 53 | lenient_config: bool, |
| 54 | api_config_overrides: &[BString], | 54 | api_config_overrides: &[BString], |
| 55 | cli_config_overrides: &[BString], | 55 | cli_config_overrides: &[BString], |
| @@ -233,12 +233,12 @@ impl Cache { · 177 unmodified lines | |||
| 233 | } | 233 | } |
| 234 | 234 | ||
| 235 | pub(crate) fn make_source_env( | 235 | pub(crate) fn make_source_env( |
| 236 | crate::permissions::Environment { | 236 | crate::open::permissions::Environment { |
| 237 | xdg_config_home, | 237 | xdg_config_home, |
| 238 | git_prefix, | 238 | git_prefix, |
| 239 | home, | 239 | home, |
| 240 | .. | 240 | .. |
| 241 | }: crate::permissions::Environment, | 241 | }: open::permissions::Environment, |
| 242 | ) -> impl FnMut(&str) -> Option<OsString> { | 242 | ) -> impl FnMut(&str) -> Option<OsString> { |
| 243 | move |name| { | 243 | move |name| { |
| 244 | match name { | 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 | pub mod attribute_stack { | 139 | pub mod attribute_stack { |
| 123 | /// The error produced when setting up the attribute stack to query `gitattributes`. | 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 | /// If true, we should default what's possible if something is misconfigured, on case by case basis, to be more resilient. | 479 | /// If true, we should default what's possible if something is misconfigured, on case by case basis, to be more resilient. |
| 463 | /// Also available in options! Keep in sync! | 480 | /// Also available in options! Keep in sync! |
| 464 | pub lenient_config: bool, | 481 | pub lenient_config: bool, |
| 465 | attributes: crate::permissions::Attributes, | 482 | attributes: crate::open::permissions::Attributes, |
| 466 | environment: crate::permissions::Environment, | 483 | environment: crate::open::permissions::Environment, |
| 467 | // TODO: make core.precomposeUnicode available as well. | 484 | // TODO: make core.precomposeUnicode available as well. |
| 468 | } | 485 | } |
| @@ -38,6 +38,8 @@ pub(crate) mod root { · 37 unmodified lines | |||
| 38 | pub const GITOXIDE: sections::Gitoxide = sections::Gitoxide; | 38 | pub const GITOXIDE: sections::Gitoxide = sections::Gitoxide; |
| 39 | /// The `http` section. | 39 | /// The `http` section. |
| 40 | pub const HTTP: sections::Http = sections::Http; | 40 | pub const HTTP: sections::Http = sections::Http; |
| 41 | /// The `index` section. | ||
| 42 | pub const INDEX: sections::Index = sections::Index; | ||
| 41 | /// The `init` section. | 43 | /// The `init` section. |
| 42 | pub const INIT: sections::Init = sections::Init; | 44 | pub const INIT: sections::Init = sections::Init; |
| 43 | /// The `pack` section. | 45 | /// The `pack` section. |
| @@ -69,6 +71,7 @@ pub(crate) mod root { · 25 unmodified lines | |||
| 69 | &Self::EXTENSIONS, | 71 | &Self::EXTENSIONS, |
| 70 | &Self::GITOXIDE, | 72 | &Self::GITOXIDE, |
| 71 | &Self::HTTP, | 73 | &Self::HTTP, |
| 74 | &Self::INDEX, | ||
| 72 | &Self::INIT, | 75 | &Self::INIT, |
| 73 | &Self::PACK, | 76 | &Self::PACK, |
| 74 | &Self::PROTOCOL, | 77 | &Self::PROTOCOL, |
| @@ -84,9 +87,9 @@ pub(crate) mod root { · 9 unmodified lines | |||
| 84 | 87 | ||
| 85 | mod sections; | 88 | mod sections; |
| 86 | pub use sections::{ | 89 | pub use sections::{ |
| 87 | branch, checkout, core, credential, diff, extensions, gitoxide, http, protocol, remote, ssh, Author, Branch, | 90 | branch, checkout, core, credential, diff, extensions, gitoxide, http, index, protocol, remote, ssh, Author, Branch, |
| 88 | Checkout, Clone, Committer, Core, Credential, Diff, Extensions, Gitoxide, Http, Init, Pack, Protocol, Remote, Safe, | 91 | Checkout, Clone, Committer, Core, Credential, Diff, Extensions, Gitoxide, Http, Index, Init, Pack, Protocol, |
| 89 | Ssh, Url, User, | 92 | Remote, Safe, Ssh, Url, User, |
| 90 | }; | 93 | }; |
| 91 | 94 | ||
| 92 | /// Generic value implementations for static instantiation. | 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 | pub struct Http; | 55 | pub struct Http; |
| 56 | pub mod http; | 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 | /// The `init` top-level section. | 63 | /// The `init` top-level section. |
| 59 | #[derive(Copy, Clone, Default)] | 64 | #[derive(Copy, Clone, Default)] |
| 60 | pub struct Init; | 65 | pub struct Init; |
| @@ -99,6 +99,9 @@ pub use hash::{oid, ObjectId}; · 98 unmodified lines | |||
| 99 | 99 | ||
| 100 | pub mod interrupt; | 100 | pub mod interrupt; |
| 101 | 101 | ||
| 102 | /// | ||
| 103 | pub mod attributes; | ||
| 104 | |||
| 102 | mod ext; | 105 | mod ext; |
| 103 | /// | 106 | /// |
| 104 | pub mod prelude; | 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 | ThreadSafeRepository::open_opts(directory, options).map(Into::into) | 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 | pub mod create; | 230 | pub mod create; |
| 235 | 231 | ||
| @@ -1,6 +1,17 @@ | |||
| 1 | use std::path::PathBuf; | 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 | /// The options used in [`ThreadSafeRepository::open_opts()`][crate::ThreadSafeRepository::open_opts()]. | 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 | #[error("The git directory at '{}' is considered unsafe as it's not owned by the current user.", .path.display())] | 55 | #[error("The git directory at '{}' is considered unsafe as it's not owned by the current user.", .path.display())] |
| 45 | UnsafeGitDir { path: PathBuf }, | 56 | UnsafeGitDir { path: PathBuf }, |
| 46 | #[error(transparent)] | 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 | mod options; | 61 | mod options; |
| 51 | 62 | pub mod permissions; | |
| 52 | mod repository; | 63 | mod repository; |
| 53 | 64 | ||
| 54 | #[cfg(test)] | 65 | #[cfg(test)] |
| @@ -1,7 +1,7 @@ | |||
| 1 | use std::path::PathBuf; | 1 | use std::path::PathBuf; |
| 2 | 2 | ||
| 3 | use super::{Error, Options}; | 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 | impl Default for Options { | 6 | impl Default for Options { |
| 7 | fn default() -> Self { | 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 | cache::{interpolate_context, util::ApplyLeniency}, | 10 | cache::{interpolate_context, util::ApplyLeniency}, |
| 11 | tree::{gitoxide, Core, Key, Safe}, | 11 | tree::{gitoxide, Core, Key, Safe}, |
| 12 | }, | 12 | }, |
| 13 | permission, Permissions, ThreadSafeRepository, | 13 | open::Permissions, |
| 14 | ThreadSafeRepository, | ||
| 14 | }; | 15 | }; |
| 15 | 16 | ||
| 16 | #[derive(Default, Clone)] | 17 | #[derive(Default, Clone)] |
| @@ -26,7 +27,7 @@ pub(crate) struct EnvironmentOverrides { · 9 unmodified lines | |||
| 26 | } | 27 | } |
| 27 | 28 | ||
| 28 | impl EnvironmentOverrides { | 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 | let mut worktree_dir = None; | 31 | let mut worktree_dir = None; |
| 31 | if let Some(path) = std::env::var_os(Core::WORKTREE.the_environment_override()) { | 32 | if let Some(path) = std::env::var_os(Core::WORKTREE.the_environment_override()) { |
| 32 | worktree_dir = PathBuf::from(path).into(); | 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 | mod cache; | 23 | mod cache; |
| 23 | mod config; | 24 | mod config; |
| 25 | mod excludes; | ||
| 24 | pub(crate) mod identity; | 26 | pub(crate) mod identity; |
| 25 | mod impls; | 27 | mod impls; |
| 26 | mod init; | 28 | mod init; |
| 27 | mod location; | 29 | mod location; |
| 28 | mod object; | 30 | mod object; |
| 29 | pub(crate) mod permissions; | ||
| 30 | mod reference; | 31 | mod reference; |
| 31 | mod remote; | 32 | mod remote; |
| 32 | mod revision; | 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 | use gix_sec::Trust; | 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 | /// Configure from which sources git configuration may be loaded. | 5 | /// Configure from which sources git configuration may be loaded. |
| 15 | /// | 6 | /// |
| 16 | /// Note that configuration from inside of the repository is always loaded as it's definitely required for correctness. | 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 | use crate::{worktree, Worktree}; | 2 | use crate::{worktree, Worktree}; |
| 2 | 3 | ||
| 3 | /// Worktree iteration | 4 | /// Interact with individual worktrees and their information. |
| 4 | impl crate::Repository { | 5 | impl crate::Repository { |
| 5 | /// Return a list of all _linked_ worktrees sorted by private git dir path as a lightweight proxy. | 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 | res.sort_by(|a, b| a.git_dir.cmp(&b.git_dir)); | 26 | res.sort_by(|a, b| a.git_dir.cmp(&b.git_dir)); |
| 26 | Ok(res) | 27 | Ok(res) |
| 27 | } | 28 | } |
| 28 | } | ||
| 29 | |||
| 30 | /// Interact with individual worktrees and their information. | ||
| 31 | impl crate::Repository { | ||
| 32 | /// Return the repository owning the main worktree, typically from a linked worktree. | 29 | /// Return the repository owning the main worktree, typically from a linked worktree. |
| 33 | /// | 30 | /// |
| 34 | /// Note that it might be the one that is currently open if this repository doesn't point to a linked worktree. | 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 | /// It will use the `index.threads` configuration key to learn how many threads to use. | 56 | /// It will use the `index.threads` configuration key to learn how many threads to use. |
| 60 | /// Note that it may fail if there is no index. | 57 | /// Note that it may fail if there is no index. |
| 61 | // TODO: test | ||
| 62 | pub fn open_index(&self) -> Result<gix_index::File, worktree::open_index::Error> { | 58 | pub fn open_index(&self) -> Result<gix_index::File, worktree::open_index::Error> { |
| 63 | let thread_limit = self | 59 | let thread_limit = self |
| 64 | .config | 60 | .config |
| 65 | .resolved | 61 | .resolved |
| 66 | .boolean("index", None, "threads") | 62 | .string("index", None, "threads") |
| 67 | .map(|res| { | 63 | .map(|value| crate::config::tree::Index::THREADS.try_into_index_threads(value)) |
| 68 | res.map(|value| usize::from(!value)).or_else(|err| { | 64 | .transpose() |
| 69 | gix_config::Integer::try_from(err.input.as_ref()) | 65 | .with_lenient_default(self.config.lenient_config)?; |
| 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()?; | ||
| 78 | gix_index::File::at( | 66 | gix_index::File::at( |
| 79 | self.index_path(), | 67 | self.index_path(), |
| 80 | self.object_hash(), | 68 | self.object_hash(), |
| @@ -71,18 +71,12 @@ pub mod proxy; · 70 unmodified lines | |||
| 71 | 71 | ||
| 72 | /// | 72 | /// |
| 73 | pub mod open_index { | 73 | pub mod open_index { |
| 74 | use crate::bstr::BString; | ||
| 75 | |||
| 76 | /// The error returned by [`Worktree::open_index()`][crate::Worktree::open_index()]. | 74 | /// The error returned by [`Worktree::open_index()`][crate::Worktree::open_index()]. |
| 77 | #[derive(Debug, thiserror::Error)] | 75 | #[derive(Debug, thiserror::Error)] |
| 78 | #[allow(missing_docs)] | 76 | #[allow(missing_docs)] |
| 79 | pub enum Error { | 77 | pub enum Error { |
| 80 | #[error("Could not interpret value '{}' as 'index.threads'", .value)] | 78 | #[error(transparent)] |
| 81 | ConfigIndexThreads { | 79 | ConfigIndexThreads(#[from] crate::config::key::GenericErrorWithValue), |
| 82 | value: BString, | ||
| 83 | #[source] | ||
| 84 | err: gix_config::value::Error, | ||
| 85 | }, | ||
| 86 | #[error(transparent)] | 80 | #[error(transparent)] |
| 87 | IndexFile(#[from] gix_index::file::init::Error), | 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 | pub mod excludes { | 98 | pub mod excludes { |
| 105 | use std::path::PathBuf; | ||
| 106 | |||
| 107 | /// The error returned by [`Worktree::excludes()`][crate::Worktree::excludes()]. | 99 | /// The error returned by [`Worktree::excludes()`][crate::Worktree::excludes()]. |
| 108 | #[derive(Debug, thiserror::Error)] | 100 | #[derive(Debug, thiserror::Error)] |
| 109 | #[allow(missing_docs)] | 101 | #[allow(missing_docs)] |
| 110 | pub enum Error { | 102 | pub enum Error { |
| 111 | #[error("Could not read repository exclude.")] | ||
| 112 | Io(#[from] std::io::Error), | ||
| 113 | #[error(transparent)] | 103 | #[error(transparent)] |
| 114 | EnvironmentPermission(#[from] gix_sec::permission::Error<PathBuf>), | 104 | OpenIndex(#[from] crate::worktree::open_index::Error), |
| 115 | #[error("The value for `core.excludesFile` could not be read from configuration")] | 105 | #[error(transparent)] |
| 116 | ExcludesFilePathInterpolation(#[from] gix_config::path::interpolate::Error), | 106 | CreateCache(#[from] crate::config::exclude_stack::Error), |
| 117 | } | 107 | } |
| 118 | 108 | ||
| 119 | impl<'repo> crate::Worktree<'repo> { | 109 | impl<'repo> crate::Worktree<'repo> { |
| @@ -124,35 +114,46 @@ pub mod excludes { · 4 unmodified lines | |||
| 124 | /// * `$XDG_CONFIG_HOME/…/ignore` if `core.excludesFile` is *not* set, otherwise use the configured file. | 114 | /// * `$XDG_CONFIG_HOME/…/ignore` if `core.excludesFile` is *not* set, otherwise use the configured file. |
| 125 | /// * `$GIT_DIR/info/exclude` if present. | 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. | 117 | /// When only excludes are desired, this is the most efficient way to obtain them. Otherwise use |
| 128 | // TODO: test, provide higher-level interface that is much easier to use and doesn't panic when accessing entries | 118 | /// [`Worktree::attributes()`][crate::Worktree::attributes()] for accessing both attributes and excludes. |
| 129 | // by non-relative path. | 119 | pub fn excludes(&self, overrides: Option<gix_ignore::Search>) -> Result<gix_worktree::Cache, Error> { |
| 130 | // TODO: `index` might be so special (given the conditions we are talking about) that it's better obtained internally | 120 | let index = self.index()?; |
| 131 | // so the caller won't have to care. | 121 | Ok(self.parent.excludes( |
| 132 | pub fn excludes( | 122 | &index, |
| 133 | &self, | 123 | overrides, |
| 134 | index: &gix_index::State, | 124 | gix_worktree::cache::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, |
| 135 | overrides: Option<gix_ignore::Search>, | 125 | )?) |
| 136 | ) -> Result<gix_worktree::Cache, Error> { | 126 | } |
| 137 | let repo = self.parent; | 127 | } |
| 138 | let case = if repo.config.ignore_case { | 128 | } |
| 139 | gix_glob::pattern::Case::Fold | 129 | |
| 140 | } else { | 130 | /// |
| 141 | gix_glob::pattern::Case::Sensitive | 131 | pub mod attributes { |
| 142 | }; | 132 | /// The error returned by [`Worktree::attributes()`][crate::Worktree::attributes()]. |
| 143 | let mut buf = Vec::with_capacity(512); | 133 | #[derive(Debug, thiserror::Error)] |
| 144 | let excludes_file = match repo.config.excludes_file().transpose()? { | 134 | #[allow(missing_docs)] |
| 145 | Some(user_path) => Some(user_path), | 135 | pub enum Error { |
| 146 | None => repo.config.xdg_config_path("ignore")?, | 136 | #[error(transparent)] |
| 147 | }; | 137 | OpenIndex(#[from] crate::worktree::open_index::Error), |
| 148 | let state = gix_worktree::cache::State::IgnoreStack(gix_worktree::cache::state::Ignore::new( | 138 | #[error(transparent)] |
| 149 | overrides.unwrap_or_default(), | 139 | CreateCache(#[from] crate::attributes::Error), |
| 150 | gix_ignore::Search::from_git_dir(repo.git_dir(), excludes_file, &mut buf)?, | 140 | } |
| 151 | None, | 141 | |
| 152 | case, | 142 | impl<'repo> crate::Worktree<'repo> { |
| 153 | )); | 143 | /// Configure a file-system cache checking if files below the repository are excluded or for querying their attributes. |
| 154 | let attribute_list = state.id_mappings_from_index(index, index.path_backing(), case); | 144 | /// |
| 155 | Ok(gix_worktree::Cache::new(self.path, state, case, buf, attribute_list)) | 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 | mod extensions { | 378 | mod extensions { |
| 355 | use gix::config::tree::{Extensions, Key}; | 379 | use gix::config::tree::{Extensions, Key}; |
| 356 | 380 | ||
| @@ -28,12 +28,12 @@ mod blocking_io { · 27 unmodified lines | |||
| 28 | let _env = env_value.map(|value| gix_testtools::Env::new().set("GIT_PROTOCOL_FROM_USER", value)); | 28 | let _env = env_value.map(|value| gix_testtools::Env::new().set("GIT_PROTOCOL_FROM_USER", value)); |
| 29 | let repo = gix::open_opts( | 29 | let repo = gix::open_opts( |
| 30 | remote::repo("protocol_file_user").git_dir(), | 30 | remote::repo("protocol_file_user").git_dir(), |
| 31 | gix::open::Options::isolated().permissions(gix::Permissions { | 31 | gix::open::Options::isolated().permissions(gix::open::Permissions { |
| 32 | env: gix::permissions::Environment { | 32 | env: gix::open::permissions::Environment { |
| 33 | git_prefix: gix_sec::Permission::Allow, | 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 | let remote = repo.find_remote("origin")?; | 39 | let remote = repo.find_remote("origin")?; |
| @@ -29,14 +29,17 @@ fn author_and_committer_and_fallback() -> crate::Result { · 28 unmodified lines | |||
| 29 | .set("GIT_CONFIG_VALUE_0", work_dir.join("c.config").display().to_string()); | 29 | .set("GIT_CONFIG_VALUE_0", work_dir.join("c.config").display().to_string()); |
| 30 | let repo = gix::open_opts( | 30 | let repo = gix::open_opts( |
| 31 | repo.git_dir(), | 31 | repo.git_dir(), |
| 32 | repo.open_options().clone().with(trust).permissions(gix::Permissions { | 32 | repo.open_options() |
| 33 | env: gix::permissions::Environment { | 33 | .clone() |
| 34 | xdg_config_home: Permission::Deny, | 34 | .with(trust) |
| 35 | home: Permission::Deny, | 35 | .permissions(gix::open::Permissions { |
| 36 | ..gix::permissions::Environment::all() | 36 | env: gix::open::permissions::Environment { |
| 37 | }, | 37 | xdg_config_home: Permission::Deny, |
| 38 | ..Default::default() | 38 | home: Permission::Deny, |
| 39 | }), | 39 | ..gix::open::permissions::Environment::all() |
| 40 | }, | ||
| 41 | ..Default::default() | ||
| 42 | }), | ||
| 40 | )?; | 43 | )?; |
| 41 | 44 | ||
| 42 | assert_eq!( | 45 | assert_eq!( |
| @@ -148,11 +151,11 @@ fn author_from_different_config_sections() -> crate::Result { · 105 unmodified lines | |||
| 148 | .clone() | 151 | .clone() |
| 149 | .config_overrides(None::<&str>) | 152 | .config_overrides(None::<&str>) |
| 150 | .with(gix_sec::Trust::Full) | 153 | .with(gix_sec::Trust::Full) |
| 151 | .permissions(gix::Permissions { | 154 | .permissions(gix::open::Permissions { |
| 152 | env: gix::permissions::Environment { | 155 | env: gix::open::permissions::Environment { |
| 153 | xdg_config_home: Permission::Deny, | 156 | xdg_config_home: Permission::Deny, |
| 154 | home: Permission::Deny, | 157 | home: Permission::Deny, |
| 155 | ..gix::permissions::Environment::all() | 158 | ..gix::open::permissions::Environment::all() |
| 156 | }, | 159 | }, |
| 157 | ..Default::default() | 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 | free::index::Subcommands::Verify => prepare_and_run( | 342 | free::index::Subcommands::Verify => prepare_and_run( |
| 353 | "index-verify", | 343 | "index-verify", |
| 354 | auto_verbose, | 344 | auto_verbose, |
| @@ -865,6 +855,37 @@ pub fn main() -> Result<()> { · 510 unmodified lines | |||
| 865 | ), | 855 | ), |
| 866 | }, | 856 | }, |
| 867 | Subcommands::Index(cmd) => match cmd { | 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 | index::Subcommands::FromTree { | 889 | index::Subcommands::FromTree { |
| 869 | force, | 890 | force, |
| 870 | index_output_path, | 891 | index_output_path, |
| @@ -899,3 +920,14 @@ fn verify_mode(decode: bool, re_encode: bool) -> verify::Mode { · 28 unmodified lines | |||
| 899 | (false, false) => verify::Mode::HashCrc32, | 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 | pub enum Subcommands { | 55 | pub enum Subcommands { |
| 56 | /// Validate constraints and assumptions of an index along with its integrity. | 56 | /// Validate constraints and assumptions of an index along with its integrity. |
| 57 | Verify, | 57 | Verify, |
| 58 | /// Print all entries to standard output | ||
| 59 | Entries, | ||
| 60 | /// Print information about the index structure | 58 | /// Print information about the index structure |
| 61 | Info { | 59 | Info { |
| 62 | /// Do not extract specific extension information to gain only a superficial idea of the index's composition. | 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 | /// Possible values are "none" and "tree-traversal". Default is "none". | 105 | /// Possible values are "none" and "tree-traversal". Default is "none". |
| 108 | expansion: Option<core::pack::create::ObjectExpansion>, | 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 | /// The amount of threads to use when counting and the `--nondeterminisitc-count` flag is set, defaulting | 109 | /// The amount of threads to use when counting and the `--nondeterminisitc-count` flag is set, defaulting |
| 112 | /// to the globally configured threads. | 110 | /// to the globally configured threads. |
| 113 | /// | 111 | /// |
| @@ -361,11 +361,11 @@ pub mod commit { · 360 unmodified lines | |||
| 361 | /// Describe the current commit or the given one using the name of the closest annotated tag in its ancestry. | 361 | /// Describe the current commit or the given one using the name of the closest annotated tag in its ancestry. |
| 362 | Describe { | 362 | Describe { |
| 363 | /// Use annotated tag references only, not all tags. | 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 | annotated_tags: bool, | 365 | annotated_tags: bool, |
| 366 | 366 | ||
| 367 | /// Use all references under the `ref/` namespaces, which includes tag references, local and remote branches. | 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 | all_refs: bool, | 369 | all_refs: bool, |
| 370 | 370 | ||
| 371 | /// Only follow the first parent when traversing the commit graph. | 371 | /// Only follow the first parent when traversing the commit graph. |
| @@ -471,6 +471,20 @@ pub mod index { · 99 unmodified lines | |||
| 471 | 471 | ||
| 472 | #[derive(Debug, clap::Subcommand)] | 472 | #[derive(Debug, clap::Subcommand)] |
| 473 | pub enum Subcommands { | 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 | /// Create an index from a tree-ish. | 488 | /// Create an index from a tree-ish. |
| 475 | #[clap(visible_alias = "read-tree")] | 489 | #[clap(visible_alias = "read-tree")] |
| 476 | FromTree { | 490 | FromTree { |
| @@ -113,7 +113,11 @@ pub mod pretty { · 112 unmodified lines | |||
| 113 | crate::shared::init_env_logger(); | 113 | crate::shared::init_env_logger(); |
| 114 | 114 | ||
| 115 | match (verbose, progress) { | 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 | (true, false) => { | 121 | (true, false) => { |
| 118 | use crate::shared::{self, STANDARD_RANGE}; | 122 | use crate::shared::{self, STANDARD_RANGE}; |
| 119 | let progress = shared::progress_tree(); | 123 | let progress = shared::progress_tree(); |