gix-discover-0.37.0/.cargo_vcs_info.json0000644000000001520000000000100135240ustar { "git": { "sha1": "4000197ecc8cf1a5d79361620e4c114f86476703" }, "path_in_vcs": "gix-discover" }gix-discover-0.37.0/Cargo.toml0000644000000073130000000000100115300ustar # 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-discover" version = "0.37.0" authors = ["Sebastian Thiel "] build = false include = [ "src/**/*", "LICENSE-*", ] autobins = false autoexamples = false autotests = false autobenches = false description = "Discover git repositories and check if a directory is a git repository" readme = false license = "MIT OR Apache-2.0" repository = "https://github.com/GitoxideLabs/gitoxide" [lib] name = "gix_discover" path = "src/lib.rs" doctest = false [dependencies.bstr] version = "1.3.0" features = [ "std", "unicode", ] default-features = false [dependencies.gix-fs] version = "^0.12.0" [dependencies.gix-hash] version = "^0.15.1" [dependencies.gix-path] version = "^0.10.13" [dependencies.gix-ref] version = "^0.49.0" [dependencies.gix-sec] version = "^0.10.10" [dependencies.thiserror] version = "2.0.0" [dev-dependencies.is_ci] version = "1.1.1" [dev-dependencies.serial_test] version = "3.1.0" default-features = false [target."cfg(any(unix, windows))".dev-dependencies.tempfile] version = "3.2.0" [target.'cfg(target_os = "macos")'.dev-dependencies.defer] version = "0.2.1" [target."cfg(windows)".dependencies.dunce] version = "1.0.3" [lints.clippy] bool_to_int_with_if = "allow" borrow_as_ptr = "allow" cast_lossless = "allow" cast_possible_truncation = "allow" cast_possible_wrap = "allow" cast_precision_loss = "allow" cast_sign_loss = "allow" checked_conversions = "allow" copy_iterator = "allow" default_trait_access = "allow" doc_markdown = "allow" empty_docs = "allow" enum_glob_use = "allow" explicit_deref_methods = "allow" explicit_into_iter_loop = "allow" explicit_iter_loop = "allow" filter_map_next = "allow" fn_params_excessive_bools = "allow" from_iter_instead_of_collect = "allow" if_not_else = "allow" ignored_unit_patterns = "allow" implicit_clone = "allow" inconsistent_struct_constructor = "allow" inefficient_to_string = "allow" inline_always = "allow" items_after_statements = "allow" iter_not_returning_iterator = "allow" iter_without_into_iter = "allow" manual_assert = "allow" manual_is_variant_and = "allow" manual_let_else = "allow" manual_string_new = "allow" many_single_char_names = "allow" match_bool = "allow" match_same_arms = "allow" match_wild_err_arm = "allow" match_wildcard_for_single_variants = "allow" missing_errors_doc = "allow" missing_panics_doc = "allow" module_name_repetitions = "allow" must_use_candidate = "allow" mut_mut = "allow" naive_bytecount = "allow" needless_for_each = "allow" needless_pass_by_value = "allow" needless_raw_string_hashes = "allow" no_effect_underscore_binding = "allow" option_option = "allow" range_plus_one = "allow" redundant_else = "allow" return_self_not_must_use = "allow" should_panic_without_expect = "allow" similar_names = "allow" single_match_else = "allow" stable_sort_primitive = "allow" struct_excessive_bools = "allow" struct_field_names = "allow" too_long_first_doc_paragraph = "allow" too_many_lines = "allow" transmute_ptr_to_ptr = "allow" trivially_copy_pass_by_ref = "allow" unnecessary_join = "allow" unnecessary_wraps = "allow" unreadable_literal = "allow" unused_self = "allow" used_underscore_binding = "allow" wildcard_imports = "allow" [lints.clippy.pedantic] level = "warn" priority = -1 [lints.rust] gix-discover-0.37.0/Cargo.toml.orig000064400000000000000000000021711046102023000152060ustar 00000000000000lints.workspace = true [package] name = "gix-discover" version = "0.37.0" repository = "https://github.com/GitoxideLabs/gitoxide" license = "MIT OR Apache-2.0" description = "Discover git repositories and check if a directory is a git repository" authors = ["Sebastian Thiel "] edition = "2021" include = ["src/**/*", "LICENSE-*"] rust-version = "1.65" [lib] doctest = false [dependencies] gix-sec = { version = "^0.10.10", path = "../gix-sec" } gix-path = { version = "^0.10.13", path = "../gix-path" } gix-ref = { version = "^0.49.0", path = "../gix-ref" } gix-hash = { version = "^0.15.1", path = "../gix-hash" } gix-fs = { version = "^0.12.0", path = "../gix-fs" } bstr = { version = "1.3.0", default-features = false, features = ["std", "unicode"] } thiserror = "2.0.0" [target.'cfg(windows)'.dependencies] dunce = "1.0.3" [dev-dependencies] gix-testtools = { path = "../tests/tools" } serial_test = { version = "3.1.0", default-features = false } is_ci = "1.1.1" [target.'cfg(target_os = "macos")'.dev-dependencies] defer = "0.2.1" [target.'cfg(any(unix, windows))'.dev-dependencies] tempfile = "3.2.0" gix-discover-0.37.0/LICENSE-APACHE000064400000000000000000000247461046102023000142570ustar 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-discover-0.37.0/LICENSE-MIT000064400000000000000000000017771046102023000137660ustar 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-discover-0.37.0/src/is.rs000064400000000000000000000160621046102023000140730ustar 00000000000000use std::{borrow::Cow, ffi::OsStr, path::Path}; use crate::{DOT_GIT_DIR, MODULES}; /// Returns true if the given `git_dir` seems to be a bare repository. /// /// Please note that repositories without an index generally _look_ bare, even though they might also be uninitialized. pub fn bare(git_dir_candidate: &Path) -> bool { !(git_dir_candidate.join("index").exists() || (git_dir_candidate.file_name() == Some(OsStr::new(DOT_GIT_DIR)))) } /// Returns true if `git_dir` is located within a `.git/modules` directory, indicating it's a submodule clone. pub fn submodule_git_dir(git_dir: &Path) -> bool { let mut last_comp = None; git_dir.file_name() != Some(OsStr::new(DOT_GIT_DIR)) && git_dir.components().rev().skip(1).any(|c| { if c.as_os_str() == OsStr::new(DOT_GIT_DIR) { true } else { last_comp = Some(c.as_os_str()); false } }) && last_comp == Some(OsStr::new(MODULES)) } /// What constitutes a valid git repository, returning the guessed repository kind /// purely based on the presence of files. Note that the git-config ultimately decides what's bare. /// /// Returns the `Kind` of git directory that was passed, possibly alongside the supporting private worktree git dir. /// /// Note that `.git` files are followed to a valid git directory, which then requires… /// /// * …a valid head /// * …an objects directory /// * …a refs directory /// pub fn git(git_dir: &Path) -> Result { let git_dir_metadata = git_dir.metadata().map_err(|err| crate::is_git::Error::Metadata { source: err, path: git_dir.into(), })?; // precompose-unicode can't be known here, so we just default it to false, hoping it won't matter. let cwd = gix_fs::current_dir(false)?; git_with_metadata(git_dir, git_dir_metadata, &cwd) } pub(crate) fn git_with_metadata( git_dir: &Path, git_dir_metadata: std::fs::Metadata, cwd: &Path, ) -> Result { #[derive(Eq, PartialEq)] enum Kind { MaybeRepo, Submodule, LinkedWorkTreeDir, WorkTreeGitDir { work_dir: std::path::PathBuf }, } let dot_git = if git_dir_metadata.is_file() { let private_git_dir = crate::path::from_gitdir_file(git_dir)?; Cow::Owned(private_git_dir) } else { Cow::Borrowed(git_dir) }; { // Fast-path: avoid doing the complete search if HEAD is already not there. // TODO(reftable): use a ref-store to lookup HEAD if ref-tables should be supported, or detect ref-tables beforehand. // Actually ref-tables still keep a specially marked `HEAD` around, so nothing might be needed here // Even though our head-check later would fail without supporting it. if !dot_git.join("HEAD").exists() { return Err(crate::is_git::Error::MissingHead); } // We expect to be able to parse any ref-hash, so we shouldn't have to know the repos hash here. // With ref-table, the has is probably stored as part of the ref-db itself, so we can handle it from there. // In other words, it's important not to fail on detached heads here because we guessed the hash kind wrongly. let refs = gix_ref::file::Store::at(dot_git.as_ref().into(), Default::default()); let head = refs.find_loose("HEAD")?; if head.name.as_bstr() != "HEAD" { return Err(crate::is_git::Error::MisplacedHead { name: head.name.into_inner(), }); } } let (common_dir, kind) = if git_dir_metadata.is_file() { let common_dir = dot_git.join("commondir"); match crate::path::from_plain_file(&common_dir) { Some(Err(err)) => { return Err(crate::is_git::Error::MissingCommonDir { missing: common_dir, source: err, }) } Some(Ok(common_dir)) => { let common_dir = dot_git.join(common_dir); (Cow::Owned(common_dir), Kind::LinkedWorkTreeDir) } None => (dot_git.clone(), Kind::Submodule), } } else { let common_dir = dot_git.join("commondir"); let worktree_and_common_dir = crate::path::from_plain_file(&common_dir) .and_then(Result::ok) .and_then(|cd| { crate::path::from_plain_file(&dot_git.join("gitdir")) .and_then(Result::ok) .map(|worktree_gitfile| (crate::path::without_dot_git_dir(worktree_gitfile), cd)) }); match worktree_and_common_dir { Some((work_dir, common_dir)) => { let common_dir = dot_git.join(common_dir); (Cow::Owned(common_dir), Kind::WorkTreeGitDir { work_dir }) } None => (dot_git.clone(), Kind::MaybeRepo), } }; { let objects_path = common_dir.join("objects"); if !objects_path.is_dir() { return Err(crate::is_git::Error::MissingObjectsDirectory { missing: objects_path }); } } { let refs_path = common_dir.join("refs"); if !refs_path.is_dir() { return Err(crate::is_git::Error::MissingRefsDirectory { missing: refs_path }); } } Ok(match kind { Kind::LinkedWorkTreeDir => crate::repository::Kind::WorkTree { linked_git_dir: Some(dot_git.into_owned()), }, Kind::WorkTreeGitDir { work_dir } => crate::repository::Kind::WorkTreeGitDir { work_dir }, Kind::Submodule => crate::repository::Kind::Submodule { git_dir: dot_git.into_owned(), }, Kind::MaybeRepo => { let conformed_git_dir = if git_dir == Path::new(".") { gix_path::realpath_opts(git_dir, cwd, gix_path::realpath::MAX_SYMLINKS) .map(Cow::Owned) .unwrap_or(Cow::Borrowed(git_dir)) } else { gix_path::normalize(git_dir.into(), cwd).unwrap_or(Cow::Borrowed(git_dir)) }; if bare(conformed_git_dir.as_ref()) || conformed_git_dir.extension() == Some(OsStr::new("git")) { crate::repository::Kind::PossiblyBare } else if submodule_git_dir(conformed_git_dir.as_ref()) { crate::repository::Kind::SubmoduleGitDir } else if conformed_git_dir.file_name() == Some(OsStr::new(DOT_GIT_DIR)) { crate::repository::Kind::WorkTree { linked_git_dir: None } // } else if !bare_by_config(conformed_git_dir.as_ref()) // .map_err(|err| crate::is_git::Error::Metadata { // source: err, // path: conformed_git_dir.join("config"), // })? // .ok_or(crate::is_git::Error::Inconclusive)? // { // crate::repository::Kind::WorktreePossiblyInConfiguration } else { crate::repository::Kind::PossiblyBare } } }) } gix-discover-0.37.0/src/lib.rs000064400000000000000000000043441046102023000142260ustar 00000000000000//! Find git repositories or search them upwards from a starting point, or determine if a directory looks like a git repository. //! //! Note that detection methods are educated guesses using the presence of files, without looking too much into the details. #![deny(missing_docs, rust_2018_idioms)] #![forbid(unsafe_code)] /// The name of the `.git` directory. pub const DOT_GIT_DIR: &str = ".git"; /// The name of the `modules` sub-directory within a `.git` directory for keeping submodule checkouts. pub const MODULES: &str = "modules"; /// pub mod repository; /// pub mod is_git { use std::path::PathBuf; /// The error returned by [`crate::is_git()`]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("Could not find a valid HEAD reference")] FindHeadRef(#[from] gix_ref::file::find::existing::Error), #[error("Missing HEAD at '.git/HEAD'")] MissingHead, #[error("Expected HEAD at '.git/HEAD', got '.git/{}'", .name)] MisplacedHead { name: bstr::BString }, #[error("Expected an objects directory at '{}'", .missing.display())] MissingObjectsDirectory { missing: PathBuf }, #[error("The worktree's private repo's commondir file at '{}' or it could not be read", .missing.display())] MissingCommonDir { missing: PathBuf, source: std::io::Error }, #[error("Expected a refs directory at '{}'", .missing.display())] MissingRefsDirectory { missing: PathBuf }, #[error(transparent)] GitFile(#[from] crate::path::from_gitdir_file::Error), #[error("Could not retrieve metadata of \"{path}\"")] Metadata { source: std::io::Error, path: PathBuf }, #[error("The repository's config file doesn't exist or didn't have a 'bare' configuration or contained core.worktree without value")] Inconclusive, #[error("Could not obtain current directory for resolving the '.' repository path")] CurrentDir(#[from] std::io::Error), } } mod is; pub use is::{bare as is_bare, git as is_git, submodule_git_dir as is_submodule_git_dir}; /// pub mod upwards; pub use upwards::function::{discover as upwards, discover_opts as upwards_opts}; /// pub mod path; /// pub mod parse; gix-discover-0.37.0/src/parse.rs000064400000000000000000000020271046102023000145660ustar 00000000000000use std::path::PathBuf; use bstr::ByteSlice; /// pub mod gitdir { use bstr::BString; /// The error returned by [`parse::gitdir()`][super::gitdir()]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("Format should be 'gitdir: ', but got: {:?}", .input)] InvalidFormat { input: BString }, #[error("Couldn't decode {:?} as UTF8", .input)] IllformedUtf8 { input: BString }, } } /// Parse typical `gitdir` files as seen in worktrees and submodules. pub fn gitdir(input: &[u8]) -> Result { let path = input .strip_prefix(b"gitdir: ") .ok_or_else(|| gitdir::Error::InvalidFormat { input: input.into() })? .as_bstr(); let path = path.trim_end().as_bstr(); if path.is_empty() { return Err(gitdir::Error::InvalidFormat { input: input.into() }); } Ok(gix_path::try_from_bstr(path) .map_err(|_| gitdir::Error::IllformedUtf8 { input: input.into() })? .into_owned()) } gix-discover-0.37.0/src/path.rs000064400000000000000000000043771046102023000144220ustar 00000000000000use std::{io::Read, path::PathBuf}; use crate::DOT_GIT_DIR; /// pub mod from_gitdir_file { /// The error returned by [`from_gitdir_file()`][crate::path::from_gitdir_file()]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] Parse(#[from] crate::parse::gitdir::Error), } } fn read_regular_file_content_with_size_limit(path: &std::path::Path) -> std::io::Result> { let mut file = std::fs::File::open(path)?; let max_file_size = 1024 * 64; // NOTE: git allows 1MB here let file_size = file.metadata()?.len(); if file_size > max_file_size { return Err(std::io::Error::new( std::io::ErrorKind::Other, format!( "Refusing to open files larger than {} bytes, '{}' was {} bytes large", max_file_size, path.display(), file_size ), )); } let mut buf = Vec::with_capacity(512); file.read_to_end(&mut buf)?; Ok(buf) } /// Reads a plain path from a file that contains it as its only content, with trailing newlines trimmed. pub fn from_plain_file(path: &std::path::Path) -> Option> { use bstr::ByteSlice; let mut buf = match read_regular_file_content_with_size_limit(path) { Ok(buf) => buf, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return None, Err(err) => return Some(Err(err)), }; let trimmed_len = buf.trim_end().len(); buf.truncate(trimmed_len); Some(Ok(gix_path::from_bstring(buf))) } /// Reads typical `gitdir: ` files from disk as used by worktrees and submodules. pub fn from_gitdir_file(path: &std::path::Path) -> Result { let buf = read_regular_file_content_with_size_limit(path)?; let mut gitdir = crate::parse::gitdir(&buf)?; if let Some(parent) = path.parent() { gitdir = parent.join(gitdir); } Ok(gitdir) } /// Conditionally pop a trailing `.git` dir if present. pub fn without_dot_git_dir(mut path: PathBuf) -> PathBuf { if path.file_name().and_then(std::ffi::OsStr::to_str) == Some(DOT_GIT_DIR) { path.pop(); } path } gix-discover-0.37.0/src/repository.rs000064400000000000000000000151331046102023000156750ustar 00000000000000use std::path::PathBuf; /// A repository path which either points to a work tree or the `.git` repository itself. #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Path { /// The currently checked out linked worktree along with its connected and existing git directory, or the worktree checkout of a /// submodule. LinkedWorkTree { /// The base of the work tree. work_dir: PathBuf, /// The worktree-private git dir, located within the main git directory which holds most of the information. git_dir: PathBuf, }, /// The currently checked out or nascent work tree of a git repository WorkTree(PathBuf), /// The git repository itself, typically bare and without known worktree. /// It could also be non-bare with a worktree configured using git configuration, or no worktree at all despite /// not being bare (due to mis-configuration for example). /// /// Note that it might still have linked work-trees which can be accessed later, bare or not, or it might be a /// submodule git directory in the `.git/modules/**/` directory of the parent repository. Repository(PathBuf), } mod path { use std::path::PathBuf; use crate::{ path::without_dot_git_dir, repository::{Kind, Path}, DOT_GIT_DIR, }; impl AsRef for Path { fn as_ref(&self) -> &std::path::Path { match self { Path::WorkTree(path) | Path::Repository(path) | Path::LinkedWorkTree { work_dir: _, git_dir: path, } => path, } } } impl Path { /// Instantiate a new path from `dir` which is expected to be the `.git` directory, with `kind` indicating /// whether it's a bare repository or not, with `current_dir` being used to normalize relative paths /// as needed. /// /// `None` is returned if `dir` could not be resolved due to being relative and trying to reach outside of the filesystem root. pub fn from_dot_git_dir(dir: PathBuf, kind: Kind, current_dir: &std::path::Path) -> Option { let cwd = current_dir; let normalize_on_trailing_dot_dot = |dir: PathBuf| -> Option { if !matches!(dir.components().next_back(), Some(std::path::Component::ParentDir)) { dir } else { gix_path::normalize(dir.into(), cwd)?.into_owned() } .into() }; match kind { Kind::Submodule { git_dir } => Path::LinkedWorkTree { git_dir: gix_path::normalize(git_dir.into(), cwd)?.into_owned(), work_dir: without_dot_git_dir(normalize_on_trailing_dot_dot(dir)?), }, Kind::SubmoduleGitDir => Path::Repository(dir), Kind::WorkTreeGitDir { work_dir } => Path::LinkedWorkTree { git_dir: dir, work_dir }, Kind::WorkTree { linked_git_dir } => match linked_git_dir { Some(git_dir) => Path::LinkedWorkTree { git_dir, work_dir: without_dot_git_dir(normalize_on_trailing_dot_dot(dir)?), }, None => { let mut dir = normalize_on_trailing_dot_dot(dir)?; dir.pop(); // ".git" suffix let work_dir = dir.as_os_str().is_empty().then(|| PathBuf::from(".")).unwrap_or(dir); Path::WorkTree(work_dir) } }, Kind::PossiblyBare => Path::Repository(dir), } .into() } /// Returns the [kind][Kind] of this repository path. pub fn kind(&self) -> Kind { match self { Path::LinkedWorkTree { work_dir: _, git_dir } => Kind::WorkTree { linked_git_dir: Some(git_dir.to_owned()), }, Path::WorkTree(_) => Kind::WorkTree { linked_git_dir: None }, Path::Repository(_) => Kind::PossiblyBare, } } /// Consume and split this path into the location of the `.git` directory as well as an optional path to the work tree. pub fn into_repository_and_work_tree_directories(self) -> (PathBuf, Option) { match self { Path::LinkedWorkTree { work_dir, git_dir } => (git_dir, Some(work_dir)), Path::WorkTree(working_tree) => (working_tree.join(DOT_GIT_DIR), Some(working_tree)), Path::Repository(repository) => (repository, None), } } } } /// The kind of repository path. #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Kind { /// A bare repository does not have a work tree, that is files on disk beyond the `git` repository itself. /// /// Note that this is merely a guess at this point as we didn't read the configuration yet. /// /// Also note that due to optimizing for performance and *just* making an educated *guess in some situations*, /// we may consider a non-bare repository bare if it doesn't have an index yet due to be freshly initialized. /// The caller has to handle this, typically by reading the configuration. /// /// It could also be a directory which is non-bare by configuration, but is *not* named `.git`. /// Unusual, but it's possible that a worktree is configured via `core.worktree`. PossiblyBare, /// A `git` repository along with checked out files in a work tree. WorkTree { /// If set, this is the git dir associated with this _linked_ worktree. /// If `None`, the git_dir is the `.git` directory inside the _main_ worktree we represent. linked_git_dir: Option, }, /// A worktree's git directory in the common`.git` directory in `worktrees/`. WorkTreeGitDir { /// Path to the worktree directory. work_dir: PathBuf, }, /// The directory is a `.git` dir file of a submodule worktree. Submodule { /// The git repository itself that is referenced by the `.git` dir file, typically in the `.git/modules/**/` directory of the parent /// repository. git_dir: PathBuf, }, /// The git directory in the `.git/modules/**/` directory tree of the parent repository SubmoduleGitDir, } impl Kind { /// Returns true if this is a bare repository, one without a work tree. pub fn is_bare(&self) -> bool { matches!(self, Kind::PossiblyBare) } } gix-discover-0.37.0/src/upwards/mod.rs000064400000000000000000000206551046102023000157270ustar 00000000000000mod types; pub use types::{Error, Options}; mod util; pub(crate) mod function { use std::{borrow::Cow, ffi::OsStr, path::Path}; use gix_sec::Trust; use super::{Error, Options}; #[cfg(unix)] use crate::upwards::util::device_id; use crate::{ is::git_with_metadata as is_git_with_metadata, is_git, upwards::util::{find_ceiling_height, shorten_path_with_cwd}, DOT_GIT_DIR, }; /// Find the location of the git repository directly in `directory` or in any of its parent directories and provide /// an associated Trust level by looking at the git directory's ownership, and control discovery using `options`. /// /// Fail if no valid-looking git repository could be found. // TODO: tests for trust-based discovery #[cfg_attr(not(unix), allow(unused_variables))] pub fn discover_opts( directory: &Path, Options { required_trust, ceiling_dirs, match_ceiling_dir_or_error, cross_fs, current_dir, dot_git_only, }: Options<'_>, ) -> Result<(crate::repository::Path, Trust), Error> { // Normalize the path so that `Path::parent()` _actually_ gives // us the parent directory. (`Path::parent` just strips off the last // path component, which means it will not do what you expect when // working with paths that contain '..'.) let cwd = current_dir.map_or_else( || { // The paths we return are relevant to the repository, but at this time it's impossible to know // what `core.precomposeUnicode` is going to be. Hence, the one using these paths will have to // transform the paths as needed, because we can't. `false` means to leave the obtained path as is. gix_fs::current_dir(false).map(Cow::Owned) }, |cwd| Ok(Cow::Borrowed(cwd)), )?; #[cfg(windows)] let directory = dunce::simplified(directory); let dir = gix_path::normalize(directory.into(), cwd.as_ref()).ok_or_else(|| Error::InvalidInput { directory: directory.into(), })?; let dir_metadata = dir.metadata().map_err(|_| Error::InaccessibleDirectory { path: dir.to_path_buf(), })?; if !dir_metadata.is_dir() { return Err(Error::InaccessibleDirectory { path: dir.into_owned() }); } let mut dir_made_absolute = !directory.is_absolute() && cwd .as_ref() .strip_prefix(dir.as_ref()) .or_else(|_| dir.as_ref().strip_prefix(cwd.as_ref())) .is_ok(); let filter_by_trust = |x: &Path| -> Result, Error> { let trust = Trust::from_path_ownership(x).map_err(|err| Error::CheckTrust { path: x.into(), err })?; Ok((trust >= required_trust).then_some(trust)) }; let max_height = if !ceiling_dirs.is_empty() { let max_height = find_ceiling_height(&dir, &ceiling_dirs, cwd.as_ref()); if max_height.is_none() && match_ceiling_dir_or_error { return Err(Error::NoMatchingCeilingDir); } max_height } else { None }; #[cfg(unix)] let initial_device = device_id(&dir_metadata); let mut cursor = dir.clone().into_owned(); let mut current_height = 0; let mut cursor_metadata = Some(dir_metadata); 'outer: loop { if max_height.map_or(false, |x| current_height > x) { return Err(Error::NoGitRepositoryWithinCeiling { path: dir.into_owned(), ceiling_height: current_height, }); } current_height += 1; #[cfg(unix)] if current_height != 0 && !cross_fs { let metadata = cursor_metadata.take().map_or_else( || { if cursor.as_os_str().is_empty() { Path::new(".") } else { cursor.as_ref() } .metadata() .map_err(|_| Error::InaccessibleDirectory { path: cursor.clone() }) }, Ok, )?; if device_id(&metadata) != initial_device { return Err(Error::NoGitRepositoryWithinFs { path: dir.into_owned(), limit: cursor.clone(), }); } cursor_metadata = Some(metadata); } let mut cursor_metadata_backup = None; let started_as_dot_git = cursor.file_name() == Some(OsStr::new(DOT_GIT_DIR)); let dir_manipulation = if dot_git_only { &[true] as &[_] } else { &[true, false] }; for append_dot_git in dir_manipulation { if *append_dot_git && !started_as_dot_git { cursor.push(DOT_GIT_DIR); cursor_metadata_backup = cursor_metadata.take(); } if let Ok(kind) = match cursor_metadata.take() { Some(metadata) => is_git_with_metadata(&cursor, metadata, &cwd), None => is_git(&cursor), } { match filter_by_trust(&cursor)? { Some(trust) => { // TODO: test this more, it definitely doesn't always find the shortest path to a directory let path = if dir_made_absolute { shorten_path_with_cwd(cursor, cwd.as_ref()) } else { cursor }; break 'outer Ok(( crate::repository::Path::from_dot_git_dir(path, kind, cwd.as_ref()).ok_or_else( || Error::InvalidInput { directory: directory.into(), }, )?, trust, )); } None => { break 'outer Err(Error::NoTrustedGitRepository { path: dir.into_owned(), candidate: cursor, required: required_trust, }) } } } // Usually `.git` (started_as_dot_git == true) will be a git dir, but if not we can quickly skip over it. if *append_dot_git || started_as_dot_git { cursor.pop(); if let Some(metadata) = cursor_metadata_backup.take() { cursor_metadata = Some(metadata); } } } if cursor.parent().map_or(false, |p| p.as_os_str().is_empty()) { cursor = cwd.to_path_buf(); dir_made_absolute = true; } if !cursor.pop() { if dir_made_absolute || matches!( cursor.components().next(), Some(std::path::Component::RootDir | std::path::Component::Prefix(_)) ) { break Err(Error::NoGitRepository { path: dir.into_owned() }); } else { dir_made_absolute = true; debug_assert!(!cursor.as_os_str().is_empty()); // TODO: realpath or normalize? No test runs into this. cursor = gix_path::normalize(cursor.clone().into(), cwd.as_ref()) .ok_or_else(|| Error::InvalidInput { directory: cursor.clone(), })? .into_owned(); } } } } /// Find the location of the git repository directly in `directory` or in any of its parent directories, and provide /// the trust level derived from Path ownership. /// /// Fail if no valid-looking git repository could be found. pub fn discover(directory: &Path) -> Result<(crate::repository::Path, Trust), Error> { discover_opts(directory, Default::default()) } } gix-discover-0.37.0/src/upwards/types.rs000064400000000000000000000202021046102023000163000ustar 00000000000000use std::{env, ffi::OsStr, path::PathBuf}; /// The error returned by [`gix_discover::upwards()`][crate::upwards()]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { #[error("Could not obtain the current working directory")] CurrentDir(#[from] std::io::Error), #[error("Relative path \"{}\"tries to reach beyond root filesystem", directory.display())] InvalidInput { directory: PathBuf }, #[error("Failed to access a directory, or path is not a directory: '{}'", .path.display())] InaccessibleDirectory { path: PathBuf }, #[error("Could not find a git repository in '{}' or in any of its parents", .path.display())] NoGitRepository { path: PathBuf }, #[error("Could not find a git repository in '{}' or in any of its parents within ceiling height of {}", .path.display(), .ceiling_height)] NoGitRepositoryWithinCeiling { path: PathBuf, ceiling_height: usize }, #[error("Could not find a git repository in '{}' or in any of its parents within device limits below '{}'", .path.display(), .limit.display())] NoGitRepositoryWithinFs { path: PathBuf, limit: PathBuf }, #[error("None of the passed ceiling directories prefixed the git-dir candidate, making them ineffective.")] NoMatchingCeilingDir, #[error("Could not find a trusted git repository in '{}' or in any of its parents, candidate at '{}' discarded", .path.display(), .candidate.display())] NoTrustedGitRepository { path: PathBuf, candidate: PathBuf, required: gix_sec::Trust, }, #[error("Could not determine trust level for path '{}'.", .path.display())] CheckTrust { path: PathBuf, #[source] err: std::io::Error, }, } /// Options to help guide the [discovery][crate::upwards()] of repositories, along with their options /// when instantiated. pub struct Options<'a> { /// When discovering a repository, assure it has at least this trust level or ignore it otherwise. /// /// This defaults to [`Reduced`][gix_sec::Trust::Reduced] as our default settings are geared towards avoiding abuse. /// Set it to `Full` to only see repositories that [are owned by the current user][gix_sec::Trust::from_path_ownership()]. pub required_trust: gix_sec::Trust, /// When discovering a repository, ignore any repositories that are located in these directories or any of their parents. /// /// Note that we ignore ceiling directories if the search directory is directly on top of one, which by default is an error /// if `match_ceiling_dir_or_error` is true, the default. pub ceiling_dirs: Vec, /// If true, default true, and `ceiling_dirs` is not empty, we expect at least one ceiling directory to /// contain our search dir or else there will be an error. pub match_ceiling_dir_or_error: bool, /// if `true` avoid crossing filesystem boundaries. /// Only supported on Unix-like systems. // TODO: test on Linux // TODO: Handle WASI once https://github.com/rust-lang/rust/issues/71213 is resolved pub cross_fs: bool, /// If true, limit discovery to `.git` directories. /// /// This will fail to find typical bare repositories, but would find them if they happen to be named `.git`. /// Use this option if repos with worktrees are the only kind of repositories you are interested in for /// optimal discovery performance. pub dot_git_only: bool, /// If set, the _current working directory_ (absolute path) to use when resolving relative paths. Note that /// that this is merely an optimization for those who discover a lot of repositories in the same process. /// /// If unset, the current working directory will be obtained automatically. /// Note that the path here might or might not contained decomposed unicode, which may end up in a path /// relevant us, like the git-dir or the worktree-dir. However, when opening the repository, it will /// change decomposed unicode to precomposed unicode based on the value of `core.precomposeUnicode`, and we /// don't have to deal with that value here just yet. pub current_dir: Option<&'a std::path::Path>, } impl Default for Options<'_> { fn default() -> Self { Options { required_trust: gix_sec::Trust::Reduced, ceiling_dirs: vec![], match_ceiling_dir_or_error: true, cross_fs: false, dot_git_only: false, current_dir: None, } } } impl Options<'_> { /// Loads discovery options overrides from the environment. /// /// The environment variables are: /// - `GIT_CEILING_DIRECTORIES` for `ceiling_dirs` /// /// Note that `GIT_DISCOVERY_ACROSS_FILESYSTEM` for `cross_fs` is **not** read, /// as it requires parsing of `git-config` style boolean values. // TODO: test pub fn apply_environment(mut self) -> Self { let name = "GIT_CEILING_DIRECTORIES"; if let Some(ceiling_dirs) = env::var_os(name) { self.ceiling_dirs = parse_ceiling_dirs(&ceiling_dirs); } self } } /// Parse a byte-string of `:`-separated paths into `Vec`. /// On Windows, paths are separated by `;`. /// Non-absolute paths are discarded. /// To match git, all paths are normalized, until an empty path is encountered. pub(crate) fn parse_ceiling_dirs(ceiling_dirs: &OsStr) -> Vec { let mut should_normalize = true; let mut out = Vec::new(); for ceiling_dir in std::env::split_paths(ceiling_dirs) { if ceiling_dir.as_os_str().is_empty() { should_normalize = false; continue; } // Only absolute paths are allowed if ceiling_dir.is_relative() { continue; } let mut dir = ceiling_dir; if should_normalize { if let Ok(normalized) = gix_path::realpath(&dir) { dir = normalized; } } out.push(dir); } out } #[cfg(test)] mod tests { #[test] #[cfg(unix)] fn parse_ceiling_dirs_from_environment_format() -> std::io::Result<()> { use std::{fs, os::unix::fs::symlink}; use super::*; // Setup filesystem let dir = tempfile::tempdir().expect("success creating temp dir"); let direct_path = dir.path().join("direct"); let symlink_path = dir.path().join("symlink"); fs::create_dir(&direct_path)?; symlink(&direct_path, &symlink_path)?; // Parse & build ceiling dirs string let symlink_str = symlink_path.to_str().expect("symlink path is valid utf8"); let ceiling_dir_string = format!("{symlink_str}:relative::{symlink_str}"); let ceiling_dirs = parse_ceiling_dirs(OsStr::new(ceiling_dir_string.as_str())); assert_eq!(ceiling_dirs.len(), 2, "Relative path is discarded"); assert_eq!( ceiling_dirs[0], symlink_path.canonicalize().expect("symlink path exists"), "Symlinks are resolved" ); assert_eq!( ceiling_dirs[1], symlink_path, "Symlink are not resolved after empty item" ); dir.close() } #[test] #[cfg(windows)] fn parse_ceiling_dirs_from_environment_format() -> std::io::Result<()> { use std::{fs, os::windows::fs::symlink_dir}; use super::*; // Setup filesystem let dir = tempfile::tempdir().expect("success creating temp dir"); let direct_path = dir.path().join("direct"); let symlink_path = dir.path().join("symlink"); fs::create_dir(&direct_path)?; symlink_dir(&direct_path, &symlink_path)?; // Parse & build ceiling dirs string let symlink_str = symlink_path.to_str().expect("symlink path is valid utf8"); let ceiling_dir_string = format!("{};relative;;{}", symlink_str, symlink_str); let ceiling_dirs = parse_ceiling_dirs(OsStr::new(ceiling_dir_string.as_str())); assert_eq!(ceiling_dirs.len(), 2, "Relative path is discarded"); assert_eq!(ceiling_dirs[0], direct_path, "Symlinks are resolved"); assert_eq!( ceiling_dirs[1], symlink_path, "Symlink are not resolved after empty item" ); dir.close() } } gix-discover-0.37.0/src/upwards/util.rs000064400000000000000000000053731046102023000161250ustar 00000000000000use std::path::{Path, PathBuf}; use crate::DOT_GIT_DIR; pub(crate) fn shorten_path_with_cwd(cursor: PathBuf, cwd: &Path) -> PathBuf { fn comp_len(c: std::path::Component<'_>) -> usize { use std::path::Component::*; match c { Prefix(p) => p.as_os_str().len(), CurDir => 1, ParentDir => 2, Normal(p) => p.len(), RootDir => 1, } } debug_assert_eq!(cursor.file_name().and_then(std::ffi::OsStr::to_str), Some(DOT_GIT_DIR)); let parent = cursor.parent().expect(".git appended"); cwd.strip_prefix(parent) .ok() .and_then(|path_relative_to_cwd| { let relative_path_components = path_relative_to_cwd.components().count(); let current_component_len = cursor.components().map(comp_len).sum::(); (relative_path_components * "..".len() < current_component_len).then(|| { std::iter::repeat("..") .take(relative_path_components) .chain(Some(DOT_GIT_DIR)) .collect() }) }) .unwrap_or(cursor) } /// Find the number of components parenting the `search_dir` before the first directory in `ceiling_dirs`. /// `search_dir` needs to be normalized, and we normalize every ceiling as well. pub(crate) fn find_ceiling_height(search_dir: &Path, ceiling_dirs: &[PathBuf], cwd: &Path) -> Option { if ceiling_dirs.is_empty() { return None; } let search_realpath; let search_dir = if search_dir.is_absolute() { search_dir } else { search_realpath = gix_path::realpath_opts(search_dir, cwd, gix_path::realpath::MAX_SYMLINKS).ok()?; search_realpath.as_path() }; ceiling_dirs .iter() .filter_map(|ceiling_dir| { #[cfg(windows)] let ceiling_dir = dunce::simplified(ceiling_dir); let mut ceiling_dir = gix_path::normalize(ceiling_dir.into(), cwd)?; if !ceiling_dir.is_absolute() { ceiling_dir = gix_path::normalize(cwd.join(ceiling_dir.as_ref()).into(), cwd)?; } search_dir .strip_prefix(ceiling_dir.as_ref()) .ok() .map(|path_relative_to_ceiling| path_relative_to_ceiling.components().count()) .filter(|height| *height > 0) }) .min() } /// Returns the device ID of the directory. #[cfg(target_os = "linux")] pub(crate) fn device_id(m: &std::fs::Metadata) -> u64 { use std::os::linux::fs::MetadataExt; m.st_dev() } /// Returns the device ID of the directory. #[cfg(all(unix, not(target_os = "linux")))] pub(crate) fn device_id(m: &std::fs::Metadata) -> u64 { use std::os::unix::fs::MetadataExt; m.dev() }