mirror of
https://github.com/facebook/sapling.git
synced 2024-10-10 16:57:49 +03:00
case-conflict checking functions
Summary: Adds case conflict checking functions - `manifest + path` case - `[path]` case Reviewed By: StanislavGlebik Differential Revision: D9700760 fbshipit-source-id: 582430f61bed1ae279dafbe7804a562d5b2ddf59
This commit is contained in:
parent
c4ece89763
commit
af69be4b3b
@ -15,7 +15,7 @@ use bytes::Bytes;
|
||||
use db::{get_connection_params, InstanceRequirement, ProxyRequirement};
|
||||
use failure::{Error, FutureFailureErrorExt, FutureFailureExt, Result, prelude::*};
|
||||
use futures::{Async, IntoFuture, Poll};
|
||||
use futures::future::{self, Either, Future};
|
||||
use futures::future::{self, loop_fn, Either, Future, Loop};
|
||||
use futures::stream::{self, Stream};
|
||||
use futures::sync::oneshot;
|
||||
use futures_ext::{BoxFuture, BoxStream, FutureExt, StreamExt};
|
||||
@ -754,6 +754,60 @@ impl BlobRepo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if adding path to manifest would cause case-conflict
|
||||
///
|
||||
/// Implementation traverses manifest and checks if correspoinding path element is present,
|
||||
/// if path element is not present, it lowercases current path element and checks if it
|
||||
/// collides with any existing elements inside manifest, if so it is a case-conflict.
|
||||
pub fn check_case_conflict_in_manifest(
|
||||
&self,
|
||||
mf_id: &HgManifestId,
|
||||
path: MPath,
|
||||
) -> impl Future<Item = bool, Error = Error> {
|
||||
self.get_manifest_by_nodeid(&mf_id.into_nodehash())
|
||||
.and_then(|mf| {
|
||||
loop_fn((mf, path.into_iter()), move |(mf, mut elements)| {
|
||||
match elements.next() {
|
||||
None => future::ok(Loop::Break(false)).left_future(),
|
||||
Some(element) => {
|
||||
match mf.lookup(&element) {
|
||||
Some(entry) => {
|
||||
// avoid fetching file content
|
||||
match entry.get_type() {
|
||||
Type::File(_) => {
|
||||
future::ok(Loop::Break(false)).left_future()
|
||||
}
|
||||
Type::Tree => entry
|
||||
.get_content()
|
||||
.map(move |content| match content {
|
||||
Content::Tree(mf) => Loop::Continue((mf, elements)),
|
||||
_ => Loop::Break(false),
|
||||
})
|
||||
.right_future(),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let element_utf8 = String::from_utf8(element.to_bytes());
|
||||
let has_case_conflict = mf.list().any(|entry| {
|
||||
let name = entry
|
||||
.get_name()
|
||||
.expect("Non-root entry has empty name");
|
||||
match (&element_utf8, String::from_utf8(name.to_bytes())) {
|
||||
(Ok(ref element), Ok(ref name)) => {
|
||||
name.to_lowercase() == element.to_lowercase()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
future::ok(Loop::Break(has_case_conflict)).left_future()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn find_path_in_manifest(
|
||||
&self,
|
||||
path: Option<MPath>,
|
||||
|
@ -752,3 +752,34 @@ fn test_get_manifest_from_bonsai() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_case_conflict_in_manifest() {
|
||||
async_unit::tokio_unit_test(|| {
|
||||
let repo = many_files_dirs::getrepo(None);
|
||||
let get_manifest_for_changeset = |cs_nodehash: &str| -> HgManifestId {
|
||||
*run_future(repo.get_changeset_by_changesetid(&HgChangesetId::new(
|
||||
string_to_nodehash(cs_nodehash),
|
||||
))).unwrap()
|
||||
.manifestid()
|
||||
};
|
||||
|
||||
let mf = get_manifest_for_changeset("2f866e7e549760934e31bf0420a873f65100ad63");
|
||||
|
||||
for (path, result) in &[
|
||||
("dir1/file_1_in_dir1", false),
|
||||
("dir1/file_1_IN_dir1", true),
|
||||
("DiR1/file_1_in_dir1", true),
|
||||
("dir1/other_dir/file", false),
|
||||
] {
|
||||
assert_eq!(
|
||||
run_future(repo.check_case_conflict_in_manifest(&mf, MPath::new(path).unwrap()))
|
||||
.unwrap(),
|
||||
*result,
|
||||
"{} expected to {} cause conflict",
|
||||
path,
|
||||
if *result { "" } else { "not" },
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -5,10 +5,11 @@
|
||||
// GNU General Public License version 2 or any later version.
|
||||
|
||||
use std::cmp;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::convert::{From, TryFrom, TryInto};
|
||||
use std::fmt::{self, Display};
|
||||
use std::io::{self, Write};
|
||||
use std::iter::{once, Once};
|
||||
use std::iter::{once, FromIterator, Once};
|
||||
use std::mem;
|
||||
use std::slice::Iter;
|
||||
|
||||
@ -701,6 +702,69 @@ impl fmt::Debug for MPath {
|
||||
}
|
||||
}
|
||||
|
||||
struct CaseConflictTrie {
|
||||
children: HashMap<MPathElement, CaseConflictTrie>,
|
||||
lowercase: HashSet<String>,
|
||||
}
|
||||
|
||||
impl CaseConflictTrie {
|
||||
fn new() -> CaseConflictTrie {
|
||||
CaseConflictTrie {
|
||||
children: HashMap::new(),
|
||||
lowercase: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if element was added successfully, or `false`
|
||||
/// if trie already contains case conflicting entry.
|
||||
fn add<P: IntoIterator<Item = MPathElement>>(&mut self, path: P) -> bool {
|
||||
let mut iter = path.into_iter();
|
||||
match iter.next() {
|
||||
None => true,
|
||||
Some(element) => {
|
||||
if let Some(child) = self.children.get_mut(&element) {
|
||||
return child.add(iter);
|
||||
}
|
||||
|
||||
if let Ok(ref element) = String::from_utf8(element.to_bytes()) {
|
||||
let element_lower = element.to_lowercase();
|
||||
if self.lowercase.contains(&element_lower) {
|
||||
return false;
|
||||
} else {
|
||||
self.lowercase.insert(element_lower);
|
||||
}
|
||||
}
|
||||
|
||||
self.children
|
||||
.entry(element)
|
||||
.or_insert(CaseConflictTrie::new())
|
||||
.add(iter)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<MPath> for CaseConflictTrie {
|
||||
fn from_iter<I: IntoIterator<Item = MPath>>(iter: I) -> Self {
|
||||
let mut trie = CaseConflictTrie::new();
|
||||
for mpath in iter {
|
||||
trie.add(mpath);
|
||||
}
|
||||
trie
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns first path that would introduce a case-conflict, if any
|
||||
pub fn check_case_conflicts<I: IntoIterator<Item = MPath>>(iter: I) -> Option<MPath> {
|
||||
let mut trie = CaseConflictTrie::new();
|
||||
for path in iter {
|
||||
if !trie.add(path.clone()) {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use quickcheck::TestResult;
|
||||
@ -878,6 +942,29 @@ mod test {
|
||||
]).expect_err("unexpected OK - other paths and prefixes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn case_conflicts() {
|
||||
let mut trie: CaseConflictTrie = vec!["a/b/c", "a/d", "c/d/a"]
|
||||
.into_iter()
|
||||
.map(|p| MPath::new(p).unwrap())
|
||||
.collect();
|
||||
|
||||
assert!(trie.add(MPath::new("a/b/c").unwrap()));
|
||||
assert!(trie.add(MPath::new("a/B/d").unwrap()) == false);
|
||||
assert!(trie.add(MPath::new("a/b/C").unwrap()) == false);
|
||||
|
||||
assert_eq!(
|
||||
check_case_conflicts(vec![
|
||||
MPath::new("a/b/c").unwrap(),
|
||||
MPath::new("a/b/c").unwrap(), // not a case conflict
|
||||
MPath::new("a/d").unwrap(),
|
||||
MPath::new("a/B/d").unwrap(),
|
||||
MPath::new("a/c").unwrap(),
|
||||
]),
|
||||
Some(MPath::new("a/B/d").unwrap()),
|
||||
);
|
||||
}
|
||||
|
||||
fn check_pcf_paths<I, T>(paths: I) -> Result<()>
|
||||
where
|
||||
I: IntoIterator<Item = (T, bool)>,
|
||||
|
Loading…
Reference in New Issue
Block a user