gix-revision-0.27.0/.cargo_vcs_info.json0000644000000001520000000000100135430ustar { "git": { "sha1": "b050327e76f234b19be921b78b7b28e034319fdb" }, "path_in_vcs": "gix-revision" }gix-revision-0.27.0/Cargo.toml0000644000000032730000000000100115500ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.65" name = "gix-revision" version = "0.27.0" authors = ["Sebastian Thiel "] include = [ "src/**/*", "LICENSE-*", "README.md", ] description = "A crate of the gitoxide project dealing with finding names for revisions and parsing specifications" readme = "README.md" license = "MIT OR Apache-2.0" repository = "https://github.com/Byron/gitoxide" [package.metadata.docs.rs] all-features = true features = ["document-features"] [lib] doctest = false [dependencies.bstr] version = "1.3.0" features = ["std"] default-features = false [dependencies.document-features] version = "0.2.1" optional = true [dependencies.gix-date] version = "^0.8.5" [dependencies.gix-hash] version = "^0.14.2" [dependencies.gix-hashtable] version = "^0.5.2" [dependencies.gix-object] version = "^0.42.0" [dependencies.gix-revwalk] version = "^0.13.0" [dependencies.gix-trace] version = "^0.1.8" [dependencies.serde] version = "1.0.114" features = ["derive"] optional = true default-features = false [dependencies.thiserror] version = "1.0.26" [dev-dependencies] [features] default = ["describe"] describe = [] serde = [ "dep:serde", "gix-hash/serde", "gix-object/serde", ] gix-revision-0.27.0/Cargo.toml.orig000064400000000000000000000027261046102023000152330ustar 00000000000000[package] name = "gix-revision" version = "0.27.0" repository = "https://github.com/Byron/gitoxide" license = "MIT OR Apache-2.0" description = "A crate of the gitoxide project dealing with finding names for revisions and parsing specifications" authors = ["Sebastian Thiel "] edition = "2021" include = ["src/**/*", "LICENSE-*", "README.md"] rust-version = "1.65" [lib] doctest = false [features] default = ["describe"] ## `git describe` functionality describe = [] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. serde = [ "dep:serde", "gix-hash/serde", "gix-object/serde" ] [dependencies] gix-hash = { version = "^0.14.2", path = "../gix-hash" } gix-object = { version = "^0.42.0", path = "../gix-object" } gix-date = { version = "^0.8.5", path = "../gix-date" } gix-hashtable = { version = "^0.5.2", path = "../gix-hashtable" } gix-revwalk = { version = "^0.13.0", path = "../gix-revwalk" } gix-trace = { version = "^0.1.8", path = "../gix-trace" } bstr = { version = "1.3.0", default-features = false, features = ["std"]} thiserror = "1.0.26" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } document-features = { version = "0.2.1", optional = true } [dev-dependencies] gix-odb = { path = "../gix-odb" } gix-testtools = { path = "../tests/tools" } gix-commitgraph = { path = "../gix-commitgraph" } [package.metadata.docs.rs] all-features = true features = ["document-features"] gix-revision-0.27.0/LICENSE-APACHE000064400000000000000000000247461046102023000142760ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. gix-revision-0.27.0/LICENSE-MIT000064400000000000000000000017771046102023000140050ustar 00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gix-revision-0.27.0/README.md000064400000000000000000000004151046102023000136140ustar 00000000000000# `gix-revision` ### Testing #### Fuzzing `cargo fuzz` is used for fuzzing, installable with `cargo install cargo-fuzz`. Targets can be listed with `cargo fuzz list` and executed via `cargo +nightly fuzz run `, where `` can be `parse` for example. gix-revision-0.27.0/src/describe.rs000064400000000000000000000314331046102023000152560ustar 00000000000000use std::{ borrow::Cow, fmt::{Display, Formatter}, }; use bstr::BStr; use gix_hashtable::HashMap; /// The positive result produced by [describe()][function::describe()]. #[derive(Debug, Clone)] pub struct Outcome<'name> { /// The name of the tag or branch that is closest to the commit `id`. /// /// If `None`, no name was found but it was requested to provide the `id` itself as fallback. pub name: Option>, /// The input commit object id that we describe. pub id: gix_hash::ObjectId, /// The number of commits that are between the tag or branch with `name` and `id`. /// These commits are all in the future of the named tag or branch. pub depth: u32, /// The mapping between object ids and their names initially provided by the describe call. pub name_by_oid: HashMap>, /// The amount of commits we traversed. pub commits_seen: u32, } impl<'a> Outcome<'a> { /// Turn this outcome into a structure that can display itself in the typical `git describe` format. pub fn into_format(self, hex_len: usize) -> Format<'a> { Format { name: self.name, id: self.id, hex_len, depth: self.depth, long: false, dirty_suffix: None, } } } /// A structure implementing `Display`, producing a `git describe` like string. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] pub struct Format<'a> { /// The name of the branch or tag to display, as is. /// /// If `None`, the `id` will be displayed as a fallback. pub name: Option>, /// The `id` of the commit to describe. pub id: gix_hash::ObjectId, /// The amount of hex characters to use to display `id`. pub hex_len: usize, /// The amount of commits between `name` and `id`, where `id` is in the future of `name`. pub depth: u32, /// If true, the long form of the describe string will be produced even if `id` lies directly on `name`, /// hence has a depth of 0. pub long: bool, /// If `Some(suffix)`, it will be appended to the describe string. /// This should be set if the working tree was determined to be dirty. pub dirty_suffix: Option, } impl<'a> Format<'a> { /// Return true if the `name` is directly associated with `id`, i.e. there are no commits between them. pub fn is_exact_match(&self) -> bool { self.depth == 0 } /// Set this instance to print in long mode, that is if `depth` is 0, it will still print the whole /// long form even though it's not quite necessary. /// /// Otherwise, it is allowed to shorten itself. pub fn long(&mut self, long: bool) -> &mut Self { self.long = long; self } } impl<'a> Display for Format<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if let Some(name) = self.name.as_deref() { if !self.long && self.is_exact_match() { name.fmt(f)?; } else { write!(f, "{}-{}-g{}", name, self.depth, self.id.to_hex_with_len(self.hex_len))?; } } else { self.id.to_hex_with_len(self.hex_len).fmt(f)?; } if let Some(suffix) = &self.dirty_suffix { write!(f, "-{suffix}")?; } Ok(()) } } /// A bit-field which keeps track of which commit is reachable by one of 32 candidates names. pub type Flags = u32; const MAX_CANDIDATES: usize = std::mem::size_of::() * 8; /// The options required to call [`describe()`][function::describe()]. #[derive(Clone, Debug)] pub struct Options<'name> { /// The candidate names from which to determine the `name` to use for the describe string, /// as a mapping from a commit id and the name associated with it. pub name_by_oid: HashMap>, /// The amount of names we will keep track of. Defaults to the maximum of 32. /// /// If the number is exceeded, it will be capped at 32 and defaults to 10. pub max_candidates: usize, /// If no candidate for naming, always show the abbreviated hash. Default: false. pub fallback_to_oid: bool, /// Only follow the first parent during graph traversal. Default: false. /// /// This may speed up the traversal at the cost of accuracy. pub first_parent: bool, } impl<'name> Default for Options<'name> { fn default() -> Self { Options { max_candidates: 10, // the same number as git uses, otherwise we perform worse by default on big repos name_by_oid: Default::default(), fallback_to_oid: false, first_parent: false, } } } /// The error returned by the [`describe()`][function::describe()] function. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("The parents of commit {} could not be added to graph during traversal", oid.to_hex())] InsertParentsToGraph { #[source] err: crate::graph::insert_parents::Error, oid: gix_hash::ObjectId, }, #[error("A commit could not be decoded during traversal")] Decode(#[from] gix_object::decode::Error), } pub(crate) mod function { use std::{borrow::Cow, cmp::Ordering}; use bstr::BStr; use gix_hash::oid; use super::{Error, Outcome}; use crate::{ describe::{CommitTime, Flags, Options, MAX_CANDIDATES}, Graph, PriorityQueue, }; /// Given a `commit` id, traverse the commit `graph` and collect candidate names from the `name_by_oid` mapping to produce /// an `Outcome`, which converted [`into_format()`][Outcome::into_format()] will produce a typical `git describe` string. /// /// Note that the `name_by_oid` map is returned in the [`Outcome`], which can be forcefully returned even if there was no matching /// candidate by setting `fallback_to_oid` to true. pub fn describe<'name>( commit: &oid, graph: &mut Graph<'_, Flags>, Options { name_by_oid, mut max_candidates, fallback_to_oid, first_parent, }: Options<'name>, ) -> Result>, Error> { let _span = gix_trace::coarse!( "gix_revision::describe()", commit = %commit, name_count = name_by_oid.len(), max_candidates, first_parent ); max_candidates = max_candidates.min(MAX_CANDIDATES); if let Some(name) = name_by_oid.get(commit) { return Ok(Some(Outcome { name: name.clone().into(), id: commit.to_owned(), depth: 0, name_by_oid, commits_seen: 0, })); } if max_candidates == 0 || name_by_oid.is_empty() { return if fallback_to_oid { Ok(Some(Outcome { id: commit.to_owned(), name: None, name_by_oid, depth: 0, commits_seen: 0, })) } else { Ok(None) }; } let mut queue = PriorityQueue::from_iter(Some((u32::MAX, commit.to_owned()))); let mut candidates = Vec::new(); let mut commits_seen = 0; let mut gave_up_on_commit = None; graph.clear(); graph.insert(commit.to_owned(), 0u32); while let Some(commit) = queue.pop_value() { commits_seen += 1; let flags = if let Some(name) = name_by_oid.get(&commit) { if candidates.len() < max_candidates { let identity_bit = 1 << candidates.len(); candidates.push(Candidate { name: name.clone(), commits_in_its_future: commits_seen - 1, identity_bit, order: candidates.len(), }); let flags = graph.get_mut(&commit).expect("inserted"); *flags |= identity_bit; *flags } else { gave_up_on_commit = Some(commit); break; } } else { graph[&commit] }; for candidate in candidates .iter_mut() .filter(|c| (flags & c.identity_bit) != c.identity_bit) { candidate.commits_in_its_future += 1; } if queue.is_empty() && !candidates.is_empty() { // single-trunk history that waits to be replenished. // Abort early if the best-candidate is in the current commits past. let mut shortest_depth = Flags::MAX; let mut best_candidates_at_same_depth = 0_u32; for candidate in &candidates { match candidate.commits_in_its_future.cmp(&shortest_depth) { Ordering::Less => { shortest_depth = candidate.commits_in_its_future; best_candidates_at_same_depth = candidate.identity_bit; } Ordering::Equal => { best_candidates_at_same_depth |= candidate.identity_bit; } Ordering::Greater => {} } } if (flags & best_candidates_at_same_depth) == best_candidates_at_same_depth { break; } } parents_by_date_onto_queue_and_track_names(graph, &mut queue, commit, flags, first_parent)?; } if candidates.is_empty() { return if fallback_to_oid { Ok(Some(Outcome { id: commit.to_owned(), name: None, name_by_oid, depth: 0, commits_seen, })) } else { Ok(None) }; } candidates.sort_by(|a, b| { a.commits_in_its_future .cmp(&b.commits_in_its_future) .then_with(|| a.order.cmp(&b.order)) }); if let Some(commit_id) = gave_up_on_commit { queue.insert(u32::MAX, commit_id); commits_seen -= 1; } commits_seen += finish_depth_computation( queue, graph, candidates.first_mut().expect("at least one candidate"), first_parent, )?; Ok(candidates.into_iter().next().map(|c| Outcome { name: c.name.into(), id: commit.to_owned(), depth: c.commits_in_its_future, name_by_oid, commits_seen, })) } fn parents_by_date_onto_queue_and_track_names( graph: &mut Graph<'_, Flags>, queue: &mut PriorityQueue, commit: gix_hash::ObjectId, commit_flags: Flags, first_parent: bool, ) -> Result<(), Error> { graph .insert_parents( &commit, &mut |parent_id, parent_commit_date| { queue.insert(parent_commit_date as u32, parent_id); commit_flags }, &mut |_parent_id, flags| *flags |= commit_flags, first_parent, ) .map_err(|err| Error::InsertParentsToGraph { err, oid: commit })?; Ok(()) } fn finish_depth_computation( mut queue: PriorityQueue, graph: &mut Graph<'_, Flags>, best_candidate: &mut Candidate<'_>, first_parent: bool, ) -> Result { let mut commits_seen = 0; while let Some(commit) = queue.pop_value() { commits_seen += 1; let flags = graph[&commit]; if (flags & best_candidate.identity_bit) == best_candidate.identity_bit { if queue .iter_unordered() .all(|id| (graph[id] & best_candidate.identity_bit) == best_candidate.identity_bit) { break; } } else { best_candidate.commits_in_its_future += 1; } parents_by_date_onto_queue_and_track_names(graph, &mut queue, commit, flags, first_parent)?; } Ok(commits_seen) } #[derive(Debug)] struct Candidate<'a> { name: Cow<'a, BStr>, commits_in_its_future: Flags, /// A single bit identifying this candidate uniquely in a bitset identity_bit: Flags, /// The order at which we found the candidate, first one has order = 0 order: usize, } } /// The timestamp for the creation date of a commit in seconds since unix epoch. type CommitTime = u32; gix-revision-0.27.0/src/lib.rs000064400000000000000000000012311046102023000142350ustar 00000000000000//! Interact with git revisions by parsing them from rev-specs and describing them in terms of reference names. //! //! ## Feature Flags #![cfg_attr( all(doc, feature = "document-features"), doc = ::document_features::document_features!() )] #![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg, doc_auto_cfg))] #![deny(missing_docs, rust_2018_idioms, unsafe_code)] /// #[allow(clippy::empty_docs)] #[cfg(feature = "describe")] pub mod describe; #[cfg(feature = "describe")] pub use describe::function::describe; /// #[allow(clippy::empty_docs)] pub mod spec; pub use gix_revwalk::{graph, Graph, PriorityQueue}; pub use spec::types::Spec; gix-revision-0.27.0/src/spec/mod.rs000064400000000000000000000115301046102023000152030ustar 00000000000000use crate::Spec; /// How to interpret a revision specification, or `revspec`. #[derive(Default, Debug, Copy, Clone, PartialOrd, PartialEq, Ord, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Kind { /// Include commits reachable from this revision, the default when parsing revision `a` for example, i.e. `a` and its ancestors. /// Example: `a`. #[default] IncludeReachable, /// Exclude commits reachable from this revision, i.e. `a` and its ancestors. Example: `^a`. ExcludeReachable, /// Every commit that is reachable from `b` but not from `a`. Example: `a..b`. RangeBetween, /// Every commit reachable through either `a` or `b` but no commit that is reachable by both. Example: `a...b`. ReachableToMergeBase, /// Include every commit of all parents of `a`, but not `a` itself. Example: `a^@`. IncludeReachableFromParents, /// Exclude every commit of all parents of `a`, but not `a` itself. Example: `a^!`. ExcludeReachableFromParents, } impl Spec { /// Return the kind of this specification. pub fn kind(&self) -> Kind { match self { Spec::Include(_) => Kind::IncludeReachable, Spec::Exclude(_) => Kind::ExcludeReachable, Spec::Range { .. } => Kind::RangeBetween, Spec::Merge { .. } => Kind::ReachableToMergeBase, Spec::IncludeOnlyParents { .. } => Kind::IncludeReachableFromParents, Spec::ExcludeParents { .. } => Kind::ExcludeReachableFromParents, } } } mod _impls { use std::fmt::{Display, Formatter}; use crate::Spec; impl Display for Spec { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Spec::Include(oid) => Display::fmt(oid, f), Spec::Exclude(oid) => write!(f, "^{oid}"), Spec::Range { from, to } => write!(f, "{from}..{to}"), Spec::Merge { theirs, ours } => write!(f, "{theirs}...{ours}"), Spec::IncludeOnlyParents(from_exclusive) => write!(f, "{from_exclusive}^@"), Spec::ExcludeParents(oid) => write!(f, "{oid}^!"), } } } } pub(crate) mod types { /// A revision specification without any bindings to a repository, useful for serialization or movement over thread boundaries. /// /// Note that all [object ids][gix_hash::ObjectId] should be a committish, but don't have to be. /// Unless the field name contains `_exclusive`, the respective objects are included in the set. #[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Spec { /// Include commits reachable from this revision, i.e. `a` and its ancestors. /// /// The equivalent to [crate::spec::Kind::IncludeReachable], but with data. Include(gix_hash::ObjectId), /// Exclude commits reachable from this revision, i.e. `a` and its ancestors. Example: `^a`. /// /// The equivalent to [crate::spec::Kind::ExcludeReachable], but with data. Exclude(gix_hash::ObjectId), /// Every commit that is reachable from `from` to `to`, but not any ancestors of `from`. Example: `from..to`. /// /// The equivalent to [crate::spec::Kind::RangeBetween], but with data. Range { /// The starting point of the range, which is included in the set. from: gix_hash::ObjectId, /// The end point of the range, which is included in the set. to: gix_hash::ObjectId, }, /// Every commit reachable through either `theirs` or `ours`, but no commit that is reachable by both. Example: `theirs...ours`. /// /// The equivalent to [crate::spec::Kind::ReachableToMergeBase], but with data. Merge { /// Their side of the merge, which is included in the set. theirs: gix_hash::ObjectId, /// Our side of the merge, which is included in the set. ours: gix_hash::ObjectId, }, /// Include every commit of all parents of `a`, but not `a` itself. Example: `a^@`. /// /// The equivalent to [crate::spec::Kind::IncludeReachableFromParents], but with data. IncludeOnlyParents( /// Include only the parents of this object, but not the object itself. gix_hash::ObjectId, ), /// Exclude every commit of all parents of `a`, but not `a` itself. Example: `a^!`. /// /// The equivalent to [crate::spec::Kind::ExcludeReachableFromParents], but with data. ExcludeParents( /// Exclude the parents of this object, but not the object itself. gix_hash::ObjectId, ), } } /// #[allow(clippy::empty_docs)] pub mod parse; pub use parse::function::parse; gix-revision-0.27.0/src/spec/parse/delegate.rs000064400000000000000000000173461046102023000173230ustar 00000000000000use bstr::BStr; /// Usually the first methods to call when parsing a rev-spec to set an anchoring revision (which is typically a `Commit` object). /// Methods can be called multiple time to either try input or to parse another rev-spec that is part of a range. /// /// In one case they will not be called at all, e.g. `@{[-]n}` indicates the current branch (what `HEAD` dereferences to), /// without ever naming it, and so does `@{upstream}` or `@{}`. /// /// Note that when dereferencing `HEAD` implicitly, a revision must be set for later navigation. pub trait Revision { /// Resolve `name` as reference which might not be a valid reference name. The name may be partial like `main` or full like /// `refs/heads/main` solely depending on the users input. /// Symbolic referenced should be followed till their object, but objects **must not yet** be peeled. fn find_ref(&mut self, name: &BStr) -> Option<()>; /// An object prefix to disambiguate, returning `None` if it is ambiguous or wasn't found at all. /// /// If `hint` is set, it should be used to disambiguate multiple objects with the same prefix. fn disambiguate_prefix(&mut self, prefix: gix_hash::Prefix, hint: Option>) -> Option<()>; /// Lookup the reflog of the previously set reference, or dereference `HEAD` to its reference /// to obtain the ref name (as opposed to `HEAD` itself). /// If there is no such reflog entry, return `None`. fn reflog(&mut self, query: ReflogLookup) -> Option<()>; /// When looking at `HEAD`, `branch_no` is the non-null checkout in the path, e.g. `1` means the last branch checked out, /// `2` is the one before that. /// Return `None` if there is no branch as the checkout history (via the reflog) isn't long enough. fn nth_checked_out_branch(&mut self, branch_no: usize) -> Option<()>; /// Lookup the previously set branch or dereference `HEAD` to its reference to use its name to lookup the sibling branch of `kind` /// in the configuration (typically in `refs/remotes/…`). The sibling branches are always local tracking branches. /// Return `None` of no such configuration exists and no sibling could be found, which is also the case for all reference outside /// of `refs/heads/`. /// Note that the caller isn't aware if the previously set reference is a branch or not and might call this method even though no reference /// is known. fn sibling_branch(&mut self, kind: SiblingBranch) -> Option<()>; } /// Combine one or more specs into a range of multiple. pub trait Kind { /// Set the kind of the spec, which happens only once if it happens at all. /// In case this method isn't called, assume `Single`. /// Reject a kind by returning `None` to stop the parsing. /// /// Note that ranges don't necessarily assure that a second specification will be parsed. /// If `^rev` is given, this method is called with [`spec::Kind::RangeBetween`][crate::spec::Kind::RangeBetween] /// and no second specification is provided. /// /// Note that the method can be called even if other invariants are not fulfilled, treat these as errors. fn kind(&mut self, kind: crate::spec::Kind) -> Option<()>; } /// Once an anchor is set one can adjust it using traversal methods. pub trait Navigate { /// Adjust the current revision to traverse the graph according to `kind`. fn traverse(&mut self, kind: Traversal) -> Option<()>; /// Peel the current object until it reached `kind` or `None` if the chain does not contain such object. fn peel_until(&mut self, kind: PeelTo<'_>) -> Option<()>; /// Find the first revision/commit whose message matches the given `regex` (which is never empty). /// to see how it should be matched. /// If `negated` is `true`, the first non-match will be a match. /// /// If no revision is known yet, find the _youngest_ matching commit from _any_ reference, including `HEAD`. /// Otherwise, only find commits reachable from the currently set revision. fn find(&mut self, regex: &BStr, negated: bool) -> Option<()>; /// Look up the given `path` at the given `stage` in the index returning its blob id, /// or return `None` if it doesn't exist at this `stage`. /// Note that this implies no revision is needed and no anchor is set yet. /// /// * `stage` ranges from 0 to 2, with 0 being the base, 1 being ours, 2 being theirs. /// * `path` without prefix is relative to the root of the repository, while prefixes like `./` and `../` make it /// relative to the current working directory. fn index_lookup(&mut self, path: &BStr, stage: u8) -> Option<()>; } /// A hint to make disambiguation when looking up prefixes possible. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] pub enum PrefixHint<'a> { /// The prefix must be a commit. MustBeCommit, /// The prefix refers to a commit, anchored to a ref and a revision generation in its future. DescribeAnchor { /// The name of the reference, like `v1.2.3` or `main`. ref_name: &'a BStr, /// The future generation of the commit we look for, with 0 meaning the commit is referenced by /// `ref_name` directly. generation: usize, }, } /// A lookup into the reflog of a reference. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] pub enum ReflogLookup { /// Lookup by entry, where `0` is the most recent entry, and `1` is the older one behind `0`. Entry(usize), /// Lookup the reflog at the given time and find the closest matching entry. Date(gix_date::Time), } /// Define how to traverse the commit graph. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] pub enum Traversal { /// Select the given parent commit of the currently selected commit, start at `1` for the first parent. /// The value will never be `0`. NthParent(usize), /// Select the given ancestor of the currently selected commit, start at `1` for the first ancestor. /// The value will never be `0`. NthAncestor(usize), } /// Define where a tag object should be peeled to. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] pub enum PeelTo<'a> { /// An object of the given kind. ObjectKind(gix_object::Kind), /// Ensure the object at hand exists and is valid (actually without peeling it), /// without imposing any restrictions to its type. /// The object needs to be looked up to assure that it is valid, but it doesn't need to be decoded. ValidObject, /// Follow an annotated tag object recursively until an object is found. RecursiveTagObject, /// The path to drill into as seen relative to the current tree-ish. /// /// Note that the path can be relative, and `./` and `../` prefixes are seen as relative to the current /// working directory. /// /// The path may be empty, which makes it refer to the tree at the current revision, similar to `^{tree}`. /// Note that paths like `../` are valid and refer to a tree as seen relative to the current working directory. Path(&'a BStr), } /// The kind of sibling branch to obtain. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] pub enum SiblingBranch { /// The upstream branch as configured in `branch..remote` or `branch..merge`. Upstream, /// The upstream branch to which we would push. Push, } impl SiblingBranch { /// Parse `input` as branch representation, if possible. pub fn parse(input: &BStr) -> Option { if input.eq_ignore_ascii_case(b"u") || input.eq_ignore_ascii_case(b"upstream") { SiblingBranch::Upstream.into() } else if input.eq_ignore_ascii_case(b"push") { SiblingBranch::Push.into() } else { None } } } gix-revision-0.27.0/src/spec/parse/function.rs000064400000000000000000000601201046102023000173620ustar 00000000000000use std::{str::FromStr, time::SystemTime}; use bstr::{BStr, BString, ByteSlice, ByteVec}; use crate::{ spec, spec::parse::{delegate, delegate::SiblingBranch, Delegate, Error}, }; /// Parse a git [`revspec`](https://git-scm.com/docs/git-rev-parse#_specifying_revisions) and call `delegate` for each token /// successfully parsed. /// /// Note that the `delegate` is expected to maintain enough state to lookup revisions properly. /// Returns `Ok(())` if all of `input` was consumed, or the error if either the `revspec` syntax was incorrect or /// the `delegate` failed to perform the request. pub fn parse(mut input: &BStr, delegate: &mut impl Delegate) -> Result<(), Error> { use delegate::{Kind, Revision}; let mut delegate = InterceptRev::new(delegate); let mut prev_kind = None; if let Some(b'^') = input.first() { input = next(input).1; let kind = spec::Kind::ExcludeReachable; delegate.kind(kind).ok_or(Error::Delegate)?; prev_kind = kind.into(); } let mut found_revision; (input, found_revision) = { let rest = revision(input, &mut delegate)?; (rest, rest != input) }; if delegate.done { return if input.is_empty() { Ok(()) } else { Err(Error::UnconsumedInput { input: input.into() }) }; } if let Some((rest, kind)) = try_range(input) { if let Some(prev_kind) = prev_kind { return Err(Error::KindSetTwice { prev_kind, kind }); } if !found_revision { delegate.find_ref("HEAD".into()).ok_or(Error::Delegate)?; } delegate.kind(kind).ok_or(Error::Delegate)?; (input, found_revision) = { let remainder = revision(rest.as_bstr(), &mut delegate)?; (remainder, remainder != rest) }; if !found_revision { delegate.find_ref("HEAD".into()).ok_or(Error::Delegate)?; } } if input.is_empty() { delegate.done(); Ok(()) } else { Err(Error::UnconsumedInput { input: input.into() }) } } mod intercept { use bstr::{BStr, BString}; use crate::spec::parse::{delegate, Delegate}; #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] pub(crate) enum PrefixHintOwned { MustBeCommit, DescribeAnchor { ref_name: BString, generation: usize }, } impl PrefixHintOwned { pub fn to_ref(&self) -> delegate::PrefixHint<'_> { match self { PrefixHintOwned::MustBeCommit => delegate::PrefixHint::MustBeCommit, PrefixHintOwned::DescribeAnchor { ref_name, generation } => delegate::PrefixHint::DescribeAnchor { ref_name: ref_name.as_ref(), generation: *generation, }, } } } impl<'a> From> for PrefixHintOwned { fn from(v: delegate::PrefixHint<'a>) -> Self { match v { delegate::PrefixHint::MustBeCommit => PrefixHintOwned::MustBeCommit, delegate::PrefixHint::DescribeAnchor { generation, ref_name } => PrefixHintOwned::DescribeAnchor { ref_name: ref_name.to_owned(), generation, }, } } } pub(crate) struct InterceptRev<'a, T> { pub inner: &'a mut T, pub last_ref: Option, // TODO: smallvec to save the unnecessary allocation? Can't keep ref due to lifetime constraints in traits pub last_prefix: Option<(gix_hash::Prefix, Option)>, pub done: bool, } impl<'a, T> InterceptRev<'a, T> where T: Delegate, { pub fn new(delegate: &'a mut T) -> Self { InterceptRev { inner: delegate, last_ref: None, last_prefix: None, done: false, } } } impl<'a, T> Delegate for InterceptRev<'a, T> where T: Delegate, { fn done(&mut self) { self.done = true; self.inner.done() } } impl<'a, T> delegate::Revision for InterceptRev<'a, T> where T: Delegate, { fn find_ref(&mut self, name: &BStr) -> Option<()> { self.last_ref = name.to_owned().into(); self.inner.find_ref(name) } fn disambiguate_prefix( &mut self, prefix: gix_hash::Prefix, hint: Option>, ) -> Option<()> { self.last_prefix = Some((prefix, hint.map(Into::into))); self.inner.disambiguate_prefix(prefix, hint) } fn reflog(&mut self, query: delegate::ReflogLookup) -> Option<()> { self.inner.reflog(query) } fn nth_checked_out_branch(&mut self, branch_no: usize) -> Option<()> { self.inner.nth_checked_out_branch(branch_no) } fn sibling_branch(&mut self, kind: delegate::SiblingBranch) -> Option<()> { self.inner.sibling_branch(kind) } } impl<'a, T> delegate::Navigate for InterceptRev<'a, T> where T: Delegate, { fn traverse(&mut self, kind: delegate::Traversal) -> Option<()> { self.inner.traverse(kind) } fn peel_until(&mut self, kind: delegate::PeelTo<'_>) -> Option<()> { self.inner.peel_until(kind) } fn find(&mut self, regex: &BStr, negated: bool) -> Option<()> { self.inner.find(regex, negated) } fn index_lookup(&mut self, path: &BStr, stage: u8) -> Option<()> { self.inner.index_lookup(path, stage) } } impl<'a, T> delegate::Kind for InterceptRev<'a, T> where T: Delegate, { fn kind(&mut self, kind: crate::spec::Kind) -> Option<()> { self.inner.kind(kind) } } } use intercept::InterceptRev; fn try_set_prefix(delegate: &mut impl Delegate, hex_name: &BStr, hint: Option>) -> Option<()> { gix_hash::Prefix::from_hex(hex_name.to_str().expect("hexadecimal only")) .ok() .and_then(|prefix| delegate.disambiguate_prefix(prefix, hint)) } fn long_describe_prefix(name: &BStr) -> Option<(&BStr, delegate::PrefixHint<'_>)> { let mut iter = name.rsplit(|b| *b == b'-'); let candidate = iter.by_ref().find_map(|substr| { if substr.first()? != &b'g' { return None; }; let rest = substr.get(1..)?; rest.iter().all(u8::is_ascii_hexdigit).then(|| rest.as_bstr()) })?; let candidate = iter.clone().any(|token| !token.is_empty()).then_some(candidate); let hint = iter .next() .and_then(|gen| gen.to_str().ok().and_then(|gen| usize::from_str(gen).ok())) .and_then(|generation| { iter.next().map(|token| { let last_token_len = token.len(); let first_token_ptr = iter.last().map_or(token.as_ptr(), <[_]>::as_ptr); // SAFETY: both pointers are definitely part of the same object #[allow(unsafe_code)] let prior_tokens_len: usize = unsafe { token.as_ptr().offset_from(first_token_ptr) } .try_into() .expect("positive value"); delegate::PrefixHint::DescribeAnchor { ref_name: name[..prior_tokens_len + last_token_len].as_bstr(), generation, } }) }) .unwrap_or(delegate::PrefixHint::MustBeCommit); candidate.map(|c| (c, hint)) } fn short_describe_prefix(name: &BStr) -> Option<&BStr> { let mut iter = name.split(|b| *b == b'-'); let candidate = iter .next() .and_then(|prefix| prefix.iter().all(u8::is_ascii_hexdigit).then(|| prefix.as_bstr())); (iter.count() == 1).then_some(candidate).flatten() } type InsideParensRestConsumed<'a> = (std::borrow::Cow<'a, BStr>, &'a BStr, usize); fn parens(input: &[u8]) -> Result>, Error> { if input.first() != Some(&b'{') { return Ok(None); } let mut open_braces = 0; let mut ignore_next = false; let mut skip_list = Vec::new(); for (idx, b) in input.iter().enumerate() { match *b { b'{' => { if ignore_next { ignore_next = false; } else { open_braces += 1 } } b'}' => { if ignore_next { ignore_next = false; } else { open_braces -= 1 } } b'\\' => { skip_list.push(idx); if ignore_next { skip_list.pop(); ignore_next = false; } else { ignore_next = true; } } _ => { if ignore_next { skip_list.pop(); }; ignore_next = false } } if open_braces == 0 { let inner: std::borrow::Cow<'_, _> = if skip_list.is_empty() { input[1..idx].as_bstr().into() } else { let mut from = 1; let mut buf = BString::default(); for next in skip_list.into_iter() { buf.push_str(&input[from..next]); from = next + 1; } if let Some(rest) = input.get(from..idx) { buf.push_str(rest); } buf.into() }; return Ok(Some((inner, input[idx + 1..].as_bstr(), idx + 1))); } } Err(Error::UnclosedBracePair { input: input.into() }) } fn try_parse(input: &BStr) -> Result, Error> { input .to_str() .ok() .and_then(|n| { n.parse().ok().map(|n| { if n == T::default() && input[0] == b'-' { return Err(Error::NegativeZero { input: input.into() }); }; Ok(n) }) }) .transpose() } fn revision<'a, T>(mut input: &'a BStr, delegate: &mut InterceptRev<'_, T>) -> Result<&'a BStr, Error> where T: Delegate, { use delegate::{Navigate, Revision}; fn consume_all(res: Option<()>) -> Result<&'static BStr, Error> { res.ok_or(Error::Delegate).map(|_| "".into()) } match input.as_bytes() { [b':'] => return Err(Error::MissingColonSuffix), [b':', b'/'] => return Err(Error::EmptyTopLevelRegex), [b':', b'/', regex @ ..] => { let (regex, negated) = parse_regex_prefix(regex.as_bstr())?; if regex.is_empty() { return Err(Error::UnconsumedInput { input: input.into() }); } return consume_all(delegate.find(regex, negated)); } [b':', b'0', b':', path @ ..] => return consume_all(delegate.index_lookup(path.as_bstr(), 0)), [b':', b'1', b':', path @ ..] => return consume_all(delegate.index_lookup(path.as_bstr(), 1)), [b':', b'2', b':', path @ ..] => return consume_all(delegate.index_lookup(path.as_bstr(), 2)), [b':', path @ ..] => return consume_all(delegate.index_lookup(path.as_bstr(), 0)), _ => {} }; let mut sep_pos = None; let mut consecutive_hex_chars = Some(0); { let mut cursor = input; let mut ofs = 0; const SEPARATORS: &[u8] = b"~^:."; while let Some((pos, b)) = cursor.iter().enumerate().find(|(pos, b)| { if **b == b'@' { if cursor.len() == 1 { return true; } let next = cursor.get(pos + 1); let next_next = cursor.get(pos + 2); if *pos != 0 && (next, next_next) == (Some(&b'.'), Some(&b'.')) { return false; } next == Some(&b'{') || next.map_or(false, |b| SEPARATORS.contains(b)) } else if SEPARATORS.contains(b) { true } else { if let Some(num) = consecutive_hex_chars.as_mut() { if b.is_ascii_hexdigit() { *num += 1; } else { consecutive_hex_chars = None; } } false } }) { if *b != b'.' || cursor.get(pos + 1) == Some(&b'.') { sep_pos = Some(ofs + pos); break; } ofs += pos + 1; cursor = &cursor[pos + 1..]; } } let name = &input[..sep_pos.unwrap_or(input.len())].as_bstr(); let mut sep = sep_pos.map(|pos| input[pos]); let mut has_ref_or_implied_name = name.is_empty(); if name.is_empty() && sep == Some(b'@') && sep_pos.and_then(|pos| input.get(pos + 1)) != Some(&b'{') { delegate.find_ref("HEAD".into()).ok_or(Error::Delegate)?; sep_pos = sep_pos.map(|pos| pos + 1); sep = match sep_pos.and_then(|pos| input.get(pos).copied()) { None => return Ok("".into()), Some(pos) => Some(pos), }; } else { (consecutive_hex_chars.unwrap_or(0) >= gix_hash::Prefix::MIN_HEX_LEN) .then(|| try_set_prefix(delegate, name, None)) .flatten() .or_else(|| { let (prefix, hint) = long_describe_prefix(name) .map(|(c, h)| (c, Some(h))) .or_else(|| short_describe_prefix(name).map(|c| (c, None)))?; try_set_prefix(delegate, prefix, hint) }) .or_else(|| { name.is_empty().then_some(()).or_else(|| { #[allow(clippy::let_unit_value)] { let res = delegate.find_ref(name)?; has_ref_or_implied_name = true; res.into() } }) }) .ok_or(Error::Delegate)?; } input = { if let Some(b'@') = sep { let past_sep = input[sep_pos.map_or(input.len(), |pos| pos + 1)..].as_bstr(); let (nav, rest, _consumed) = parens(past_sep)?.ok_or_else(|| Error::AtNeedsCurlyBrackets { input: input[sep_pos.unwrap_or(input.len())..].into(), })?; let nav = nav.as_ref(); if let Some(n) = try_parse::(nav)? { if n < 0 { if name.is_empty() { delegate .nth_checked_out_branch(n.unsigned_abs()) .ok_or(Error::Delegate)?; } else { return Err(Error::RefnameNeedsPositiveReflogEntries { nav: nav.into() }); } } else if has_ref_or_implied_name { delegate .reflog(delegate::ReflogLookup::Entry( n.try_into().expect("non-negative isize fits usize"), )) .ok_or(Error::Delegate)?; } else { return Err(Error::ReflogLookupNeedsRefName { name: (*name).into() }); } } else if let Some(kind) = SiblingBranch::parse(nav) { if has_ref_or_implied_name { delegate.sibling_branch(kind).ok_or(Error::Delegate) } else { Err(Error::SiblingBranchNeedsBranchName { name: (*name).into() }) }? } else if has_ref_or_implied_name { let time = nav .to_str() .map_err(|_| Error::Time { input: nav.into(), source: None, }) .and_then(|date| { gix_date::parse(date, Some(SystemTime::now())).map_err(|err| Error::Time { input: nav.into(), source: err.into(), }) })?; delegate .reflog(delegate::ReflogLookup::Date(time)) .ok_or(Error::Delegate)?; } else { return Err(Error::ReflogLookupNeedsRefName { name: (*name).into() }); } rest } else { if sep_pos == Some(0) && sep == Some(b'~') { return Err(Error::MissingTildeAnchor); } input[sep_pos.unwrap_or(input.len())..].as_bstr() } }; navigate(input, delegate) } fn navigate<'a, T>(input: &'a BStr, delegate: &mut InterceptRev<'_, T>) -> Result<&'a BStr, Error> where T: Delegate, { use delegate::{Kind, Navigate, Revision}; let mut cursor = 0; while let Some(b) = input.get(cursor) { cursor += 1; match *b { b'~' => { let (number, consumed) = input .get(cursor..) .and_then(|past_sep| try_parse_usize(past_sep.as_bstr()).transpose()) .transpose()? .unwrap_or((1, 0)); if number != 0 { delegate .traverse(delegate::Traversal::NthAncestor(number)) .ok_or(Error::Delegate)?; } cursor += consumed; } b'^' => { let past_sep = input.get(cursor..); if let Some((number, negative, consumed)) = past_sep .and_then(|past_sep| try_parse_isize(past_sep.as_bstr()).transpose()) .transpose()? { if negative { delegate .traverse(delegate::Traversal::NthParent( number .checked_mul(-1) .ok_or_else(|| Error::InvalidNumber { input: past_sep.expect("present").into(), })? .try_into() .expect("non-negative"), )) .ok_or(Error::Delegate)?; delegate.kind(spec::Kind::RangeBetween).ok_or(Error::Delegate)?; if let Some((prefix, hint)) = delegate.last_prefix.take() { match hint { Some(hint) => delegate.disambiguate_prefix(prefix, hint.to_ref().into()), None => delegate.disambiguate_prefix(prefix, None), } .ok_or(Error::Delegate)?; } else if let Some(name) = delegate.last_ref.take() { delegate.find_ref(name.as_bstr()).ok_or(Error::Delegate)?; } else { return Err(Error::UnconsumedInput { input: input[cursor..].into(), }); } delegate.done(); cursor += consumed; return Ok(input[cursor..].as_bstr()); } else if number == 0 { delegate.peel_until(delegate::PeelTo::ObjectKind(gix_object::Kind::Commit)) } else { delegate.traverse(delegate::Traversal::NthParent( number.try_into().expect("positive number"), )) } .ok_or(Error::Delegate)?; cursor += consumed; } else if let Some((kind, _rest, consumed)) = past_sep.and_then(|past_sep| parens(past_sep).transpose()).transpose()? { cursor += consumed; let target = match kind.as_ref().as_bytes() { b"commit" => delegate::PeelTo::ObjectKind(gix_object::Kind::Commit), b"tag" => delegate::PeelTo::ObjectKind(gix_object::Kind::Tag), b"tree" => delegate::PeelTo::ObjectKind(gix_object::Kind::Tree), b"blob" => delegate::PeelTo::ObjectKind(gix_object::Kind::Blob), b"object" => delegate::PeelTo::ValidObject, b"" => delegate::PeelTo::RecursiveTagObject, regex if regex.starts_with(b"/") => { let (regex, negated) = parse_regex_prefix(regex[1..].as_bstr())?; if !regex.is_empty() { delegate.find(regex, negated).ok_or(Error::Delegate)?; } continue; } invalid => return Err(Error::InvalidObject { input: invalid.into() }), }; delegate.peel_until(target).ok_or(Error::Delegate)?; } else if past_sep.and_then(<[_]>::first) == Some(&b'!') { delegate .kind(spec::Kind::ExcludeReachableFromParents) .ok_or(Error::Delegate)?; delegate.done(); return Ok(input[cursor + 1..].as_bstr()); } else if past_sep.and_then(<[_]>::first) == Some(&b'@') { delegate .kind(spec::Kind::IncludeReachableFromParents) .ok_or(Error::Delegate)?; delegate.done(); return Ok(input[cursor + 1..].as_bstr()); } else { delegate .traverse(delegate::Traversal::NthParent(1)) .ok_or(Error::Delegate)?; } } b':' => { delegate .peel_until(delegate::PeelTo::Path(input[cursor..].as_bstr())) .ok_or(Error::Delegate)?; return Ok("".into()); } _ => return Ok(input[cursor - 1..].as_bstr()), } } Ok("".into()) } fn parse_regex_prefix(regex: &BStr) -> Result<(&BStr, bool), Error> { Ok(match regex.strip_prefix(b"!") { Some(regex) if regex.first() == Some(&b'!') => (regex.as_bstr(), false), Some(regex) if regex.first() == Some(&b'-') => (regex[1..].as_bstr(), true), Some(_regex) => return Err(Error::UnspecifiedRegexModifier { regex: regex.into() }), None => (regex, false), }) } fn try_parse_usize(input: &BStr) -> Result, Error> { let mut bytes = input.iter().peekable(); if bytes.peek().filter(|&&&b| b == b'-' || b == b'+').is_some() { return Err(Error::SignedNumber { input: input.into() }); } let num_digits = bytes.take_while(|b| b.is_ascii_digit()).count(); if num_digits == 0 { return Ok(None); } let input = &input[..num_digits]; let number = try_parse(input)?.ok_or_else(|| Error::InvalidNumber { input: input.into() })?; Ok(Some((number, num_digits))) } fn try_parse_isize(input: &BStr) -> Result, Error> { let mut bytes = input.iter().peekable(); if bytes.peek().filter(|&&&b| b == b'+').is_some() { return Err(Error::SignedNumber { input: input.into() }); } let negative = bytes.peek() == Some(&&b'-'); let num_digits = bytes.take_while(|b| b.is_ascii_digit() || *b == &b'-').count(); if num_digits == 0 { return Ok(None); } else if num_digits == 1 && negative { return Ok(Some((-1, negative, num_digits))); } let input = &input[..num_digits]; let number = try_parse(input)?.ok_or_else(|| Error::InvalidNumber { input: input.into() })?; Ok(Some((number, negative, num_digits))) } fn try_range(input: &BStr) -> Option<(&[u8], spec::Kind)> { input .strip_prefix(b"...") .map(|rest| (rest, spec::Kind::ReachableToMergeBase)) .or_else(|| input.strip_prefix(b"..").map(|rest| (rest, spec::Kind::RangeBetween))) } fn next(i: &BStr) -> (u8, &BStr) { let b = i[0]; (b, i[1..].as_bstr()) } gix-revision-0.27.0/src/spec/parse/mod.rs000064400000000000000000000057231046102023000163240ustar 00000000000000use bstr::BString; use crate::spec; /// The error returned by [`spec::parse()`][crate::spec::parse()]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("'~' needs to follow an anchor, like '@~'.")] MissingTildeAnchor, #[error("':' needs to be followed by either '/' and regex or the path to lookup in the HEAD tree.")] MissingColonSuffix, #[error("':/' must be followed by a regular expression.")] EmptyTopLevelRegex, #[error("Need one character after '/!', typically '-', but got {:?}", .regex)] UnspecifiedRegexModifier { regex: BString }, #[error("Cannot peel to {:?} - unknown target.", .input)] InvalidObject { input: BString }, #[error("Could not parse time {:?} for revlog lookup.", .input)] Time { input: BString, source: Option, }, #[error("Sibling branches like 'upstream' or 'push' require a branch name with remote configuration, got {:?}", .name)] SiblingBranchNeedsBranchName { name: BString }, #[error("Reflog entries require a ref name, got {:?}", .name)] ReflogLookupNeedsRefName { name: BString }, #[error("A reference name must be followed by positive numbers in '@{{n}}', got {:?}", .nav)] RefnameNeedsPositiveReflogEntries { nav: BString }, #[error("Negative or explicitly positive numbers are invalid here: {:?}", .input)] SignedNumber { input: BString }, #[error("Could not parse number from {input:?}")] InvalidNumber { input: BString }, #[error("Negative zeroes are invalid: {:?} - remove the '-'", .input)] NegativeZero { input: BString }, #[error("The opening brace in {:?} was not matched", .input)] UnclosedBracePair { input: BString }, #[error("Cannot set spec kind more than once. Previous value was {:?}, now it is {:?}", .prev_kind, .kind)] KindSetTwice { prev_kind: spec::Kind, kind: spec::Kind }, #[error("The @ character is either standing alone or followed by `{{}}`, got {:?}", .input)] AtNeedsCurlyBrackets { input: BString }, #[error("A portion of the input could not be parsed: {:?}", .input)] UnconsumedInput { input: BString }, #[error("The delegate didn't indicate success - check delegate for more information")] Delegate, } /// #[allow(clippy::empty_docs)] pub mod delegate; /// A delegate to be informed about parse events, with methods split into categories. /// /// - **Anchors** - which revision to use as starting point for… /// - **Navigation** - where to go once from the initial revision /// - **Range** - to learn if the specification is for a single or multiple references, and how to combine them. pub trait Delegate: delegate::Revision + delegate::Navigate + delegate::Kind { /// Called at the end of a successful parsing operation. /// It can be used as a marker to finalize internal data structures. /// /// Note that it will not be called if there is unconsumed input. fn done(&mut self); } pub(crate) mod function;