diff --git a/.cargo/config.toml b/.cargo/config.toml index 7f9f65305a7..05bf8918570 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,5 @@ +[profile.dev] +debug = false + [build] rustflags = "--cfg unsound_local_offset -C target-cpu=native" diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e6bb6c6e355..1d06c5f263c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -11,6 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 +# - uses: Swatinem/rust-cache@v1 - todo: figure out why windows doesn't seem to get its own caches, maybe? - name: clippy run: cargo clippy --all - name: fmt @@ -44,6 +45,7 @@ jobs: profile: default toolchain: stable override: true + - uses: Swatinem/rust-cache@v1 - name: "Check default features build on windows" uses: actions-rs/cargo@v1 with: @@ -75,4 +77,4 @@ jobs: - uses: actions/checkout@v2 - uses: EmbarkStudios/cargo-deny-action@v1 with: - command: check ${{ matrix.checks }} \ No newline at end of file + command: check ${{ matrix.checks }} diff --git a/Cargo.lock b/Cargo.lock index b470b54440e..bfde5c10ccc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -971,6 +971,7 @@ version = "0.5.0" dependencies = [ "bstr", "btoi", + "git-features", "git-testtools", "itoa", "nom", @@ -1039,7 +1040,7 @@ dependencies = [ [[package]] name = "git-hash" -version = "0.5.1" +version = "0.6.0" dependencies = [ "hex", "quick-error", @@ -1095,7 +1096,7 @@ dependencies = [ [[package]] name = "git-pack" -version = "0.9.0" +version = "0.10.0" dependencies = [ "bstr", "btoi", @@ -1162,9 +1163,8 @@ dependencies = [ [[package]] name = "git-ref" -version = "0.6.0" +version = "0.7.0" dependencies = [ - "bstr", "filebuffer", "git-actor", "git-features", @@ -1187,6 +1187,7 @@ name = "git-repository" version = "0.8.1" dependencies = [ "anyhow", + "bstr", "git-actor", "git-config", "git-diff", diff --git a/Cargo.toml b/Cargo.toml index 311ed97aa24..6adc947235e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ pretty-cli = ["clap", "prodash/local-time", "prodash-render-tui", "prodash-render-line", + "gitoxide-core/local-time-support", "env_logger", "futures-lite"] lean-cli = ["argh", "prodash/progress-log", "env_logger" ] @@ -79,8 +80,17 @@ env_logger = { version = "0.9.0", optional = true, default-features = false, fea crosstermion = { version = "0.8.0", optional = true, default-features = false } futures-lite = { version = "1.12.0", optional = true, default-features = false, features = ["std"] } -[profile.dev] -incremental = false +# toml_edit can't parse this :( +#[profile.dev.package] +#git-object.opt-level = 3 +#git-ref.opt-level = 3 +#git-pack.opt-level = 3 +#git-hash.opt-level = 3 +#git-actor.opt-level = 3 +#git-config.opt-level = 3 +#miniz_oxide.opt-level = 3 +#sha-1.opt-level = 3 +#sha1.opt-level = 3 [profile.release] overflow-checks = false diff --git a/Makefile b/Makefile index e4b1836baed..b00bf8bf3f5 100644 --- a/Makefile +++ b/Makefile @@ -85,10 +85,12 @@ check: ## Build all code in suitable configurations cargo check --no-default-features --features lean-termion cargo check --no-default-features --features max cargo check --no-default-features --features max-termion - cd git-actor && cargo check + cd git-actor && cargo check \ + && cargo check --features local-time-support cd gitoxide-core && cargo check \ && cargo check --features blocking-client \ - && cargo check --features async-client + && cargo check --features async-client \ + && cargo check --features local-time-support cd gitoxide-core && if cargo check --all-features 2>/dev/null; then false; else true; fi cd git-hash && cargo check --all-features \ && cargo check @@ -110,6 +112,7 @@ check: ## Build all code in suitable configurations && cargo check --features rustsha1 \ && cargo check --features fast-sha1 \ && cargo check --features progress \ + && cargo check --features time \ && cargo check --features io-pipe \ && cargo check --features crc32 \ && cargo check --features zlib \ @@ -131,6 +134,7 @@ check: ## Build all code in suitable configurations cd git-repository && cargo check --all-features \ && cargo check --no-default-features --features local \ && cargo check --no-default-features --features network \ + && cargo check --no-default-features --features one-stop-shop \ && cargo check --no-default-features cd cargo-smart-release && cargo check cd experiments/object-access && cargo check diff --git a/STABILITY.md b/STABILITY.md index 2391730b707..efab8d5b47c 100644 --- a/STABILITY.md +++ b/STABILITY.md @@ -7,19 +7,19 @@ Please note that this guide isn't stable itself and may be adjusted to fit chang ## Terminology -* _dependent crate_ +- _dependent crate_ - A crate which depends on a crate in this workspace directly. -* _downstream crate_ +- _downstream crate_ - A crate which directly or indirectly depends on a crate in this workspace. -* _workspace crate_ +- _workspace crate_ - A crate which is a member of this workspace and hence is stored in this repository -* _breaking change_ +- _breaking change_ - A change in code that requires a `dependent crate` to adjust their code to fix compile errors. -* _release_ +- _release_ - A new version of a crate is published to crates.io -* _development version_ +- _development version_ - A crate version whose _major_ version is 0. -* _release version_ +- _release version_ - A crate version whose _major_ version is 1 or higher. ## Tiers @@ -56,9 +56,13 @@ The following schematic helps to visualize what follows. ║ │ ┌─────────────┐ ┌─────────────┐ │ ║ ║ │ │ git-ref │ │ git-config │ │ ║ │ ║ │ └─────────────┘ └─────────────┘ │ ║ - ║ └───────────────────────────────────┘ ║ │ - ║ ║ - ╚═════════════════════════════════════════════╝ │ + ║ │ ┌─────────────┐ ┌─────────────┐ │ ║ │ + ║ │ │ git-object │ │ git-lock │ │ ║ + ║ │ └─────────────┘ └─────────────┘ │ ║ │ + ║ └───────────────────────────────────┘ ║ + ║ ║ │ + ╚═════════════════════════════════════════════╝ + │ Stability Tier 2 ─────────────────────────────┐ │ │ │ │ Plumbing Crates─────────────────────┐ │ @@ -95,7 +99,7 @@ If there are additional breaking changes without a release, these push back the ### Tier 1: released apps and application crates -Released apps and application crates are marked with major version number 1 or above, like `2.3.0+21.06` and live in tier 1 _(->ST1)_, +Released apps and application crates are marked with major version number 1 or above, like `2.3.0+21.06` and live in tier 1 _(->ST1)_, with the build identifiers for year (`21`) and and month `06` appended, based on the actual release year and month. Breaking changes are collected and may be released no more often than every 6 months by incrementing the major version number. If there are additional breaking changes, diff --git a/cargo-features.md b/cargo-features.md index f44e283e0dc..bc3c7278865 100644 --- a/cargo-features.md +++ b/cargo-features.md @@ -72,6 +72,8 @@ The library powering the command-line interface. - **async-client** - The client to connect to git servers will be async, while supporting only the 'git' transport itself. It's the most limited and can be seen as example on how to use custom transports for custom servers. +* **local-time-support** + - Functions dealing with time may include the local timezone offset, not just UTC with the offset being zero. [skim]: https://github.com/lotabout/skim [git-hours]: https://github.com/kimmobrunfeldt/git-hours @@ -83,6 +85,11 @@ The library powering the command-line interface. for the LRU-cache itself low. * **pack-cache-lru-dynamic** * Provide a hash-map based LRU cache whose eviction is based a memory cap calculated from object data. + +### git-actor + +* **local-time-support** + - Make `Signature` initializers using the local time (with UTC offset) available. ### git-features @@ -185,6 +192,8 @@ be selected. * **unstable** - Re-export stability tier 2 crates for convenience and make `Repository` struct fields with types from these crates publicly accessible. - Doing so is less stable than the stability tier 1 that `git-repository` is a member of. +* **local-time-support** + - Functions dealing with time may include the local timezone offset, not just UTC with the offset being zero. The following toggles can be used to reduce dependencies. diff --git a/cargo-smart-release/src/command/release/git.rs b/cargo-smart-release/src/command/release/git.rs index 28e34819c68..89c38e024f7 100644 --- a/cargo-smart-release/src/command/release/git.rs +++ b/cargo-smart-release/src/command/release/git.rs @@ -6,7 +6,7 @@ use cargo_metadata::{ camino::{Utf8Component, Utf8Path}, Package, }; -use git_repository::{easy::object, prelude::ReferenceAccessExt, refs}; +use git_repository::{easy::object, prelude::ReferenceAccessExt, refs, refs::transaction::PreviousValue}; use super::{tag_name_for, utils::will, Context, Oid, Options}; @@ -46,8 +46,8 @@ pub(in crate::command::release_impl) fn has_changed_since_last_release( .strip_prefix(&ctx.root) .expect("workspace members are releative to the root directory"); - let current_commit = ctx.git_easy.find_reference("HEAD")?.peel_to_oid_in_place()?; - let released_target = tag_ref.peel_to_oid_in_place()?; + let current_commit = ctx.git_easy.find_reference("HEAD")?.peel_to_id_in_place()?; + let released_target = tag_ref.peel_to_id_in_place()?; if repo_relative_crate_dir.as_os_str().is_empty() { Ok(current_commit != released_target) @@ -126,7 +126,7 @@ pub(in crate::command::release_impl) fn commit_changes( if !cmd.status()?.success() { bail!("Failed to commit changed manifests"); } - Ok(Some(ctx.git_easy.find_reference("HEAD")?.peel_to_oid_in_place()?)) + Ok(Some(ctx.git_easy.find_reference("HEAD")?.peel_to_id_in_place()?)) } pub(in crate::command::release_impl) fn create_version_tag<'repo>( @@ -155,12 +155,9 @@ pub(in crate::command::release_impl) fn create_version_tag<'repo>( } Ok(Some(format!("refs/tags/{}", tag_name).try_into()?)) } else { - let edits = ctx.git_easy.tag( - tag_name, - commit_id.expect("set in --execute mode"), - git_lock::acquire::Fail::Immediately, - false, - )?; + let edits = ctx + .git_easy + .tag(tag_name, commit_id.expect("set in --execute mode"), PreviousValue::Any)?; assert_eq!(edits.len(), 1, "We create only one tag and there is no expansion"); let tag = edits.into_iter().next().expect("the promised tag"); log::info!("Created tag {}", tag.name.as_bstr()); diff --git a/crate-status.md b/crate-status.md index d23797fac3b..03a48244449 100644 --- a/crate-status.md +++ b/crate-status.md @@ -239,6 +239,7 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/git-lock/README. ### git-repository * [x] utilities for applications to make long running operations interruptible gracefully and to support timeouts in servers. +* [ ] handle `core.repositoryFormatVersion` and extensions * [x] discovery * [ ] option to not cross file systems * [ ] handle git-common-dir diff --git a/etc/check-package-size.sh b/etc/check-package-size.sh index 15975ce8974..8022b9adcb5 100755 --- a/etc/check-package-size.sh +++ b/etc/check-package-size.sh @@ -15,7 +15,7 @@ function indent () { } echo "in root: gitoxide CLI" -indent cargo diet -n --package-size-limit 25KB +#indent cargo diet -n --package-size-limit 25KB - fails right now because of dotted profile.dev.package (enter cargo-smart-release && indent cargo diet -n --package-size-limit 15KB) (enter git-actor && indent cargo diet -n --package-size-limit 5KB) (enter git-tempfile && indent cargo diet -n --package-size-limit 20KB) @@ -34,6 +34,6 @@ indent cargo diet -n --package-size-limit 25KB (enter git-odb && indent cargo diet -n --package-size-limit 15KB) (enter git-protocol && indent cargo diet -n --package-size-limit 25KB) (enter git-packetline && indent cargo diet -n --package-size-limit 15KB) -(enter git-repository && indent cargo diet -n --package-size-limit 30KB) +(enter git-repository && indent cargo diet -n --package-size-limit 35KB) (enter git-transport && indent cargo diet -n --package-size-limit 30KB) (enter gitoxide-core && indent cargo diet -n --package-size-limit 20KB) diff --git a/etc/crate-structure.monopic b/etc/crate-structure.monopic index 768eb25b20d..dca74eb698e 100644 Binary files a/etc/crate-structure.monopic and b/etc/crate-structure.monopic differ diff --git a/experiments/diffing/src/main.rs b/experiments/diffing/src/main.rs index 2d657d1107b..c8201c7081e 100644 --- a/experiments/diffing/src/main.rs +++ b/experiments/diffing/src/main.rs @@ -9,7 +9,7 @@ use git_repository::{ objs::{bstr::BStr, TreeRefIter}, odb, prelude::*, - refs::file::loose::reference::peel, + refs::{file::ReferenceExt, peel}, }; use rayon::prelude::*; @@ -255,7 +255,7 @@ where where L: for<'a> FnMut(&oid, &'a mut Vec) -> Option>, { - find(id, buf).and_then(|o| o.into_tree_iter()) + find(id, buf).and_then(|o| o.try_into_tree_iter()) } fn tree_iter_by_commit<'b, L>(id: &oid, buf: &'b mut Vec, mut find: L) -> TreeRefIter<'b> @@ -264,7 +264,7 @@ where { let tid = find(id, buf) .expect("commit present") - .into_commit_iter() + .try_into_commit_iter() .expect("a commit") .tree_id() .expect("tree id present and decodable"); diff --git a/experiments/traversal/src/main.rs b/experiments/traversal/src/main.rs index c3079b55923..e2867998c6e 100644 --- a/experiments/traversal/src/main.rs +++ b/experiments/traversal/src/main.rs @@ -10,7 +10,7 @@ use git_repository::{ objs::{bstr::BStr, tree::EntryRef}, odb, prelude::*, - refs::file::loose::reference::peel, + refs::{file::ReferenceExt, peel}, traverse::{tree, tree::visit::Action}, }; @@ -209,13 +209,13 @@ where for commit in commits { let tree_id = db .try_find(commit, &mut buf, &mut cache)? - .and_then(|o| o.into_commit_iter().and_then(|mut c| c.tree_id())) + .and_then(|o| o.try_into_commit_iter().and_then(|mut c| c.tree_id())) .expect("commit as starting point"); let mut count = Count { entries: 0, seen }; db.find_tree_iter(tree_id, &mut buf2, &mut cache)?.traverse( &mut state, - |oid, buf| db.find(oid, buf, &mut cache).ok().and_then(|o| o.into_tree_iter()), + |oid, buf| db.find(oid, buf, &mut cache).ok().and_then(|o| o.try_into_tree_iter()), &mut count, )?; entries += count.entries as u64; diff --git a/git-actor/Cargo.toml b/git-actor/Cargo.toml index 87adb2415d4..1587274a9dc 100644 --- a/git-actor/Cargo.toml +++ b/git-actor/Cargo.toml @@ -13,11 +13,13 @@ doctest = false [features] serde1 = ["serde", "bstr/serde1"] +local-time-support = ["git-features/time"] [package.metadata.docs.rs] all-features = true [dependencies] +git-features = { version = "^0.16.0", path = "../git-features", optional = true } quick-error = "2.0.0" btoi = "0.4.2" bstr = { version = "0.2.13", default-features = false, features = ["std"]} diff --git a/git-actor/src/signature/mod.rs b/git-actor/src/signature/mod.rs index 55a4db1426c..6ef38245a2d 100644 --- a/git-actor/src/signature/mod.rs +++ b/git-actor/src/signature/mod.rs @@ -3,9 +3,10 @@ mod _ref { impl<'a> SignatureRef<'a> { /// Deserialize a signature from the given `data`. - pub fn from_bytes + nom::error::ContextError<&'a [u8]>>( - data: &'a [u8], - ) -> Result, nom::Err> { + pub fn from_bytes(data: &'a [u8]) -> Result, nom::Err> + where + E: nom::error::ParseError<&'a [u8]> + nom::error::ContextError<&'a [u8]>, + { decode(data).map(|(_, t)| t) } } @@ -14,6 +15,12 @@ mod _ref { mod convert { use crate::{Sign, Signature, SignatureRef, Time}; + impl Default for Signature { + fn default() -> Self { + Signature::empty() + } + } + impl Signature { /// An empty signature, similar to 'null'. pub fn empty() -> Self { @@ -99,6 +106,78 @@ mod write { } } +mod init { + use bstr::BString; + + use crate::{Signature, Time}; + + impl Signature { + /// Return an actor identified `name` and `email` at the current local time, that is a time with a timezone offset from + /// UTC based on the hosts configuration. + #[cfg(feature = "local-time-support")] + pub fn now_local( + name: impl Into, + email: impl Into, + ) -> Result { + let offset = git_features::time::tz::current_utc_offset()?; + Ok(Signature { + name: name.into(), + email: email.into(), + time: Time { + time: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("the system time doesn't run backwards that much") + .as_secs() as u32, + offset, + sign: offset.into(), + }, + }) + } + + /// Return an actor identified `name` and `email` at the current local time, or UTC time if the current time zone could + /// not be obtained. + #[cfg(feature = "local-time-support")] + pub fn now_local_or_utc(name: impl Into, email: impl Into) -> Self { + let offset = git_features::time::tz::current_utc_offset().unwrap_or(0); + Signature { + name: name.into(), + email: email.into(), + time: Time { + time: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("the system time doesn't run backwards that much") + .as_secs() as u32, + offset, + sign: offset.into(), + }, + } + } + + /// Return an actor identified by `name` and `email` at the current time in UTC. + /// + /// This would be most useful for bot users, otherwise the [`now_local()`][Signature::now_local()] method should be preferred. + pub fn now_utc(name: impl Into, email: impl Into) -> Self { + let utc_offset = 0; + Signature { + name: name.into(), + email: email.into(), + time: Time { + time: seconds_since_epoch(), + offset: utc_offset, + sign: utc_offset.into(), + }, + } + } + } + + fn seconds_since_epoch() -> u32 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("the system time doesn't run backwards that much") + .as_secs() as u32 + } +} + /// mod decode; pub use decode::decode; diff --git a/git-actor/src/time.rs b/git-actor/src/time.rs index 39a52df9901..14f7fc5684f 100644 --- a/git-actor/src/time.rs +++ b/git-actor/src/time.rs @@ -2,6 +2,16 @@ use std::io; use crate::{Sign, Time, SPACE}; +impl From for Sign { + fn from(v: i32) -> Self { + if v < 0 { + Sign::Minus + } else { + Sign::Plus + } + } +} + impl Time { /// Serialize this instance to `out` in a format suitable for use in header fields of serialized git commits or tags. pub fn write_to(&self, mut out: impl io::Write) -> io::Result<()> { diff --git a/git-actor/tests/actor.rs b/git-actor/tests/actor.rs index 89d6b6d6b97..a629086be6c 100644 --- a/git-actor/tests/actor.rs +++ b/git-actor/tests/actor.rs @@ -1,9 +1,10 @@ use std::path::PathBuf; -mod signature; - pub use git_testtools::hex_to_id; pub fn fixture(path: &str) -> PathBuf { PathBuf::from("tests/fixtures").join(path) } + +mod signature; +mod time; diff --git a/git-actor/tests/signature/mod.rs b/git-actor/tests/signature/mod.rs index 2782b93baa3..611d88f0486 100644 --- a/git-actor/tests/signature/mod.rs +++ b/git-actor/tests/signature/mod.rs @@ -1,43 +1,3 @@ -mod time { - use bstr::ByteSlice; - use git_actor::{Sign, Time}; - - #[test] - fn write_to() -> Result<(), Box> { - for (time, expected) in &[ - ( - Time { - time: 500, - offset: 9000, - sign: Sign::Plus, - }, - "500 +0230", - ), - ( - Time { - time: 189009009, - offset: 36000, - sign: Sign::Minus, - }, - "189009009 -1000", - ), - ( - Time { - time: 0, - offset: 0, - sign: Sign::Minus, - }, - "0 -0000", - ), - ] { - let mut output = Vec::new(); - time.write_to(&mut output)?; - assert_eq!(output.as_bstr(), expected); - } - Ok(()) - } -} - mod write_to { mod invalid { use git_actor::{Sign, Signature, Time}; diff --git a/git-actor/tests/time/mod.rs b/git-actor/tests/time/mod.rs new file mode 100644 index 00000000000..e28719a8d38 --- /dev/null +++ b/git-actor/tests/time/mod.rs @@ -0,0 +1,37 @@ +use bstr::ByteSlice; +use git_actor::{Sign, Time}; + +#[test] +fn write_to() -> Result<(), Box> { + for (time, expected) in &[ + ( + Time { + time: 500, + offset: 9000, + sign: Sign::Plus, + }, + "500 +0230", + ), + ( + Time { + time: 189009009, + offset: 36000, + sign: Sign::Minus, + }, + "189009009 -1000", + ), + ( + Time { + time: 0, + offset: 0, + sign: Sign::Minus, + }, + "0 -0000", + ), + ] { + let mut output = Vec::new(); + time.write_to(&mut output)?; + assert_eq!(output.as_bstr(), expected); + } + Ok(()) +} diff --git a/git-commitgraph/Cargo.toml b/git-commitgraph/Cargo.toml index 66871a6badb..1014ad3a1d3 100644 --- a/git-commitgraph/Cargo.toml +++ b/git-commitgraph/Cargo.toml @@ -17,7 +17,7 @@ serde1 = ["serde", "git-hash/serde1", "bstr/serde1"] [dependencies] git-features = { version = "^0.16.0", path = "../git-features", features = ["rustsha1"] } -git-hash = { version = "^0.5.0", path = "../git-hash" } +git-hash = { version ="^0.6.0", path = "../git-hash" } bstr = { version = "0.2.13", default-features = false, features = ["std"] } byteorder = "1.2.3" diff --git a/git-diff/Cargo.toml b/git-diff/Cargo.toml index c2fdb81d871..1d66e178bba 100644 --- a/git-diff/Cargo.toml +++ b/git-diff/Cargo.toml @@ -14,7 +14,7 @@ doctest = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -git-hash = { version = "^0.5.0", path = "../git-hash" } +git-hash = { version ="^0.6.0", path = "../git-hash" } git-object = { version ="^0.13.0", path = "../git-object" } quick-error = "2.0.0" diff --git a/git-diff/tests/visit/mod.rs b/git-diff/tests/visit/mod.rs index 23faff0f47f..de705a662d2 100644 --- a/git-diff/tests/visit/mod.rs +++ b/git-diff/tests/visit/mod.rs @@ -34,7 +34,7 @@ mod changes { Ok(db .try_find(tree_id, buf, &mut pack::cache::Never)? .expect("main tree present") - .into_tree_iter() + .try_into_tree_iter() .expect("id to be a tree")) } @@ -53,7 +53,7 @@ mod changes { db.try_find(oid, buf, &mut pack::cache::Never) .ok() .flatten() - .and_then(|obj| obj.into_tree_iter()) + .and_then(|obj| obj.try_into_tree_iter()) }, &mut recorder, )?; @@ -78,7 +78,7 @@ mod changes { let current_tree = db .try_find(main_tree_id, &mut buf, &mut pack::cache::Never)? .expect("main tree present") - .into_tree_iter() + .try_into_tree_iter() .expect("id to be a tree"); let mut buf2 = Vec::new(); let previous_tree: Option<_> = { @@ -88,7 +88,7 @@ mod changes { .and_then(|c| c.into_commit()) .map(|c| c.tree()) .and_then(|tree| db.try_find(tree, &mut buf2, &mut pack::cache::Never).ok().flatten()) - .and_then(|tree| tree.into_tree_iter()) + .and_then(|tree| tree.try_into_tree_iter()) }; let mut recorder = git_diff::tree::Recorder::default(); @@ -99,7 +99,7 @@ mod changes { db.try_find(oid, buf, &mut pack::cache::Never) .ok() .flatten() - .and_then(|obj| obj.into_tree_iter()) + .and_then(|obj| obj.try_into_tree_iter()) }, &mut recorder, )?; @@ -133,7 +133,7 @@ mod changes { db.try_find(oid, buf, &mut pack::cache::Never) .ok() .flatten() - .and_then(|o| o.into_commit_iter()) + .and_then(|o| o.try_into_commit_iter()) }) .collect::>() .into_iter() diff --git a/git-features/Cargo.toml b/git-features/Cargo.toml index 0d0b3dc89aa..a9c2ec5b1cd 100644 --- a/git-features/Cargo.toml +++ b/git-features/Cargo.toml @@ -49,7 +49,7 @@ path = "tests/pipe.rs" required-features = ["io-pipe"] [dependencies] -git-hash = { version = "^0.5.0", path = "../git-hash" } +git-hash = { version ="^0.6.0", path = "../git-hash" } # 'parallel' feature crossbeam-utils = { version = "0.8.5", optional = true } @@ -80,5 +80,6 @@ time = { version = "0.3.2", optional = true, default-features = false, features [package.metadata.docs.rs] all-features = true -[target.'cfg(not(windows))'.dependencies] +# Assembly doesn't yet compile on MSVC on windows, but does on GNU, see https://github.com/RustCrypto/asm-hashes/issues/17 +[target.'cfg(not(all(target_os = "windows", target_env = "msvc")))'.dependencies] sha-1 = { version = "0.9.1", optional = true, features = ["asm"] } diff --git a/git-features/src/time.rs b/git-features/src/time.rs index 85470d18f5b..e0a6b5b712e 100644 --- a/git-features/src/time.rs +++ b/git-features/src/time.rs @@ -3,7 +3,7 @@ pub mod tz { mod error { use std::fmt; - /// The error returned by [`offset()`] + /// The error returned by [`current_utc_offset()`][super::current_utc_offset()] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Error; @@ -25,7 +25,9 @@ pub mod tz { /// Note that there may be various legitimate reasons for failure, which should be accounted for. pub fn current_utc_offset() -> Result { // TODO: make this work without cfg(unsound_local_offset), see - // https://github.com/time-rs/time/issues/293#issuecomment-909158529 + // https://github.com/time-rs/time/issues/293#issuecomment-909158529 + // TODO: get a function to return the current time as well to avoid double-lookups + // (to get the offset, the current time is needed) time::UtcOffset::current_local_offset() .map(|ofs| ofs.whole_seconds()) .map_err(|_| Error) diff --git a/git-hash/CHANGELOG.md b/git-hash/CHANGELOG.md new file mode 100644 index 00000000000..8056a99e898 --- /dev/null +++ b/git-hash/CHANGELOG.md @@ -0,0 +1,6 @@ +### 0.6.0 + +#### Breaking + +- `ObjectId::empty_tree()` now has a parameter: `Kind` +- `ObjectId::null_sha(…)` -> `ObjectId::null(…)` diff --git a/git-hash/Cargo.toml b/git-hash/Cargo.toml index d5acd4376d6..22194b21407 100644 --- a/git-hash/Cargo.toml +++ b/git-hash/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "git-hash" -version = "0.5.1" +version = "0.6.0" description = "Borrowed and owned git hash digests used to identify git objects" authors = ["Sebastian Thiel "] repository = "https://github.com/Byron/gitoxide" license = "MIT/Apache-2.0" edition = "2018" -include = ["src/**/*"] +include = ["src/**/*", "CHANGELOG.md"] [lib] doctest = false diff --git a/git-hash/src/lib.rs b/git-hash/src/lib.rs index 116c9bc7773..6fd705c27a1 100644 --- a/git-hash/src/lib.rs +++ b/git-hash/src/lib.rs @@ -7,7 +7,6 @@ mod borrowed; pub use borrowed::oid; -#[allow(missing_docs)] mod owned; pub use owned::ObjectId; diff --git a/git-hash/src/owned.rs b/git-hash/src/owned.rs index 1352f1342a1..2858fd676e7 100644 --- a/git-hash/src/owned.rs +++ b/git-hash/src/owned.rs @@ -1,11 +1,12 @@ -use std::{borrow::Borrow, fmt, io, ops::Deref}; +use std::{borrow::Borrow, convert::TryInto, fmt, io, ops::Deref}; -use crate::{borrowed::oid, SIZE_OF_SHA1_DIGEST}; +use crate::{borrowed::oid, Kind, SIZE_OF_SHA1_DIGEST}; /// An owned hash identifying objects, most commonly Sha1 #[derive(PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] pub enum ObjectId { + /// A SHA 1 hash digest Sha1([u8; SIZE_OF_SHA1_DIGEST]), } @@ -45,8 +46,13 @@ impl ObjectId { out.write_all(&self.to_sha1_hex()) } - pub const fn empty_tree() -> ObjectId { - ObjectId::Sha1(*b"\x4b\x82\x5d\xc6\x42\xcb\x6e\xb9\xa0\x60\xe5\x4b\xf8\xd6\x92\x88\xfb\xee\x49\x04") + /// The hash of an empty tree + pub const fn empty_tree(hash: Kind) -> ObjectId { + match hash { + Kind::Sha1 => { + ObjectId::Sha1(*b"\x4b\x82\x5d\xc6\x42\xcb\x6e\xb9\xa0\x60\xe5\x4b\xf8\xd6\x92\x88\xfb\xee\x49\x04") + } + } } /// Returns true if this hash consists of all null bytes @@ -57,7 +63,7 @@ impl ObjectId { } /// Returns an Digest representing a hash with whose memory is zeroed. - pub const fn null_sha(kind: crate::Kind) -> ObjectId { + pub const fn null(kind: crate::Kind) -> ObjectId { match kind { crate::Kind::Sha1 => Self::null_sha1(), } @@ -118,7 +124,6 @@ impl ObjectId { } /// Returns an Digest representing a Sha1 with whose memory is zeroed. - /// TODO: remove this method replace its usage with `null_sha(kind)` to probably become hash independent. pub const fn null_sha1() -> ObjectId { ObjectId::Sha1([0u8; 20]) } @@ -130,6 +135,15 @@ impl From<[u8; SIZE_OF_SHA1_DIGEST]> for ObjectId { } } +impl From<&[u8]> for ObjectId { + fn from(v: &[u8]) -> Self { + match v.len() { + 20 => Self::Sha1(v.try_into().expect("prior length validation")), + other => panic!("BUG: unsupported hash len: {}", other), + } + } +} + impl From<&crate::oid> for ObjectId { fn from(v: &oid) -> Self { match v.kind() { diff --git a/git-object/Cargo.toml b/git-object/Cargo.toml index e5066684842..9ff8f4edf4c 100644 --- a/git-object/Cargo.toml +++ b/git-object/Cargo.toml @@ -19,7 +19,7 @@ verbose-object-parsing-errors = ["nom/std"] all-features = true [dependencies] -git-hash = { version = "^0.5.0", path = "../git-hash" } +git-hash = { version ="^0.6.0", path = "../git-hash" } git-validate = { version = "^0.5.0", path = "../git-validate" } git-actor = { version ="^0.5.0", path = "../git-actor" } diff --git a/git-object/src/lib.rs b/git-object/src/lib.rs index cad68e32f6f..da4ef88a42e 100644 --- a/git-object/src/lib.rs +++ b/git-object/src/lib.rs @@ -217,6 +217,13 @@ pub struct Tree { pub entries: Vec, } +impl Tree { + /// Return an empty tree which serializes to a well-known hash + pub fn empty() -> Self { + Tree { entries: Vec::new() } + } +} + /// #[cfg(feature = "verbose-object-parsing-errors")] pub mod decode { diff --git a/git-object/src/object/mod.rs b/git-object/src/object/mod.rs index 6ec2b263ec8..9223e0c20d6 100644 --- a/git-object/src/object/mod.rs +++ b/git-object/src/object/mod.rs @@ -24,6 +24,63 @@ mod write { /// Convenient extraction of typed object. impl Object { + /// Turns this instance into a [`Blob`][Blob], panic otherwise. + pub fn into_blob(self) -> Blob { + match self { + Object::Blob(v) => v, + _ => panic!("BUG: not a blob"), + } + } + /// Turns this instance into a [`Commit`][Commit] panic otherwise. + pub fn into_commit(self) -> Commit { + match self { + Object::Commit(v) => v, + _ => panic!("BUG: not a commit"), + } + } + /// Turns this instance into a [`Tree`][Tree] panic otherwise. + pub fn into_tree(self) -> Tree { + match self { + Object::Tree(v) => v, + _ => panic!("BUG: not a tree"), + } + } + /// Turns this instance into a [`Tag`][Tag] panic otherwise. + pub fn into_tag(self) -> Tag { + match self { + Object::Tag(v) => v, + _ => panic!("BUG: not a tag"), + } + } + /// Turns this instance into a [`Blob`][Blob] if it is one. + pub fn try_into_blob(self) -> Result { + match self { + Object::Blob(v) => Ok(v), + _ => Err(self), + } + } + /// Turns this instance into a [`Commit`][Commit] if it is one. + pub fn try_into_commit(self) -> Result { + match self { + Object::Commit(v) => Ok(v), + _ => Err(self), + } + } + /// Turns this instance into a [`Tree`][Tree] if it is one. + pub fn try_into_tree(self) -> Result { + match self { + Object::Tree(v) => Ok(v), + _ => Err(self), + } + } + /// Turns this instance into a [`Tag`][Tag] if it is one. + pub fn try_into_tag(self) -> Result { + match self { + Object::Tag(v) => Ok(v), + _ => Err(self), + } + } + /// Returns a [`Blob`][Blob] if it is one. pub fn as_blob(&self) -> Option<&Blob> { match self { diff --git a/git-odb/Cargo.toml b/git-odb/Cargo.toml index 27655ffa838..3b670a49855 100644 --- a/git-odb/Cargo.toml +++ b/git-odb/Cargo.toml @@ -29,9 +29,9 @@ all-features = true [dependencies] git-features = { version = "^0.16.0", path = "../git-features", features = ["rustsha1", "walkdir", "zlib"] } -git-hash = { version = "^0.5.0", path = "../git-hash" } +git-hash = { version ="^0.6.0", path = "../git-hash" } git-object = { version ="^0.13.0", path = "../git-object" } -git-pack = { version ="^0.9.0", path = "../git-pack" } +git-pack = { version ="^0.10.0", path = "../git-pack" } btoi = "0.4.2" tempfile = "3.1.0" diff --git a/git-odb/src/store/compound/init.rs b/git-odb/src/store/compound/init.rs index dfc09e1bf64..a02adf0e402 100644 --- a/git-odb/src/store/compound/init.rs +++ b/git-odb/src/store/compound/init.rs @@ -34,10 +34,7 @@ impl compound::Store { .filter_map(Result::ok) .filter_map(|e| e.metadata().map(|md| (e.path(), md)).ok()) .filter(|(_, md)| md.file_type().is_file()) - .filter(|(p, _)| { - p.extension().unwrap_or_default() == "idx" - && p.file_name().unwrap_or_default().to_string_lossy().starts_with("pack-") - }) + .filter(|(p, _)| p.extension().unwrap_or_default() == "idx") // TODO: make this configurable, git for instance sorts by modification date // https://github.com/libgit2/libgit2/blob/main/src/odb_pack.c#L41-L158 .map(|(p, md)| pack::Bundle::at(p).map(|b| (b, md.len()))) diff --git a/git-odb/tests/fixtures/repos/small-packs.git/HEAD b/git-odb/tests/fixtures/repos/small-packs.git/HEAD new file mode 100644 index 00000000000..b870d82622c --- /dev/null +++ b/git-odb/tests/fixtures/repos/small-packs.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/git-odb/tests/fixtures/repos/small-packs.git/config b/git-odb/tests/fixtures/repos/small-packs.git/config new file mode 100644 index 00000000000..c911c67ba6f --- /dev/null +++ b/git-odb/tests/fixtures/repos/small-packs.git/config @@ -0,0 +1,7 @@ +[core] + bare = true + repositoryformatversion = 0 + filemode = true + ignorecase = true + precomposeunicode = true + logallrefupdates = true diff --git a/git-odb/tests/fixtures/repos/small-packs.git/description b/git-odb/tests/fixtures/repos/small-packs.git/description new file mode 100644 index 00000000000..498b267a8c7 --- /dev/null +++ b/git-odb/tests/fixtures/repos/small-packs.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/git-odb/tests/fixtures/repos/small-packs.git/info/exclude b/git-odb/tests/fixtures/repos/small-packs.git/info/exclude new file mode 100644 index 00000000000..6d05881d3a0 --- /dev/null +++ b/git-odb/tests/fixtures/repos/small-packs.git/info/exclude @@ -0,0 +1,2 @@ +# File patterns to ignore; see `git help ignore` for more information. +# Lines that start with '#' are comments. diff --git a/git-odb/tests/fixtures/repos/small-packs.git/objects/pack/09a90be3fe6db2b49e35858c529e4b9c687b9a08.idx b/git-odb/tests/fixtures/repos/small-packs.git/objects/pack/09a90be3fe6db2b49e35858c529e4b9c687b9a08.idx new file mode 100644 index 00000000000..c8aaedf764b Binary files /dev/null and b/git-odb/tests/fixtures/repos/small-packs.git/objects/pack/09a90be3fe6db2b49e35858c529e4b9c687b9a08.idx differ diff --git a/git-odb/tests/fixtures/repos/small-packs.git/objects/pack/09a90be3fe6db2b49e35858c529e4b9c687b9a08.pack b/git-odb/tests/fixtures/repos/small-packs.git/objects/pack/09a90be3fe6db2b49e35858c529e4b9c687b9a08.pack new file mode 100644 index 00000000000..5bb3025f23e Binary files /dev/null and b/git-odb/tests/fixtures/repos/small-packs.git/objects/pack/09a90be3fe6db2b49e35858c529e4b9c687b9a08.pack differ diff --git a/git-odb/tests/fixtures/repos/small-packs.git/objects/pack/216edf2f7a671742705e6ca8a639daacfcf91217.idx b/git-odb/tests/fixtures/repos/small-packs.git/objects/pack/216edf2f7a671742705e6ca8a639daacfcf91217.idx new file mode 100644 index 00000000000..06709bce7cf Binary files /dev/null and b/git-odb/tests/fixtures/repos/small-packs.git/objects/pack/216edf2f7a671742705e6ca8a639daacfcf91217.idx differ diff --git a/git-odb/tests/fixtures/repos/small-packs.git/objects/pack/216edf2f7a671742705e6ca8a639daacfcf91217.pack b/git-odb/tests/fixtures/repos/small-packs.git/objects/pack/216edf2f7a671742705e6ca8a639daacfcf91217.pack new file mode 100644 index 00000000000..9bcf521aea3 Binary files /dev/null and b/git-odb/tests/fixtures/repos/small-packs.git/objects/pack/216edf2f7a671742705e6ca8a639daacfcf91217.pack differ diff --git a/git-odb/tests/fixtures/repos/small-packs.git/refs/heads/next b/git-odb/tests/fixtures/repos/small-packs.git/refs/heads/next new file mode 100644 index 00000000000..541dba416ac --- /dev/null +++ b/git-odb/tests/fixtures/repos/small-packs.git/refs/heads/next @@ -0,0 +1 @@ +ecc68100297fff843a7eef8df0d0fb80c1c8bac5 diff --git a/git-odb/tests/odb/mod.rs b/git-odb/tests/odb/mod.rs index 906571e70c4..dd71d689379 100644 --- a/git-odb/tests/odb/mod.rs +++ b/git-odb/tests/odb/mod.rs @@ -3,4 +3,5 @@ pub use git_testtools::{fixture_path, hex_to_id, scripted_fixture_repo_read_only pub type Result = std::result::Result>; pub mod alternate; +pub mod regression; pub mod store; diff --git a/git-odb/tests/odb/regression/mod.rs b/git-odb/tests/odb/regression/mod.rs new file mode 100644 index 00000000000..6018ade261d --- /dev/null +++ b/git-odb/tests/odb/regression/mod.rs @@ -0,0 +1,23 @@ +mod repo_with_small_packs { + use crate::odb::{fixture_path, hex_to_id}; + use git_odb::pack; + + #[test] + fn all_packed_objects_can_be_found() { + let store = git_odb::linked::Store::at(fixture_path("repos/small-packs.git/objects")).unwrap(); + assert_eq!(store.dbs.len(), 1, "a simple repo"); + let db = &store.dbs[0]; + assert_eq!(db.bundles.len(), 2, "small packs"); + let mut buf = Vec::new(); + assert!( + db.try_find( + hex_to_id("ecc68100297fff843a7eef8df0d0fb80c1c8bac5"), + &mut buf, + &mut pack::cache::Never + ) + .unwrap() + .is_some(), + "object is present and available" + ); + } +} diff --git a/git-pack/CHANGELOG.md b/git-pack/CHANGELOG.md index 651f6dc6483..7bc45a7fc70 100644 --- a/git-pack/CHANGELOG.md +++ b/git-pack/CHANGELOG.md @@ -1,3 +1,10 @@ +### 0.10.0 (2021-08-??) + +- **renames** + - `data::Object::into_commit_iter()` -> `data::Object::try_into_commit_iter()` + - `data::Object::into_tree_iter()` -> `data::Object::try_into_tree_iter()` + - `data::Object::into_tag_iter()` -> `data::Object::try_into_tag_iter()` + ### 0.9.0 (2021-08-27) - **renames / moves / visibility** diff --git a/git-pack/Cargo.toml b/git-pack/Cargo.toml index 3b2c9079eb4..442c4bea0e6 100644 --- a/git-pack/Cargo.toml +++ b/git-pack/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "git-pack" -version = "0.9.0" +version = "0.10.0" repository = "https://github.com/Byron/gitoxide" authors = ["Sebastian Thiel "] license = "MIT/Apache-2.0" @@ -33,7 +33,7 @@ all-features = true [dependencies] git-features = { version = "^0.16.0", path = "../git-features", features = ["crc32", "rustsha1", "progress", "zlib"] } -git-hash = { version = "^0.5.0", path = "../git-hash" } +git-hash = { version ="^0.6.0", path = "../git-hash" } git-object = { version ="^0.13.0", path = "../git-object" } git-traverse = { version ="^0.8.0", path = "../git-traverse" } git-diff = { version ="^0.9.0", path = "../git-diff" } diff --git a/git-pack/src/data/object.rs b/git-pack/src/data/object.rs index 580e3812501..4de8ae9baf4 100644 --- a/git-pack/src/data/object.rs +++ b/git-pack/src/data/object.rs @@ -29,7 +29,7 @@ impl<'a> Object<'a> { /// Returns this object as tree iterator to parse entries one at a time to avoid allocations, or /// `None` if this is not a tree object. - pub fn into_tree_iter(self) -> Option> { + pub fn try_into_tree_iter(self) -> Option> { match self.kind { git_object::Kind::Tree => Some(TreeRefIter::from_bytes(self.data)), _ => None, @@ -38,7 +38,7 @@ impl<'a> Object<'a> { /// Returns this object as commit iterator to parse tokens one at a time to avoid allocations, or /// `None` if this is not a commit object. - pub fn into_commit_iter(self) -> Option> { + pub fn try_into_commit_iter(self) -> Option> { match self.kind { git_object::Kind::Commit => Some(CommitRefIter::from_bytes(self.data)), _ => None, @@ -47,7 +47,7 @@ impl<'a> Object<'a> { /// Returns this object as tag iterator to parse tokens one at a time to avoid allocations, or /// `None` if this is not a tag object. - pub fn into_tag_iter(self) -> Option> { + pub fn try_into_tag_iter(self) -> Option> { match self.kind { git_object::Kind::Tag => Some(TagRefIter::from_bytes(self.data)), _ => None, diff --git a/git-pack/src/data/output/count/objects.rs b/git-pack/src/data/output/count/objects.rs index c358533261d..2cf58e101b4 100644 --- a/git-pack/src/data/output/count/objects.rs +++ b/git-pack/src/data/output/count/objects.rs @@ -217,7 +217,7 @@ where progress.inc(); stats.expanded_objects += 1; out.push(output::Count::from_data(oid, &obj)); - obj.into_tree_iter() + obj.try_into_tree_iter() } None => None, } @@ -300,7 +300,7 @@ where progress.inc(); stats.expanded_objects += 1; out.push(output::Count::from_data(oid, &obj)); - obj.into_tree_iter() + obj.try_into_tree_iter() } None => None, } diff --git a/git-pack/src/find_traits.rs b/git-pack/src/find_traits.rs index 3c766b106cf..aeb2c28efbd 100644 --- a/git-pack/src/find_traits.rs +++ b/git-pack/src/find_traits.rs @@ -127,9 +127,9 @@ mod ext { make_obj_lookup!(find_tree, ObjectRef::Tree, Kind::Tree, TreeRef<'a>); make_obj_lookup!(find_tag, ObjectRef::Tag, Kind::Tag, TagRef<'a>); make_obj_lookup!(find_blob, ObjectRef::Blob, Kind::Blob, BlobRef<'a>); - make_iter_lookup!(find_commit_iter, Kind::Blob, CommitRefIter<'a>, into_commit_iter); - make_iter_lookup!(find_tree_iter, Kind::Tree, TreeRefIter<'a>, into_tree_iter); - make_iter_lookup!(find_tag_iter, Kind::Tag, TagRefIter<'a>, into_tag_iter); + make_iter_lookup!(find_commit_iter, Kind::Blob, CommitRefIter<'a>, try_into_commit_iter); + make_iter_lookup!(find_tree_iter, Kind::Tree, TreeRefIter<'a>, try_into_tree_iter); + make_iter_lookup!(find_tag_iter, Kind::Tag, TagRefIter<'a>, try_into_tag_iter); } impl FindExt for T {} diff --git a/git-packetline/src/line/mod.rs b/git-packetline/src/line/mod.rs index ffe3977d7b7..15604d891e2 100644 --- a/git-packetline/src/line/mod.rs +++ b/git-packetline/src/line/mod.rs @@ -52,7 +52,7 @@ impl<'a> PacketLineRef<'a> { }) } - /// Decode the band of this [`slice`][PacketLineRef::as_slice()], or panic if it is not actually a side-band line. + /// Decode the band of this [`slice`][PacketLineRef::as_slice()] pub fn decode_band(&self) -> Result, decode::band::Error> { let d = self.as_slice().ok_or(decode::band::Error::NonDataLine)?; Ok(match d[0] { diff --git a/git-protocol/Cargo.toml b/git-protocol/Cargo.toml index d6d7ac4e249..20b12293527 100644 --- a/git-protocol/Cargo.toml +++ b/git-protocol/Cargo.toml @@ -29,7 +29,7 @@ required-features = ["async-client"] [dependencies] git-features = { version = "^0.16.0", path = "../git-features", features = ["progress"] } git-transport = { version ="^0.11.0", path = "../git-transport" } -git-hash = { version = "^0.5.0", path = "../git-hash" } +git-hash = { version ="^0.6.0", path = "../git-hash" } quick-error = "2.0.0" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} diff --git a/git-ref/CHANGELOG.md b/git-ref/CHANGELOG.md index b0cdc068605..a126f75f0e6 100644 --- a/git-ref/CHANGELOG.md +++ b/git-ref/CHANGELOG.md @@ -1,3 +1,20 @@ +### v0.7.0 + +#### Breaking + +* Replace `transaction::Create` with `transaction::PreviousValue` and remove `transaction::Create` +* Remove `file::Reference` in favor of `Reference` +* Move `file::log::Line` to `log::Line` +* `TargetRef::Symbolic(&BStr)` -> `TargetRef::Symbolic(FullNameRef)` +* replace `Transaction::namespacce()` with `file::Store::namespace` + +### v0.6.1 + +#### Bugfixes + +* splits of edits to symbolic references will now 'move' the desired previous values down to the + referents while resorting to not having any requirements in the symbolic ref instead. + ### v0.6.0 #### BREAKING diff --git a/git-ref/Cargo.toml b/git-ref/Cargo.toml index 43139a4607d..864a2292d28 100644 --- a/git-ref/Cargo.toml +++ b/git-ref/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "git-ref" -version = "0.6.0" +version = "0.7.0" repository = "https://github.com/Byron/gitoxide" license = "MIT/Apache-2.0" description = "A crate to handle git references" @@ -13,7 +13,7 @@ doctest = false test = true [features] -serde1 = ["serde", "bstr/serde1", "git-hash/serde1", "git-actor/serde1"] +serde1 = ["serde", "git-hash/serde1", "git-actor/serde1", "git-object/serde1"] internal-testing-git-features-parallel = ["git-features/parallel"] # test sorted parallel loose file traversal [[test]] @@ -25,7 +25,7 @@ required-features = ["internal-testing-git-features-parallel"] [dependencies] git-features = { version = "^0.16.0", path = "../git-features", features = ["walkdir"]} -git-hash = { version = "^0.5.0", path = "../git-hash" } +git-hash = { version ="^0.6.0", path = "../git-hash" } git-object = { version ="^0.13.0", path = "../git-object" } git-validate = { version = "^0.5.0", path = "../git-validate" } git-actor = { version ="^0.5.0", path = "../git-actor" } @@ -33,7 +33,6 @@ git-lock = { version ="^1.0.0", path = "../git-lock" } git-tempfile = { version ="^1.0.0", path = "../git-tempfile" } quick-error = "2.0.0" -bstr = { version = "0.2.13", default-features = false, features = ["std"] } nom = { version = "7", default-features = false, features = ["std"]} serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"]} os_str_bytes = "3.1.0" diff --git a/git-ref/src/fullname.rs b/git-ref/src/fullname.rs index df8eaf7a43c..bd213f06adc 100644 --- a/git-ref/src/fullname.rs +++ b/git-ref/src/fullname.rs @@ -1,8 +1,8 @@ use std::{borrow::Cow, convert::TryFrom, path::Path}; -use bstr::{BStr, BString, ByteSlice}; +use git_object::bstr::{BStr, BString, ByteSlice}; -use crate::FullName; +use crate::{bstr::ByteVec, FullName, FullNameRef, Namespace}; impl TryFrom<&str> for FullName { type Error = git_validate::refname::Error; @@ -38,6 +38,18 @@ impl TryFrom for FullName { } } +impl From for BString { + fn from(name: FullName) -> Self { + name.0 + } +} + +impl<'a> From> for &'a BStr { + fn from(name: FullNameRef<'a>) -> Self { + name.0 + } +} + impl<'a> From> for FullName { fn from(value: crate::FullNameRef<'a>) -> Self { FullName(value.as_bstr().into()) @@ -64,8 +76,34 @@ impl FullName { pub fn into_inner(self) -> BString { self.0 } + /// Return ourselves as byte string which is a valid refname pub fn as_bstr(&self) -> &BStr { self.0.as_bstr() } + + /// Modify ourself so that we use `namespace` as prefix, if it is not yet in the `namespace` + pub fn prefix_namespace(&mut self, namespace: &Namespace) -> &mut Self { + if !self.0.starts_with_str(&namespace.0) { + self.0.insert_str(0, &namespace.0); + } + self + } + + /// Strip the given `namespace` off the beginning of this name, if it is in this namespace. + pub fn strip_namespace(&mut self, namespace: &Namespace) -> &mut Self { + if self.0.starts_with_str(&namespace.0) { + let prev_len = self.0.len(); + self.0.copy_within(namespace.0.len().., 0); + self.0.resize(prev_len - namespace.0.len(), 0); + } + self + } +} + +impl<'a> FullNameRef<'a> { + /// Create an owned copy of ourself + pub fn to_owned(&self) -> FullName { + FullName(self.0.to_owned()) + } } diff --git a/git-ref/src/lib.rs b/git-ref/src/lib.rs index 8b9f7163a38..34b8c98acab 100644 --- a/git-ref/src/lib.rs +++ b/git-ref/src/lib.rs @@ -18,8 +18,9 @@ //! * supersedes all of the above to allow handling hundreds of thousands of references. #![forbid(unsafe_code)] #![deny(missing_docs, rust_2018_idioms)] -use bstr::{BStr, BString}; use git_hash::{oid, ObjectId}; +pub use git_object::bstr; +use git_object::bstr::{BStr, BString}; mod store; pub use store::{file, packed}; @@ -32,6 +33,19 @@ pub mod namespace; /// pub mod transaction; +mod parse; +mod raw; + +pub use raw::Reference; + +mod target; + +/// +pub mod log; + +/// +pub mod peel; + /// Indicate that the given BString is a validate reference name or path that can be used as path on disk or written as target /// of a symbolic reference #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] @@ -44,7 +58,7 @@ pub struct FullNameRef<'a>(&'a BStr); #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] pub struct PartialNameRef<'a>(&'a BStr); -/// A validated prefix for references to act as a namespace. +/// A _validated_ prefix for references to act as a namespace. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] pub struct Namespace(BString); @@ -56,7 +70,7 @@ pub enum Kind { Peeled, /// A ref that points to another reference, adding a level of indirection. /// - /// It can be resolved to an id using the [`peel_in_place_to_id()`][file::Reference::peel_to_id_in_place()] method. + /// It can be resolved to an id using the [`peel_in_place_to_id()`][`crate::file::ReferenceExt::peel_to_id_in_place()`] method. Symbolic, } @@ -77,8 +91,5 @@ pub enum TargetRef<'a> { /// A ref that points to an object id Peeled(&'a oid), /// A ref that points to another reference by its validated name, adding a level of indirection. - Symbolic(&'a BStr), + Symbolic(FullNameRef<'a>), } - -mod parse; -mod target; diff --git a/git-ref/src/log.rs b/git-ref/src/log.rs new file mode 100644 index 00000000000..78b2673f40a --- /dev/null +++ b/git-ref/src/log.rs @@ -0,0 +1,16 @@ +use git_hash::ObjectId; +use git_object::bstr::BString; + +/// A parsed ref log line that can be changed +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] +pub struct Line { + /// The previous object id. Can be a null-sha to indicate this is a line for a new ref. + pub previous_oid: ObjectId, + /// The new object id. Can be a null-sha to indicate this ref is being deleted. + pub new_oid: ObjectId, + /// The signature of the currently configured committer. + pub signature: git_actor::Signature, + /// The message providing details about the operation performed in this log line. + pub message: BString, +} diff --git a/git-ref/src/name.rs b/git-ref/src/name.rs index 4613335f8fe..dddded15d4c 100644 --- a/git-ref/src/name.rs +++ b/git-ref/src/name.rs @@ -4,7 +4,7 @@ use std::{ path::Path, }; -use bstr::{BStr, ByteSlice}; +use git_object::bstr::{BStr, ByteSlice}; use crate::{FullNameRef, PartialNameRef}; diff --git a/git-ref/src/namespace.rs b/git-ref/src/namespace.rs index 9cc9ec3d435..c7286f1340f 100644 --- a/git-ref/src/namespace.rs +++ b/git-ref/src/namespace.rs @@ -1,6 +1,11 @@ -use std::{borrow::Cow, convert::TryInto, path::Path}; +use std::{ + borrow::Cow, + convert::TryInto, + path::{Path, PathBuf}, +}; -use bstr::{BStr, BString, ByteSlice, ByteVec}; +use git_object::bstr::{BStr, BString, ByteSlice, ByteVec}; +use os_str_bytes::OsStrBytes; use crate::{Namespace, PartialNameRef}; @@ -17,6 +22,15 @@ impl Namespace { pub fn to_path(&self) -> Cow<'_, Path> { self.0.to_path().expect("UTF-8 conversion succeeds").into() } + /// Append the given `prefix` to this namespace so it becomes usable for prefixed iteration. + pub fn into_namespaced_prefix(mut self, prefix: impl AsRef) -> PathBuf { + self.0.push_str(prefix.as_ref().to_raw_bytes()); + #[cfg(windows)] + let path = self.0.replace(b"/", b"\\").into_path_buf(); + #[cfg(not(windows))] + let path = self.0.replace(b"\\", b"/").into_path_buf(); + path.expect("UTF-8 conversion succeeds") + } } /// Given a `namespace` 'foo we output 'refs/namespaces/foo', and given 'foo/bar' we output 'refs/namespaces/foo/refs/namespaces/bar'. diff --git a/git-ref/src/parse.rs b/git-ref/src/parse.rs index fe3d058c7e1..f009f6fede9 100644 --- a/git-ref/src/parse.rs +++ b/git-ref/src/parse.rs @@ -1,4 +1,4 @@ -use bstr::{BStr, ByteSlice}; +use git_object::bstr::{BStr, ByteSlice}; use nom::{ branch::alt, bytes::complete::{tag, take_while_m_n}, diff --git a/git-ref/src/peel.rs b/git-ref/src/peel.rs new file mode 100644 index 00000000000..02913d5ff79 --- /dev/null +++ b/git-ref/src/peel.rs @@ -0,0 +1,44 @@ +/// A function for use in [`crate::file::ReferenceExt::peel_to_id_in_place()`] to indicate no peeling should happen. +pub fn none( + _id: git_hash::ObjectId, + _buf: &mut Vec, +) -> Result, std::convert::Infallible> { + Ok(Some((git_object::Kind::Commit, &[]))) +} + +/// +pub mod to_id { + use std::path::PathBuf; + + use git_object::bstr::BString; + use quick_error::quick_error; + + use crate::file; + + quick_error! { + /// The error returned by [`crate::file::ReferenceExt::peel_to_id_in_place()`]. + #[derive(Debug)] + #[allow(missing_docs)] + pub enum Error { + Follow(err: file::find::existing::Error) { + display("Could not follow a single level of a symbolic reference") + from() + source(err) + } + Cycle(start_absolute: PathBuf){ + display("Aborting due to reference cycle with first seen path being '{}'", start_absolute.display()) + } + DepthLimitExceeded { max_depth: usize } { + display("Refusing to follow more than {} levels of indirection", max_depth) + } + Find(err: Box) { + display("An error occurred when trying to resolve an object a refererence points to") + from() + source(&**err) + } + NotFound{oid: git_hash::ObjectId, name: BString} { + display("Object {} as referred to by '{}' could not be found", oid, name) + } + } + } +} diff --git a/git-ref/src/raw.rs b/git-ref/src/raw.rs new file mode 100644 index 00000000000..7d692e653b7 --- /dev/null +++ b/git-ref/src/raw.rs @@ -0,0 +1,102 @@ +use git_hash::ObjectId; + +use crate::{FullName, Target}; + +/// A fully owned backend agnostic reference +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] +pub struct Reference { + /// The path to uniquely identify this ref within its store. + pub name: FullName, + /// The target of the reference, either a symbolic reference by full name or a possibly intermediate object by its id. + pub target: Target, + /// The fully peeled object to which this reference ultimately points to. Only guaranteed to be set after `peel_to_id_in_place()` was called. + pub peeled: Option, +} + +mod convert { + use git_hash::ObjectId; + + use crate::{ + raw::Reference, + store::{file::loose, packed}, + Target, + }; + + impl From for loose::Reference { + fn from(value: Reference) -> Self { + loose::Reference { + name: value.name, + target: value.target, + } + } + } + + impl From for Reference { + fn from(value: loose::Reference) -> Self { + Reference { + name: value.name, + target: value.target, + peeled: None, + } + } + } + + impl<'p> From> for Reference { + fn from(value: packed::Reference<'p>) -> Self { + Reference { + name: value.name.into(), + target: Target::Peeled(value.target()), + peeled: value + .object + .map(|hex| ObjectId::from_hex(hex).expect("parser validation")), + } + } + } +} + +mod access { + use git_object::bstr::ByteSlice; + + use crate::{raw::Reference, FullNameRef, Namespace, Target}; + + impl Reference { + /// Returns the kind of reference based on its target + pub fn kind(&self) -> crate::Kind { + self.target.kind() + } + + /// Return the full validated name of the reference, with the given namespace stripped if possible. + /// + /// If the reference name wasn't prefixed with `namespace`, `None` is returned instead. + pub fn name_without_namespace(&self, namespace: &Namespace) -> Option> { + self.name + .0 + .as_bstr() + .strip_prefix(namespace.0.as_bstr().as_ref()) + .map(|stripped| FullNameRef(stripped.as_bstr())) + } + + /// Strip the given namespace from our name as well as the name, but not the reference we point to. + pub fn strip_namespace(&mut self, namespace: &Namespace) -> &mut Self { + self.name.strip_namespace(namespace); + if let Target::Symbolic(name) = &mut self.target { + name.strip_namespace(namespace); + } + self + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn size_of_reference() { + assert_eq!( + std::mem::size_of::(), + 80, + "let's not let it change size undetected" + ); + } +} diff --git a/git-ref/src/store/file/find.rs b/git-ref/src/store/file/find.rs index 9f2d736ba04..c66b5a34a7c 100644 --- a/git-ref/src/store/file/find.rs +++ b/git-ref/src/store/file/find.rs @@ -4,8 +4,8 @@ use std::{ path::{Path, PathBuf}, }; -use bstr::ByteSlice; pub use error::Error; +use git_object::bstr::ByteSlice; use crate::{ file, @@ -13,7 +13,7 @@ use crate::{ file::{loose, path_to_name}, packed, }, - FullName, PartialNameRef, + FullName, PartialNameRef, Reference, }; enum Transform { @@ -31,14 +31,17 @@ impl file::Store { /// /// ### Note /// - /// The lookup algorithm follows the one in [the git documentation][git-lookup-docs]. + /// * The lookup algorithm follows the one in [the git documentation][git-lookup-docs]. + /// * Namespaced names will only be found if they are fully qualified. They can, however, be found during iteration. + /// This shortcoming can be fixed if there is demand by introducing `try_find_in_namespace(…)` or by letting `PartialNameRef` have + /// a namespace reference, too. /// /// [git-lookup-docs]: https://github.com/git/git/blob/5d5b1473453400224ebb126bf3947e0a3276bdf5/Documentation/revisions.txt#L34-L46 - pub fn try_find<'a, 'p, 's, Name, E>( - &'s self, + pub fn try_find<'a, Name, E>( + &self, partial: Name, - packed: Option<&'p packed::Buffer>, - ) -> Result>, Error> + packed: Option<&packed::Buffer>, + ) -> Result, Error> where Name: TryInto, Error = E>, Error: From, @@ -61,11 +64,11 @@ impl file::Store { .map(|r| r.map(|r| r.try_into().expect("only loose refs are found without pack"))) } - pub(in crate::store::file) fn find_one_with_verified_input<'p>( + pub(crate) fn find_one_with_verified_input<'p>( &self, relative_path: &Path, packed: Option<&'p packed::Buffer>, - ) -> Result>, Error> { + ) -> Result, Error> { let is_all_uppercase = relative_path .to_string_lossy() .as_ref() @@ -94,13 +97,13 @@ impl file::Store { ) } - fn find_inner<'p>( + fn find_inner( &self, inbetween: &str, relative_path: &Path, - packed: Option<&'p packed::Buffer>, + packed: Option<&packed::Buffer>, transform: Transform, - ) -> Result>, Error> { + ) -> Result, Error> { let (base, is_definitely_absolute) = match transform { Transform::EnforceRefsPrefix => ( if relative_path.starts_with("refs") { @@ -118,10 +121,17 @@ impl file::Store { None => { if is_definitely_absolute { if let Some(packed) = packed { - let full_name = path_to_name(relative_path); + let full_name = path_to_name(match &self.namespace { + None => relative_path, + Some(namespace) => namespace.to_owned().into_namespaced_prefix(relative_path), + }); let full_name = PartialNameRef((*full_name).as_bstr()); if let Some(packed_ref) = packed.try_find(full_name)? { - return Ok(Some(file::Reference::Packed(packed_ref))); + let mut res: Reference = packed_ref.into(); + if let Some(namespace) = &self.namespace { + res.strip_namespace(namespace); + } + return Ok(Some(res)); }; } } @@ -132,7 +142,13 @@ impl file::Store { Ok(Some({ let full_name = path_to_name(&relative_path); loose::Reference::try_from_path(FullName(full_name), &contents) - .map(file::Reference::Loose) + .map(Into::into) + .map(|mut r: Reference| { + if let Some(namespace) = &self.namespace { + r.strip_namespace(namespace); + } + r + }) .map_err(|err| Error::ReferenceCreation { err, relative_path })? })) } @@ -141,7 +157,10 @@ impl file::Store { impl file::Store { /// Implements the logic required to transform a fully qualified refname into a filesystem path pub(crate) fn reference_path(&self, name: &Path) -> PathBuf { - self.base.join(name) + match &self.namespace { + None => self.base.join(name), + Some(namespace) => self.base.join(namespace.to_path()).join(name), + } } /// Read the file contents with a verified full reference path and return it in the given vector if possible. @@ -176,16 +195,12 @@ pub mod existing { file::{find, loose}, packed, }, - PartialNameRef, + PartialNameRef, Reference, }; impl file::Store { /// Similar to [`file::Store::find()`] but a non-existing ref is treated as error. - pub fn find<'a, 'p, 's, Name, E>( - &'s self, - partial: Name, - packed: Option<&'p packed::Buffer>, - ) -> Result, Error> + pub fn find<'a, Name, E>(&self, partial: Name, packed: Option<&packed::Buffer>) -> Result where Name: TryInto, Error = E>, crate::name::Error: From, diff --git a/git-ref/src/store/file/log/iter.rs b/git-ref/src/store/file/log/iter.rs index f50aebcb246..28d4e0a1ecd 100644 --- a/git-ref/src/store/file/log/iter.rs +++ b/git-ref/src/store/file/log/iter.rs @@ -1,9 +1,6 @@ -use bstr::ByteSlice; +use git_object::bstr::ByteSlice; -use crate::store::{ - file, - file::{log, log::iter::decode::LineNumber}, -}; +use crate::store::file::{log, log::iter::decode::LineNumber}; /// pub mod decode { @@ -54,14 +51,28 @@ pub mod decode { /// This iterator is useful when the ref log file is going to be rewritten which forces processing of the entire file. /// It will continue parsing even if individual log entries failed to parse, leaving it to the driver to decide whether to /// abort or continue. -pub fn forward(lines: &[u8]) -> impl Iterator, decode::Error>> { - lines.as_bstr().lines().enumerate().map(|(ln, line)| { - log::LineRef::from_bytes(line).map_err(|err| decode::Error::new(err, decode::LineNumber::FromStart(ln))) - }) +pub fn forward(lines: &[u8]) -> Forward<'_> { + Forward { + inner: lines.as_bstr().lines().enumerate(), + } +} + +/// An iterator yielding parsed lines in a file from start to end, oldest to newest. +pub struct Forward<'a> { + inner: std::iter::Enumerate>, +} + +impl<'a> Iterator for Forward<'a> { + type Item = Result, decode::Error>; + + fn next(&mut self) -> Option { + self.inner.next().map(|(ln, line)| { + log::LineRef::from_bytes(line).map_err(|err| decode::Error::new(err, decode::LineNumber::FromStart(ln))) + }) + } } -/// An iterator yielding parsed lines in a file in reverse. -#[allow(dead_code)] +/// An iterator yielding parsed lines in a file in reverse, most recent to oldest. pub struct Reverse<'a, F> { buf: &'a mut [u8], count: usize, @@ -84,6 +95,12 @@ where F: std::io::Read + std::io::Seek, { let pos = log.seek(std::io::SeekFrom::End(0))?; + if buf.is_empty() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Zero sized buffers are not allowed, use 256 bytes or more for typical logs", + )); + } Ok(Reverse { buf, count: 0, @@ -92,11 +109,36 @@ where }) } +/// +pub mod reverse { + use quick_error::quick_error; + + use super::decode; + + quick_error! { + /// The error returned by the [`Reverse`][super::Reverse] iterator + #[derive(Debug)] + #[allow(missing_docs)] + pub enum Error { + Io(err: std::io::Error) { + display("The buffer could not be filled to make more lines available") + from() + source(err) + } + Decode(err: decode::Error) { + display("Could not decode log line") + from() + source(err) + } + } + } +} + impl<'a, F> Iterator for Reverse<'a, F> where F: std::io::Read + std::io::Seek, { - type Item = std::io::Result>; + type Item = Result; fn next(&mut self) -> Option { match (self.last_nl_pos.take(), self.read_and_pos.take()) { @@ -104,7 +146,7 @@ where (None, Some((mut read, pos))) => { let npos = pos.saturating_sub(self.buf.len() as u64); if let Err(err) = read.seek(std::io::SeekFrom::Start(npos)) { - return Some(Err(err)); + return Some(Err(err.into())); } let n = (pos - npos) as usize; @@ -113,7 +155,7 @@ where } let buf = &mut self.buf[..n]; if let Err(err) = read.read_exact(buf) { - return Some(Err(err)); + return Some(Err(err.into())); }; let last_byte = *buf.last().expect("we have read non-zero bytes before"); @@ -127,9 +169,13 @@ where self.read_and_pos = Some(read_and_pos); self.last_nl_pos = Some(start); let buf = &self.buf[start + 1..end]; - let res = Some(Ok(log::LineRef::from_bytes(buf) - .map_err(|err| decode::Error::new(err, LineNumber::FromEnd(self.count))) - .map(Into::into))); + let res = Some( + log::LineRef::from_bytes(buf) + .map_err(|err| { + reverse::Error::Decode(decode::Error::new(err, LineNumber::FromEnd(self.count))) + }) + .map(Into::into), + ); self.count += 1; res } @@ -137,24 +183,29 @@ where let (mut read, last_read_pos) = read_and_pos; if last_read_pos == 0 { let buf = &self.buf[..end]; - Some(Ok(log::LineRef::from_bytes(buf) - .map_err(|err| decode::Error::new(err, LineNumber::FromEnd(self.count))) - .map(Into::into))) + Some( + log::LineRef::from_bytes(buf) + .map_err(|err| { + reverse::Error::Decode(decode::Error::new(err, LineNumber::FromEnd(self.count))) + }) + .map(Into::into), + ) } else { let npos = last_read_pos.saturating_sub((self.buf.len() - end) as u64); if npos == last_read_pos { return Some(Err(std::io::Error::new( std::io::ErrorKind::Other, "buffer too small for line size", - ))); + ) + .into())); } let n = (last_read_pos - npos) as usize; self.buf.copy_within(0..end, n); if let Err(err) = read.seek(std::io::SeekFrom::Start(npos)) { - return Some(Err(err)); + return Some(Err(err.into())); } if let Err(err) = read.read_exact(&mut self.buf[..n]) { - return Some(Err(err)); + return Some(Err(err.into())); } self.read_and_pos = Some((read, npos)); self.last_nl_pos = Some(n + end); diff --git a/git-ref/src/store/file/log/line.rs b/git-ref/src/store/file/log/line.rs index 3c7cd67b06e..a9dfeb21f8e 100644 --- a/git-ref/src/store/file/log/line.rs +++ b/git-ref/src/store/file/log/line.rs @@ -1,6 +1,6 @@ use git_hash::ObjectId; -use crate::store::file::log::{Line, LineRef}; +use crate::{log::Line, store::file::log::LineRef}; impl<'a> LineRef<'a> { /// Convert this instance into its mutable counterpart @@ -12,10 +12,10 @@ impl<'a> LineRef<'a> { mod write { use std::io; - use bstr::{BStr, ByteSlice}; + use git_object::bstr::{BStr, ByteSlice}; use quick_error::quick_error; - use crate::store::file::log::Line; + use crate::log::Line; quick_error! { /// The Error produced by [`Line::write_to()`] (but wrapped in an io error). @@ -77,7 +77,7 @@ impl<'a> From> for Line { /// pub mod decode { - use bstr::{BStr, ByteSlice}; + use git_object::bstr::{BStr, ByteSlice}; use nom::{ bytes::complete::{tag, take_while}, combinator::opt, @@ -90,7 +90,7 @@ pub mod decode { /// mod error { - use bstr::{BString, ByteSlice}; + use git_object::bstr::{BString, ByteSlice}; /// The error returned by [from_bytes(…)][super::Line::from_bytes()] #[derive(Debug)] @@ -172,9 +172,9 @@ pub mod decode { #[cfg(test)] mod test { - use bstr::ByteSlice; use git_actor::{Sign, Time}; use git_hash::ObjectId; + use git_object::bstr::ByteSlice; use super::*; diff --git a/git-ref/src/store/file/log/mod.rs b/git-ref/src/store/file/log/mod.rs index c4630a72484..46d64c26b6d 100644 --- a/git-ref/src/store/file/log/mod.rs +++ b/git-ref/src/store/file/log/mod.rs @@ -1,6 +1,4 @@ -use bstr::BStr; -use git_hash::ObjectId; -use git_object::bstr::BString; +use git_object::bstr::BStr; pub use super::loose::reflog::{create_or_update, Error}; @@ -23,17 +21,3 @@ pub struct LineRef<'a> { /// The message providing details about the operation performed in this log line. pub message: &'a BStr, } - -/// A parsed ref log line that can be changed -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] -pub struct Line { - /// The previous object id. Can be a null-sha to indicate this is a line for a new ref. - pub previous_oid: ObjectId, - /// The new object id. Can be a null-sha to indicate this ref is being deleted. - pub new_oid: ObjectId, - /// The signature of the currently configured committer. - pub signature: git_actor::Signature, - /// The message providing details about the operation performed in this log line. - pub message: BString, -} diff --git a/git-ref/src/store/file/loose/iter.rs b/git-ref/src/store/file/loose/iter.rs index 2e5bd6d9c1f..0377b517306 100644 --- a/git-ref/src/store/file/loose/iter.rs +++ b/git-ref/src/store/file/loose/iter.rs @@ -3,8 +3,8 @@ use std::{ path::{Path, PathBuf}, }; -use bstr::ByteSlice; use git_features::fs::walkdir::DirEntryIter; +use git_object::bstr::ByteSlice; use os_str_bytes::OsStrBytes; use crate::{ @@ -113,6 +113,10 @@ impl file::Store { /// /// Reference files that do not constitute valid names will be silently ignored. /// + /// # Note + /// + /// There is no namespace support in loose file iterators. It can be emulated using `loose_iter_prefixed(…)`. + /// /// See [`Store::packed()`][file::Store::packed_buffer()] for interacting with packed references. pub fn loose_iter(&self) -> std::io::Result { let refs = self.refs_dir(); diff --git a/git-ref/src/store/file/loose/mod.rs b/git-ref/src/store/file/loose/mod.rs index 1278830e24b..c07a50c2f94 100644 --- a/git-ref/src/store/file/loose/mod.rs +++ b/git-ref/src/store/file/loose/mod.rs @@ -36,6 +36,7 @@ mod init { file::Store { base: git_dir.into(), write_reflog, + namespace: None, } } } diff --git a/git-ref/src/store/file/loose/reference/decode.rs b/git-ref/src/store/file/loose/reference/decode.rs index c59c1f9802f..21db27cb2ac 100644 --- a/git-ref/src/store/file/loose/reference/decode.rs +++ b/git-ref/src/store/file/loose/reference/decode.rs @@ -1,7 +1,7 @@ use std::convert::{TryFrom, TryInto}; -use bstr::BString; use git_hash::ObjectId; +use git_object::bstr::BString; use nom::{ bytes::complete::{tag, take_while}, combinator::{map, opt}, diff --git a/git-ref/src/store/file/loose/reference/logiter.rs b/git-ref/src/store/file/loose/reference/logiter.rs index 0b6ee882105..cf82b6c938b 100644 --- a/git-ref/src/store/file/loose/reference/logiter.rs +++ b/git-ref/src/store/file/loose/reference/logiter.rs @@ -3,7 +3,7 @@ use crate::store::{ file::{log, loose, loose::Reference}, }; -pub(in crate::store::file) fn must_be_io_err(err: loose::reflog::Error) -> std::io::Error { +pub(crate) fn must_be_io_err(err: loose::reflog::Error) -> std::io::Error { match err { loose::reflog::Error::Io(err) => err, loose::reflog::Error::RefnameValidation(_) => unreachable!("we are called from a valid ref"), diff --git a/git-ref/src/store/file/loose/reference/mod.rs b/git-ref/src/store/file/loose/reference/mod.rs index fa1811942e8..78bb22c1091 100644 --- a/git-ref/src/store/file/loose/reference/mod.rs +++ b/git-ref/src/store/file/loose/reference/mod.rs @@ -1,7 +1,4 @@ -pub(in crate::store::file) mod logiter; - -/// -pub mod peel; +pub(in crate) mod logiter; /// pub mod decode; diff --git a/git-ref/src/store/file/loose/reference/peel.rs b/git-ref/src/store/file/loose/reference/peel.rs deleted file mode 100644 index 865ebd8af71..00000000000 --- a/git-ref/src/store/file/loose/reference/peel.rs +++ /dev/null @@ -1,173 +0,0 @@ -use quick_error::quick_error; - -use crate::{ - file, - store::{ - file::{find, loose}, - packed, - }, - Target, -}; - -quick_error! { - /// The error returned by [`loose::Reference::follow_symbolic()`]. - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - FindExisting(err: find::existing::Error) { - display("Could not resolve symbolic reference name that is expected to exist") - source(err) - } - Decode(err: loose::reference::decode::Error) { - display("The reference could not be decoded.") - source(err) - } - } -} - -/// A function for use in [`loose::Reference::peel_to_id_in_place()`] to indicate no peeling should happen. -pub fn none( - _id: git_hash::ObjectId, - _buf: &mut Vec, -) -> Result, std::convert::Infallible> { - Ok(Some((git_object::Kind::Commit, &[]))) -} - -impl loose::Reference { - /// Follow this symbolic reference one level and return the ref it refers to, possibly providing access to `packed` references for lookup. - /// - /// Returns `None` if this is not a symbolic reference, hence the leaf of the chain. - pub fn follow_symbolic<'p>( - &self, - store: &file::Store, - packed: Option<&'p packed::Buffer>, - ) -> Option, Error>> { - match &self.target { - Target::Peeled(_) => None, - Target::Symbolic(full_name) => { - let path = full_name.to_path(); - match store.find_one_with_verified_input(path.as_ref(), packed) { - Ok(Some(next)) => Some(Ok(next)), - Ok(None) => Some(Err(Error::FindExisting(find::existing::Error::NotFound( - path.into_owned(), - )))), - Err(err) => Some(Err(Error::FindExisting(find::existing::Error::Find(err)))), - } - } - } - } -} - -/// -pub mod to_id { - use std::{collections::BTreeSet, path::PathBuf}; - - use bstr::BString; - use git_hash::oid; - use quick_error::quick_error; - - use crate::{ - store::{file, file::loose, packed}, - FullName, Target, - }; - - quick_error! { - /// The error returned by [`Reference::peel_to_id_in_place()`]. - #[derive(Debug)] - #[allow(missing_docs)] - pub enum Error { - Follow(err: loose::reference::peel::Error) { - display("Could not follow a single level of a symbolic reference") - from() - source(err) - } - Cycle(start_absolute: PathBuf){ - display("Aborting due to reference cycle with first seen path being '{}'", start_absolute.display()) - } - DepthLimitExceeded{ max_depth: usize } { - display("Refusing to follow more than {} levels of indirection", max_depth) - } - Find(err: Box) { - display("An error occurred when trying to resolve an object a refererence points to") - from() - source(&**err) - } - NotFound{oid: git_hash::ObjectId, name: BString} { - display("Object {} as referred to by '{}' could not be found", oid, name) - } - } - } - - impl loose::Reference { - /// Follow this symbolic reference until the end of the chain is reached and an object ID is available, - /// and possibly peel this object until the final target object is revealed. - /// - /// Use [`peel::none()`][super::none()] - /// - /// If an error occurs this reference remains unchanged. - pub fn peel_to_id_in_place( - &mut self, - store: &file::Store, - packed: Option<&packed::Buffer>, - mut find: impl FnMut(git_hash::ObjectId, &mut Vec) -> Result, E>, - ) -> Result<&oid, Error> { - let mut seen = BTreeSet::new(); - let mut storage; - let mut cursor = &mut *self; - while let Some(next) = cursor.follow_symbolic(store, packed) { - let next_ref = next?; - if let crate::Kind::Peeled = next_ref.kind() { - match next_ref { - file::Reference::Loose(r) => { - *self = r; - break; - } - file::Reference::Packed(p) => { - self.target = Target::Peeled(p.object()); - self.name = FullName(p.name.0.to_owned()); - return Ok(self.target.as_id().expect("we just set a peeled id")); - } - }; - } - storage = next_ref; - cursor = match &mut storage { - file::Reference::Loose(r) => r, - file::Reference::Packed(_) => unreachable!("handled above - we are done"), - }; - if seen.contains(&cursor.name) { - return Err(Error::Cycle(store.base.join(cursor.name.to_path()))); - } - seen.insert(cursor.name.clone()); - const MAX_REF_DEPTH: usize = 5; - if seen.len() == MAX_REF_DEPTH { - return Err(Error::DepthLimitExceeded { - max_depth: MAX_REF_DEPTH, - }); - } - } - - let mut buf = Vec::new(); - let mut oid = self.target.as_id().expect("peeled ref").to_owned(); - self.target = Target::Peeled(loop { - let (kind, data) = find(oid, &mut buf) - .map_err(|err| Box::new(err) as Box)? - .ok_or_else(|| Error::NotFound { - oid, - name: self.name.0.clone(), - })?; - match kind { - git_object::Kind::Tag => { - oid = git_object::TagRefIter::from_bytes(data) - .target_id() - .ok_or_else(|| Error::NotFound { - oid, - name: self.name.0.clone(), - })?; - } - _ => break oid, - }; - }); - Ok(self.target.as_id().expect("to be peeled")) - } - } -} diff --git a/git-ref/src/store/file/loose/reflog.rs b/git-ref/src/store/file/loose/reflog.rs index e602c1a263e..daff52c3456 100644 --- a/git-ref/src/store/file/loose/reflog.rs +++ b/git-ref/src/store/file/loose/reflog.rs @@ -52,7 +52,7 @@ impl file::Store { &self, name: Name, buf: &'b mut Vec, - ) -> Result, log::iter::decode::Error>>>, Error> + ) -> Result>, Error> where Name: TryInto, Error = E>, crate::name::Error: From, @@ -89,8 +89,8 @@ pub mod create_or_update { path::{Path, PathBuf}, }; - use bstr::BStr; use git_hash::{oid, ObjectId}; + use git_object::bstr::BStr; use crate::store::{file, file::WriteReflog}; @@ -152,7 +152,7 @@ pub mod create_or_update { write!( file, "{} {} ", - previous_oid.unwrap_or_else(|| ObjectId::null_sha(new.kind())), + previous_oid.unwrap_or_else(|| ObjectId::null(new.kind())), new ) .and_then(|_| committer.write_to(&mut file)) diff --git a/git-ref/src/store/file/loose/reflog/create_or_update/tests.rs b/git-ref/src/store/file/loose/reflog/create_or_update/tests.rs index 95389187b6b..3f7102b9c98 100644 --- a/git-ref/src/store/file/loose/reflog/create_or_update/tests.rs +++ b/git-ref/src/store/file/loose/reflog/create_or_update/tests.rs @@ -1,8 +1,8 @@ use super::*; -use crate::{file::WriteReflog, store::file::log, FullNameRef}; -use bstr::ByteSlice; +use crate::{file::WriteReflog, FullNameRef}; use git_actor::{Sign, Signature, Time}; use git_lock::acquire::Fail; +use git_object::bstr::ByteSlice; use git_testtools::hex_to_id; use std::{convert::TryInto, path::Path}; use tempfile::TempDir; @@ -25,11 +25,11 @@ fn reflock(store: &file::Store, full_name: &str) -> Result { .map_err(Into::into) } -fn reflog_lines(store: &file::Store, name: &str, buf: &mut Vec) -> Result> { +fn reflog_lines(store: &file::Store, name: &str, buf: &mut Vec) -> Result> { store .reflog_iter(name, buf)? .expect("existing reflog") - .map(|l| l.map(log::Line::from)) + .map(|l| l.map(crate::log::Line::from)) .collect::, _>>() .map_err(Into::into) } @@ -83,7 +83,7 @@ fn missing_reflog_creates_it_even_if_similarly_named_empty_dir_exists_and_append WriteReflog::Normal => { assert_eq!( reflog_lines(&store, full_name, &mut buf)?, - vec![log::Line { + vec![crate::log::Line { previous_oid: ObjectId::null_sha1(), new_oid: new, signature: committer.clone(), @@ -104,7 +104,7 @@ fn missing_reflog_creates_it_even_if_similarly_named_empty_dir_exists_and_append assert_eq!(lines.len(), 2, "now there is another line"); assert_eq!( lines.last().expect("non-empty"), - &log::Line { + &crate::log::Line { previous_oid: previous, new_oid: new, signature: committer.clone(), diff --git a/git-ref/src/store/file/mod.rs b/git-ref/src/store/file/mod.rs index 023a907426c..892fc74004e 100644 --- a/git-ref/src/store/file/mod.rs +++ b/git-ref/src/store/file/mod.rs @@ -18,7 +18,7 @@ impl Default for WriteReflog { /// A store for reference which uses plain files. /// /// Each ref is represented as a single file on disk in a folder structure that follows the relative path -/// used to identify [references][Reference]. +/// used to identify [references][crate::Reference]. #[derive(Debug, PartialOrd, PartialEq, Ord, Eq, Hash, Clone)] pub struct Store { /// The location at which loose references can be found as per conventions of a typical git repository. @@ -27,6 +27,8 @@ pub struct Store { pub base: PathBuf, /// The way to handle reflog edits pub write_reflog: WriteReflog, + /// The namespace to use for edits and reads + pub namespace: Option, } /// A transaction on a file store @@ -35,15 +37,14 @@ pub struct Transaction<'s> { packed_transaction: Option, updates: Option>, packed_refs: transaction::PackedRefs, - namespace: Option, } -pub(in crate::store::file) fn path_to_name(path: impl Into) -> bstr::BString { +pub(in crate::store::file) fn path_to_name(path: impl Into) -> git_object::bstr::BString { use os_str_bytes::OsStringBytes; let path = path.into().into_raw_vec(); #[cfg(windows)] let path = { - use bstr::ByteSlice; + use git_object::bstr::ByteSlice; path.replace(b"\\", b"/") }; path.into() @@ -51,18 +52,18 @@ pub(in crate::store::file) fn path_to_name(path: impl Into) -> bstr::BS /// pub mod loose; -mod overlay; +mod overlay_iter; /// pub mod iter { pub use super::{ loose::iter::{loose, Loose}, - overlay::LooseThenPacked, + overlay_iter::LooseThenPacked, }; /// pub mod loose_then_packed { - pub use super::super::overlay::Error; + pub use super::super::overlay_iter::Error; } } @@ -72,11 +73,13 @@ pub mod log; /// pub mod find; -mod reference; -pub use reference::Reference; - /// pub mod transaction; /// pub mod packed; + +mod raw_ext; +pub use raw_ext::ReferenceExt; + +use crate::Namespace; diff --git a/git-ref/src/store/file/overlay.rs b/git-ref/src/store/file/overlay_iter.rs similarity index 69% rename from git-ref/src/store/file/overlay.rs rename to git-ref/src/store/file/overlay_iter.rs index f77805229e3..21fb2c18052 100644 --- a/git-ref/src/store/file/overlay.rs +++ b/git-ref/src/store/file/overlay_iter.rs @@ -6,9 +6,9 @@ use std::{ }; use crate::{ - file::{loose, path_to_name, Reference}, + file::{loose, path_to_name}, store::{file, packed}, - FullName, + FullName, Namespace, Reference, }; /// An iterator stepping through sorted input of loose references and packed references, preferring loose refs over otherwise @@ -20,26 +20,37 @@ pub struct LooseThenPacked<'p, 's> { packed: Option>>, loose: Peekable, buf: Vec, + namespace: Option<&'s Namespace>, } impl<'p, 's> LooseThenPacked<'p, 's> { + fn strip_namespace(&self, mut r: Reference) -> Reference { + if let Some(namespace) = &self.namespace { + r.strip_namespace(namespace); + } + r + } + fn convert_packed( &mut self, packed: Result, packed::iter::Error>, - ) -> Result, Error> { - packed.map(Reference::Packed).map_err(|err| match err { - packed::iter::Error::Reference { - invalid_line, - line_number, - } => Error::PackedReference { - invalid_line, - line_number, - }, - packed::iter::Error::Header { .. } => unreachable!("this one only happens on iteration creation"), - }) + ) -> Result { + packed + .map(Into::into) + .map(|r| self.strip_namespace(r)) + .map_err(|err| match err { + packed::iter::Error::Reference { + invalid_line, + line_number, + } => Error::PackedReference { + invalid_line, + line_number, + }, + packed::iter::Error::Header { .. } => unreachable!("this one only happens on iteration creation"), + }) } - fn convert_loose(&mut self, res: std::io::Result<(PathBuf, FullName)>) -> Result, Error> { + fn convert_loose(&mut self, res: std::io::Result<(PathBuf, FullName)>) -> Result { let (refpath, name) = res.map_err(Error::Traversal)?; std::fs::File::open(&refpath) .and_then(|mut f| { @@ -52,12 +63,13 @@ impl<'p, 's> LooseThenPacked<'p, 's> { err, relative_path: refpath.strip_prefix(&self.base).expect("base contains path").into(), }) - .map(Reference::Loose) + .map(Into::into) + .map(|r| self.strip_namespace(r)) } } impl<'p, 's> Iterator for LooseThenPacked<'p, 's> { - type Item = Result, Error>; + type Item = Result; fn next(&mut self) -> Option { match self.packed.as_mut() { @@ -104,20 +116,25 @@ impl file::Store { /// /// Errors are returned similarly to what would happen when loose and packed refs where iterated by themeselves. pub fn iter<'p, 's>(&'s self, packed: Option<&'p packed::Buffer>) -> std::io::Result> { - Ok(LooseThenPacked { - base: &self.base, - packed: match packed { - Some(packed) => Some( - packed - .iter() - .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))? - .peekable(), - ), - None => None, - }, - loose: loose::iter::SortedLoosePaths::at_root_with_names(self.refs_dir(), self.base.to_owned()).peekable(), - buf: Vec::new(), - }) + match &self.namespace { + Some(namespace) => self.iter_prefixed_unvalidated(packed, namespace.to_path()), + None => Ok(LooseThenPacked { + base: &self.base, + packed: match packed { + Some(packed) => Some( + packed + .iter() + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))? + .peekable(), + ), + None => None, + }, + loose: loose::iter::SortedLoosePaths::at_root_with_names(self.refs_dir(), self.base.to_owned()) + .peekable(), + buf: Vec::new(), + namespace: None, + }), + } } /// As [`iter(…)`][file::Store::iter()], but filters by `prefix`, i.e. "refs/heads". @@ -128,7 +145,21 @@ impl file::Store { packed: Option<&'p packed::Buffer>, prefix: impl AsRef, ) -> std::io::Result> { - let packed_prefix = path_to_name(self.validate_prefix(prefix.as_ref())?); + self.validate_prefix(prefix.as_ref())?; + match &self.namespace { + None => self.iter_prefixed_unvalidated(packed, prefix), + Some(namespace) => { + self.iter_prefixed_unvalidated(packed, namespace.to_owned().into_namespaced_prefix(prefix)) + } + } + } + + fn iter_prefixed_unvalidated<'p, 's>( + &'s self, + packed: Option<&'p packed::Buffer>, + prefix: impl AsRef, + ) -> std::io::Result> { + let packed_prefix = path_to_name(prefix.as_ref()); Ok(LooseThenPacked { base: &self.base, packed: match packed { @@ -143,6 +174,7 @@ impl file::Store { loose: loose::iter::SortedLoosePaths::at_root_with_names(self.base.join(prefix), self.base.to_owned()) .peekable(), buf: Vec::new(), + namespace: self.namespace.as_ref(), }) } } @@ -150,7 +182,7 @@ impl file::Store { mod error { use std::{io, path::PathBuf}; - use bstr::BString; + use git_object::bstr::BString; use quick_error::quick_error; use crate::store::file; diff --git a/git-ref/src/store/file/raw_ext.rs b/git-ref/src/store/file/raw_ext.rs new file mode 100644 index 00000000000..7d03a231ee6 --- /dev/null +++ b/git-ref/src/store/file/raw_ext.rs @@ -0,0 +1,162 @@ +use std::collections::BTreeSet; + +use git_hash::ObjectId; + +use crate::{ + peel, + raw::Reference, + store::{ + file, + file::{log, loose::reference::logiter::must_be_io_err}, + packed, + }, + Target, +}; + +pub trait Sealed {} +impl Sealed for crate::Reference {} + +/// A trait to extend [Reference][crate::Reference] with functionality requiring a [file::Store]. +pub trait ReferenceExt: Sealed { + /// Obtain a reverse iterator over logs of this reference. See [crate::file::loose::Reference::log_iter_rev()] for details. + fn log_iter_rev<'b>( + &self, + store: &file::Store, + buf: &'b mut [u8], + ) -> std::io::Result>>; + + /// Obtain an iterator over logs of this reference. See [crate::file::loose::Reference::log_iter()] for details. + fn log_iter<'a, 'b: 'a>( + &'a self, + store: &file::Store, + buf: &'b mut Vec, + ) -> std::io::Result>>; + + /// For details, see [Reference::log_exists()]. + fn log_exists(&self, store: &file::Store) -> bool; + + /// For details, see [Reference::peel_to_id_in_place()]. + fn peel_to_id_in_place( + &mut self, + store: &file::Store, + packed: Option<&packed::Buffer>, + find: impl FnMut(git_hash::ObjectId, &mut Vec) -> Result, E>, + ) -> Result; + + /// Follow this symbolic reference one level and return the ref it refers to, + /// possibly providing access to `packed` references for lookup if it contains the referent. + /// + /// Returns `None` if this is not a symbolic reference, hence the leaf of the chain. + fn follow( + &self, + store: &file::Store, + packed: Option<&packed::Buffer>, + ) -> Option>; +} + +impl ReferenceExt for Reference { + fn log_iter_rev<'b>( + &self, + store: &file::Store, + buf: &'b mut [u8], + ) -> std::io::Result>> { + store.reflog_iter_rev(self.name.to_ref(), buf).map_err(must_be_io_err) + } + + fn log_iter<'a, 'b: 'a>( + &'a self, + store: &file::Store, + buf: &'b mut Vec, + ) -> std::io::Result>> { + store.reflog_iter(self.name.to_ref(), buf).map_err(must_be_io_err) + } + + fn log_exists(&self, store: &file::Store) -> bool { + store + .reflog_exists(self.name.to_ref()) + .expect("infallible name conversion") + } + + fn peel_to_id_in_place( + &mut self, + store: &file::Store, + packed: Option<&packed::Buffer>, + mut find: impl FnMut(git_hash::ObjectId, &mut Vec) -> Result, E>, + ) -> Result { + match self.peeled { + Some(peeled) => { + self.target = Target::Peeled(peeled.to_owned()); + Ok(peeled) + } + None => { + if self.target.kind() == crate::Kind::Symbolic { + let mut seen = BTreeSet::new(); + let cursor = &mut *self; + while let Some(next) = cursor.follow(store, packed) { + let next = next?; + if seen.contains(&next.name) { + return Err(peel::to_id::Error::Cycle(store.base.join(cursor.name.to_path()))); + } + *cursor = next; + seen.insert(cursor.name.clone()); + const MAX_REF_DEPTH: usize = 5; + if seen.len() == MAX_REF_DEPTH { + return Err(peel::to_id::Error::DepthLimitExceeded { + max_depth: MAX_REF_DEPTH, + }); + } + } + }; + let mut buf = Vec::new(); + let mut oid = self.target.as_id().expect("peeled ref").to_owned(); + let peeled_id = loop { + let (kind, data) = find(oid, &mut buf) + .map_err(|err| Box::new(err) as Box)? + .ok_or_else(|| peel::to_id::Error::NotFound { + oid, + name: self.name.0.clone(), + })?; + match kind { + git_object::Kind::Tag => { + oid = git_object::TagRefIter::from_bytes(data).target_id().ok_or_else(|| { + peel::to_id::Error::NotFound { + oid, + name: self.name.0.clone(), + } + })?; + } + _ => break oid, + }; + }; + self.peeled = Some(peeled_id); + self.target = Target::Peeled(peeled_id); + Ok(self.target.as_id().expect("to be peeled").to_owned()) + } + } + } + + fn follow( + &self, + store: &file::Store, + packed: Option<&packed::Buffer>, + ) -> Option> { + match self.peeled { + Some(peeled) => Some(Ok(Reference { + name: self.name.clone(), + target: Target::Peeled(peeled), + peeled: None, + })), + None => match &self.target { + Target::Peeled(_) => None, + Target::Symbolic(full_name) => { + let path = full_name.to_path(); + match store.find_one_with_verified_input(path.as_ref(), packed) { + Ok(Some(next)) => Some(Ok(next)), + Ok(None) => Some(Err(file::find::existing::Error::NotFound(path.into_owned()))), + Err(err) => Some(Err(file::find::existing::Error::Find(err))), + } + } + }, + } + } +} diff --git a/git-ref/src/store/file/reference.rs b/git-ref/src/store/file/reference.rs deleted file mode 100644 index c79f992f0c2..00000000000 --- a/git-ref/src/store/file/reference.rs +++ /dev/null @@ -1,171 +0,0 @@ -use std::convert::TryFrom; - -use bstr::ByteSlice; -use git_hash::ObjectId; - -use crate::{ - file::loose::reference::logiter::must_be_io_err, - store::{ - file, - file::{log, loose}, - packed, - }, - FullNameRef, Namespace, Target, -}; - -/// Either a loose or packed reference, depending on where it was found. -#[derive(Debug)] -pub enum Reference<'p> { - /// A reference originating in a pack - Packed(packed::Reference<'p>), - /// A reference from the filesystem - Loose(loose::Reference), -} - -impl<'p> TryFrom> for loose::Reference { - type Error = (); - - fn try_from(value: Reference<'p>) -> Result { - match value { - Reference::Loose(l) => Ok(l), - Reference::Packed(_) => Err(()), - } - } -} - -impl<'p> TryFrom> for packed::Reference<'p> { - type Error = (); - - fn try_from(value: Reference<'p>) -> Result { - match value { - Reference::Loose(_) => Err(()), - Reference::Packed(p) => Ok(p), - } - } -} - -impl<'p> Reference<'p> { - /// For details, see [loose::Reference::log_exists()]. - pub fn log_exists(&self, store: &file::Store) -> bool { - match self { - Reference::Loose(r) => r.log_exists(store), - Reference::Packed(p) => store.reflog_exists(p.name).expect("infallible name conversion"), - } - } - - /// For details, see [crate::file::loose::Reference::peel_to_id_in_place]. - pub fn peel_to_id_in_place( - &mut self, - store: &file::Store, - packed: Option<&packed::Buffer>, - find: impl FnMut(git_hash::ObjectId, &mut Vec) -> Result, E>, - ) -> Result { - match self { - Reference::Loose(r) => r.peel_to_id_in_place(store, packed, find).map(ToOwned::to_owned), - Reference::Packed(p) => { - if let Some(object) = p.object { - p.target = object; - } - p.object = None; - Ok(p.target()) - } - } - } - - /// For details, see [crate::file::loose::Reference::follow_symbolic()]. - pub fn peel_one_level<'p2>( - &self, - store: &file::Store, - packed: Option<&'p2 packed::Buffer>, - ) -> Option, crate::store::file::loose::reference::peel::Error>> { - match self { - Reference::Loose(r) => r.follow_symbolic(store, packed), - Reference::Packed(p) => packed - .and_then(|packed| packed.try_find(p.name).ok().flatten()) // needed to get data with 'p2 lifetime - .and_then(|np| { - p.object.and(np.object).map(|peeled| { - Ok(Reference::Packed(packed::Reference { - name: np.name, - target: peeled, - object: None, - })) - }) - }), - } - } - - /// Obtain a reverse iterator over logs of this reference. See [crate::file::loose::Reference::log_iter_rev()] for details. - pub fn log_iter_rev<'b>( - &self, - store: &file::Store, - buf: &'b mut [u8], - ) -> std::io::Result>> { - match self { - Reference::Loose(r) => r.log_iter_rev(store, buf), - Reference::Packed(p) => store.reflog_iter_rev(p.name, buf).map_err(must_be_io_err), - } - } - - /// Obtain an iterator over logs of this reference. See [crate::file::loose::Reference::log_iter()] for details. - pub fn log_iter<'a, 'b: 'a>( - &'a self, - store: &file::Store, - buf: &'b mut Vec, - ) -> std::io::Result, log::iter::decode::Error>> + 'a>> { - match self { - Reference::Loose(r) => store.reflog_iter(r.name.to_ref(), buf).map_err(must_be_io_err), - Reference::Packed(p) => store.reflog_iter(p.name, buf).map_err(must_be_io_err), - } - } - - /// Returns the kind of reference - pub fn kind(&self) -> crate::Kind { - match self { - Reference::Loose(r) => r.kind(), - Reference::Packed(_) => crate::Kind::Peeled, - } - } - - /// Transform this reference into an owned `Target` - pub fn into_target(self) -> Target { - match self { - Reference::Packed(p) => Target::Peeled(p.object()), - Reference::Loose(r) => r.target, - } - } - - /// Returns true if this ref is located in a packed ref buffer. - pub fn is_packed(&self) -> bool { - match self { - Reference::Packed(_) => true, - Reference::Loose(_) => false, - } - } - - /// Return the full validated name of the reference, which may include a namespace. - pub fn name(&self) -> FullNameRef<'_> { - match self { - Reference::Packed(p) => p.name, - Reference::Loose(l) => l.name.to_ref(), - } - } - - /// Return the full validated name of the reference, with the given namespace stripped if possible. - /// - /// If the reference name wasn't prefixed with `namespace`, `None` is returned instead. - pub fn name_without_namespace(&self, namespace: &Namespace) -> Option> { - self.name() - .0 - .as_bstr() - .strip_prefix(namespace.0.as_bstr().as_ref()) - .map(|stripped| FullNameRef(stripped.as_bstr())) - } - - /// Return the target to which the reference points to. - pub fn target(&self) -> Target { - match self { - Reference::Packed(p) => Target::Peeled(p.target()), - Reference::Loose(l) => l.target.clone(), - } - } -} diff --git a/git-ref/src/store/file/transaction/commit.rs b/git-ref/src/store/file/transaction/commit.rs index 5e149095491..75fe61bdb32 100644 --- a/git-ref/src/store/file/transaction/commit.rs +++ b/git-ref/src/store/file/transaction/commit.rs @@ -35,7 +35,7 @@ impl<'s> Transaction<'s> { assert!(!change.update.deref, "Deref mode is turned into splits and turned off"); match &change.update.change { // reflog first, then reference - Change::Update { log, new, mode } => { + Change::Update { log, new, expected } => { let lock = change.lock.take().expect("each ref is locked"); let (update_ref, update_reflog) = match log.mode { RefLog::Only => (false, true), @@ -45,7 +45,11 @@ impl<'s> Transaction<'s> { match new { Target::Symbolic(_) => {} // no reflog for symref changes Target::Peeled(new_oid) => { - let previous = mode.previous_oid().or(change.leaf_referent_previous_oid); + let previous = match expected { + PreviousValue::MustExistAndMatch(Target::Peeled(oid)) => Some(oid.to_owned()), + _ => None, + } + .or(change.leaf_referent_previous_oid); let do_update = previous.as_ref().map_or(true, |previous| previous != new_oid); if do_update { self.store.reflog_create_or_append( @@ -151,7 +155,7 @@ impl<'s> Transaction<'s> { } } mod error { - use bstr::BString; + use git_object::bstr::BString; use quick_error::quick_error; use crate::store::{file, packed}; @@ -190,3 +194,5 @@ mod error { } } pub use error::Error; + +use crate::transaction::PreviousValue; diff --git a/git-ref/src/store/file/transaction/mod.rs b/git-ref/src/store/file/transaction/mod.rs index 9c8bb647b52..92b085b329f 100644 --- a/git-ref/src/store/file/transaction/mod.rs +++ b/git-ref/src/store/file/transaction/mod.rs @@ -1,10 +1,9 @@ -use bstr::BString; use git_hash::ObjectId; +use git_object::bstr::BString; use crate::{ store::{file, file::Transaction}, transaction::RefEdit, - Namespace, }; /// A function receiving an object id to resolve, returning its decompressed bytes. @@ -69,13 +68,14 @@ impl file::Store { /// A snapshot of packed references will be obtained automatically if needed to fulfill this transaction /// and will be provided as result of a successful transaction. Note that upon transaction failure, packed-refs /// will never have been altered. + /// + /// The transaction inherits the parent namespace. pub fn transaction(&self) -> Transaction<'_> { Transaction { store: self, packed_transaction: None, updates: None, packed_refs: PackedRefs::default(), - namespace: None, } } } @@ -86,17 +86,6 @@ impl<'s> Transaction<'s> { self.packed_refs = packed_refs; self } - - /// Configure the namespace within which all edits should take place. - /// For example, with namespace `foo`, edits destined for `HEAD` will affect `refs/namespaces/foo/HEAD` instead. - /// Set `None` to not use any namespace, which also is the default. - /// - /// This also means that edits returned when [`commit(…)`ing](Transaction::commit()) will have their name altered to include - /// the namespace automatically, so it must be stripped when returning them to the user to keep them 'invisible'. - pub fn namespace(mut self, namespace: impl Into>) -> Self { - self.namespace = namespace.into(); - self - } } /// diff --git a/git-ref/src/store/file/transaction/prepare.rs b/git-ref/src/store/file/transaction/prepare.rs index 280fa49a4f9..0d56b09faa2 100644 --- a/git-ref/src/store/file/transaction/prepare.rs +++ b/git-ref/src/store/file/transaction/prepare.rs @@ -8,8 +8,8 @@ use crate::{ Transaction, }, }, - transaction::{Change, Create, LogChange, RefEdit, RefEditsExt, RefLog}, - Target, + transaction::{Change, LogChange, RefEdit, RefEditsExt, RefLog}, + Reference, Target, }; impl<'s> Transaction<'s> { @@ -33,7 +33,7 @@ impl<'s> Transaction<'s> { maybe_loose .map(|buf| { loose::Reference::try_from_path(change.update.name.clone(), &buf) - .map(file::Reference::Loose) + .map(Reference::from) .map_err(Error::from) }) .transpose() @@ -45,13 +45,13 @@ impl<'s> Transaction<'s> { .and_then(|maybe_loose| match (maybe_loose, packed) { (None, Some(packed)) => packed .try_find(change.update.name.to_ref()) - .map(|opt| opt.map(file::Reference::Packed)) + .map(|opt| opt.map(Into::into)) .map_err(Error::from), (None, None) => Ok(None), (maybe_loose, _) => Ok(maybe_loose), }); let lock = match &mut change.update.change { - Change::Delete { previous, .. } => { + Change::Delete { expected, .. } => { let lock = git_lock::Marker::acquire_to_hold_resource( store.reference_path(&relative_path), lock_fail_mode, @@ -62,16 +62,24 @@ impl<'s> Transaction<'s> { full_name: "borrowchk wont allow change.name()".into(), })?; let existing_ref = existing_ref?; - match (&previous, &existing_ref) { - (None, None | Some(_)) => {} - (Some(_previous), None) => { + match (&expected, &existing_ref) { + (PreviousValue::MustNotExist, _) => { + panic!("BUG: MustNotExist constraint makes no sense if references are to be deleted") + } + (PreviousValue::ExistingMustMatch(_), None) + | (PreviousValue::MustExist, Some(_)) + | (PreviousValue::Any, None | Some(_)) => {} + (PreviousValue::MustExist | PreviousValue::MustExistAndMatch(_), None) => { return Err(Error::DeleteReferenceMustExist { full_name: change.name(), }) } - (Some(previous), Some(existing)) => { - let actual = existing.target(); - if !previous.is_null() && *previous != actual { + ( + PreviousValue::MustExistAndMatch(previous) | PreviousValue::ExistingMustMatch(previous), + Some(existing), + ) => { + let actual = existing.target.clone(); + if *previous != actual { let expected = previous.clone(); return Err(Error::ReferenceOutOfDate { full_name: change.name(), @@ -84,14 +92,12 @@ impl<'s> Transaction<'s> { // Keep the previous value for the caller and ourselves. Maybe they want to keep a log of sorts. if let Some(existing) = existing_ref { - *previous = Some(existing.target()); + *expected = PreviousValue::MustExistAndMatch(existing.target); } lock } - Change::Update { - mode: previous, new, .. - } => { + Change::Update { expected, new, .. } => { let mut lock = git_lock::File::acquire_to_update_resource( store.reference_path(&relative_path), lock_fail_mode, @@ -103,25 +109,31 @@ impl<'s> Transaction<'s> { })?; let existing_ref = existing_ref?; - match (&previous, &existing_ref) { - (Create::Only, Some(existing)) if existing.target() != new.to_ref() => { - let new = new.clone(); - return Err(Error::MustNotExist { - full_name: change.name(), - actual: existing.target(), - new, - }); + match (&expected, &existing_ref) { + (PreviousValue::Any, _) + | (PreviousValue::MustExist, Some(_)) + | (PreviousValue::MustNotExist | PreviousValue::ExistingMustMatch(_), None) => {} + (PreviousValue::MustExist, None) => { + let expected = Target::Peeled(git_hash::ObjectId::null_sha1()); + let full_name = change.name(); + return Err(Error::MustExist { full_name, expected }); + } + (PreviousValue::MustNotExist, Some(existing)) => { + if existing.target != *new { + let new = new.clone(); + return Err(Error::MustNotExist { + full_name: change.name(), + actual: existing.target.clone(), + new, + }); + } } ( - Create::OrUpdate { - previous: Some(previous), - }, + PreviousValue::MustExistAndMatch(previous) | PreviousValue::ExistingMustMatch(previous), Some(existing), - ) => match previous { - Target::Peeled(oid) if oid.is_null() => {} - any_target if *any_target == existing.target() => {} - _target_mismatch => { - let actual = existing.target(); + ) => { + if *previous != existing.target { + let actual = existing.target.clone(); let expected = previous.to_owned(); let full_name = change.name(); return Err(Error::ReferenceOutOfDate { @@ -130,25 +142,17 @@ impl<'s> Transaction<'s> { expected, }); } - }, - ( - Create::OrUpdate { - previous: Some(previous), - }, - None, - ) => { + } + + (PreviousValue::MustExistAndMatch(previous), None) => { let expected = previous.to_owned(); let full_name = change.name(); return Err(Error::MustExist { full_name, expected }); } - (Create::Only | Create::OrUpdate { previous: None }, None | Some(_)) => {} }; - *previous = match existing_ref { - None => Create::Only, - Some(existing) => Create::OrUpdate { - previous: Some(existing.target()), - }, + if let Some(existing) = existing_ref { + *expected = PreviousValue::MustExistAndMatch(existing.target); }; lock.with_mut(|file| match new { @@ -190,10 +194,7 @@ impl<'s> Transaction<'s> { .pre_process( |name| { let symbolic_refs_are_never_packed = None; - store - .find(name, symbolic_refs_are_never_packed) - .map(|r| r.into_target()) - .ok() + store.find(name, symbolic_refs_are_never_packed).map(|r| r.target).ok() }, |idx, update| Edit { update, @@ -201,7 +202,6 @@ impl<'s> Transaction<'s> { parent_index: Some(idx), leaf_referent_previous_oid: None, }, - self.namespace.take(), ) .map_err(Error::PreprocessingFailed)?; @@ -236,9 +236,9 @@ impl<'s> Transaction<'s> { } match edit.update.change { Change::Update { - mode: Create::OrUpdate { previous: None }, + expected: PreviousValue::ExistingMustMatch(_) | PreviousValue::MustExistAndMatch(_), .. - } => continue, + } => needs_packed_refs_lookups = true, Change::Delete { .. } => { edits_for_packed_transaction.push(edit.update.clone()); } @@ -338,7 +338,7 @@ impl<'s> Transaction<'s> { } mod error { - use bstr::BString; + use git_object::bstr::BString; use quick_error::quick_error; use crate::{ @@ -405,3 +405,5 @@ mod error { } pub use error::Error; + +use crate::transaction::PreviousValue; diff --git a/git-ref/src/store/packed/decode.rs b/git-ref/src/store/packed/decode.rs index 10d91f154e0..5a90a57c230 100644 --- a/git-ref/src/store/packed/decode.rs +++ b/git-ref/src/store/packed/decode.rs @@ -1,6 +1,6 @@ use std::convert::TryFrom; -use bstr::{BStr, ByteSlice}; +use git_object::bstr::{BStr, ByteSlice}; use nom::{ bytes::complete::{tag, take_while}, combinator::{map, map_res, opt}, diff --git a/git-ref/src/store/packed/decode/tests.rs b/git-ref/src/store/packed/decode/tests.rs index 7fb4443a239..da48526e380 100644 --- a/git-ref/src/store/packed/decode/tests.rs +++ b/git-ref/src/store/packed/decode/tests.rs @@ -46,7 +46,7 @@ mod reference { } mod header { - use bstr::ByteSlice; + use git_object::bstr::ByteSlice; use git_testtools::to_bstr_err; use super::Result; diff --git a/git-ref/src/store/packed/find.rs b/git-ref/src/store/packed/find.rs index 4aa4218fdfc..1621e57ba36 100644 --- a/git-ref/src/store/packed/find.rs +++ b/git-ref/src/store/packed/find.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, convert::TryInto}; -use bstr::{BStr, BString, ByteSlice}; +use git_object::bstr::{BStr, BString, ByteSlice}; use crate::{store::packed, PartialNameRef}; diff --git a/git-ref/src/store/packed/iter.rs b/git-ref/src/store/packed/iter.rs index ce84ad4e5a4..7e423472189 100644 --- a/git-ref/src/store/packed/iter.rs +++ b/git-ref/src/store/packed/iter.rs @@ -1,10 +1,14 @@ -use bstr::{BString, ByteSlice}; +use git_object::bstr::{BString, ByteSlice}; use crate::store::{packed, packed::decode}; /// packed-refs specific functionality impl packed::Buffer { /// Return an iterator of references stored in this packed refs buffer, ordered by reference name. + /// + /// # Note + /// + /// There is no namespace support in packed iterators. It can be emulated using `iter_prefixed(…)`. pub fn iter(&self) -> Result, packed::iter::Error> { packed::Iter::new(self.as_ref()) } @@ -94,7 +98,7 @@ impl<'a> packed::Iter<'a> { } mod error { - use bstr::BString; + use git_object::bstr::BString; use quick_error::quick_error; quick_error! { diff --git a/git-ref/src/store/packed/mod.rs b/git-ref/src/store/packed/mod.rs index a3d7bc60278..630a22c7600 100644 --- a/git-ref/src/store/packed/mod.rs +++ b/git-ref/src/store/packed/mod.rs @@ -1,8 +1,8 @@ use std::path::PathBuf; -use bstr::{BStr, BString}; use filebuffer::FileBuffer; use git_hash::ObjectId; +use git_object::bstr::{BStr, BString}; use crate::{transaction::RefEdit, FullNameRef}; @@ -35,6 +35,7 @@ pub(crate) struct Transaction { buffer: Option, edits: Option>, lock: Option, + #[allow(dead_code)] // It just has to be kept alive, hence no reads closed_lock: Option, } diff --git a/git-ref/src/target.rs b/git-ref/src/target.rs index 34b9f5bf705..af5a8f9c5dc 100644 --- a/git-ref/src/target.rs +++ b/git-ref/src/target.rs @@ -1,7 +1,7 @@ -use std::fmt; +use std::{convert::TryFrom, fmt}; -use bstr::{BStr, ByteSlice}; use git_hash::{oid, ObjectId}; +use git_object::bstr::BStr; use crate::{FullName, Kind, Target, TargetRef}; @@ -23,7 +23,7 @@ impl<'a> TargetRef<'a> { /// Interpret this target as name of the reference it points to which maybe `None` if it an object id. pub fn as_name(&self) -> Option<&BStr> { match self { - TargetRef::Symbolic(path) => Some(path), + TargetRef::Symbolic(path) => Some(path.as_bstr()), TargetRef::Peeled(_) => None, } } @@ -54,7 +54,7 @@ impl Target { pub fn to_ref(&self) -> crate::TargetRef<'_> { match self { Target::Peeled(oid) => crate::TargetRef::Peeled(oid), - Target::Symbolic(name) => crate::TargetRef::Symbolic(name.0.as_bstr()), + Target::Symbolic(name) => crate::TargetRef::Symbolic(name.to_ref()), } } @@ -65,6 +65,21 @@ impl Target { Target::Peeled(oid) => Some(oid), } } + /// Return the contained object id or panic + pub fn into_id(self) -> ObjectId { + match self { + Target::Symbolic(_) => panic!("BUG: expected peeled reference target but found symbolic one"), + Target::Peeled(oid) => oid, + } + } + + /// Return the contained object id if the target is peeled or itself if it is not. + pub fn try_into_id(self) -> Result { + match self { + Target::Symbolic(_) => Err(self), + Target::Peeled(oid) => Ok(oid), + } + } /// Interpret this target as name of the reference it points to which maybe `None` if it an object id. pub fn as_name(&self) -> Option<&BStr> { match self { @@ -83,7 +98,7 @@ impl<'a> From> for Target { fn from(src: crate::TargetRef<'a>) -> Self { match src { crate::TargetRef::Peeled(oid) => Target::Peeled(oid.to_owned()), - crate::TargetRef::Symbolic(name) => Target::Symbolic(FullName(name.to_owned())), + crate::TargetRef::Symbolic(name) => Target::Symbolic(name.to_owned()), } } } @@ -92,12 +107,35 @@ impl<'a> PartialEq> for Target { fn eq(&self, other: &crate::TargetRef<'a>) -> bool { match (self, other) { (Target::Peeled(lhs), crate::TargetRef::Peeled(rhs)) => lhs == rhs, - (Target::Symbolic(lhs), crate::TargetRef::Symbolic(rhs)) => lhs.as_bstr() == *rhs, + (Target::Symbolic(lhs), crate::TargetRef::Symbolic(rhs)) => lhs.as_bstr() == rhs.as_bstr(), _ => false, } } } +impl From for Target { + fn from(id: ObjectId) -> Self { + Target::Peeled(id) + } +} + +impl TryFrom for ObjectId { + type Error = Target; + + fn try_from(value: Target) -> Result { + match value { + Target::Peeled(id) => Ok(id), + Target::Symbolic(_) => Err(value), + } + } +} + +impl From for Target { + fn from(name: FullName) -> Self { + Target::Symbolic(name) + } +} + impl fmt::Display for Target { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { diff --git a/git-ref/src/transaction.rs b/git-ref/src/transaction.rs deleted file mode 100644 index c3c459811d8..00000000000 --- a/git-ref/src/transaction.rs +++ /dev/null @@ -1,305 +0,0 @@ -//! **Transactions** are the only way make changes to the ref store in order to increase the chance of consistency in a multi-threaded -//! environment. -//! -//! Transactions currently allow to… -//! -//! * create or update reference -//! * delete references -//! -//! The following guarantees are made: -//! -//! * transactions are prepared which is when other writers are prevented from changing them -//! - errors during preparations will cause a perfect rollback -//! * prepared transactions are committed to finalize the change -//! - errors when committing while leave the ref store in an inconsistent, but operational state. -use bstr::BString; -use git_hash::ObjectId; - -use crate::{FullName, Target}; - -/// A change to the reflog. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -pub struct LogChange { - /// How to treat the reference log. - pub mode: RefLog, - /// If set, create a reflog even though it would otherwise not be the case as prohibited by general rules. - /// Note that ref-log writing might be prohibited in the entire repository which is when this flag has no effect either. - pub force_create_reflog: bool, - /// The message to put into the reference log. It must be a single line, hence newlines are forbidden. - /// The string can be empty to indicate there should be no message at all. - pub message: BString, -} - -impl Default for LogChange { - fn default() -> Self { - LogChange { - mode: RefLog::AndReference, - force_create_reflog: false, - message: Default::default(), - } - } -} - -/// A way to determine if a value should be created or created or updated. In the latter case the previous -/// value can be specified to indicate to what extend the previous value matters. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -pub enum Create { - /// Create a ref only. This fails if the ref exists and does not match the desired new value. - Only, - /// Create or update the reference with the `previous` value being controlling how to deal with existing ref values. - /// - OrUpdate { - /// Interpret… - /// * `None` so that existing values do not matter at all. This is the mode that always creates or updates a reference to the - /// desired new value. - /// * `Some(Target::Peeled(ObjectId::null_sha1())` so that the reference is required to exist even though its value doesn't matter. - /// * `Some(value)` so that the reference is required to exist and have the given `value`. - previous: Option, - }, -} - -impl Create { - pub(crate) fn previous_oid(&self) -> Option { - match self { - Create::OrUpdate { - previous: Some(Target::Peeled(oid)), - } => Some(*oid), - Create::Only | Create::OrUpdate { .. } => None, - } - } -} - -/// A description of an edit to perform. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -pub enum Change { - /// If previous is not `None`, the ref must exist and its `oid` must agree with the `previous`, and - /// we function like `update`. - /// Otherwise it functions as `create-or-update`. - Update { - /// The desired change to the reference log. - log: LogChange, - /// The create mode. - /// If a ref was existing previously it will be updated to reflect the previous value for bookkeeping purposes - /// and for use in the reflog. - mode: Create, - /// The new state of the reference, either for updating an existing one or creating a new one. - new: Target, - }, - /// Delete a reference and optionally check if `previous` is its content. - Delete { - /// The previous state of the reference. If set, the reference is expected to exist and match the given value. - /// If the value is a peeled null-id the reference is expected to exist but the value doesn't matter, neither peeled nor symbolic. - /// If `None`, the actual value does not matter. - /// - /// If a previous ref existed, this value will be filled in automatically and can be accessed - /// if the transaction was committed successfully. - previous: Option, - /// How to thread the reference log during deletion. - log: RefLog, - }, -} - -impl Change { - /// Return references to values that are in common between all variants. - pub fn previous_value(&self) -> Option> { - match self { - Change::Update { mode: Create::Only, .. } => None, - Change::Update { - mode: Create::OrUpdate { previous }, - .. - } - | Change::Delete { previous, .. } => previous.as_ref().map(|t| t.to_ref()), - } - } -} - -/// A reference that is to be changed -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -pub struct RefEdit { - /// The change itself - pub change: Change, - /// The name of the reference to apply the change to - pub name: FullName, - /// If set, symbolic references identified by `name` will be dereferenced to have the `change` applied to their target. - /// This flag has no effect if the reference isn't symbolic. - pub deref: bool, -} - -/// The way to deal with the Reflog in deletions. -#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] -pub enum RefLog { - /// Delete or update the reference and the log - AndReference, - /// Delete or update only the reflog - Only, -} - -mod ext { - use bstr::{BString, ByteVec}; - - use crate::{ - transaction::{Change, LogChange, RefEdit, RefLog, Target}, - Namespace, PartialNameRef, - }; - - /// An extension trait to perform commonly used operations on edits across different ref stores. - pub trait RefEditsExt - where - T: std::borrow::Borrow + std::borrow::BorrowMut, - { - /// Return true if each ref `name` has exactly one `edit` across multiple ref edits - fn assure_one_name_has_one_edit(&self) -> Result<(), BString>; - - /// Split all symbolic refs into updates for the symbolic ref as well as all their referents if the `deref` flag is enabled. - /// - /// Note no action is performed if deref isn't specified. - fn extend_with_splits_of_symbolic_refs( - &mut self, - find: impl FnMut(PartialNameRef<'_>) -> Option, - make_entry: impl FnMut(usize, RefEdit) -> T, - ) -> Result<(), std::io::Error>; - - /// If `namespace` is not `None`, alter all edit names by prefixing them with the given namespace. - /// Note that symbolic reference targets will also be rewritten to point into the namespace instead. - fn adjust_namespace(&mut self, namespace: Option); - - /// All processing steps in one and in the correct order. - /// - /// Users call this to assure derefs are honored and duplicate checks are done. - fn pre_process( - &mut self, - find: impl FnMut(PartialNameRef<'_>) -> Option, - make_entry: impl FnMut(usize, RefEdit) -> T, - namespace: impl Into>, - ) -> Result<(), std::io::Error> { - self.adjust_namespace(namespace.into()); - self.extend_with_splits_of_symbolic_refs(find, make_entry)?; - self.assure_one_name_has_one_edit().map_err(|name| { - std::io::Error::new( - std::io::ErrorKind::AlreadyExists, - format!("A reference named '{}' has multiple edits", name), - ) - }) - } - } - - impl RefEditsExt for Vec - where - E: std::borrow::Borrow + std::borrow::BorrowMut, - { - fn assure_one_name_has_one_edit(&self) -> Result<(), BString> { - let mut names: Vec<_> = self.iter().map(|e| &e.borrow().name).collect(); - names.sort(); - match names.windows(2).find(|v| v[0] == v[1]) { - Some(name) => Err(name[0].as_bstr().to_owned()), - None => Ok(()), - } - } - - fn extend_with_splits_of_symbolic_refs( - &mut self, - mut find: impl FnMut(PartialNameRef<'_>) -> Option, - mut make_entry: impl FnMut(usize, RefEdit) -> E, - ) -> Result<(), std::io::Error> { - let mut new_edits = Vec::new(); - let mut first = 0; - let mut round = 1; - loop { - for (eid, edit) in self[first..].iter_mut().enumerate().map(|(eid, v)| (eid + first, v)) { - let edit = edit.borrow_mut(); - if !edit.deref { - continue; - }; - - // we can't tell what happened and we are here because it's a non-existing ref or an invalid one. - // In any case, we don't want the following algorithms to try dereffing it and assume they deal with - // broken refs gracefully. - edit.deref = false; - if let Some(Target::Symbolic(referent)) = find(edit.name.to_partial()) { - new_edits.push(make_entry( - eid, - match &mut edit.change { - Change::Delete { previous, log: mode } => { - let current_mode = *mode; - *mode = RefLog::Only; - RefEdit { - change: Change::Delete { - previous: previous.clone(), - log: current_mode, - }, - name: referent, - deref: true, - } - } - Change::Update { - log, - mode: previous, - new, - } => { - let current = std::mem::replace( - log, - LogChange { - message: log.message.clone(), - mode: RefLog::Only, - force_create_reflog: log.force_create_reflog, - }, - ); - RefEdit { - change: Change::Update { - mode: previous.clone(), - new: new.clone(), - log: current, - }, - name: referent, - deref: true, - } - } - }, - )); - } - } - if new_edits.is_empty() { - break Ok(()); - } - if round == 5 { - break Err(std::io::Error::new( - std::io::ErrorKind::WouldBlock, - format!( - "Could not follow all splits after {} rounds, assuming reference cycle", - round - ), - )); - } - round += 1; - first = self.len(); - - self.extend(new_edits.drain(..)); - } - } - - fn adjust_namespace(&mut self, namespace: Option) { - if let Some(namespace) = namespace { - for entry in self.iter_mut() { - let entry = entry.borrow_mut(); - entry.name.0 = { - let mut new_name = namespace.0.clone(); - new_name.push_str(&entry.name.0); - new_name - }; - if let Change::Update { - new: Target::Symbolic(ref mut name), - .. - } = entry.change - { - name.0 = { - let mut new_name = namespace.0.clone(); - new_name.push_str(&name.0); - new_name - }; - } - } - } - } - } -} -pub use ext::RefEditsExt; diff --git a/git-ref/src/transaction/ext.rs b/git-ref/src/transaction/ext.rs new file mode 100644 index 00000000000..1ff2d642b21 --- /dev/null +++ b/git-ref/src/transaction/ext.rs @@ -0,0 +1,136 @@ +use git_object::bstr::BString; + +use crate::{ + transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog, Target}, + PartialNameRef, +}; + +/// An extension trait to perform commonly used operations on edits across different ref stores. +pub trait RefEditsExt +where + T: std::borrow::Borrow + std::borrow::BorrowMut, +{ + /// Return true if each ref `name` has exactly one `edit` across multiple ref edits + fn assure_one_name_has_one_edit(&self) -> Result<(), BString>; + + /// Split all symbolic refs into updates for the symbolic ref as well as all their referents if the `deref` flag is enabled. + /// + /// Note no action is performed if deref isn't specified. + fn extend_with_splits_of_symbolic_refs( + &mut self, + find: impl FnMut(PartialNameRef<'_>) -> Option, + make_entry: impl FnMut(usize, RefEdit) -> T, + ) -> Result<(), std::io::Error>; + + /// All processing steps in one and in the correct order. + /// + /// Users call this to assure derefs are honored and duplicate checks are done. + fn pre_process( + &mut self, + find: impl FnMut(PartialNameRef<'_>) -> Option, + make_entry: impl FnMut(usize, RefEdit) -> T, + ) -> Result<(), std::io::Error> { + self.extend_with_splits_of_symbolic_refs(find, make_entry)?; + self.assure_one_name_has_one_edit().map_err(|name| { + std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + format!("A reference named '{}' has multiple edits", name), + ) + }) + } +} + +impl RefEditsExt for Vec +where + E: std::borrow::Borrow + std::borrow::BorrowMut, +{ + fn assure_one_name_has_one_edit(&self) -> Result<(), BString> { + let mut names: Vec<_> = self.iter().map(|e| &e.borrow().name).collect(); + names.sort(); + match names.windows(2).find(|v| v[0] == v[1]) { + Some(name) => Err(name[0].as_bstr().to_owned()), + None => Ok(()), + } + } + + fn extend_with_splits_of_symbolic_refs( + &mut self, + mut find: impl FnMut(PartialNameRef<'_>) -> Option, + mut make_entry: impl FnMut(usize, RefEdit) -> E, + ) -> Result<(), std::io::Error> { + let mut new_edits = Vec::new(); + let mut first = 0; + let mut round = 1; + loop { + for (eid, edit) in self[first..].iter_mut().enumerate().map(|(eid, v)| (eid + first, v)) { + let edit = edit.borrow_mut(); + if !edit.deref { + continue; + }; + + // we can't tell what happened and we are here because it's a non-existing ref or an invalid one. + // In any case, we don't want the following algorithms to try dereffing it and assume they deal with + // broken refs gracefully. + edit.deref = false; + if let Some(Target::Symbolic(referent)) = find(edit.name.to_partial()) { + new_edits.push(make_entry( + eid, + match &mut edit.change { + Change::Delete { + expected: previous, + log: mode, + } => { + let current_mode = *mode; + *mode = RefLog::Only; + RefEdit { + change: Change::Delete { + expected: previous.clone(), + log: current_mode, + }, + name: referent, + deref: true, + } + } + Change::Update { log, expected, new } => { + let current = std::mem::replace( + log, + LogChange { + message: log.message.clone(), + mode: RefLog::Only, + force_create_reflog: log.force_create_reflog, + }, + ); + let next = std::mem::replace(expected, PreviousValue::Any); + RefEdit { + change: Change::Update { + expected: next, + new: new.clone(), + log: current, + }, + name: referent, + deref: true, + } + } + }, + )); + } + } + if new_edits.is_empty() { + break Ok(()); + } + if round == 5 { + break Err(std::io::Error::new( + std::io::ErrorKind::WouldBlock, + format!( + "Could not follow all splits after {} rounds, assuming reference cycle", + round + ), + )); + } + round += 1; + first = self.len(); + + self.extend(new_edits.drain(..)); + } + } +} diff --git a/git-ref/src/transaction/mod.rs b/git-ref/src/transaction/mod.rs new file mode 100644 index 00000000000..06f6f3416d3 --- /dev/null +++ b/git-ref/src/transaction/mod.rs @@ -0,0 +1,126 @@ +//! **Transactions** are the only way make changes to the ref store in order to increase the chance of consistency in a multi-threaded +//! environment. +//! +//! Transactions currently allow to… +//! +//! * create or update reference +//! * delete references +//! +//! The following guarantees are made: +//! +//! * transactions are prepared which is when other writers are prevented from changing them +//! - errors during preparations will cause a perfect rollback +//! * prepared transactions are committed to finalize the change +//! - errors when committing while leave the ref store in an inconsistent, but operational state. +use git_object::bstr::BString; + +use crate::{FullName, Target}; + +/// A change to the reflog. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] +pub struct LogChange { + /// How to treat the reference log. + pub mode: RefLog, + /// If set, create a reflog even though it would otherwise not be the case as prohibited by general rules. + /// Note that ref-log writing might be prohibited in the entire repository which is when this flag has no effect either. + pub force_create_reflog: bool, + /// The message to put into the reference log. It must be a single line, hence newlines are forbidden. + /// The string can be empty to indicate there should be no message at all. + pub message: BString, +} + +impl Default for LogChange { + fn default() -> Self { + LogChange { + mode: RefLog::AndReference, + force_create_reflog: false, + message: Default::default(), + } + } +} + +/// The desired value of an updated value +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] +pub enum PreviousValue { + /// No requirements are made towards the current value, and the new value is set unconditionally. + Any, + /// The reference must exist and may have any value. + MustExist, + /// Create the ref only, hence the reference must not exist. + MustNotExist, + /// The ref _must_ exist and have the given value. + MustExistAndMatch(Target), + /// The ref _may_ exist and have the given value, or may not exist at all. + ExistingMustMatch(Target), +} + +/// A description of an edit to perform. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] +pub enum Change { + /// If previous is not `None`, the ref must exist and its `oid` must agree with the `previous`, and + /// we function like `update`. + /// Otherwise it functions as `create-or-update`. + Update { + /// The desired change to the reference log. + log: LogChange, + /// The expected value already present in the reference. + /// If a ref was existing previously it will be overwritten at `MustExistAndMatch(actual_value)` for use after + /// the transaction was committed successfully. + expected: PreviousValue, + /// The new state of the reference, either for updating an existing one or creating a new one. + new: Target, + }, + /// Delete a reference and optionally check if `previous` is its content. + Delete { + /// The expected value of the reference, with the `MustNotExist` variant being invalid. + /// + /// If a previous ref existed, this value will be filled in automatically as `MustExistAndMatch(actual_value)` and + /// can be accessed if the transaction was committed successfully. + expected: PreviousValue, + /// How to thread the reference log during deletion. + log: RefLog, + }, +} + +impl Change { + /// Return references to values that are in common between all variants. + pub fn previous_value(&self) -> Option> { + match self { + Change::Update { + expected: PreviousValue::MustExistAndMatch(previous) | PreviousValue::ExistingMustMatch(previous), + .. + } => previous, + Change::Delete { + expected: PreviousValue::MustExistAndMatch(previous) | PreviousValue::ExistingMustMatch(previous), + .. + } => previous, + _ => return None, + } + .to_ref() + .into() + } +} + +/// A reference that is to be changed +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] +pub struct RefEdit { + /// The change itself + pub change: Change, + /// The name of the reference to apply the change to + pub name: FullName, + /// If set, symbolic references identified by `name` will be dereferenced to have the `change` applied to their target. + /// This flag has no effect if the reference isn't symbolic. + pub deref: bool, +} + +/// The way to deal with the Reflog in deletions. +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] +pub enum RefLog { + /// Delete or update the reference and the log + AndReference, + /// Delete or update only the reflog + Only, +} + +mod ext; +pub use ext::RefEditsExt; diff --git a/git-ref/tests/file/log.rs b/git-ref/tests/file/log.rs index 6f1af0a80d2..be86fd304bf 100644 --- a/git-ref/tests/file/log.rs +++ b/git-ref/tests/file/log.rs @@ -1,6 +1,6 @@ mod line { mod write_to { - use bstr::ByteVec; + use git_object::bstr::ByteVec; use git_ref::file::log; #[test] @@ -46,7 +46,22 @@ mod iter { } mod backward { + mod with_zero_sized_buffer { + + #[test] + fn any_line() { + let mut buf = [0u8; 0]; + assert!( + git_ref::file::log::iter::reverse(std::io::Cursor::new(b"won't matter".as_ref()), &mut buf) + .is_err(), + "zero sized buffers aren't allowed" + ); + } + } + mod with_buffer_too_small_for_single_line { + use std::error::Error; + #[test] fn single_line() -> crate::Result { let mut buf = [0u8; 128]; @@ -63,8 +78,8 @@ mod iter { iter.next() .expect("an error") .expect_err("buffer too small") - .get_ref() - .expect("inner error") + .source() + .expect("source") .to_string(), "buffer too small for line size" ); @@ -75,7 +90,7 @@ mod iter { } mod with_buffer_big_enough_for_largest_line { - use git_ref::file::log::Line; + use git_ref::log::Line; use git_testtools::hex_to_id; #[test] @@ -95,7 +110,7 @@ mod iter { new_oid, signature: _, message, - } = iter.next().expect("a single line")??; + } = iter.next().expect("a single line")?; assert_eq!(previous_oid, hex_to_id("0000000000000000000000000000000000000000")); assert_eq!(new_oid, hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03")); assert_eq!(message, "commit (initial): c1"); @@ -123,7 +138,7 @@ mod iter { new_oid, signature: _, message, - } = iter.next().expect("a single line")??; + } = iter.next().expect("a single line")?; assert_eq!(previous_oid, hex_to_id("0000000000000000000000000000000000000000")); assert_eq!(new_oid, hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03")); assert_eq!(message, "commit (initial): c1"); @@ -132,7 +147,7 @@ mod iter { new_oid, signature: _, message, - } = iter.next().expect("a single line")??; + } = iter.next().expect("a single line")?; assert_eq!(message, "commit (initial): c2"); assert_eq!(previous_oid, hex_to_id("1000000000000000000000000000000000000000")); assert_eq!(new_oid, hex_to_id("234385f6d781b7e97062102c6a483440bfda2a03")); @@ -144,8 +159,8 @@ mod iter { } } mod forward { - use bstr::B; use git_hash::ObjectId; + use git_object::bstr::B; use crate::file::log::iter::reflog; diff --git a/git-ref/tests/file/reference.rs b/git-ref/tests/file/reference.rs index 0e481625c5e..1e0bd6f2c6f 100644 --- a/git-ref/tests/file/reference.rs +++ b/git-ref/tests/file/reference.rs @@ -1,5 +1,7 @@ mod reflog { - mod packedd { + mod packed { + use git_ref::file::ReferenceExt; + use crate::file; #[test] @@ -49,10 +51,8 @@ mod reflog { } mod peel { - use std::convert::TryFrom; - use git_odb::Find; - use git_ref::file::loose::reference::peel; + use git_ref::{file::ReferenceExt, peel, Reference}; use git_testtools::hex_to_id; use crate::{file, file::store_with_packed_refs}; @@ -63,15 +63,15 @@ mod peel { let r = store.find_loose("HEAD")?; assert_eq!(r.kind(), git_ref::Kind::Symbolic, "there is something to peel"); - let nr = git_ref::file::loose::Reference::try_from( - r.follow_symbolic(&store, None).expect("exists").expect("no failure"), - ) - .expect("loose ref"); + let nr = Reference::from(r) + .follow(&store, None) + .expect("exists") + .expect("no failure"); assert!( matches!(nr.target.to_ref(), git_ref::TargetRef::Peeled(_)), "iteration peels a single level" ); - assert!(nr.follow_symbolic(&store, None).is_none(), "end of iteration"); + assert!(nr.follow(&store, None).is_none(), "end of iteration"); assert_eq!( nr.target.to_ref(), git_ref::TargetRef::Peeled(&hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03")), @@ -83,7 +83,7 @@ mod peel { #[test] fn peel_with_packed_involvement() -> crate::Result { let store = store_with_packed_refs()?; - let mut head = store.find_loose("HEAD")?; + let mut head: Reference = store.find_loose("HEAD")?.into(); let packed = store.packed_buffer()?; let expected = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"); assert_eq!(head.peel_to_id_in_place(&store, packed.as_ref(), peel::none)?, expected); @@ -91,7 +91,7 @@ mod peel { let mut head = store.find("dt1", packed.as_ref())?; assert_eq!(head.peel_to_id_in_place(&store, packed.as_ref(), peel::none)?, expected); - assert_eq!(head.target().as_id().map(ToOwned::to_owned), Some(expected)); + assert_eq!(head.target.into_id(), expected); Ok(()) } @@ -101,9 +101,8 @@ mod peel { let packed = store.packed_buffer()?; let head = store.find("dt1", packed.as_ref())?; - assert!(head.is_packed()); assert_eq!( - head.target().as_id().map(ToOwned::to_owned), + head.target.as_id().map(ToOwned::to_owned), Some(hex_to_id("4c3f4cce493d7beb45012e478021b5f65295e5a3")) ); assert_eq!( @@ -113,29 +112,29 @@ mod peel { ); let peeled = head - .peel_one_level(&store, packed.as_ref()) + .follow(&store, packed.as_ref()) .expect("a peeled ref for the object")?; assert_eq!( - peeled.target().as_id().map(ToOwned::to_owned), + peeled.target.as_id().map(ToOwned::to_owned), Some(hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03")), "packed refs are always peeled (at least the ones we choose to read)" ); assert_eq!(peeled.kind(), git_ref::Kind::Peeled, "it's terminally peeled now"); - assert!(peeled.peel_one_level(&store, packed.as_ref()).is_none()); + assert!(peeled.follow(&store, packed.as_ref()).is_none()); Ok(()) } #[test] fn to_id_multi_hop() -> crate::Result { let store = file::store()?; - let mut r = store.find_loose("multi-link")?; + let mut r: Reference = store.find_loose("multi-link")?.into(); assert_eq!(r.kind(), git_ref::Kind::Symbolic, "there is something to peel"); let commit = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"); assert_eq!(r.peel_to_id_in_place(&store, None, peel::none)?, commit); assert_eq!(r.name.as_bstr(), "refs/remotes/origin/multi-link-target3"); - let mut r = store.find_loose("dt1")?; + let mut r: Reference = store.find_loose("dt1")?.into(); assert_eq!( r.peel_to_id_in_place(&store, None, peel::none)?, hex_to_id("4c3f4cce493d7beb45012e478021b5f65295e5a3"), @@ -143,6 +142,7 @@ mod peel { ); let odb = git_odb::linked::Store::at(store.base.join("objects"))?; + let mut r: Reference = store.find_loose("dt1")?.into(); assert_eq!( r.peel_to_id_in_place(&store, None, |oid, buf| { odb.try_find(oid, buf, &mut git_odb::pack::cache::Never) @@ -158,13 +158,13 @@ mod peel { #[test] fn to_id_cycle() -> crate::Result { let store = file::store()?; - let mut r = store.find_loose("loop-a")?; + let mut r: Reference = store.find_loose("loop-a")?.into(); assert_eq!(r.kind(), git_ref::Kind::Symbolic, "there is something to peel"); assert_eq!(r.name.as_bstr(), "refs/loop-a"); assert!(matches!( r.peel_to_id_in_place(&store, None, peel::none).unwrap_err(), - git_ref::file::loose::reference::peel::to_id::Error::Cycle { .. } + git_ref::peel::to_id::Error::Cycle { .. } )); assert_eq!(r.name.as_bstr(), "refs/loop-a", "the ref is not changed on error"); Ok(()) @@ -191,7 +191,7 @@ mod parse { mktest!(ref_tag, b"reff: hello", "\"reff: hello\" could not be parsed"); } mod valid { - use bstr::ByteSlice; + use git_object::bstr::ByteSlice; use git_ref::file::loose::Reference; use git_testtools::hex_to_id; diff --git a/git-ref/tests/file/store/find.rs b/git-ref/tests/file/store/find.rs index e6f8c4dabcf..45b511353e6 100644 --- a/git-ref/tests/file/store/find.rs +++ b/git-ref/tests/file/store/find.rs @@ -9,8 +9,8 @@ mod existing { let c1 = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"); let packed = store.packed_buffer()?; let r = store.find("main", packed.as_ref())?; - assert_eq!(r.target().to_ref().as_id().expect("peeled"), c1); - assert_eq!(r.name().as_bstr(), "refs/heads/main"); + assert_eq!(r.target.into_id(), c1); + assert_eq!(r.name.as_bstr(), "refs/heads/main"); Ok(()) } } diff --git a/git-ref/tests/file/store/iter.rs b/git-ref/tests/file/store/iter.rs index ce46c3cd8bd..ed75186964c 100644 --- a/git-ref/tests/file/store/iter.rs +++ b/git-ref/tests/file/store/iter.rs @@ -1,45 +1,92 @@ use std::convert::TryInto; -use bstr::ByteSlice; +use git_object::bstr::ByteSlice; use git_testtools::hex_to_id; use crate::file::{store, store_at, store_with_packed_refs}; mod with_namespace { - use bstr::BString; - use git_object::bstr::ByteSlice; + use git_object::bstr::{BString, ByteSlice}; use crate::file::store_at; #[test] - fn general_iteration_can_trivially_use_namespaces_as_prefixes() { - let store = store_at("make_namespaced_packed_ref_repository.sh").unwrap(); - let packed = store.packed_buffer().unwrap(); + fn iteration_can_trivially_use_namespaces_as_prefixes() -> crate::Result { + let store = store_at("make_namespaced_packed_ref_repository.sh")?; + let packed = store.packed_buffer()?; - let ns_two = git_ref::namespace::expand("bar").unwrap(); + let ns_two = git_ref::namespace::expand("bar")?; + let namespaced_refs = store + .iter_prefixed(packed.as_ref(), ns_two.to_path())? + .map(Result::unwrap) + .map(|r: git_ref::Reference| r.name) + .collect::>(); + let expected_namespaced_refs = vec![ + "refs/namespaces/bar/refs/heads/multi-link-target1", + "refs/namespaces/bar/refs/multi-link", + "refs/namespaces/bar/refs/remotes/origin/multi-link-target3", + "refs/namespaces/bar/refs/tags/multi-link-target2", + ]; + assert_eq!( + namespaced_refs.iter().map(|n| n.as_bstr()).collect::>(), + expected_namespaced_refs + ); assert_eq!( store - .iter_prefixed(packed.as_ref(), ns_two.to_path()) - .unwrap() + .loose_iter_prefixed(ns_two.to_path())? .map(Result::unwrap) - .map(|r: git_ref::file::Reference| r.name().as_bstr().to_owned()) + .map(|r| r.name.into_inner()) .collect::>(), - vec![ + [ "refs/namespaces/bar/refs/heads/multi-link-target1", "refs/namespaces/bar/refs/multi-link", - "refs/namespaces/bar/refs/remotes/origin/multi-link-target3", "refs/namespaces/bar/refs/tags/multi-link-target2" ] ); + assert_eq!( + packed + .as_ref() + .expect("present") + .iter_prefixed(ns_two.as_bstr())? + .map(Result::unwrap) + .map(|r| r.name.to_owned().into_inner()) + .collect::>(), + ["refs/namespaces/bar/refs/remotes/origin/multi-link-target3"] + ); + for fullname in namespaced_refs { + let reference = store.find(fullname.as_bstr(), packed.as_ref())?; + assert_eq!( + reference.name, fullname, + "it finds namespaced items by fully qualified name" + ); + assert_eq!( + store + .find( + fullname.as_bstr().splitn_str(2, b"/").nth(1).expect("name").as_bstr(), + packed.as_ref() + )? + .name, + fullname, + "it will find namespaced items just by their shortened (but not shortest) name" + ); + assert!( + store + .try_find( + reference.name_without_namespace(&ns_two).expect("namespaced"), + packed.as_ref() + )? + .is_none(), + "it won't find namespaced items by their full name without namespace" + ); + } - let ns_one = git_ref::namespace::expand("foo").unwrap(); + let ns_one = git_ref::namespace::expand("foo")?; assert_eq!( store - .iter_prefixed(packed.as_ref(), ns_one.to_path()) - .unwrap() + .iter_prefixed(packed.as_ref(), ns_one.to_path())? .map(Result::unwrap) - .map(|r: git_ref::file::Reference| ( - r.name().as_bstr().to_owned(), + .map(|r: git_ref::Reference| ( + r.name.as_bstr().to_owned(), r.name_without_namespace(&ns_one) .expect("stripping correct namespace always works") .as_bstr() @@ -61,14 +108,13 @@ mod with_namespace { assert_eq!( store - .iter(packed.as_ref()) - .unwrap() + .iter(packed.as_ref())? .map(Result::unwrap) .filter_map( - |r: git_ref::file::Reference| if r.name().as_bstr().starts_with_str("refs/namespaces") { + |r: git_ref::Reference| if r.name.as_bstr().starts_with_str("refs/namespaces") { None } else { - Some(r.name().as_bstr().to_owned()) + Some(r.name.as_bstr().to_owned()) } ) .collect::>(), @@ -81,6 +127,88 @@ mod with_namespace { ], "we can find refs without namespace by manual filter, really just for testing purposes" ); + Ok(()) + } + + #[test] + fn iteration_on_store_with_namespace_makes_namespace_transparent() -> crate::Result { + let ns_two = git_ref::namespace::expand("bar")?; + let mut ns_store = { + let mut s = store_at("make_namespaced_packed_ref_repository.sh")?; + s.namespace = ns_two.clone().into(); + s + }; + let packed = ns_store.packed_buffer()?; + + let expected_refs = vec![ + "refs/heads/multi-link-target1", + "refs/multi-link", + "refs/remotes/origin/multi-link-target3", + "refs/tags/multi-link-target2", + ]; + let ref_names = ns_store + .iter(packed.as_ref())? + .map(Result::unwrap) + .map(|r: git_ref::Reference| r.name) + .collect::>(); + assert_eq!(ref_names.iter().map(|n| n.as_bstr()).collect::>(), expected_refs); + + for fullname in ref_names { + assert_eq!( + ns_store.find(fullname.as_bstr(), packed.as_ref())?.name, + fullname, + "it finds namespaced items by fully qualified name, excluding namespace" + ); + assert!( + ns_store + .try_find(fullname.clone().prefix_namespace(&ns_two).to_partial(), packed.as_ref())? + .is_none(), + "it won't find namespaced items by their store-relative name with namespace" + ); + assert_eq!( + ns_store + .find( + fullname.as_bstr().splitn_str(2, b"/").nth(1).expect("name").as_bstr(), + packed.as_ref() + )? + .name, + fullname, + "it finds partial names within the namespace" + ); + } + + assert_eq!( + packed.as_ref().expect("present").iter()?.map(Result::unwrap).count(), + 8, + "packed refs have no namespace support at all" + ); + assert_eq!( + ns_store + .loose_iter()? + .map(Result::unwrap) + .map(|r| r.name.into_inner()) + .collect::>(), + [ + "refs/namespaces/bar/refs/heads/multi-link-target1", + "refs/namespaces/bar/refs/multi-link", + "refs/namespaces/bar/refs/tags/multi-link-target2", + "refs/namespaces/foo/refs/remotes/origin/HEAD" + ], + "loose iterators have no namespace support at all" + ); + + let ns_one = git_ref::namespace::expand("foo")?; + ns_store.namespace = ns_one.into(); + + assert_eq!( + ns_store + .iter(packed.as_ref())? + .map(Result::unwrap) + .map(|r: git_ref::Reference| r.name.into_inner()) + .collect::>(), + vec!["refs/d1", "refs/remotes/origin/HEAD", "refs/remotes/origin/main"], + ); + Ok(()) } } @@ -207,25 +335,23 @@ fn overlay_iter() -> crate::Result { let store = store_at("make_packed_ref_repository_for_overlay.sh")?; let ref_names = store .iter(store.packed_buffer()?.as_ref())? - .map(|r| r.map(|r| (r.name().as_bstr().to_owned(), r.target(), r.is_packed()))) + .map(|r| r.map(|r| (r.name.as_bstr().to_owned(), r.target))) .collect::, _>>()?; let c1 = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"); let c2 = hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7"); assert_eq!( ref_names, vec![ - (b"refs/heads/main".as_bstr().to_owned(), Peeled(c1), true), - ("refs/heads/newer-as-loose".into(), Peeled(c2), false), + (b"refs/heads/main".as_bstr().to_owned(), Peeled(c1)), + ("refs/heads/newer-as-loose".into(), Peeled(c2)), ( "refs/remotes/origin/HEAD".into(), Symbolic("refs/remotes/origin/main".try_into()?), - false ), - ("refs/remotes/origin/main".into(), Peeled(c1), true), + ("refs/remotes/origin/main".into(), Peeled(c1)), ( "refs/tags/tag-object".into(), Peeled(hex_to_id("b3109a7e51fc593f85b145a76c70ddd1d133fafd")), - true ) ] ); @@ -254,15 +380,15 @@ fn overlay_prefixed_iter() -> crate::Result { let store = store_at("make_packed_ref_repository_for_overlay.sh")?; let ref_names = store .iter_prefixed(store.packed_buffer()?.as_ref(), "refs/heads")? - .map(|r| r.map(|r| (r.name().as_bstr().to_owned(), r.target(), r.is_packed()))) + .map(|r| r.map(|r| (r.name.as_bstr().to_owned(), r.target))) .collect::, _>>()?; let c1 = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"); let c2 = hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7"); assert_eq!( ref_names, vec![ - (b"refs/heads/main".as_bstr().to_owned(), Peeled(c1), true), - ("refs/heads/newer-as-loose".into(), Peeled(c2), false), + (b"refs/heads/main".as_bstr().to_owned(), Peeled(c1)), + ("refs/heads/newer-as-loose".into(), Peeled(c2)), ] ); Ok(()) diff --git a/git-ref/tests/file/transaction/mod.rs b/git-ref/tests/file/transaction/mod.rs index b5c52b83d58..10b96a9bfef 100644 --- a/git-ref/tests/file/transaction/mod.rs +++ b/git-ref/tests/file/transaction/mod.rs @@ -1,15 +1,15 @@ mod prepare_and_commit { - use bstr::BString; use git_actor::{Sign, Time}; use git_hash::ObjectId; + use git_object::bstr::BString; use git_ref::file; - fn reflog_lines(store: &file::Store, name: &str) -> crate::Result> { + fn reflog_lines(store: &file::Store, name: &str) -> crate::Result> { let mut buf = Vec::new(); let res = store .reflog_iter(name, &mut buf)? .expect("existing reflog") - .map(|l| l.map(file::log::Line::from)) + .map(|l| l.map(git_ref::log::Line::from)) .collect::, _>>()?; Ok(res) } @@ -32,8 +32,8 @@ mod prepare_and_commit { } } - fn log_line(previous: ObjectId, new: ObjectId, message: impl Into) -> file::log::Line { - file::log::Line { + fn log_line(previous: ObjectId, new: ObjectId, message: impl Into) -> git_ref::log::Line { + git_ref::log::Line { previous_oid: previous, new_oid: new, signature: committer(), diff --git a/git-ref/tests/file/transaction/prepare_and_commit/create_or_update.rs b/git-ref/tests/file/transaction/prepare_and_commit/create_or_update.rs index 43b0a15cc00..df4024de4cb 100644 --- a/git-ref/tests/file/transaction/prepare_and_commit/create_or_update.rs +++ b/git-ref/tests/file/transaction/prepare_and_commit/create_or_update.rs @@ -2,16 +2,18 @@ use crate::file::{ store_with_packed_refs, store_writable, transaction::prepare_and_commit::{committer, empty_store, log_line, reflog_lines}, }; -use bstr::ByteSlice; use git_hash::ObjectId; use git_lock::acquire::Fail; use git_object::bstr::BString; +use git_object::bstr::ByteSlice; +use git_ref::file::ReferenceExt; +use git_ref::transaction::PreviousValue; use git_ref::{ file::{ transaction::{self, PackedRefs}, WriteReflog, }, - transaction::{Change, Create, LogChange, RefEdit, RefLog}, + transaction::{Change, LogChange, RefEdit, RefLog}, Target, }; use git_testtools::hex_to_id; @@ -34,7 +36,7 @@ fn reference_with_equally_named_empty_or_non_empty_directory_already_in_place_ca Some(RefEdit { change: Change::Update { log: LogChange::default(), - mode: Create::Only, + expected: PreviousValue::MustNotExist, new: Target::Symbolic("refs/heads/main".try_into().unwrap()), }, name: "HEAD".try_into()?, @@ -73,9 +75,7 @@ fn reference_with_old_value_must_exist_when_creating_it() -> crate::Result { change: Change::Update { log: LogChange::default(), new: Target::Peeled(ObjectId::null_sha1()), - mode: Create::OrUpdate { - previous: Some(Target::must_exist()), - }, + expected: PreviousValue::MustExist, }, name: "HEAD".try_into()?, deref: false, @@ -104,9 +104,75 @@ fn reference_with_explicit_value_must_match_the_value_on_update() -> crate::Resu change: Change::Update { log: LogChange::default(), new: Target::Peeled(ObjectId::null_sha1()), - mode: Create::OrUpdate { - previous: Some(Target::Peeled(hex_to_id("28ce6a8b26aa170e1de65536fe8abe1832bd3242"))), + expected: PreviousValue::MustExistAndMatch(Target::Peeled(hex_to_id( + "28ce6a8b26aa170e1de65536fe8abe1832bd3242", + ))), + }, + name: "HEAD".try_into()?, + deref: false, + }), + Fail::Immediately, + ); + match res { + Err(transaction::prepare::Error::ReferenceOutOfDate { full_name, actual, .. }) => { + assert_eq!(full_name, "HEAD"); + assert_eq!(actual, target); + } + _ => unreachable!("unexpected result"), + } + Ok(()) +} + +#[test] +fn the_existing_must_match_constraint_allow_non_existing_references_to_be_created() -> crate::Result { + let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; + let expected = PreviousValue::ExistingMustMatch(Target::Peeled(ObjectId::empty_tree(git_hash::Kind::Sha1))); + let edits = store + .transaction() + .prepare( + Some(RefEdit { + change: Change::Update { + log: LogChange::default(), + new: Target::Peeled(ObjectId::null_sha1()), + expected: expected.clone(), }, + name: "refs/heads/new".try_into()?, + deref: false, + }), + Fail::Immediately, + )? + .commit(&committer())?; + + assert_eq!( + edits, + vec![RefEdit { + change: Change::Update { + log: LogChange::default(), + new: Target::Peeled(ObjectId::null_sha1()), + expected, + }, + name: "refs/heads/new".try_into()?, + deref: false, + }] + ); + Ok(()) +} + +#[test] +fn the_existing_must_match_constraint_requires_existing_references_to_have_the_given_value_to_cause_failure_on_mismatch( +) -> crate::Result { + let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; + let head = store.try_find_loose("HEAD")?.expect("head exists already"); + let target = head.target; + + let res = store.transaction().prepare( + Some(RefEdit { + change: Change::Update { + log: LogChange::default(), + new: Target::Peeled(ObjectId::null_sha1()), + expected: PreviousValue::ExistingMustMatch(Target::Peeled(hex_to_id( + "28ce6a8b26aa170e1de65536fe8abe1832bd3242", + ))), }, name: "HEAD".try_into()?, deref: false, @@ -124,7 +190,7 @@ fn reference_with_explicit_value_must_match_the_value_on_update() -> crate::Resu } #[test] -fn reference_with_create_only_must_not_exist_already_when_creating_it_if_the_value_does_not_match() -> crate::Result { +fn reference_with_must_not_exist_constraint_cannot_be_created_if_it_exists_already() -> crate::Result { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; let head = store.try_find_loose("HEAD")?.expect("head exists already"); let target = head.target; @@ -134,7 +200,7 @@ fn reference_with_create_only_must_not_exist_already_when_creating_it_if_the_val change: Change::Update { log: LogChange::default(), new: Target::Peeled(ObjectId::null_sha1()), - mode: Create::Only, + expected: PreviousValue::MustNotExist, }, name: "HEAD".try_into()?, deref: false, @@ -152,18 +218,16 @@ fn reference_with_create_only_must_not_exist_already_when_creating_it_if_the_val } #[test] -fn namespaced_updates_or_deletions_cause_reference_names_to_be_rewritten_and_observable_in_the_output() -> crate::Result -{ - let (_keep, store) = empty_store()?; - +fn namespaced_updates_or_deletions_are_transparent_and_not_observable() -> crate::Result { + let (_keep, mut store) = empty_store()?; + store.namespace = git_ref::namespace::expand("foo")?.into(); let edits = store .transaction() - .namespace(git_ref::namespace::expand("foo")?) .prepare( vec![ RefEdit { change: Change::Delete { - previous: None, + expected: PreviousValue::Any, log: RefLog::AndReference, }, name: "refs/for/deletion".try_into()?, @@ -173,7 +237,7 @@ fn namespaced_updates_or_deletions_cause_reference_names_to_be_rewritten_and_obs change: Change::Update { log: LogChange::default(), new: Target::Symbolic("refs/heads/hello".try_into()?), - mode: Create::Only, + expected: PreviousValue::MustNotExist, }, name: "HEAD".try_into()?, deref: false, @@ -188,19 +252,19 @@ fn namespaced_updates_or_deletions_cause_reference_names_to_be_rewritten_and_obs vec![ RefEdit { change: Change::Delete { - previous: None, + expected: PreviousValue::Any, log: RefLog::AndReference, }, - name: "refs/namespaces/foo/refs/for/deletion".try_into()?, + name: "refs/for/deletion".try_into()?, deref: false, }, RefEdit { change: Change::Update { log: LogChange::default(), - new: Target::Symbolic("refs/namespaces/foo/refs/heads/hello".try_into()?), - mode: Create::Only, + new: Target::Symbolic("refs/heads/hello".try_into()?), + expected: PreviousValue::MustNotExist, }, - name: "refs/namespaces/foo/HEAD".try_into()?, + name: "HEAD".try_into()?, deref: false, } ] @@ -209,7 +273,53 @@ fn namespaced_updates_or_deletions_cause_reference_names_to_be_rewritten_and_obs } #[test] -fn reference_with_create_only_must_not_exist_already_when_creating_it_unless_the_value_matches() -> crate::Result { +fn reference_with_must_exist_constraint_must_exist_already_with_any_value() -> crate::Result { + let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; + let head = store.try_find_loose("HEAD")?.expect("head exists already"); + let target = head.target; + let previous_reflog_count = reflog_lines(&store, "HEAD")?.len(); + + let new_target = Target::Peeled(ObjectId::empty_tree(git_hash::Kind::Sha1)); + let edits = store + .transaction() + .prepare( + Some(RefEdit { + change: Change::Update { + log: LogChange::default(), + new: new_target.clone(), + expected: PreviousValue::MustExist, + }, + name: "HEAD".try_into()?, + deref: false, + }), + Fail::Immediately, + )? + .commit(&committer())?; + + assert_eq!( + edits, + vec![RefEdit { + change: Change::Update { + log: LogChange::default(), + new: new_target, + expected: PreviousValue::MustExistAndMatch(target) + }, + name: "HEAD".try_into()?, + deref: false, + }] + ); + + assert_eq!( + reflog_lines(&store, "HEAD")?.len(), + previous_reflog_count + 1, + "a new reflog is added" + ); + Ok(()) +} + +#[test] +fn reference_with_must_not_exist_constraint_may_exist_already_if_the_new_value_matches_the_existing_one( +) -> crate::Result { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; let head = store.try_find_loose("HEAD")?.expect("head exists already"); let target = head.target; @@ -222,7 +332,7 @@ fn reference_with_create_only_must_not_exist_already_when_creating_it_unless_the change: Change::Update { log: LogChange::default(), new: target.clone(), - mode: Create::Only, + expected: PreviousValue::MustNotExist, }, name: "HEAD".try_into()?, deref: false, @@ -237,7 +347,7 @@ fn reference_with_create_only_must_not_exist_already_when_creating_it_unless_the change: Change::Update { log: LogChange::default(), new: target.clone(), - mode: Create::OrUpdate { previous: Some(target) }, + expected: PreviousValue::MustExistAndMatch(target) }, name: "HEAD".try_into()?, deref: false, @@ -269,7 +379,7 @@ fn cancellation_after_preparation_leaves_no_change() -> crate::Result { change: Change::Update { log: LogChange::default(), new: Target::Symbolic("refs/heads/main".try_into().unwrap()), - mode: Create::Only, + expected: PreviousValue::MustNotExist, }, name: "HEAD".try_into()?, deref: false, @@ -306,7 +416,7 @@ fn symbolic_head_missing_referent_then_update_referent() -> crate::Result { change: Change::Update { log: log_ignored.clone(), new: new_head_value.clone(), - mode: Create::Only, + expected: PreviousValue::MustNotExist, }, name: "HEAD".try_into()?, deref: false, @@ -320,7 +430,7 @@ fn symbolic_head_missing_referent_then_update_referent() -> crate::Result { change: Change::Update { log: log_ignored.clone(), new: new_head_value.clone(), - mode: Create::Only, + expected: PreviousValue::MustNotExist, }, name: "HEAD".try_into()?, deref: false, @@ -354,7 +464,7 @@ fn symbolic_head_missing_referent_then_update_referent() -> crate::Result { change: Change::Update { log: log.clone(), new: new.clone(), - mode: Create::OrUpdate { previous: None }, + expected: PreviousValue::Any, }, name: "HEAD".try_into()?, deref: true, @@ -370,9 +480,7 @@ fn symbolic_head_missing_referent_then_update_referent() -> crate::Result { change: Change::Update { log: log_only.clone(), new: new.clone(), - mode: Create::OrUpdate { - previous: Some(new_head_value.clone()) - }, + expected: PreviousValue::MustExistAndMatch(new_head_value.clone()), }, name: "HEAD".try_into()?, deref: false, @@ -381,7 +489,7 @@ fn symbolic_head_missing_referent_then_update_referent() -> crate::Result { change: Change::Update { log, new: new.clone(), - mode: Create::Only, + expected: PreviousValue::Any, }, name: referent.try_into()?, deref: false, @@ -448,9 +556,7 @@ fn write_reference_to_which_head_points_to_does_not_update_heads_reflog_even_tho force_create_reflog: false, message: "".into(), }, - mode: Create::OrUpdate { - previous: Some(Target::must_exist()), - }, + expected: PreviousValue::MustExist, new: Target::Peeled(new_id), }, name: referent.as_bstr().try_into()?, @@ -470,9 +576,9 @@ fn write_reference_to_which_head_points_to_does_not_update_heads_reflog_even_tho force_create_reflog: false, message: "".into(), }, - mode: Create::OrUpdate { - previous: Some(Target::Peeled(hex_to_id("02a7a22d90d7c02fb494ed25551850b868e634f0"))), - }, + expected: PreviousValue::MustExistAndMatch(Target::Peeled(hex_to_id( + "02a7a22d90d7c02fb494ed25551850b868e634f0" + )),), new: Target::Peeled(new_id), }, name: referent.as_bstr().try_into()?, @@ -515,9 +621,7 @@ fn packed_refs_are_looked_up_when_checking_existing_values() -> crate::Result { force_create_reflog: false, message: "for pack".into(), }, - mode: Create::OrUpdate { - previous: Some(Target::Peeled(old_id)), - }, + expected: PreviousValue::MustExistAndMatch(Target::Peeled(old_id)), new: Target::Peeled(new_id), }, name: "refs/heads/main".try_into()?, @@ -572,9 +676,7 @@ fn packed_refs_creation_with_packed_refs_mode_prune_removes_original_loose_refs( .map(|r| RefEdit { change: Change::Update { log: LogChange::default(), - mode: Create::OrUpdate { - previous: Some(r.target.clone()), - }, + expected: PreviousValue::MustExistAndMatch(r.target.clone()), new: r.target, }, name: r.name, @@ -615,7 +717,7 @@ fn packed_refs_creation_with_packed_refs_mode_leave_keeps_original_loose_refs() let packed = store.packed_buffer()?.expect("packed-refs"); assert_ne!( packed.find("newer-as-loose")?.target(), - branch.target().as_id().expect("peeled"), + branch.target.as_id().expect("peeled"), "the packed ref is outdated" ); let mut buf = Vec::new(); @@ -625,9 +727,7 @@ fn packed_refs_creation_with_packed_refs_mode_leave_keeps_original_loose_refs() let edits = store.loose_iter()?.map(|r| r.expect("valid ref")).map(|r| RefEdit { change: Change::Update { log: LogChange::default(), - mode: Create::OrUpdate { - previous: r.target.clone().into(), - }, + expected: PreviousValue::MustExistAndMatch(r.target.clone()), new: r.target, }, name: r.name, @@ -666,7 +766,7 @@ fn packed_refs_creation_with_packed_refs_mode_leave_keeps_original_loose_refs() ); assert_eq!( packed.find("newer-as-loose")?.target(), - store.find("newer-as-loose", None)?.target().as_id().expect("peeled"), + store.find("newer-as-loose", None)?.target.into_id(), "the packed ref is now up to date and the loose ref definitely still exists" ); Ok(()) diff --git a/git-ref/tests/file/transaction/prepare_and_commit/delete.rs b/git-ref/tests/file/transaction/prepare_and_commit/delete.rs index 2d9732de036..8d36f66ea1d 100644 --- a/git-ref/tests/file/transaction/prepare_and_commit/delete.rs +++ b/git-ref/tests/file/transaction/prepare_and_commit/delete.rs @@ -3,9 +3,11 @@ use crate::file::{ transaction::prepare_and_commit::{committer, empty_store}, }; use git_lock::acquire::Fail; +use git_ref::file::ReferenceExt; +use git_ref::transaction::PreviousValue; use git_ref::{ transaction::{Change, RefEdit, RefLog}, - Target, + Reference, Target, }; use git_testtools::hex_to_id; use std::convert::TryInto; @@ -18,7 +20,7 @@ fn delete_a_ref_which_is_gone_succeeds() -> crate::Result { .prepare( Some(RefEdit { change: Change::Delete { - previous: None, + expected: PreviousValue::Any, log: RefLog::AndReference, }, name: "DOES_NOT_EXIST".try_into()?, @@ -37,7 +39,7 @@ fn delete_a_ref_which_is_gone_but_must_exist_fails() -> crate::Result { let res = store.transaction().prepare( Some(RefEdit { change: Change::Delete { - previous: Some(Target::must_exist()), + expected: PreviousValue::MustExist, log: RefLog::AndReference, }, name: "DOES_NOT_EXIST".try_into()?, @@ -67,7 +69,7 @@ fn delete_ref_and_reflog_on_symbolic_no_deref() -> crate::Result { .prepare( Some(RefEdit { change: Change::Delete { - previous: Some(Target::must_exist()), + expected: PreviousValue::MustExist, log: RefLog::AndReference, }, name: head.name.clone(), @@ -81,7 +83,7 @@ fn delete_ref_and_reflog_on_symbolic_no_deref() -> crate::Result { edits, vec![RefEdit { change: Change::Delete { - previous: Some(Target::Symbolic("refs/heads/main".try_into()?)), + expected: PreviousValue::MustExistAndMatch(Target::Symbolic("refs/heads/main".try_into()?)), log: RefLog::AndReference, }, name: head.name, @@ -107,7 +109,7 @@ fn delete_ref_with_incorrect_previous_value_fails() -> crate::Result { let res = store.transaction().prepare( Some(RefEdit { change: Change::Delete { - previous: Some(Target::Symbolic("refs/heads/main".try_into()?)), + expected: PreviousValue::MustExistAndMatch(Target::Symbolic("refs/heads/main".try_into()?)), log: RefLog::Only, }, name: head.name, @@ -141,7 +143,7 @@ fn delete_reflog_only_of_symbolic_no_deref() -> crate::Result { .prepare( Some(RefEdit { change: Change::Delete { - previous: Some(Target::Symbolic("refs/heads/main".try_into()?)), + expected: PreviousValue::MustExistAndMatch(Target::Symbolic("refs/heads/main".try_into()?)), log: RefLog::Only, }, name: head.name, @@ -152,13 +154,13 @@ fn delete_reflog_only_of_symbolic_no_deref() -> crate::Result { .commit(&committer())?; assert_eq!(edits.len(), 1); - let head = store.find_loose("HEAD")?; + let head: Reference = store.find_loose("HEAD")?.into(); assert!(!head.log_exists(&store)); let main = store.find_loose("main").expect("referent still exists"); assert!(main.log_exists(&store), "log is untouched, too"); assert_eq!( main.target, - head.follow_symbolic(&store, None).expect("a symref")?.target(), + head.follow(&store, None).expect("a symref")?.target, "head points to main" ); Ok(()) @@ -175,7 +177,7 @@ fn delete_reflog_only_of_symbolic_with_deref() -> crate::Result { .prepare( Some(RefEdit { change: Change::Delete { - previous: Some(Target::must_exist()), + expected: PreviousValue::MustExist, log: RefLog::Only, }, name: head.name, @@ -186,13 +188,13 @@ fn delete_reflog_only_of_symbolic_with_deref() -> crate::Result { .commit(&committer())?; assert_eq!(edits.len(), 2); - let head = store.find_loose("HEAD")?; + let head: Reference = store.find_loose("HEAD")?.into(); assert!(!head.log_exists(&store)); let main = store.find_loose("main").expect("referent still exists"); assert!(!main.log_exists(&store), "log is removed"); assert_eq!( main.target, - head.follow_symbolic(&store, None).expect("a symref")?.target(), + head.follow(&store, None).expect("a symref")?.target, "head points to main" ); Ok(()) @@ -208,7 +210,7 @@ fn delete_broken_ref_that_must_exist_fails_as_it_is_no_valid_ref() -> crate::Res let res = store.transaction().prepare( Some(RefEdit { change: Change::Delete { - previous: Some(Target::must_exist()), + expected: PreviousValue::MustExist, log: RefLog::AndReference, }, name: "HEAD".try_into()?, @@ -228,6 +230,40 @@ fn delete_broken_ref_that_must_exist_fails_as_it_is_no_valid_ref() -> crate::Res Ok(()) } +#[test] +fn non_existing_can_be_deleted_with_the_may_exist_match_constraint() -> crate::Result { + let (_keep, store) = empty_store()?; + let previous_value = + PreviousValue::ExistingMustMatch(Target::Peeled(hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"))); + let edits = store + .transaction() + .prepare( + Some(RefEdit { + change: Change::Delete { + expected: previous_value.clone(), + log: RefLog::AndReference, + }, + name: "refs/heads/not-there".try_into()?, + deref: true, + }), + Fail::Immediately, + )? + .commit(&committer())?; + + assert_eq!( + edits, + vec![RefEdit { + change: Change::Delete { + expected: previous_value, + log: RefLog::AndReference, + }, + name: "refs/heads/not-there".try_into()?, + deref: false, + }] + ); + Ok(()) +} + #[test] /// Based on https://github.com/git/git/blob/master/refs/files-backend.c#L514:L515 fn delete_broken_ref_that_may_not_exist_works_even_in_deref_mode() -> crate::Result { @@ -240,7 +276,7 @@ fn delete_broken_ref_that_may_not_exist_works_even_in_deref_mode() -> crate::Res .prepare( Some(RefEdit { change: Change::Delete { - previous: None, + expected: PreviousValue::Any, log: RefLog::AndReference, }, name: "HEAD".try_into()?, @@ -255,7 +291,7 @@ fn delete_broken_ref_that_may_not_exist_works_even_in_deref_mode() -> crate::Res edits, vec![RefEdit { change: Change::Delete { - previous: None, + expected: PreviousValue::Any, log: RefLog::AndReference, }, name: "HEAD".try_into()?, @@ -278,7 +314,7 @@ fn store_write_mode_has_no_effect_and_reflogs_are_always_deleted() -> crate::Res .prepare( Some(RefEdit { change: Change::Delete { - previous: None, + expected: PreviousValue::Any, log: RefLog::Only, }, name: "HEAD".try_into()?, @@ -313,7 +349,7 @@ fn packed_refs_are_consulted_when_determining_previous_value_of_ref_to_be_delete .prepare( Some(RefEdit { change: Change::Delete { - previous: Some(Target::Peeled(old_id)), + expected: PreviousValue::MustExistAndMatch(Target::Peeled(old_id)), log: RefLog::AndReference, }, name: "refs/heads/main".try_into()?, @@ -334,7 +370,7 @@ fn a_loose_ref_with_old_value_check_and_outdated_packed_refs_value_deletes_both_ let (_keep, store) = store_writable("make_packed_ref_repository_for_overlay.sh")?; let packed = store.packed_buffer()?.expect("packed-refs"); let branch = store.find("newer-as-loose", Some(&packed))?; - let branch_id = branch.target().as_id().map(ToOwned::to_owned).expect("peeled"); + let branch_id = branch.target.as_id().map(ToOwned::to_owned).expect("peeled"); assert_ne!( packed.find("newer-as-loose")?.target(), branch_id, @@ -346,10 +382,10 @@ fn a_loose_ref_with_old_value_check_and_outdated_packed_refs_value_deletes_both_ .prepare( Some(RefEdit { change: Change::Delete { - previous: Some(Target::Peeled(branch_id)), + expected: PreviousValue::MustExistAndMatch(Target::Peeled(branch_id)), log: RefLog::AndReference, }, - name: branch.name().into(), + name: branch.name, deref: false, }), git_lock::acquire::Fail::Immediately, @@ -371,46 +407,42 @@ fn a_loose_ref_with_old_value_check_and_outdated_packed_refs_value_deletes_both_ } #[test] -fn all_contained_references_deletes_the_packed_ref_file_too() { - let (_keep, store) = store_writable("make_packed_ref_repository.sh").unwrap(); +fn all_contained_references_deletes_the_packed_ref_file_too() -> crate::Result { + for mode in ["must-exist", "may-exist"] { + let (_keep, store) = store_writable("make_packed_ref_repository.sh")?; - let edits = store - .transaction() - .prepare( - store - .packed_buffer() - .unwrap() - .expect("packed-refs") - .iter() - .unwrap() - .map(|r| { + let edits = store + .transaction() + .prepare( + store.packed_buffer()?.expect("packed-refs").iter()?.map(|r| { let r = r.expect("valid ref"); RefEdit { change: Change::Delete { - previous: Target::Peeled(r.target()).into(), + expected: match mode { + "must-exist" => PreviousValue::MustExistAndMatch(Target::Peeled(r.target())), + "may-exist" => PreviousValue::ExistingMustMatch(Target::Peeled(r.target())), + _ => unimplemented!("unknown mode: {}", mode), + }, log: RefLog::AndReference, }, name: r.name.into(), deref: false, } }), - git_lock::acquire::Fail::Immediately, - ) - .unwrap() - .commit(&committer()) - .unwrap(); - - assert!(!store.packed_refs_path().is_file(), "packed-refs was entirely removed"); - - let packed = store.packed_buffer().unwrap(); - assert!(packed.is_none(), "it won't make up packed refs"); - for edit in edits { - assert!( - store - .try_find(edit.name.to_partial(), packed.as_ref()) - .unwrap() - .is_none(), - "delete ref cannot be found" - ); + git_lock::acquire::Fail::Immediately, + )? + .commit(&committer())?; + + assert!(!store.packed_refs_path().is_file(), "packed-refs was entirely removed"); + + let packed = store.packed_buffer()?; + assert!(packed.is_none(), "it won't make up packed refs"); + for edit in edits { + assert!( + store.try_find(edit.name.to_partial(), packed.as_ref())?.is_none(), + "delete ref cannot be found" + ); + } } + Ok(()) } diff --git a/git-ref/tests/fullname/mod.rs b/git-ref/tests/fullname/mod.rs new file mode 100644 index 00000000000..97b0283aee2 --- /dev/null +++ b/git-ref/tests/fullname/mod.rs @@ -0,0 +1,22 @@ +use std::convert::TryInto; + +#[test] +fn prefix_with_namespace_and_stripping() { + let ns = git_ref::namespace::expand("foo").unwrap(); + let mut name: git_ref::FullName = "refs/heads/main".try_into().unwrap(); + assert_eq!( + name.prefix_namespace(&ns).as_bstr(), + "refs/namespaces/foo/refs/heads/main" + ); + assert_eq!( + name.prefix_namespace(&ns).as_bstr(), + "refs/namespaces/foo/refs/heads/main", + "idempotent prefixing" + ); + assert_eq!(name.strip_namespace(&ns).as_bstr(), "refs/heads/main"); + assert_eq!( + name.strip_namespace(&ns).as_bstr(), + "refs/heads/main", + "idempotent stripping" + ); +} diff --git a/git-ref/tests/namespace/mod.rs b/git-ref/tests/namespace/mod.rs index 45d83136d7b..a785ab64696 100644 --- a/git-ref/tests/namespace/mod.rs +++ b/git-ref/tests/namespace/mod.rs @@ -1,3 +1,15 @@ +use std::path::Path; + +#[test] +fn into_namespaced_prefix() { + assert_eq!( + git_ref::namespace::expand("foo") + .unwrap() + .into_namespaced_prefix("prefix"), + Path::new("refs").join("namespaces").join("foo").join("prefix") + ) +} + mod expand { #[test] fn components_end_with_trailing_slash_to_help_with_prefix_stripping() { diff --git a/git-ref/tests/packed/iter.rs b/git-ref/tests/packed/iter.rs index dbaf884352f..680cb94509c 100644 --- a/git-ref/tests/packed/iter.rs +++ b/git-ref/tests/packed/iter.rs @@ -1,6 +1,6 @@ use std::convert::TryInto; -use bstr::ByteSlice; +use git_object::bstr::ByteSlice; use git_ref::packed; use crate::file::{store_at, store_with_packed_refs}; diff --git a/git-ref/tests/reference/mod.rs b/git-ref/tests/reference/mod.rs new file mode 100644 index 00000000000..a519653be77 --- /dev/null +++ b/git-ref/tests/reference/mod.rs @@ -0,0 +1,27 @@ +use std::convert::TryInto; + +use git_ref::{FullName, Target}; + +#[test] +fn strip_namespace() { + let ns = git_ref::namespace::expand("ns").unwrap(); + let mut r = git_ref::Reference { + name: { + let mut n: FullName = "refs/heads/main".try_into().unwrap(); + n.prefix_namespace(&ns); + n + }, + target: Target::Symbolic({ + let mut n: FullName = "refs/tags/foo".try_into().unwrap(); + n.prefix_namespace(&ns); + n + }), + peeled: None, + }; + r.strip_namespace(&ns); + assert_eq!(r.name.as_bstr(), "refs/heads/main", "name is stripped"); + assert!( + matches!(r.target, Target::Symbolic(n) if n.as_bstr() == "refs/tags/foo"), + "and the symbolic target as well" + ); +} diff --git a/git-ref/tests/refs.rs b/git-ref/tests/refs.rs index f6e7421a809..6d1ab45a52d 100644 --- a/git-ref/tests/refs.rs +++ b/git-ref/tests/refs.rs @@ -1,6 +1,8 @@ type Result = std::result::Result>; mod file; +mod fullname; mod namespace; mod packed; +mod reference; mod transaction; diff --git a/git-ref/tests/transaction/mod.rs b/git-ref/tests/transaction/mod.rs index e2443d17596..c03f9a03748 100644 --- a/git-ref/tests/transaction/mod.rs +++ b/git-ref/tests/transaction/mod.rs @@ -1,9 +1,9 @@ mod refedit_ext { use std::{cell::RefCell, collections::BTreeMap, convert::TryInto}; - use bstr::{BString, ByteSlice}; + use git_object::bstr::{BString, ByteSlice}; use git_ref::{ - transaction::{Change, Create, RefEdit, RefEditsExt, RefLog}, + transaction::{Change, PreviousValue, RefEdit, RefEditsExt, RefLog}, PartialNameRef, Target, }; @@ -33,7 +33,7 @@ mod refedit_ext { fn named_edit(name: &str) -> RefEdit { RefEdit { change: Change::Delete { - previous: None, + expected: PreviousValue::Any, log: RefLog::AndReference, }, name: name.try_into().expect("valid name"), @@ -48,7 +48,7 @@ mod refedit_ext { let mut edits = vec![ RefEdit { change: Change::Delete { - previous: None, + expected: PreviousValue::Any, log: RefLog::AndReference, }, name: "HEAD".try_into()?, @@ -56,7 +56,7 @@ mod refedit_ext { }, RefEdit { change: Change::Delete { - previous: None, + expected: PreviousValue::Any, log: RefLog::AndReference, }, name: "refs/heads/main".try_into()?, @@ -65,7 +65,7 @@ mod refedit_ext { ]; let err = edits - .pre_process(|n| store.find_existing(n), |_, e| e, None) + .pre_process(|n| store.find_existing(n), |_, e| e) .expect_err("duplicate detected"); assert_eq!( err.to_string(), @@ -95,60 +95,12 @@ mod refedit_ext { ); } - #[test] - fn namespaces_are_rewriting_names_and_symbolic_ref_targets_when_provided() -> crate::Result { - let mut edits = vec![ - RefEdit { - change: Change::Delete { - previous: None, - log: RefLog::AndReference, - }, - name: "refs/tags/deleted".try_into()?, - deref: false, - }, - RefEdit { - change: Change::Update { - log: Default::default(), - mode: Create::Only, - new: Target::Symbolic("refs/heads/main".try_into()?), - }, - name: "HEAD".try_into()?, - deref: false, - }, - ]; - edits.adjust_namespace(git_ref::namespace::expand("foo")?.into()); - assert_eq!( - edits, - vec![ - RefEdit { - change: Change::Delete { - previous: None, - log: RefLog::AndReference, - }, - name: "refs/namespaces/foo/refs/tags/deleted".try_into()?, - deref: false, - }, - RefEdit { - change: Change::Update { - log: Default::default(), - mode: Create::Only, - new: Target::Symbolic("refs/namespaces/foo/refs/heads/main".try_into()?), - }, - name: "refs/namespaces/foo/HEAD".try_into()?, - deref: false, - } - ], - "it rewrites both names as well as symbolic ref targets" - ); - Ok(()) - } - mod splitting { use std::{cell::Cell, convert::TryInto}; use git_hash::ObjectId; use git_ref::{ - transaction::{Change, Create, LogChange, RefEdit, RefEditsExt, RefLog}, + transaction::{Change, LogChange, PreviousValue, RefEdit, RefEditsExt, RefLog}, FullNameRef, PartialNameRef, Target, }; use git_testtools::hex_to_id; @@ -172,7 +124,7 @@ mod refedit_ext { let mut edits = vec![ RefEdit { change: Change::Delete { - previous: None, + expected: PreviousValue::Any, log: RefLog::AndReference, }, name: "SYMBOLIC_PROBABLY_BUT_DEREF_IS_FALSE_SO_IGNORED".try_into()?, @@ -180,7 +132,7 @@ mod refedit_ext { }, RefEdit { change: Change::Delete { - previous: None, + expected: PreviousValue::Any, log: RefLog::AndReference, }, name: "refs/heads/anything-but-not-symbolic".try_into()?, @@ -188,7 +140,7 @@ mod refedit_ext { }, RefEdit { change: Change::Delete { - previous: None, + expected: PreviousValue::Any, log: RefLog::AndReference, }, name: "refs/heads/does-not-exist-and-deref-is-ignored".try_into()?, @@ -241,7 +193,7 @@ mod refedit_ext { let mut edits = vec![ RefEdit { change: Change::Delete { - previous: None, + expected: PreviousValue::Any, log: RefLog::AndReference, }, name: "refs/heads/delete-symbolic-1".try_into()?, @@ -249,7 +201,7 @@ mod refedit_ext { }, RefEdit { change: Change::Update { - mode: Create::Only, + expected: PreviousValue::MustNotExist, log: LogChange { mode: RefLog::AndReference, force_create_reflog: true, @@ -274,7 +226,8 @@ mod refedit_ext { } #[test] - fn symbolic_refs_are_split_into_referents_handling_the_reflog_recursively() -> crate::Result { + fn symbolic_refs_are_split_into_referents_handling_the_reflog_and_previous_values_recursively() -> crate::Result + { let store = MockStore::with(vec![ ( "refs/heads/delete-symbolic-1", @@ -314,7 +267,7 @@ mod refedit_ext { let mut edits = vec![ RefEdit { change: Change::Delete { - previous: None, + expected: PreviousValue::Any, log: RefLog::AndReference, }, name: "refs/heads/delete-symbolic-1".try_into()?, @@ -322,7 +275,7 @@ mod refedit_ext { }, RefEdit { change: Change::Update { - mode: Create::Only, + expected: PreviousValue::MustNotExist, log: log.clone(), new: Target::Peeled(ObjectId::null_sha1()), }, @@ -350,7 +303,7 @@ mod refedit_ext { vec![ RefEdit { change: Change::Delete { - previous: None, + expected: PreviousValue::Any, log: RefLog::Only, }, name: "refs/heads/delete-symbolic-1".try_into()?, @@ -358,7 +311,7 @@ mod refedit_ext { }, RefEdit { change: Change::Update { - mode: Create::Only, + expected: PreviousValue::Any, log: log_only.clone(), new: Target::Peeled(ObjectId::null_sha1()), }, @@ -367,7 +320,7 @@ mod refedit_ext { }, RefEdit { change: Change::Delete { - previous: None, + expected: PreviousValue::Any, log: RefLog::Only, }, name: "refs/heads/delete-symbolic-2".try_into()?, @@ -375,7 +328,7 @@ mod refedit_ext { }, RefEdit { change: Change::Update { - mode: Create::Only, + expected: PreviousValue::Any, log: log_only, new: Target::Peeled(ObjectId::null_sha1()), }, @@ -384,7 +337,7 @@ mod refedit_ext { }, RefEdit { change: Change::Delete { - previous: None, + expected: PreviousValue::Any, log: RefLog::AndReference, }, name: "refs/heads/delete-symbolic-3".try_into()?, @@ -392,7 +345,7 @@ mod refedit_ext { }, RefEdit { change: Change::Update { - mode: Create::Only, + expected: PreviousValue::MustNotExist, log, new: Target::Peeled(ObjectId::null_sha1()), }, diff --git a/git-repository/CHANGELOG.md b/git-repository/CHANGELOG.md index 94899973d62..50800f678fe 100644 --- a/git-repository/CHANGELOG.md +++ b/git-repository/CHANGELOG.md @@ -1,4 +1,4 @@ -### v0.9.0 (2021-08-??) +### v0.9.0 (2021-09-??) #### New @@ -7,6 +7,9 @@ - `Repository::init(Kind)` - `open()` - `Repository::open()` +- `easy::Head` +- `easy::ext::ReferenceAccessExt::head()` +- `ext::ReferenceExt` trait #### Breaking - **renames / moves / Signature Changes** diff --git a/git-repository/Cargo.toml b/git-repository/Cargo.toml index efa6d4d6de1..910985b9293 100644 --- a/git-repository/Cargo.toml +++ b/git-repository/Cargo.toml @@ -17,9 +17,9 @@ default = ["max-performance", "one-stop-shop"] unstable = [] serde1 = ["git-pack/serde1", "git-object/serde1"] max-performance = ["git-features/zlib-ng-compat", "git-features/fast-sha1"] +local-time-support = ["git-actor/local-time-support"] local = [ "git-url", - "git-traverse", "git-diff", "git-pack/pack-cache-lru-dynamic", "git-pack/pack-cache-lru-static", @@ -30,28 +30,30 @@ network = [ one-stop-shop = [ "local", "network", + "local-time-support" ] [dependencies] -git-ref = { version ="^0.6.0", path = "../git-ref" } +git-ref = { version ="^0.7.0", path = "../git-ref" } git-tempfile = { version ="^1.0.0", path = "../git-tempfile" } git-lock = { version ="^1.0.0", path = "../git-lock" } git-validate = { version = "^0.5.0", path = "../git-validate" } git-config = { version ="^0.1.0", path = "../git-config" } git-odb = { version ="^0.21.0", path = "../git-odb" } -git-hash = { version = "^0.5.0", path = "../git-hash" } +git-hash = { version ="^0.6.0", path = "../git-hash" } git-object = { version ="^0.13.0", path = "../git-object" } git-actor = { version ="^0.5.0", path = "../git-actor" } -git-pack = { version ="^0.9.0", path = "../git-pack" } +git-pack = { version ="^0.10.0", path = "../git-pack" } git-url = { version = "0.3.0", path = "../git-url", optional = true } -git-traverse = { version ="^0.8.0", path = "../git-traverse", optional = true } +git-traverse = { version ="^0.8.0", path = "../git-traverse" } git-protocol = { version ="^0.10.0", path = "../git-protocol", optional = true } git-diff = { version ="^0.9.0", path = "../git-diff", optional = true } git-features = { version = "^0.16.0", path = "../git-features", features = ["progress"] } +bstr = { version = "0.2.13", default-features = false, features = ["std", "unicode"]} signal-hook = { version = "0.3.9", default-features = false } thiserror = "1.0.26" parking_lot = { version = "0.11.2", features = ["arc_lock"] } diff --git a/git-repository/src/commit.rs b/git-repository/src/commit.rs new file mode 100644 index 00000000000..a3b545c948d --- /dev/null +++ b/git-repository/src/commit.rs @@ -0,0 +1,51 @@ +use std::borrow::Cow; + +use bstr::{BStr, BString, ByteSlice, ByteVec}; + +/// An empty array of a type usable with the `git::easy` API to help declaring no parents should be used +pub const NO_PARENT_IDS: [git_hash::ObjectId; 0] = []; + +/// Produce a short commit summary for the given `message`. +/// +/// This means the following +/// +/// * Take the subject line which is delimited by two newlines (\n\n) +/// * transform intermediate consecutive whitespace including \r into one space +/// +/// The resulting summary will have folded whitespace before a newline into spaces and stopped that process +/// once two consecutive newlines are encountered. +pub fn summary(message: &BStr) -> Cow<'_, BStr> { + let message = message.trim(); + match message.find_byte(b'\n') { + Some(mut pos) => { + let mut out = BString::default(); + let mut previous_pos = None; + loop { + if let Some(previous_pos) = previous_pos { + if previous_pos + 1 == pos { + let len_after_trim = out.trim_end().len(); + out.resize(len_after_trim, 0); + break out.into(); + } + } + let message_to_newline = &message[previous_pos.map(|p| p + 1).unwrap_or(0)..pos]; + + if let Some(pos_before_whitespace) = message_to_newline.rfind_not_byteset(b"\t\n\x0C\r ") { + out.extend_from_slice(&message_to_newline[..pos_before_whitespace + 1]); + } + out.push_byte(b' '); + previous_pos = Some(pos); + match message.get(pos + 1..).and_then(|i| i.find_byte(b'\n')) { + Some(next_nl_pos) => pos += next_nl_pos + 1, + None => { + if let Some(slice) = message.get((pos + 1)..) { + out.extend_from_slice(slice); + } + break out.into(); + } + } + } + } + None => message.as_bstr().into(), + } +} diff --git a/git-repository/src/easy/commit.rs b/git-repository/src/easy/commit.rs new file mode 100644 index 00000000000..f733c2ad192 --- /dev/null +++ b/git-repository/src/easy/commit.rs @@ -0,0 +1,15 @@ +#![allow(missing_docs)] +mod error { + use crate::easy; + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + ReferenceNameValidation(#[from] git_ref::name::Error), + #[error(transparent)] + WriteObject(#[from] easy::object::write::Error), + #[error(transparent)] + ReferenceEdit(#[from] easy::reference::edit::Error), + } +} +pub use error::Error; diff --git a/git-repository/src/easy/ext/config.rs b/git-repository/src/easy/ext/config.rs new file mode 100644 index 00000000000..af25315f4c2 --- /dev/null +++ b/git-repository/src/easy/ext/config.rs @@ -0,0 +1,16 @@ +use crate::easy; + +pub trait ConfigAccessExt: easy::Access + Sized { + // TODO: actual implementation + fn committer(&self) -> git_actor::Signature { + // TODO: actually read the committer information from git-config, probably it should be provided here + git_actor::Signature::empty() + } + + /// The kind of hash the repository is configured to use + fn hash_kind(&self) -> Result { + self.repo().map(|r| r.hash_kind) + } +} + +impl ConfigAccessExt for A where A: easy::Access + Sized {} diff --git a/git-repository/src/easy/ext/mod.rs b/git-repository/src/easy/ext/mod.rs index 5fa97899240..eb1807f168a 100644 --- a/git-repository/src/easy/ext/mod.rs +++ b/git-repository/src/easy/ext/mod.rs @@ -4,3 +4,6 @@ pub use object::ObjectAccessExt; mod reference; pub use reference::ReferenceAccessExt; + +mod config; +pub use config::ConfigAccessExt; diff --git a/git-repository/src/easy/ext/object.rs b/git-repository/src/easy/ext/object.rs index 595fa826099..5b1daa4a8d8 100644 --- a/git-repository/src/easy/ext/object.rs +++ b/git-repository/src/easy/ext/object.rs @@ -1,61 +1,124 @@ -use std::ops::DerefMut; +use std::{convert::TryInto, ops::DerefMut}; +use bstr::BString; use git_hash::ObjectId; use git_odb::{Find, FindExt}; +use git_ref::{ + transaction::{LogChange, PreviousValue, RefLog}, + FullName, +}; use crate::{ easy, - easy::{object, ObjectRef}, + easy::{commit, object, ObjectRef, Oid}, + ext::ObjectIdExt, }; -pub fn find_object( - access: &A, - id: impl Into, -) -> Result, object::find::existing::Error> { - let state = access.state(); - let id = id.into(); - let kind = { - let mut buf = access.state().try_borrow_mut_buf()?; - let obj = access - .repo()? - .odb - .find(&id, &mut buf, state.try_borrow_mut_pack_cache()?.deref_mut())?; - obj.kind - }; - - ObjectRef::from_current_buf(id, kind, access).map_err(Into::into) -} - -pub fn try_find_object( - access: &A, - id: impl Into, -) -> Result>, object::find::Error> { - let state = access.state(); - let id = id.into(); - access - .repo()? - .odb - .try_find( - &id, - state.try_borrow_mut_buf()?.deref_mut(), - state.try_borrow_mut_pack_cache()?.deref_mut(), - )? - .map(|obj| { - let kind = obj.kind; - drop(obj); - ObjectRef::from_current_buf(id, kind, access).map_err(Into::into) - }) - .transpose() -} - pub trait ObjectAccessExt: easy::Access + Sized { // NOTE: in order to get the actual kind of object, is must be fully decoded from storage in case of packs // even though partial decoding is possible for loose objects, it won't matter much here. fn find_object(&self, id: impl Into) -> Result, object::find::existing::Error> { - find_object(self, id) + let state = self.state(); + let id = id.into(); + let kind = { + let mut buf = self.state().try_borrow_mut_buf()?; + let obj = self + .repo()? + .odb + .find(&id, &mut buf, state.try_borrow_mut_pack_cache()?.deref_mut())?; + obj.kind + }; + + ObjectRef::from_current_buf(id, kind, self).map_err(Into::into) } fn try_find_object(&self, id: impl Into) -> Result>, object::find::Error> { - try_find_object(self, id) + let state = self.state(); + let id = id.into(); + self.repo()? + .odb + .try_find( + &id, + state.try_borrow_mut_buf()?.deref_mut(), + state.try_borrow_mut_pack_cache()?.deref_mut(), + )? + .map(|obj| { + let kind = obj.kind; + drop(obj); + ObjectRef::from_current_buf(id, kind, self).map_err(Into::into) + }) + .transpose() + } + + fn write_object(&self, object: &git_object::Object) -> Result, object::write::Error> { + use git_odb::Write; + + let repo = self.repo()?; + repo.odb + .write(object, repo.hash_kind) + .map(|oid| oid.attach(self)) + .map_err(Into::into) + } + + // docs notes + // Fails immediately if lock can't be acquired as first parent depends on it + // Writes without message encoding + fn commit( + &self, + reference: Name, + message: impl Into, + author: impl Into, + committer: impl Into, + tree: impl Into, + parents: impl IntoIterator>, + ) -> Result, commit::Error> + where + Name: TryInto, + commit::Error: From, + { + use git_ref::{ + transaction::{Change, RefEdit}, + Target, + }; + + use crate::easy::ext::ReferenceAccessExt; + + let reference = reference.try_into()?; + let commit: git_object::Object = git_object::Commit { + message: message.into(), + tree: tree.into(), + author: author.into(), + committer: committer.into(), + encoding: None, + parents: parents.into_iter().map(|id| id.into()).collect(), + extra_headers: Default::default(), + } + .into(); + + let commit_id = self.write_object(&commit)?; + let commit = commit.into_commit(); + self.edit_reference( + RefEdit { + change: Change::Update { + log: LogChange { + mode: RefLog::AndReference, + force_create_reflog: false, + message: crate::reference::log::message("commit", &commit), + }, + expected: match commit.parents.get(0).map(|p| Target::Peeled(*p)) { + Some(previous) => PreviousValue::ExistingMustMatch(previous), + None => PreviousValue::MustNotExist, + }, + new: Target::Peeled(commit_id.inner), + }, + name: reference, + deref: true, + }, + git_lock::acquire::Fail::Immediately, + Some(&commit.committer), + )?; + Ok(commit_id) } } + +impl ObjectAccessExt for A where A: easy::Access + Sized {} diff --git a/git-repository/src/easy/ext/reference.rs b/git-repository/src/easy/ext/reference.rs index 203487b5997..0cb6394eebd 100644 --- a/git-repository/src/easy/ext/reference.rs +++ b/git-repository/src/easy/ext/reference.rs @@ -1,47 +1,113 @@ -use std::convert::TryInto; +use std::{ + convert::TryInto, + ops::{Deref, DerefMut}, +}; +use bstr::BString; use git_actor as actor; use git_hash::ObjectId; use git_lock as lock; use git_ref::{ - file::find::Error, - transaction::{Change, Create, RefEdit}, - PartialNameRef, Target, + transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog}, + FullName, PartialNameRef, Target, }; use crate::{ easy, - easy::{reference, Reference}, + easy::{ext::ConfigAccessExt, reference, Reference}, + ext::ReferenceExt, }; +const DEFAULT_LOCK_MODE: git_lock::acquire::Fail = git_lock::acquire::Fail::Immediately; + /// Obtain and alter references comfortably pub trait ReferenceAccessExt: easy::Access + Sized { fn tag( &self, name: impl AsRef, target: impl Into, - lock_mode: lock::acquire::Fail, - force: bool, + constraint: PreviousValue, ) -> Result, reference::edit::Error> { - self.edit_references( - Some(RefEdit { + self.edit_reference( + RefEdit { change: Change::Update { log: Default::default(), - mode: if force { - Create::OrUpdate { previous: None } - } else { - Create::Only - }, + expected: constraint, new: Target::Peeled(target.into()), }, name: format!("refs/tags/{}", name.as_ref()).try_into()?, deref: false, - }), - lock_mode, + }, + DEFAULT_LOCK_MODE, None, ) } + fn namespace(&self) -> Result, easy::borrow::repo::Error> { + self.repo().map(|repo| repo.deref().refs.namespace.clone()) + } + + fn clear_namespace(&mut self) -> Result, easy::borrow::repo::Error> { + self.repo_mut().map(|mut repo| repo.deref_mut().refs.namespace.take()) + } + + fn set_namespace<'a, Name, E>( + &mut self, + namespace: Name, + ) -> Result, easy::reference::namespace::set::Error> + where + Name: TryInto, Error = E>, + git_validate::refname::Error: From, + { + let namespace = git_ref::namespace::expand(namespace)?; + Ok(self.repo_mut()?.deref_mut().refs.namespace.replace(namespace)) + } + + // TODO: more tests or usage + fn reference( + &self, + name: Name, + target: impl Into, + constraint: PreviousValue, + log_message: impl Into, + ) -> Result, reference::create::Error> + where + Name: TryInto, + reference::create::Error: From, + { + let name = name.try_into()?; + let id = target.into(); + let mut edits = self.edit_reference( + RefEdit { + change: Change::Update { + log: LogChange { + mode: RefLog::AndReference, + force_create_reflog: false, + message: log_message.into(), + }, + expected: constraint, + new: Target::Peeled(id), + }, + name, + deref: false, + }, + DEFAULT_LOCK_MODE, + None, + )?; + assert_eq!( + edits.len(), + 1, + "only one reference can be created, splits aren't possible" + ); + + Ok(git_ref::Reference { + name: edits.pop().expect("exactly one edit").name, + target: Target::Peeled(id), + peeled: None, + } + .attach(self)) + } + fn edit_reference( &self, edit: RefEdit, @@ -61,41 +127,67 @@ pub trait ReferenceAccessExt: easy::Access + Sized { let committer = match log_committer { Some(c) => c, None => { - // TODO: actually read the committer information from git-config, probably it should be provided here - committer_storage = actor::Signature::empty(); + committer_storage = self.committer(); &committer_storage } }; - self.repo()? - .refs + let repo = self.repo()?; + repo.refs .transaction() .prepare(edits, lock_mode)? .commit(committer) .map_err(Into::into) } + fn head(&self) -> Result, reference::find::existing::Error> { + let head = self.find_reference("HEAD")?; + Ok(match head.inner.target { + Target::Symbolic(branch) => match self.find_reference(branch.to_partial()) { + Ok(r) => easy::head::Kind::Symbolic(r.detach()), + Err(reference::find::existing::Error::NotFound) => easy::head::Kind::Unborn(branch), + Err(err) => return Err(err), + }, + Target::Peeled(target) => easy::head::Kind::Detached { + target, + peeled: head.inner.peeled, + }, + } + .attach(self)) + } + fn find_reference<'a, Name, E>(&self, name: Name) -> Result, reference::find::existing::Error> where Name: TryInto, Error = E>, - Error: From, + git_ref::file::find::Error: From, { self.try_find_reference(name)? .ok_or(reference::find::existing::Error::NotFound) } + fn references(&self) -> Result, easy::iter::references::Error> { + let state = self.state(); + let repo = self.repo()?; + let packed_refs = state.assure_packed_refs_uptodate(&repo.refs)?; + Ok(easy::iter::references::State { + repo, + packed_refs, + access: self, + }) + } + fn try_find_reference<'a, Name, E>(&self, name: Name) -> Result>, reference::find::Error> where Name: TryInto, Error = E>, - Error: From, + git_ref::file::find::Error: From, { let state = self.state(); let repo = self.repo()?; - match repo - .refs - .try_find(name, state.assure_packed_refs_uptodate(&repo.refs)?.as_ref()) - { + match repo.refs.try_find( + name, + state.assure_packed_refs_uptodate(&repo.refs)?.packed_refs.as_ref(), + ) { Ok(r) => match r { - Some(r) => Ok(Some(Reference::from_file_ref(r, self))), + Some(r) => Ok(Some(Reference::from_ref(r, self))), None => Ok(None), }, Err(err) => Err(err.into()), diff --git a/git-repository/src/easy/head.rs b/git-repository/src/easy/head.rs new file mode 100644 index 00000000000..a8ba4e9ff33 --- /dev/null +++ b/git-repository/src/easy/head.rs @@ -0,0 +1,153 @@ +#![allow(missing_docs)] + +use git_hash::ObjectId; +use git_ref::FullNameRef; + +use crate::{easy, easy::Head, ext::ReferenceExt}; + +pub enum Kind { + /// The existing reference the symbolic HEAD points to. + Symbolic(git_ref::Reference), + /// The not-yet-existing reference the symbolic HEAD refers to. + Unborn(git_ref::FullName), + Detached { + target: ObjectId, + peeled: Option, + }, +} + +impl Kind { + pub fn attach(self, access: &A) -> Head<'_, A> { + Head { kind: self, access } + } +} + +impl<'repo, A> Head<'repo, A> { + pub fn name(&self) -> Option> { + Some(match &self.kind { + Kind::Symbolic(r) => r.name.to_ref(), + Kind::Unborn(name) => name.to_ref(), + Kind::Detached { .. } => return None, + }) + } + pub fn is_detached(&self) -> bool { + matches!(self.kind, Kind::Detached { .. }) + } +} + +impl<'repo, A> Head<'repo, A> +where + A: easy::Access + Sized, +{ + pub fn into_referent(self) -> easy::Reference<'repo, A> { + match self.kind { + Kind::Symbolic(r) => r.attach(self.access), + _ => panic!("BUG: Expected head to be a born symbolic reference"), + } + } +} + +pub mod log { + use std::marker::PhantomData; + + use crate::{ + easy, + easy::{ext::ReferenceAccessExt, Head}, + }; + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + BorrowState(#[from] easy::borrow::state::Error), + #[error(transparent)] + FindExistingReference(#[from] easy::reference::find::existing::Error), + } + + impl<'repo, A> Head<'repo, A> + where + A: easy::Access + Sized, + { + pub fn log(&self) -> Result>, Error> { + Ok(easy::reference::log::State { + reference: self.access.find_reference("HEAD")?, + buf: self.access.state().try_borrow_mut_buf()?, + _phantom: PhantomData::default(), + }) + } + } +} + +pub mod peel { + use crate::{ + easy, + easy::{head::Kind, Access, Head}, + ext::{ObjectIdExt, ReferenceExt}, + }; + + mod error { + use crate::easy::{object, reference}; + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + FindExistingObject(#[from] object::find::existing::Error), + #[error(transparent)] + PeelReference(#[from] reference::peel::Error), + } + } + pub use error::Error; + + impl<'repo, A> Head<'repo, A> + where + A: Access + Sized, + { + // TODO: tests + pub fn peel_to_id_in_place(&mut self) -> Option, Error>> { + Some(match &mut self.kind { + Kind::Unborn(_name) => return None, + Kind::Detached { + peeled: Some(peeled), .. + } => Ok((*peeled).attach(self.access)), + Kind::Detached { peeled: None, target } => { + match target + .attach(self.access) + .object() + .map_err(Into::into) + .and_then(|obj| obj.peel_to_end().map_err(Into::into)) + .map(|peeled| peeled.id) + { + Ok(peeled) => { + self.kind = Kind::Detached { + peeled: Some(peeled), + target: *target, + }; + Ok(peeled.attach(self.access)) + } + Err(err) => Err(err), + } + } + Kind::Symbolic(r) => { + let mut nr = r.clone().attach(self.access); + let peeled = nr.peel_to_id_in_place().map_err(Into::into); + *r = nr.detach(); + peeled + } + }) + } + + pub fn into_fully_peeled_id(self) -> Option, Error>> { + Some(match self.kind { + Kind::Unborn(_name) => return None, + Kind::Detached { + peeled: Some(peeled), .. + } => Ok(peeled.attach(self.access)), + Kind::Detached { peeled: None, target } => target + .attach(self.access) + .object() + .map_err(Into::into) + .and_then(|obj| obj.peel_to_end().map_err(Into::into)) + .map(|obj| obj.id.attach(self.access)), + Kind::Symbolic(r) => r.attach(self.access).peel_to_id_in_place().map_err(Into::into), + }) + } + } +} diff --git a/git-repository/src/easy/impls.rs b/git-repository/src/easy/impls.rs index 3d8d27fa02d..1195c0754ee 100644 --- a/git-repository/src/easy/impls.rs +++ b/git-repository/src/easy/impls.rs @@ -70,7 +70,7 @@ impl<'repo> easy::Access for EasyShared<'repo> { } impl easy::Access for Easy { - type RepoRef = Rc; + type RepoRef = Rc; // TODO: this could be a reference with GATs type RepoRefMut = &'static mut Repository; // this is a lie fn repo(&self) -> Result { diff --git a/git-repository/src/easy/iter.rs b/git-repository/src/easy/iter.rs new file mode 100644 index 00000000000..28181b52e9f --- /dev/null +++ b/git-repository/src/easy/iter.rs @@ -0,0 +1,91 @@ +#![allow(missing_docs)] +pub mod references { + use std::cell::Ref; + + use crate::easy; + + /// An iterator over references + #[must_use] + pub struct State<'r, A> + where + A: easy::Access + Sized, + { + pub(crate) repo: A::RepoRef, + pub(crate) packed_refs: Ref<'r, easy::reference::packed::ModifieablePackedRefsBuffer>, + pub(crate) access: &'r A, + } + + pub struct Iter<'r, A> { + inner: git_ref::file::iter::LooseThenPacked<'r, 'r>, + access: &'r A, + } + + impl<'r, A> State<'r, A> + where + A: easy::Access + Sized, + { + pub fn all(&self) -> Result, init::Error> { + let repo = self.repo.deref(); + Ok(Iter { + inner: repo.refs.iter(self.packed_refs.packed_refs.as_ref())?, + access: self.access, + }) + } + + pub fn prefixed(&self, prefix: impl AsRef) -> Result, init::Error> { + let repo = self.repo.deref(); + Ok(Iter { + inner: repo.refs.iter_prefixed(self.packed_refs.packed_refs.as_ref(), prefix)?, + access: self.access, + }) + } + } + + impl<'r, A> Iterator for Iter<'r, A> + where + A: easy::Access + Sized, + { + type Item = Result, Box>; + + fn next(&mut self) -> Option { + self.inner.next().map(|res| { + res.map_err(|err| Box::new(err) as Box) + .map(|r| easy::Reference::from_ref(r, self.access)) + }) + } + } + + pub mod init { + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + } + } + + mod error { + use crate::easy; + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + PackedRefsOpen(#[from] git_ref::packed::buffer::open::Error), + #[error("BUG: Part of interior state could not be borrowed.")] + BorrowState(#[from] easy::borrow::state::Error), + #[error("BUG: The repository could not be borrowed")] + BorrowRepo(#[from] easy::borrow::repo::Error), + } + + impl From for Error { + fn from(err: easy::reference::packed::Error) -> Self { + match err { + easy::reference::packed::Error::PackedRefsOpen(err) => Error::PackedRefsOpen(err), + easy::reference::packed::Error::BorrowState(err) => Error::BorrowState(err), + } + } + } + } + use std::{ops::Deref, path::Path}; + + pub use error::Error; +} diff --git a/git-repository/src/easy/mod.rs b/git-repository/src/easy/mod.rs index 3e6cc25af2c..a0b54d91253 100644 --- a/git-repository/src/easy/mod.rs +++ b/git-repository/src/easy/mod.rs @@ -14,14 +14,9 @@ use std::{ cell::RefCell, ops::{Deref, DerefMut}, - sync::Arc, - time::SystemTime, }; use git_hash::ObjectId; -use git_object as objs; -use git_odb as odb; -use git_ref as refs; use crate::Repository; @@ -30,15 +25,26 @@ mod impls; pub(crate) mod ext; pub mod borrow; +pub mod commit; +pub mod head; +pub mod iter; pub mod object; -mod oid; +pub mod oid; pub mod reference; pub mod state; +/// The head reference, as created from looking at `.git/HEAD`. +pub struct Head<'repo, A> { + /// One of various possible states for the HEAD reference + pub kind: head::Kind, + access: &'repo A, +} + /// An [ObjectId] with access to a repository. #[derive(Eq, Hash, Ord, PartialOrd, Clone, Copy)] pub struct Oid<'r, A> { - id: ObjectId, + /// The actual object id + pub inner: ObjectId, access: &'r A, } @@ -52,7 +58,7 @@ pub struct ObjectRef<'repo, A> { /// The id of the object pub id: ObjectId, /// The kind of the object - pub kind: objs::Kind, + pub kind: git_object::Kind, /// The fully decoded object data pub data: std::cell::Ref<'repo, [u8]>, access: &'repo A, @@ -77,36 +83,31 @@ pub struct Object { /// The id of the object pub id: ObjectId, /// The kind of the object - pub kind: objs::Kind, + pub kind: git_object::Kind, /// The fully decoded object data pub data: Vec, } /// A reference that points to an object or reference, with access to its source repository. +/// +/// Note that these are snapshots and won't recognize if they are stale. pub struct Reference<'r, A> { - pub(crate) backing: Option, + /// The actual reference data + pub inner: git_ref::Reference, pub(crate) access: &'r A, } #[cfg(not(feature = "local"))] -type PackCache = odb::pack::cache::Never; +type PackCache = git_odb::pack::cache::Never; #[cfg(feature = "local")] -type PackCache = odb::pack::cache::lru::StaticLinkedList<64>; - -#[derive(Default)] -struct ModifieablePackedRefsBuffer { - packed_refs: Option, - modified: Option, -} +type PackCache = git_odb::pack::cache::lru::StaticLinkedList<64>; /// State for use in `Easy*` to provide mutable parts of a repository such as caches and buffers. #[derive(Default)] pub struct State { - /// As the packed-buffer may hold onto a memory map, we avoid that to exist once per thread, multiplying system resources. - /// This seems worth the cost of always going through an `Arc>>`. Note that `EasyArcExclusive` uses the same construct - /// but the reason we make this distinction at all is that there are other easy's that allows to chose exactly what you need in - /// your application. `State` is one size fits all with supporting single-threaded applications only. - packed_refs: Arc>, + /// As the packed-buffer may hold onto a memory map, so ideally this State is freed after use instead of keeping it around + /// for too long. At least `packed_refs` is lazily initialized. + packed_refs: RefCell, pack_cache: RefCell, buf: RefCell>, } diff --git a/git-repository/src/easy/object/mod.rs b/git-repository/src/easy/object/mod.rs index 9b71006d2f9..19f7e7617de 100644 --- a/git-repository/src/easy/object/mod.rs +++ b/git-repository/src/easy/object/mod.rs @@ -4,7 +4,6 @@ use std::{cell::Ref, convert::TryInto}; use git_hash::ObjectId; pub use git_object::Kind; use git_object::{CommitRefIter, TagRefIter}; -use git_odb as odb; use crate::{ easy, @@ -59,11 +58,10 @@ where } pub mod find { - use git_odb as odb; use crate::easy; - pub(crate) type OdbError = odb::compound::find::Error; + pub(crate) type OdbError = git_odb::compound::find::Error; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -76,11 +74,9 @@ pub mod find { } pub mod existing { - use git_odb as odb; - use crate::easy; - pub(crate) type OdbError = odb::pack::find::existing::Error; + pub(crate) type OdbError = git_odb::pack::find::existing::Error; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -94,6 +90,18 @@ pub mod find { } } +pub mod write { + use crate::easy; + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + OdbWrite(#[from] git_odb::loose::write::Error), + #[error("BUG: The repository could not be borrowed")] + BorrowRepo(#[from] easy::borrow::repo::Error), + } +} + impl<'repo, A> ObjectRef<'repo, A> { pub fn to_owned(&self) -> Object { Object { @@ -120,32 +128,58 @@ impl<'repo, A> ObjectRef<'repo, A> where A: easy::Access + Sized, { + /// As [`to_commit_iter()`][ObjectRef::to_commit_iter()] but panics if this is not a commit + pub fn commit_iter(&self) -> CommitRefIter<'_> { + git_odb::data::Object::new(self.kind, &self.data) + .try_into_commit_iter() + .expect("BUG: This object must be a commit") + } + pub fn to_commit_iter(&self) -> Option> { - odb::data::Object::new(self.kind, &self.data).into_commit_iter() + git_odb::data::Object::new(self.kind, &self.data).try_into_commit_iter() } pub fn to_tag_iter(&self) -> Option> { - odb::data::Object::new(self.kind, &self.data).into_tag_iter() + git_odb::data::Object::new(self.kind, &self.data).try_into_tag_iter() } } -pub mod peel_to_kind { - pub use error::Error; - +pub mod peel { use crate::{ easy, easy::{ - object::{peel_to_kind, Kind}, + ext::ObjectAccessExt, + object, + object::{peel, Kind}, ObjectRef, }, }; + pub mod to_kind { + mod error { + + use crate::easy::object; + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + FindExistingObject(#[from] object::find::existing::Error), + #[error("Last encountered object kind was {} while trying to peel to {}", .actual, .expected)] + NotFound { + actual: object::Kind, + expected: object::Kind, + }, + } + } + pub use error::Error; + } + impl<'repo, A> ObjectRef<'repo, A> where A: easy::Access + Sized, { // TODO: tests - pub fn peel_to_kind(mut self, kind: Kind) -> Result { + pub fn peel_to_kind(mut self, kind: Kind) -> Result { loop { match self.kind { any_kind if kind == any_kind => { @@ -155,16 +189,16 @@ pub mod peel_to_kind { let tree_id = self.to_commit_iter().expect("commit").tree_id().expect("valid commit"); let access = self.access; drop(self); - self = crate::easy::ext::object::find_object(access, tree_id)?; + self = access.find_object(tree_id)?; } Kind::Tag => { let target_id = self.to_tag_iter().expect("tag").target_id().expect("valid tag"); let access = self.access; drop(self); - self = crate::easy::ext::object::find_object(access, target_id)?; + self = access.find_object(target_id)?; } Kind::Tree | Kind::Blob => { - return Err(peel_to_kind::Error::NotFound { + return Err(peel::to_kind::Error::NotFound { actual: self.kind, expected: kind, }) @@ -172,21 +206,20 @@ pub mod peel_to_kind { } } } - } - - mod error { - - use crate::easy::{object, object::find}; - #[derive(Debug, thiserror::Error)] - pub enum Error { - #[error(transparent)] - FindExisting(#[from] find::existing::Error), - #[error("Last encountered object kind was {} while trying to peel to {}", .actual, .expected)] - NotFound { - actual: object::Kind, - expected: object::Kind, - }, + // TODO: tests + pub fn peel_to_end(mut self) -> Result { + loop { + match self.kind { + Kind::Commit | Kind::Tree | Kind::Blob => break Ok(self), + Kind::Tag => { + let target_id = self.to_tag_iter().expect("tag").target_id().expect("valid tag"); + let access = self.access; + drop(self); + self = access.find_object(target_id)?; + } + } + } } } } diff --git a/git-repository/src/easy/object/tree.rs b/git-repository/src/easy/object/tree.rs index cc51a605b56..42feaac389d 100644 --- a/git-repository/src/easy/object/tree.rs +++ b/git-repository/src/easy/object/tree.rs @@ -2,7 +2,7 @@ use git_object::{bstr::BStr, TreeRefIter}; use crate::{ easy, - easy::{object::find, TreeRef}, + easy::{ext::ObjectAccessExt, object::find, TreeRef}, }; impl<'repo, A> TreeRef<'repo, A> @@ -30,7 +30,7 @@ where let access = self.access; drop(entry); drop(self); - self = match crate::easy::ext::object::find_object(access, next_id)?.try_into_tree() { + self = match access.find_object(next_id)?.try_into_tree() { Ok(tree) => tree, Err(_) => return Ok(None), }; diff --git a/git-repository/src/easy/oid.rs b/git-repository/src/easy/oid.rs index 1dbedaf5450..92301b905dc 100644 --- a/git-repository/src/easy/oid.rs +++ b/git-repository/src/easy/oid.rs @@ -1,85 +1,214 @@ +#![allow(missing_docs)] +use std::{cell::RefMut, ops::Deref}; + use git_hash::{oid, ObjectId}; use crate::{ easy, - easy::{object::find, Object, ObjectRef, Oid}, + easy::{ext::ObjectAccessExt, object::find, ObjectRef, Oid}, }; -impl<'repo, A, B> PartialEq> for Oid<'repo, B> { - fn eq(&self, other: &Oid<'repo, A>) -> bool { - self.id == other.id +impl<'repo, A> Oid<'repo, A> +where + A: easy::Access + Sized, +{ + // NOTE: Can't access other object data that is attached to the same cache. + /// Find the [`ObjectRef`] associated with this object id, and assume it exists. + pub fn object(&self) -> Result, find::existing::Error> { + self.access.find_object(self.inner) } -} -impl<'repo, A> PartialEq for Oid<'repo, A> { - fn eq(&self, other: &ObjectId) -> bool { - &self.id == other + // NOTE: Can't access other object data that is attached to the same cache. + /// Try find the [`ObjectRef`] associated with this object id, it might not be available locally. + pub fn try_object(&self) -> Result>, find::Error> { + self.access.try_find_object(self.inner) } } -impl<'repo, A> PartialEq for Oid<'repo, A> { - fn eq(&self, other: &oid) -> bool { - self.id == other +impl<'repo, A> Deref for Oid<'repo, A> { + type Target = oid; + + fn deref(&self) -> &Self::Target { + &self.inner } } -impl<'repo, A, B> PartialEq> for Oid<'repo, B> { - fn eq(&self, other: &ObjectRef<'repo, A>) -> bool { - self.id == other.id +impl<'repo, A> Oid<'repo, A> +where + A: easy::Access + Sized, +{ + pub(crate) fn from_id(id: impl Into, access: &'repo A) -> Self { + Oid { + inner: id.into(), + access, + } } -} -impl<'repo, A> PartialEq for Oid<'repo, A> { - fn eq(&self, other: &Object) -> bool { - self.id == other.id + /// Turn this instance into its bare [ObjectId]. + pub fn detach(self) -> ObjectId { + self.inner } } -impl<'repo, A> std::fmt::Debug for Oid<'repo, A> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.id.fmt(f) - } +pub struct Ancestors<'repo, A> +where + A: easy::Access + Sized, +{ + repo: A::RepoRef, + pack_cache: RefMut<'repo, easy::PackCache>, + access: &'repo A, + tip: ObjectId, } -impl<'repo, A> AsRef for Oid<'repo, A> { - fn as_ref(&self) -> &oid { - &self.id +pub mod ancestors { + use std::ops::{Deref, DerefMut}; + + use git_odb::Find; + + use crate::{ + easy, + easy::{oid::Ancestors, Oid}, + }; + + impl<'repo, A> Oid<'repo, A> + where + A: easy::Access + Sized, + { + pub fn ancestors(&self) -> Result, Error> { + let pack_cache = self.access.state().try_borrow_mut_pack_cache()?; + let repo = self.access.repo()?; + Ok(Ancestors { + pack_cache, + repo, + access: self.access, + tip: self.inner, + }) + } } -} -impl<'repo, A> From> for ObjectId { - fn from(v: Oid<'repo, A>) -> Self { - v.id + pub struct Iter<'a, 'repo, A> + where + A: easy::Access + Sized, + { + access: &'repo A, + inner: Box> + 'a>, } -} -impl<'repo, A> Oid<'repo, A> -where - A: easy::Access + Sized, -{ - // NOTE: Can't access other object data that is attached to the same cache. - /// Find the [`ObjectRef`] associated with this object id, and assume it exists. - pub fn object(&self) -> Result, find::existing::Error> { - crate::easy::ext::object::find_object(self.access, self.id) + impl<'repo, A> Ancestors<'repo, A> + where + A: easy::Access + Sized, + { + pub fn all(&mut self) -> Iter<'_, 'repo, A> { + Iter { + access: self.access, + inner: Box::new(git_traverse::commit::Ancestors::new( + Some(self.tip), + git_traverse::commit::ancestors::State::default(), + move |oid, buf| { + self.repo + .deref() + .odb + .try_find(oid, buf, self.pack_cache.deref_mut()) + .ok() + .flatten() + .and_then(|obj| obj.try_into_commit_iter()) + }, + )), + } + } } - // NOTE: Can't access other object data that is attached to the same cache. - /// Try find the [`ObjectRef`] associated with this object id, it might not be available locally. - pub fn try_object(&self) -> Result>, find::Error> { - crate::easy::ext::object::try_find_object(self.access, self.id) + impl<'a, 'repo, A> Iterator for Iter<'a, 'repo, A> + where + A: easy::Access + Sized, + { + type Item = Result, git_traverse::commit::ancestors::Error>; + + fn next(&mut self) -> Option { + self.inner.next().map(|res| res.map(|oid| oid.attach(self.access))) + } + } + + mod error { + use crate::easy; + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + BorrowRepo(#[from] easy::borrow::repo::Error), + #[error(transparent)] + BorrowBufMut(#[from] easy::borrow::state::Error), + } } + use error::Error; + + use crate::ext::ObjectIdExt; } -impl<'repo, A> Oid<'repo, A> -where - A: easy::Access + Sized, -{ - pub(crate) fn from_id(id: impl Into, access: &'repo A) -> Self { - Oid { id: id.into(), access } +mod impls { + use git_hash::{oid, ObjectId}; + + use crate::easy::{Object, ObjectRef, Oid}; + + impl<'repo, A, B> PartialEq> for Oid<'repo, B> { + fn eq(&self, other: &Oid<'repo, A>) -> bool { + self.inner == other.inner + } } - /// Turn this instance into its bare [ObjectId]. - pub fn detach(self) -> ObjectId { - self.id + impl<'repo, A> PartialEq for Oid<'repo, A> { + fn eq(&self, other: &ObjectId) -> bool { + &self.inner == other + } + } + + impl<'repo, A> PartialEq for Oid<'repo, A> { + fn eq(&self, other: &oid) -> bool { + self.inner == other + } + } + + impl<'repo, A, B> PartialEq> for Oid<'repo, B> { + fn eq(&self, other: &ObjectRef<'repo, A>) -> bool { + self.inner == other.id + } + } + + impl<'repo, A> PartialEq for Oid<'repo, A> { + fn eq(&self, other: &Object) -> bool { + self.inner == other.id + } + } + + impl<'repo, A> std::fmt::Debug for Oid<'repo, A> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.inner.fmt(f) + } + } + + impl<'repo, A> AsRef for Oid<'repo, A> { + fn as_ref(&self) -> &oid { + &self.inner + } + } + + impl<'repo, A> From> for ObjectId { + fn from(v: Oid<'repo, A>) -> Self { + v.inner + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn size_of_oid() { + assert_eq!( + std::mem::size_of::>(), + 32, + "size of oid shouldn't change without notice" + ) } } diff --git a/git-repository/src/easy/reference.rs b/git-repository/src/easy/reference.rs deleted file mode 100644 index 40c5c4e5155..00000000000 --- a/git-repository/src/easy/reference.rs +++ /dev/null @@ -1,167 +0,0 @@ -#![allow(missing_docs)] -use std::ops::DerefMut; - -use git_hash::ObjectId; -use git_odb::Find; -use git_ref as refs; - -use crate::{ - easy, - easy::{Oid, Reference}, -}; - -pub(crate) enum Backing { - OwnedPacked { - /// The validated full name of the reference. - name: refs::FullName, - /// The target object id of the reference, hex encoded. - target: ObjectId, - /// The fully peeled object id, hex encoded, that the ref is ultimately pointing to - /// i.e. when all indirections are removed. - object: Option, - }, - LooseFile(refs::file::loose::Reference), -} - -pub mod edit { - use git_ref as refs; - - use crate::easy; - - #[derive(Debug, thiserror::Error)] - pub enum Error { - #[error(transparent)] - FileTransactionPrepare(#[from] refs::file::transaction::prepare::Error), - #[error(transparent)] - FileTransactionCommit(#[from] refs::file::transaction::commit::Error), - #[error(transparent)] - NameValidation(#[from] git_validate::reference::name::Error), - #[error("BUG: The repository could not be borrowed")] - BorrowRepo(#[from] easy::borrow::repo::Error), - } -} - -pub mod peel_to_oid_in_place { - use git_ref as refs; - - use crate::easy; - - #[derive(Debug, thiserror::Error)] - pub enum Error { - #[error(transparent)] - LoosePeelToId(#[from] refs::file::loose::reference::peel::to_id::Error), - #[error(transparent)] - PackedRefsOpen(#[from] refs::packed::buffer::open::Error), - #[error("BUG: Part of interior state could not be borrowed.")] - BorrowState(#[from] easy::borrow::state::Error), - #[error("BUG: The repository could not be borrowed")] - BorrowRepo(#[from] easy::borrow::repo::Error), - } -} - -// TODO: think about how to detach a Reference. It should essentially be a 'Raw' reference that should exist in `git-ref` rather than here. -impl<'repo, A> Reference<'repo, A> -where - A: easy::Access + Sized, -{ - pub(crate) fn from_file_ref(reference: refs::file::Reference<'_>, access: &'repo A) -> Self { - Reference { - backing: match reference { - refs::file::Reference::Packed(p) => Backing::OwnedPacked { - name: p.name.into(), - target: p.target(), - object: p - .object - .map(|hex| ObjectId::from_hex(hex).expect("a hash kind we know")), - }, - refs::file::Reference::Loose(l) => Backing::LooseFile(l), - } - .into(), - access, - } - } - pub fn target(&self) -> refs::Target { - match self.backing.as_ref().expect("always set") { - Backing::OwnedPacked { target, .. } => refs::Target::Peeled(target.to_owned()), - Backing::LooseFile(r) => r.target.clone(), - } - } - - pub fn name(&self) -> refs::FullNameRef<'_> { - match self.backing.as_ref().expect("always set") { - Backing::OwnedPacked { name, .. } => name, - Backing::LooseFile(r) => &r.name, - } - .to_ref() - } - - pub fn peel_to_oid_in_place(&mut self) -> Result, peel_to_oid_in_place::Error> { - let repo = self.access.repo()?; - match self.backing.take().expect("a ref must be set") { - Backing::LooseFile(mut r) => { - let state = self.access.state(); - let mut pack_cache = state.try_borrow_mut_pack_cache()?; - let oid = r - .peel_to_id_in_place( - &repo.refs, - state.assure_packed_refs_uptodate(&repo.refs)?.as_ref(), - |oid, buf| { - repo.odb - .try_find(oid, buf, pack_cache.deref_mut()) - .map(|po| po.map(|o| (o.kind, o.data))) - }, - )? - .to_owned(); - self.backing = Backing::LooseFile(r).into(); - Ok(Oid::from_id(oid, self.access)) - } - Backing::OwnedPacked { - mut target, - mut object, - name, - } => { - if let Some(peeled_id) = object.take() { - target = peeled_id; - } - self.backing = Backing::OwnedPacked { - name, - target, - object: None, - } - .into(); - Ok(Oid::from_id(target, self.access)) - } - } - } -} - -pub mod find { - use git_ref as refs; - - use crate::easy; - - pub mod existing { - - use crate::easy::reference::find; - - #[derive(Debug, thiserror::Error)] - pub enum Error { - #[error(transparent)] - Find(#[from] find::Error), - #[error("The reference did not exist even though that was expected")] - NotFound, - } - } - - #[derive(Debug, thiserror::Error)] - pub enum Error { - #[error(transparent)] - Find(#[from] refs::file::find::Error), - #[error(transparent)] - PackedRefsOpen(#[from] refs::packed::buffer::open::Error), - #[error("BUG: Part of interior state could not be borrowed.")] - BorrowState(#[from] easy::borrow::state::Error), - #[error("BUG: The repository could not be borrowed")] - BorrowRepo(#[from] easy::borrow::repo::Error), - } -} diff --git a/git-repository/src/easy/reference/log.rs b/git-repository/src/easy/reference/log.rs new file mode 100644 index 00000000000..bbdf1e61eec --- /dev/null +++ b/git-repository/src/easy/reference/log.rs @@ -0,0 +1,65 @@ +use std::{borrow::Borrow, cell::RefMut, marker::PhantomData, ops::DerefMut}; + +use git_ref::file::ReferenceExt; + +use crate::{easy, easy::Reference}; + +#[must_use = "Iterators should be obtained from this log buffer"] +pub struct State<'repo, A: 'repo, R> +where + R: Borrow>, +{ + pub(crate) reference: R, + pub(crate) buf: RefMut<'repo, Vec>, + pub(crate) _phantom: PhantomData, +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + BorrowRepo(#[from] easy::borrow::repo::Error), +} + +pub type ReverseIter<'a> = git_ref::file::log::iter::Reverse<'a, std::fs::File>; +pub type ForwardIter<'a> = git_ref::file::log::iter::Forward<'a>; + +impl<'repo, A, R> State<'repo, A, R> +where + A: easy::Access + Sized, + R: Borrow>, +{ + pub fn iter_rev(&mut self) -> Result>, Error> { + let buf = self.buf.deref_mut(); + buf.resize(512, 0); + Ok(self + .reference + .borrow() + .inner + .log_iter_rev(&self.reference.borrow().access.repo()?.refs, buf)?) + } + + // TODO: tests + pub fn iter(&mut self) -> Result>, Error> { + let buf = self.buf.deref_mut(); + Ok(self + .reference + .borrow() + .inner + .log_iter(&self.reference.borrow().access.repo()?.refs, buf)?) + } +} + +impl<'repo, A> Reference<'repo, A> +where + A: easy::Access + Sized, +{ + pub fn log(&self) -> Result>, easy::borrow::state::Error> { + Ok(State { + reference: self, + buf: self.access.state().try_borrow_mut_buf()?, + _phantom: Default::default(), + }) + } +} diff --git a/git-repository/src/easy/reference/mod.rs b/git-repository/src/easy/reference/mod.rs new file mode 100644 index 00000000000..25f433e6c26 --- /dev/null +++ b/git-repository/src/easy/reference/mod.rs @@ -0,0 +1,234 @@ +#![allow(missing_docs)] +use std::ops::DerefMut; + +use git_odb::Find; +use git_ref::file::ReferenceExt; + +use crate::{ + easy, + easy::{Oid, Reference}, +}; + +pub mod namespace { + pub mod set { + use crate::easy; + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + BorrowRepoMut(#[from] easy::borrow::repo::Error), + #[error(transparent)] + NameValidation(#[from] git_validate::refname::Error), + } + } +} + +pub mod create { + use crate::easy; + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + Edit(#[from] easy::reference::edit::Error), + #[error(transparent)] + NameValidation(#[from] git_validate::reference::name::Error), + } +} + +pub mod edit { + use crate::easy; + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + FileTransactionPrepare(#[from] git_ref::file::transaction::prepare::Error), + #[error(transparent)] + FileTransactionCommit(#[from] git_ref::file::transaction::commit::Error), + #[error(transparent)] + NameValidation(#[from] git_validate::reference::name::Error), + #[error("BUG: The repository could not be borrowed")] + BorrowRepo(#[from] easy::borrow::repo::Error), + } +} + +pub mod peel { + use crate::easy; + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + PeelToId(#[from] git_ref::peel::to_id::Error), + #[error(transparent)] + PackedRefsOpen(#[from] git_ref::packed::buffer::open::Error), + #[error("BUG: Part of interior state could not be borrowed.")] + BorrowState(#[from] easy::borrow::state::Error), + #[error("BUG: The repository could not be borrowed")] + BorrowRepo(#[from] easy::borrow::repo::Error), + } + + impl From for Error { + fn from(err: easy::reference::packed::Error) -> Self { + match err { + easy::reference::packed::Error::PackedRefsOpen(err) => Error::PackedRefsOpen(err), + easy::reference::packed::Error::BorrowState(err) => Error::BorrowState(err), + } + } + } +} + +impl<'repo, A> Reference<'repo, A> { + pub fn target(&self) -> git_ref::TargetRef<'_> { + self.inner.target.to_ref() + } + + pub fn name(&self) -> git_ref::FullNameRef<'_> { + self.inner.name.to_ref() + } + + pub fn detach(self) -> git_ref::Reference { + self.inner + } +} + +impl<'repo, A> Reference<'repo, A> +where + A: easy::Access + Sized, +{ + pub(crate) fn from_ref(reference: git_ref::Reference, access: &'repo A) -> Self { + Reference { + inner: reference, + access, + } + } + + pub fn peel_to_id_in_place(&mut self) -> Result, peel::Error> { + let repo = self.access.repo()?; + let state = self.access.state(); + let mut pack_cache = state.try_borrow_mut_pack_cache()?; + let oid = self.inner.peel_to_id_in_place( + &repo.refs, + state.assure_packed_refs_uptodate(&repo.refs)?.packed_refs.as_ref(), + |oid, buf| { + repo.odb + .try_find(oid, buf, pack_cache.deref_mut()) + .map(|po| po.map(|o| (o.kind, o.data))) + }, + )?; + Ok(Oid::from_id(oid, self.access)) + } + + pub fn into_fully_peeled_id(mut self) -> Result, peel::Error> { + self.peel_to_id_in_place() + } +} + +pub mod log; + +pub(crate) mod packed { + use std::{ + cell::{BorrowError, BorrowMutError}, + time::SystemTime, + }; + + use git_ref::file; + + use crate::easy; + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + PackedRefsOpen(#[from] git_ref::packed::buffer::open::Error), + #[error("BUG: Part of interior state could not be borrowed.")] + BorrowState(#[from] easy::borrow::state::Error), + } + + impl From for Error { + fn from(err: BorrowError) -> Self { + Error::BorrowState(easy::borrow::state::Error::Borrow(err)) + } + } + impl From for Error { + fn from(err: BorrowMutError) -> Self { + Error::BorrowState(easy::borrow::state::Error::BorrowMut(err)) + } + } + + #[derive(Default)] + pub(crate) struct ModifieablePackedRefsBuffer { + pub(crate) packed_refs: Option, + modified: Option, + } + + impl ModifieablePackedRefsBuffer { + pub fn assure_packed_refs_uptodate( + &mut self, + file: &file::Store, + ) -> Result<(), git_ref::packed::buffer::open::Error> { + let packed_refs_modified_time = || file.packed_refs_path().metadata().and_then(|m| m.modified()).ok(); + if self.packed_refs.is_none() { + self.packed_refs = file.packed_buffer()?; + if self.packed_refs.is_some() { + self.modified = packed_refs_modified_time(); + } + } else { + let recent_modification = packed_refs_modified_time(); + match (&self.modified, recent_modification) { + (None, None) => {} + (Some(_), None) => { + self.packed_refs = None; + self.modified = None + } + (Some(cached_time), Some(modified_time)) => { + if *cached_time < modified_time { + self.packed_refs = file.packed_buffer()?; + self.modified = Some(modified_time); + } + } + (None, Some(modified_time)) => { + self.packed_refs = file.packed_buffer()?; + self.modified = Some(modified_time); + } + } + } + Ok(()) + } + } +} + +pub mod find { + use crate::easy; + + pub mod existing { + + use crate::easy::reference::find; + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + Find(#[from] find::Error), + #[error("The reference did not exist even though that was expected")] + NotFound, + } + } + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + Find(#[from] git_ref::file::find::Error), + #[error(transparent)] + PackedRefsOpen(#[from] git_ref::packed::buffer::open::Error), + #[error("BUG: Part of interior state could not be borrowed.")] + BorrowState(#[from] easy::borrow::state::Error), + #[error("BUG: The repository could not be borrowed")] + BorrowRepo(#[from] easy::borrow::repo::Error), + } + + impl From for Error { + fn from(err: easy::reference::packed::Error) -> Self { + match err { + easy::reference::packed::Error::PackedRefsOpen(err) => Error::PackedRefsOpen(err), + easy::reference::packed::Error::BorrowState(err) => Error::BorrowState(err), + } + } + } +} diff --git a/git-repository/src/easy/state.rs b/git-repository/src/easy/state.rs index 353045686d5..e3a29cdfe5a 100644 --- a/git-repository/src/easy/state.rs +++ b/git-repository/src/easy/state.rs @@ -1,7 +1,7 @@ #![allow(missing_docs)] use std::cell::{Ref, RefMut}; -use git_ref::{file, packed}; +use git_ref::file; use crate::{ easy, @@ -10,57 +10,19 @@ use crate::{ impl Clone for easy::State { fn clone(&self) -> Self { - easy::State { - packed_refs: self.packed_refs.clone(), - ..Default::default() - } - } -} - -impl easy::ModifieablePackedRefsBuffer { - fn assure_packed_refs_uptodate(&mut self, file: &file::Store) -> Result<(), packed::buffer::open::Error> { - let packed_refs_modified_time = || file.packed_refs_path().metadata().and_then(|m| m.modified()).ok(); - if self.packed_refs.is_none() { - self.packed_refs = file.packed_buffer()?; - if self.packed_refs.is_some() { - self.modified = packed_refs_modified_time(); - } - } else { - let recent_modification = packed_refs_modified_time(); - match (&self.modified, recent_modification) { - (None, None) => {} - (Some(_), None) => { - self.packed_refs = None; - self.modified = None - } - (Some(cached_time), Some(modified_time)) => { - if *cached_time < modified_time { - self.packed_refs = file.packed_buffer()?; - self.modified = Some(modified_time); - } - } - (None, Some(modified_time)) => { - self.packed_refs = file.packed_buffer()?; - self.modified = Some(modified_time); - } - } - } - Ok(()) + easy::State { ..Default::default() } } } impl easy::State { - // TODO: this method should be on the Store itself, as one day there will be reftable support which lacks packed-refs pub(crate) fn assure_packed_refs_uptodate( &self, file: &file::Store, - ) -> Result>, packed::buffer::open::Error> { - let mut packed_refs = self.packed_refs.write(); + ) -> Result, easy::reference::packed::Error> { + let mut packed_refs = self.packed_refs.try_borrow_mut()?; packed_refs.assure_packed_refs_uptodate(file)?; - let packed_refs = parking_lot::RwLockWriteGuard::<'_, _>::downgrade(packed_refs); - Ok(parking_lot::RwLockReadGuard::<'_, _>::map(packed_refs, |buffer| { - &buffer.packed_refs - })) + drop(packed_refs); + Ok(self.packed_refs.try_borrow()?) } #[inline] diff --git a/git-repository/src/ext/mod.rs b/git-repository/src/ext/mod.rs index 1697ca48e6c..d342bf15387 100644 --- a/git-repository/src/ext/mod.rs +++ b/git-repository/src/ext/mod.rs @@ -1,5 +1,7 @@ pub use object_id::ObjectIdExt; +pub use reference::ReferenceExt; pub use tree::TreeIterExt; mod object_id; +mod reference; mod tree; diff --git a/git-repository/src/ext/object_id.rs b/git-repository/src/ext/object_id.rs index c7e85e792ab..232501440b2 100644 --- a/git-repository/src/ext/object_id.rs +++ b/git-repository/src/ext/object_id.rs @@ -1,6 +1,5 @@ #![allow(missing_docs)] use git_hash::ObjectId; -#[cfg(feature = "git-traverse")] use git_traverse::commit::ancestors::{Ancestors, State}; use crate::easy; @@ -8,7 +7,6 @@ use crate::easy; pub trait Sealed {} pub trait ObjectIdExt: Sealed { - #[cfg(feature = "git-traverse")] fn ancestors_iter(self, find: Find) -> Ancestors bool, State> where Find: for<'a> FnMut(&git_hash::oid, &'a mut Vec) -> Option>; @@ -19,7 +17,6 @@ pub trait ObjectIdExt: Sealed { impl Sealed for ObjectId {} impl ObjectIdExt for ObjectId { - #[cfg(feature = "git-traverse")] fn ancestors_iter(self, find: Find) -> Ancestors bool, State> where Find: for<'a> FnMut(&git_hash::oid, &'a mut Vec) -> Option>, diff --git a/git-repository/src/ext/reference.rs b/git-repository/src/ext/reference.rs new file mode 100644 index 00000000000..be5835ca437 --- /dev/null +++ b/git-repository/src/ext/reference.rs @@ -0,0 +1,17 @@ +use crate::easy; + +pub trait Sealed {} + +impl Sealed for git_ref::Reference {} + +/// Extensions for [references][git_ref::Reference]. +pub trait ReferenceExt { + /// Attach [`easy::Access`] to the given reference. It can be detached later with [`detach()]`. + fn attach(self, access: &A) -> easy::Reference<'_, A>; +} + +impl ReferenceExt for git_ref::Reference { + fn attach(self, access: &A) -> easy::Reference<'_, A> { + easy::Reference::from_ref(self, access) + } +} diff --git a/git-repository/src/ext/tree.rs b/git-repository/src/ext/tree.rs index 0615201ce59..865f6c8a9f3 100644 --- a/git-repository/src/ext/tree.rs +++ b/git-repository/src/ext/tree.rs @@ -1,11 +1,8 @@ #![allow(missing_docs)] -#[cfg(feature = "git-diff")] use std::borrow::BorrowMut; -#[cfg(feature = "git-diff")] use git_hash::oid; use git_object::TreeRefIter; -#[cfg(feature = "git-traverse")] use git_traverse::tree::breadthfirst; pub trait Sealed {} @@ -25,7 +22,6 @@ pub trait TreeIterExt: Sealed { StateMut: BorrowMut; /// Use this for squeezing out the last bits of performance. - #[cfg(feature = "git-traverse")] fn traverse( &self, state: StateMut, @@ -57,7 +53,6 @@ impl<'d> TreeIterExt for TreeRefIter<'d> { git_diff::tree::Changes::from(Some(self.clone())).needed_to_obtain(other, state, find, delegate) } - #[cfg(feature = "git-traverse")] fn traverse( &self, state: StateMut, diff --git a/git-repository/src/lib.rs b/git-repository/src/lib.rs index 2e7646399a7..b1842d81e79 100644 --- a/git-repository/src/lib.rs +++ b/git-repository/src/lib.rs @@ -64,8 +64,8 @@ //! * [`hash`] //! * [`url`] //! * [`actor`] +//! * [`bstr`][bstr] //! * [`objs`] -//! * [`bstr`][objs::bstr] //! * [`odb`] //! * [`pack`][odb::pack] //! * [`refs`] @@ -88,15 +88,14 @@ use std::{path::PathBuf, rc::Rc, sync::Arc}; // Re-exports to make this a potential one-stop shop crate avoiding people from having to reference various crates themselves. // This also means that their major version changes affect our major version, but that's alright as we directly expose their // APIs/instances anyway. +pub use bstr; pub use git_actor as actor; #[cfg(all(feature = "unstable", feature = "git-diff"))] pub use git_diff as diff; #[cfg(feature = "unstable")] pub use git_features::{parallel, progress, progress::Progress}; pub use git_hash as hash; -#[cfg(feature = "unstable")] pub use git_lock as lock; -#[cfg(feature = "unstable")] pub use git_object as objs; #[cfg(feature = "unstable")] pub use git_odb as odb; @@ -105,7 +104,7 @@ pub use git_protocol as protocol; pub use git_ref as refs; #[cfg(feature = "unstable")] pub use git_tempfile as tempfile; -#[cfg(all(feature = "unstable", feature = "git-traverse"))] +#[cfg(feature = "unstable")] pub use git_traverse as traverse; #[cfg(all(feature = "unstable", feature = "git-url"))] pub use git_url as url; @@ -154,6 +153,7 @@ pub struct Repository { pub(crate) odb: git_odb::linked::Store, /// The path to the worktree at which to find checked out files pub work_tree: Option, + pub(crate) hash_kind: git_hash::Kind, // TODO: git-config should be here - it's read a lot but not written much in must applications, so shouldn't be in `State`. // Probably it's best reload it on signal (in servers) or refresh it when it's known to have been changed similar to how // packs are refreshed. This would be `git_config::fs::Config` when ready. @@ -225,6 +225,11 @@ pub struct EasyArcExclusive { pub mod easy; +/// +pub mod commit; +/// +pub mod reference; + /// The kind of `Repository` #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Kind { diff --git a/git-repository/src/path/create.rs b/git-repository/src/path/create.rs index 2aa5f6e4d98..d414faa77e3 100644 --- a/git-repository/src/path/create.rs +++ b/git-repository/src/path/create.rs @@ -4,7 +4,7 @@ use std::{ path::{Path, PathBuf}, }; -use git_object::bstr::ByteSlice; +use bstr::ByteSlice; /// The error used in [`into()`]. #[derive(Debug, thiserror::Error)] diff --git a/git-repository/src/path/is_git.rs b/git-repository/src/path/is_git.rs index d643587c1f1..1c0f0ceb51f 100644 --- a/git-repository/src/path/is_git.rs +++ b/git-repository/src/path/is_git.rs @@ -5,7 +5,7 @@ pub enum Error { #[error("Could not find a valid HEAD reference")] FindHeadRef(#[from] git_ref::file::find::existing::Error), #[error("Expected HEAD at '.git/HEAD', got '.git/{}'", .name)] - MisplacedHead { name: git_object::bstr::BString }, + MisplacedHead { name: bstr::BString }, #[error("Expected an objects directory at '{}'", .missing.display())] MissingObjectsDirectory { missing: PathBuf }, #[error("Expected a refs directory at '{}'", .missing.display())] diff --git a/git-repository/src/reference/log.rs b/git-repository/src/reference/log.rs new file mode 100644 index 00000000000..a0d95f3e4a9 --- /dev/null +++ b/git-repository/src/reference/log.rs @@ -0,0 +1,25 @@ +use bstr::{BString, ByteSlice, ByteVec}; +use git_object::Commit; + +use crate::commit; + +/// Generate a message typical for git commit logs based on the given `operation` +pub fn message(operation: &str, commit: &Commit) -> BString { + let mut out = BString::from(operation); + if let Some(commit_type) = commit_type_by_parents(commit.parents.len()) { + out.push_str(b" ("); + out.extend_from_slice(commit_type.as_bytes()); + out.push_byte(b')'); + } + out.push_str(b": "); + out.extend_from_slice(&commit::summary(commit.message.as_bstr())); + out +} + +pub(crate) fn commit_type_by_parents(count: usize) -> Option<&'static str> { + Some(match count { + 0 => "initial", + 1 => return None, + _two_or_more => "merge", + }) +} diff --git a/git-repository/src/reference/mod.rs b/git-repository/src/reference/mod.rs new file mode 100644 index 00000000000..fb8abd6b1ab --- /dev/null +++ b/git-repository/src/reference/mod.rs @@ -0,0 +1,2 @@ +/// +pub mod log; diff --git a/git-repository/src/repository.rs b/git-repository/src/repository.rs index 44d9c3249f8..a61283eca0d 100644 --- a/git-repository/src/repository.rs +++ b/git-repository/src/repository.rs @@ -35,9 +35,9 @@ pub mod from_path { } pub mod open { - use std::path::PathBuf; + use std::{borrow::Cow, path::PathBuf}; - use git_config::values::Boolean; + use git_config::values::{Boolean, Integer}; use crate::Repository; @@ -49,6 +49,8 @@ pub mod open { NotARepository(#[from] crate::path::is_git::Error), #[error(transparent)] ObjectStoreInitialization(#[from] git_odb::linked::init::Error), + #[error("Cannot handle objects formatted as {:?}", .name)] + UnsupportedObjectFormat { name: bstr::BString }, } impl Repository { @@ -70,8 +72,8 @@ pub mod open { git_dir: PathBuf, mut worktree_dir: Option, ) -> Result { + let config = git_config::file::GitConfig::open(git_dir.join("config"))?; if worktree_dir.is_none() { - let config = git_config::file::GitConfig::open(git_dir.join("config"))?; let is_bare = config .value::>("core", None, "bare") .map_or(false, |b| matches!(b, Boolean::True(_))); @@ -79,6 +81,27 @@ pub mod open { worktree_dir = Some(git_dir.parent().expect("parent is always available").to_owned()); } } + let hash_kind = if config + .value::("core", None, "repositoryFormatVersion") + .map_or(0, |v| v.value) + == 1 + { + if let Ok(format) = config.value::>("extensions", None, "objectFormat") { + match format.as_ref() { + b"sha1" => git_hash::Kind::Sha1, + _ => { + return Err(Error::UnsupportedObjectFormat { + name: format.to_vec().into(), + }) + } + } + } else { + git_hash::Kind::Sha1 + } + } else { + git_hash::Kind::Sha1 + }; + Ok(crate::Repository { odb: git_odb::linked::Store::at(git_dir.join("objects"))?, refs: git_ref::file::Store::at( @@ -90,6 +113,7 @@ pub mod open { }, ), work_tree: worktree_dir, + hash_kind, }) } } @@ -120,6 +144,22 @@ pub mod init { } } +mod location { + use crate::Repository; + + impl Repository { + /// The path to the `.git` directory itself, or equivalent if this is a bare repository. + pub fn path(&self) -> &std::path::Path { + &self.refs.base + } + + /// Return the path to the working directory if this is not a bare repository. + pub fn workdir(&self) -> Option<&std::path::Path> { + self.work_tree.as_deref() + } + } +} + pub mod discover { use std::{convert::TryInto, path::Path}; diff --git a/git-repository/tests/commit/mod.rs b/git-repository/tests/commit/mod.rs new file mode 100644 index 00000000000..78e1c03867f --- /dev/null +++ b/git-repository/tests/commit/mod.rs @@ -0,0 +1,45 @@ +pub mod summary { + use std::borrow::Cow; + + use git::bstr::ByteSlice; + use git_repository as git; + + #[test] + fn no_newline_yields_the_message_itself() { + let input = b"hello world".as_bstr(); + assert_eq!(git::commit::summary(input), Cow::Borrowed(input)); + } + + #[test] + fn trailing_newlines_and_whitespace_are_trimmed() { + let input = b"hello world \t\r\n \n".as_bstr(); + assert_eq!(git::commit::summary(input), Cow::Borrowed(b"hello world".as_bstr())); + } + + #[test] + fn prefixed_newlines_and_whitespace_are_trimmed() { + let input = b" \t\r\n \nhello world".as_bstr(); + assert_eq!(git::commit::summary(input), Cow::Borrowed(b"hello world".as_bstr())); + } + + #[test] + fn whitespace_up_to_a_newline_is_collapsed_into_a_space() { + let input = b" \t\r\n \nhello\r\nworld \t\r\n \n".as_bstr(); + assert_eq!(git::commit::summary(input), Cow::Borrowed(b"hello world".as_bstr())); + } + + #[test] + fn whitespace_without_newlines_is_ignored_except_for_leading_and_trailing_whitespace() { + let input = b" \t\r\n \nhello \t \rworld \t\r\n \n".as_bstr(); + assert_eq!( + git::commit::summary(input), + Cow::Borrowed(b"hello \t \rworld".as_bstr()) + ); + } + + #[test] + fn lines_separated_by_double_newlines_are_subjects() { + let input = b" \t\r\n \nhello\t \r\nworld \t\r \nfoo\n\nsomething else we ignore".as_bstr(); + assert_eq!(git::commit::summary(input), Cow::Borrowed(b"hello world foo".as_bstr())); + } +} diff --git a/git-repository/tests/access/mod.rs b/git-repository/tests/easy/access.rs similarity index 100% rename from git-repository/tests/access/mod.rs rename to git-repository/tests/easy/access.rs diff --git a/git-repository/tests/easy/ext/mod.rs b/git-repository/tests/easy/ext/mod.rs new file mode 100644 index 00000000000..b0e326e04f0 --- /dev/null +++ b/git-repository/tests/easy/ext/mod.rs @@ -0,0 +1,2 @@ +mod object; +mod reference; diff --git a/git-repository/tests/easy/ext/object.rs b/git-repository/tests/easy/ext/object.rs new file mode 100644 index 00000000000..0706313192e --- /dev/null +++ b/git-repository/tests/easy/ext/object.rs @@ -0,0 +1,123 @@ +mod write_object { + use git_repository::prelude::{ConfigAccessExt, ObjectAccessExt}; + + #[test] + fn empty_tree() -> crate::Result { + let tmp = tempfile::tempdir()?; + let repo = git_repository::init_bare(&tmp)?.into_easy(); + let oid = repo.write_object(&git_repository::objs::Tree::empty().into())?; + assert_eq!( + oid, + git_repository::hash::ObjectId::empty_tree(repo.hash_kind()?), + "it produces a well-known empty tree id" + ); + Ok(()) + } +} + +mod commit { + use git_repository as git; + use git_repository::prelude::{ObjectAccessExt, ReferenceAccessExt}; + use git_testtools::hex_to_id; + + #[test] + fn single_line_initial_commit_empty_tree_ref_nonexisting() -> crate::Result { + let tmp = tempfile::tempdir()?; + let repo = git::init(&tmp)?.into_easy(); + let empty_tree_id = repo.write_object(&git::objs::Tree::empty().into())?; + let author = git::actor::Signature::empty(); + let commit_id = repo.commit( + "HEAD", + "initial", + author.clone(), + author, + empty_tree_id, + git::commit::NO_PARENT_IDS, + )?; + assert_eq!( + commit_id, + hex_to_id("302ea5640358f98ba23cda66c1e664a6f274643f"), + "the commit id is stable" + ); + + let head = repo.head()?.into_referent(); + assert_eq!( + head.log()? + .iter_rev()? + .expect("log present") + .next() + .expect("one line")? + .message, + "commit (initial): initial" + ); + Ok(()) + } + + #[test] + fn multi_line_commit_message_uses_first_line_in_ref_log_ref_nonexisting() -> crate::Result { + let (repo, _keep) = crate::basic_rw_repo()?; + let parent = repo.find_reference("HEAD")?.peel_to_id_in_place()?; + let empty_tree_id = parent.object()?.commit_iter().tree_id().expect("tree to be set"); + let author = git::actor::Signature::empty(); + let first_commit_id = repo.commit( + "HEAD", + "hello there \r\n\nthe body", + author.clone(), + author.clone(), + empty_tree_id, + Some(parent), + )?; + assert_eq!( + first_commit_id, + hex_to_id("1ff7decccf76bfa15bfdb0b66bac0c9144b4b083"), + "the commit id is stable" + ); + + let head_log_entries: Vec<_> = repo + .head()? + .log()? + .iter_rev()? + .expect("log present") + .map(Result::unwrap) + .map(|l| l.message) + .collect(); + assert_eq!( + head_log_entries, + vec!["commit: hello there", "commit: c2", "commit (initial): c1"], + "we get the actual HEAD log, not the log of some reference" + ); + let current_commit = repo.head()?.into_fully_peeled_id().expect("born")?; + assert_eq!(current_commit, first_commit_id, "the commit was set"); + + let second_commit_id = repo.commit( + "refs/heads/new-branch", + "committing into a new branch creates it", + author.clone(), + author, + empty_tree_id, + Some(first_commit_id), + )?; + + assert_eq!( + second_commit_id, + hex_to_id("b0d041ade77e51d31c79c7147fb769336ccc77b1"), + "the second commit id is stable" + ); + + let mut branch = repo.find_reference("new-branch")?; + let current_commit = branch.peel_to_id_in_place()?; + assert_eq!(current_commit, second_commit_id, "the commit was set"); + + let mut log = branch.log()?; + let mut log_iter = log.iter_rev()?.expect("log present"); + assert_eq!( + log_iter.next().expect("one line")?.message, + "commit: committing into a new branch creates it" + ); + assert!( + log_iter.next().is_none(), + "there is only one log line in the new branch" + ); + Ok(()) + } +} diff --git a/git-repository/tests/easy/ext/reference.rs b/git-repository/tests/easy/ext/reference.rs new file mode 100644 index 00000000000..04e9795d959 --- /dev/null +++ b/git-repository/tests/easy/ext/reference.rs @@ -0,0 +1,191 @@ +mod set_namespace { + use git_repository as git; + use git_repository::{prelude::ReferenceAccessExt, refs::transaction::PreviousValue}; + + fn easy_repo_rw() -> crate::Result<(git::EasyArcExclusive, tempfile::TempDir)> { + crate::repo_rw("make_references_repo.sh").map(|(r, d)| (r.into_easy_arc_exclusive(), d)) + } + + #[test] + fn affects_edits_and_iteration() { + let (mut repo, _keep) = easy_repo_rw().unwrap(); + assert_eq!( + repo.references().unwrap().all().unwrap().count(), + 15, + "there are plenty of references in the default namespace" + ); + assert!(repo.namespace().unwrap().is_none(), "no namespace is set initially"); + assert!( + repo.set_namespace("foo").unwrap().is_none(), + "there is no previous namespace" + ); + + assert_eq!( + repo.references().unwrap().all().unwrap().filter_map(Result::ok).count(), + 0, + "no references are in the namespace yet" + ); + + repo.tag( + "new-tag", + git::hash::ObjectId::empty_tree(git::hash::Kind::Sha1), + PreviousValue::MustNotExist, + ) + .unwrap(); + + repo.reference( + "refs/heads/new-branch", + git::hash::ObjectId::empty_tree(git::hash::Kind::Sha1), + PreviousValue::MustNotExist, + "message", + ) + .unwrap(); + + assert_eq!( + repo.references() + .unwrap() + .all() + .unwrap() + .filter_map(Result::ok) + .map(|r| r.name().as_bstr().to_owned()) + .collect::>(), + vec!["refs/heads/new-branch", "refs/tags/new-tag"], + "namespaced references appear like normal ones" + ); + + assert_eq!( + repo.references() + .unwrap() + .prefixed("refs/tags/") + .unwrap() + .filter_map(Result::ok) + .map(|r| r.name().as_bstr().to_owned()) + .collect::>(), + vec!["refs/tags/new-tag"], + "namespaced references appear like normal ones" + ); + let fully_qualified_tag_name = "refs/tags/new-tag"; + assert_eq!( + repo.find_reference(fully_qualified_tag_name).unwrap().name().as_bstr(), + fully_qualified_tag_name, + "fully qualified (yet namespaced) names work" + ); + assert_eq!( + repo.find_reference("new-tag").unwrap().name().as_bstr(), + fully_qualified_tag_name, + "namespaces are transparent" + ); + + let previous_ns = repo.clear_namespace().unwrap().expect("namespace set"); + assert_eq!(previous_ns.as_bstr(), "refs/namespaces/foo/"); + assert!( + repo.clear_namespace().unwrap().is_none(), + "it doesn't invent namespaces" + ); + + assert_eq!( + repo.references().unwrap().all().unwrap().count(), + 17, + "it lists all references, also the ones in namespaces" + ); + } +} + +mod iter_references { + use git_repository as git; + use git_repository::prelude::ReferenceAccessExt; + + fn repo() -> crate::Result { + crate::repo("make_references_repo.sh").map(|r| r.into_easy()) + } + + #[test] + fn all() -> crate::Result { + let repo = repo()?; + assert_eq!( + repo.references()? + .all()? + .filter_map(Result::ok) + .map(|r| r.name().as_bstr().to_owned()) + .collect::>(), + vec![ + "refs/d1", + "refs/heads/d1", + "refs/heads/dt1", + "refs/heads/main", + "refs/heads/multi-link-target1", + "refs/loop-a", + "refs/loop-b", + "refs/multi-link", + "refs/remotes/origin/HEAD", + "refs/remotes/origin/main", + "refs/remotes/origin/multi-link-target3", + "refs/tags/dt1", + "refs/tags/multi-link-target2", + "refs/tags/t1" + ] + ); + Ok(()) + } + + #[test] + fn prefixed() -> crate::Result { + let repo = repo()?; + assert_eq!( + repo.references()? + .prefixed("refs/heads/")? + .filter_map(Result::ok) + .map(|r| r.name().as_bstr().to_owned()) + .collect::>(), + vec![ + "refs/heads/d1", + "refs/heads/dt1", + "refs/heads/main", + "refs/heads/multi-link-target1", + ] + ); + Ok(()) + } +} + +mod head { + + use git_ref::transaction::PreviousValue; + use git_repository as git; + use git_repository::prelude::ReferenceAccessExt; + use git_testtools::hex_to_id; + + #[test] + fn symbolic() -> crate::Result { + let repo = crate::basic_repo()?; + let head = repo.head()?; + match &head.kind { + git::easy::head::Kind::Symbolic(r) => { + assert_eq!( + r.target.as_id().map(ToOwned::to_owned), + Some(hex_to_id("3189cd3cb0af8586c39a838aa3e54fd72a872a41")) + ); + } + _ => panic!("unexpected head kind"), + } + assert_eq!(head.name().expect("born").as_bstr(), "refs/heads/main"); + assert!(!head.is_detached()); + Ok(()) + } + + #[test] + fn detached() -> crate::Result { + let (repo, _keep) = crate::basic_rw_repo()?; + repo.reference( + "HEAD", + hex_to_id("3189cd3cb0af8586c39a838aa3e54fd72a872a41"), + PreviousValue::Any, + "", + )?; + + let head = repo.head()?; + assert!(head.is_detached(), "head is detached"); + assert!(head.name().is_none()); + Ok(()) + } +} diff --git a/git-repository/tests/easy/mod.rs b/git-repository/tests/easy/mod.rs new file mode 100644 index 00000000000..03076392ef3 --- /dev/null +++ b/git-repository/tests/easy/mod.rs @@ -0,0 +1,5 @@ +mod access; +mod ext; +mod object; +mod oid; +mod reference; diff --git a/git-repository/tests/object/mod.rs b/git-repository/tests/easy/object.rs similarity index 100% rename from git-repository/tests/object/mod.rs rename to git-repository/tests/easy/object.rs diff --git a/git-repository/tests/easy/oid.rs b/git-repository/tests/easy/oid.rs new file mode 100644 index 00000000000..7036b423e05 --- /dev/null +++ b/git-repository/tests/easy/oid.rs @@ -0,0 +1,19 @@ +mod ancestors { + use git_repository::prelude::ReferenceAccessExt; + + #[test] + fn all() -> crate::Result { + let repo = crate::basic_repo()?; + assert_eq!( + repo.head()? + .into_fully_peeled_id() + .expect("born")? + .ancestors()? + .all() + .count(), + 2, + "need a specific amount of commits" + ); + Ok(()) + } +} diff --git a/git-repository/tests/easy/reference.rs b/git-repository/tests/easy/reference.rs new file mode 100644 index 00000000000..1000f216f88 --- /dev/null +++ b/git-repository/tests/easy/reference.rs @@ -0,0 +1,43 @@ +mod find { + use std::convert::TryInto; + + use git_ref as refs; + use git_repository::prelude::ReferenceAccessExt; + use git_testtools::hex_to_id; + + fn repo() -> crate::Result { + crate::repo("make_references_repo.sh").map(Into::into) + } + + #[test] + fn and_peel() { + let repo = repo().unwrap(); + let mut packed_tag_ref = repo.try_find_reference("dt1").unwrap().expect("tag to exist"); + assert_eq!(packed_tag_ref.name(), "refs/tags/dt1".try_into().unwrap()); + + assert_eq!( + packed_tag_ref.inner.target, + refs::Target::Peeled(hex_to_id("4c3f4cce493d7beb45012e478021b5f65295e5a3")), + "it points to a tag object" + ); + + let object = packed_tag_ref.peel_to_id_in_place().unwrap(); + let the_commit = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"); + assert_eq!(object, the_commit, "it is assumed to be fully peeled"); + assert_eq!( + object, + packed_tag_ref.peel_to_id_in_place().unwrap(), + "peeling again yields the same object" + ); + + let mut symbolic_ref = repo.find_reference("multi-link-target1").unwrap(); + assert_eq!(symbolic_ref.name(), "refs/heads/multi-link-target1".try_into().unwrap()); + assert_eq!(symbolic_ref.peel_to_id_in_place().unwrap(), the_commit); + assert_eq!( + symbolic_ref.name(), + "refs/remotes/origin/multi-link-target3".try_into().unwrap(), + "it follows symbolic refs, too" + ); + assert_eq!(symbolic_ref.into_fully_peeled_id().unwrap(), the_commit, "idempotency"); + } +} diff --git a/git-repository/tests/fixtures/make_basic_repo.sh b/git-repository/tests/fixtures/make_basic_repo.sh index 268f2b15662..8eef95913e5 100644 --- a/git-repository/tests/fixtures/make_basic_repo.sh +++ b/git-repository/tests/fixtures/make_basic_repo.sh @@ -8,6 +8,8 @@ git checkout -b main touch this git add this git commit -q -m c1 +echo hello >> this +git commit -q -am c2 mkdir -p some/very/deeply/nested/subdir diff --git a/git-repository/tests/reference/mod.rs b/git-repository/tests/reference/mod.rs index c0a7031690c..f607d954f06 100644 --- a/git-repository/tests/reference/mod.rs +++ b/git-repository/tests/reference/mod.rs @@ -1,45 +1,28 @@ -fn repo() -> crate::Result { - crate::repo("make_references_repo.sh").map(Into::into) -} - -mod find { - use std::convert::TryInto; - - use git_ref as refs; - use git_repository::prelude::*; - use git_testtools::hex_to_id; - - use crate::reference::repo; +mod log { + use git_repository as git; #[test] - fn find_and_peel() { - let repo = repo().unwrap(); - let mut packed_tag_ref = repo.try_find_reference("dt1").unwrap().expect("tag to exist"); - assert_eq!(packed_tag_ref.name(), "refs/tags/dt1".try_into().unwrap()); - - assert_eq!( - packed_tag_ref.target(), - refs::Target::Peeled(hex_to_id("4c3f4cce493d7beb45012e478021b5f65295e5a3")), - "it points to a tag object" - ); - - let object = packed_tag_ref.peel_to_oid_in_place().unwrap(); - let the_commit = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"); - assert_eq!(object, the_commit, "it is assumed to be fully peeled"); + fn message() { + let mut commit = git::objs::Commit { + tree: git::hash::ObjectId::empty_tree(git_hash::Kind::Sha1), + parents: Default::default(), + author: Default::default(), + committer: Default::default(), + encoding: None, + message: "the subject\n\nthe body".into(), + extra_headers: vec![], + }; assert_eq!( - object, - packed_tag_ref.peel_to_oid_in_place().unwrap(), - "peeling again yields the same object" + git::reference::log::message("commit", &commit), + "commit (initial): the subject" ); + commit.parents.push(git::hash::ObjectId::null_sha1()); + assert_eq!(git::reference::log::message("other", &commit), "other: the subject"); - let mut symbolic_ref = repo.find_reference("multi-link-target1").unwrap(); - assert_eq!(symbolic_ref.name(), "refs/heads/multi-link-target1".try_into().unwrap()); - assert_eq!(symbolic_ref.peel_to_oid_in_place().unwrap(), the_commit); + commit.parents.push(git::hash::ObjectId::null_sha1()); assert_eq!( - symbolic_ref.name(), - "refs/remotes/origin/multi-link-target3".try_into().unwrap(), - "it follows symbolic refs, too" + git::reference::log::message("rebase", &commit), + "rebase (merge): the subject" ); - assert_eq!(symbolic_ref.peel_to_oid_in_place().unwrap(), the_commit, "idempotency"); } } diff --git a/git-repository/tests/repo.rs b/git-repository/tests/repo.rs index de538206f33..750942d2d74 100644 --- a/git-repository/tests/repo.rs +++ b/git-repository/tests/repo.rs @@ -1,14 +1,31 @@ -use git_repository::Repository; +use git_repository::{Easy, Repository}; type Result = std::result::Result>; fn repo(name: &str) -> crate::Result { let repo_path = git_testtools::scripted_fixture_repo_read_only(name)?; - Ok(Repository::discover(repo_path)?) + Ok(Repository::open(repo_path)?) } -mod access; +fn repo_rw(name: &str) -> crate::Result<(Repository, tempfile::TempDir)> { + let repo_path = git_testtools::scripted_fixture_repo_writable(name)?; + Ok((Repository::discover(repo_path.path())?, repo_path)) +} + +fn easy_repo_rw(name: &str) -> crate::Result<(Easy, tempfile::TempDir)> { + repo_rw(name).map(|(repo, dir)| (repo.into(), dir)) +} + +fn basic_repo() -> crate::Result { + repo("make_basic_repo.sh").map(|r| r.into_easy()) +} + +fn basic_rw_repo() -> crate::Result<(Easy, tempfile::TempDir)> { + easy_repo_rw("make_basic_repo.sh") +} + +mod commit; mod discover; +mod easy; mod init; -mod object; mod reference; diff --git a/git-traverse/Cargo.toml b/git-traverse/Cargo.toml index 71f06abdb0e..b729a7b4b2e 100644 --- a/git-traverse/Cargo.toml +++ b/git-traverse/Cargo.toml @@ -14,7 +14,7 @@ doctest = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -git-hash = { version = "^0.5.0", path = "../git-hash" } +git-hash = { version ="^0.6.0", path = "../git-hash" } git-object = { version ="^0.13.0", path = "../git-object" } quick-error = "2.0.0" diff --git a/gitoxide-core/Cargo.toml b/gitoxide-core/Cargo.toml index 26338115439..826da953526 100644 --- a/gitoxide-core/Cargo.toml +++ b/gitoxide-core/Cargo.toml @@ -19,6 +19,8 @@ serde1 = ["git-commitgraph/serde1", "git-repository/serde1", "git-protocol-for-c blocking-client = ["git-protocol-for-configuration-only/blocking-client", "git-repository/network"] async-client = ["git-protocol-for-configuration-only/async-client", "git-repository/network", "async-trait", "futures-io", "async-net", "async-io", "futures-lite", "blocking"] +local-time-support = ["git-repository/local-time-support"] + # tools organize = ["git-url", "jwalk"] estimate-hours = ["itertools", "rayon", "bstr", "fs-err"] diff --git a/gitoxide-core/src/hours.rs b/gitoxide-core/src/hours.rs index a37b81e7649..6e0510d06f6 100644 --- a/gitoxide-core/src/hours.rs +++ b/gitoxide-core/src/hours.rs @@ -10,7 +10,9 @@ use std::{ use anyhow::{anyhow, bail}; use bstr::BString; -use git_repository::{actor, interrupt, objs, odb, odb::pack, prelude::*, progress, Progress}; +use git_repository::{ + actor, interrupt, objs, odb, odb::pack, prelude::*, progress, refs::file::ReferenceExt, Progress, +}; use itertools::Itertools; use rayon::prelude::*; diff --git a/gitoxide-core/src/pack/create.rs b/gitoxide-core/src/pack/create.rs index b574f7e097b..7d1c59e3f2b 100644 --- a/gitoxide-core/src/pack/create.rs +++ b/gitoxide-core/src/pack/create.rs @@ -8,7 +8,9 @@ use git_repository::{ objs::bstr::ByteVec, odb::{pack, pack::cache::DecodeEntry, Find}, prelude::{Finalize, FindExt}, - progress, traverse, Progress, + progress, + refs::file::ReferenceExt, + traverse, Progress, }; use crate::OutputFormat; diff --git a/tests/tools/Cargo.toml b/tests/tools/Cargo.toml index ff745069c0d..8b4dde592f9 100644 --- a/tests/tools/Cargo.toml +++ b/tests/tools/Cargo.toml @@ -13,7 +13,7 @@ path = "src/main.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -git-hash = { version = "^0.5.0", path = "../../git-hash" } +git-hash = { version ="^0.6.0", path = "../../git-hash" } nom = { version = "7", default-features = false, features = ["std"]} bstr = "0.2.15" crc = "2.0.0"