rust/hg-core/src/sparse.rs
changeset 49485 ffd4b1f1c9cb
child 49488 7c93e38a0bbd
equal deleted inserted replaced
49484:85f5d11c77dd 49485:ffd4b1f1c9cb
       
     1 use std::{collections::HashSet, path::Path};
       
     2 
       
     3 use format_bytes::{write_bytes, DisplayBytes};
       
     4 
       
     5 use crate::{
       
     6     errors::HgError,
       
     7     filepatterns::parse_pattern_file_contents,
       
     8     matchers::{
       
     9         AlwaysMatcher, DifferenceMatcher, IncludeMatcher, Matcher,
       
    10         UnionMatcher,
       
    11     },
       
    12     operations::cat,
       
    13     repo::Repo,
       
    14     requirements::SPARSE_REQUIREMENT,
       
    15     utils::{hg_path::HgPath, SliceExt},
       
    16     IgnorePattern, PatternError, PatternFileWarning, PatternSyntax, Revision,
       
    17     NULL_REVISION,
       
    18 };
       
    19 
       
    20 /// Command which is triggering the config read
       
    21 #[derive(Copy, Clone, Debug)]
       
    22 pub enum SparseConfigContext {
       
    23     Sparse,
       
    24     Narrow,
       
    25 }
       
    26 
       
    27 impl DisplayBytes for SparseConfigContext {
       
    28     fn display_bytes(
       
    29         &self,
       
    30         output: &mut dyn std::io::Write,
       
    31     ) -> std::io::Result<()> {
       
    32         match self {
       
    33             SparseConfigContext::Sparse => write_bytes!(output, b"sparse"),
       
    34             SparseConfigContext::Narrow => write_bytes!(output, b"narrow"),
       
    35         }
       
    36     }
       
    37 }
       
    38 
       
    39 /// Possible warnings when reading sparse configuration
       
    40 #[derive(Debug, derive_more::From)]
       
    41 pub enum SparseWarning {
       
    42     /// Warns about improper paths that start with "/"
       
    43     RootWarning {
       
    44         context: SparseConfigContext,
       
    45         line: Vec<u8>,
       
    46     },
       
    47     /// Warns about a profile missing from the given changelog revision
       
    48     ProfileNotFound { profile: Vec<u8>, rev: Revision },
       
    49     #[from]
       
    50     Pattern(PatternFileWarning),
       
    51 }
       
    52 
       
    53 /// Parsed sparse config
       
    54 #[derive(Debug, Default)]
       
    55 pub struct SparseConfig {
       
    56     // Line-separated
       
    57     includes: Vec<u8>,
       
    58     // Line-separated
       
    59     excludes: Vec<u8>,
       
    60     profiles: HashSet<Vec<u8>>,
       
    61     warnings: Vec<SparseWarning>,
       
    62 }
       
    63 
       
    64 /// All possible errors when reading sparse config
       
    65 #[derive(Debug, derive_more::From)]
       
    66 pub enum SparseConfigError {
       
    67     IncludesAfterExcludes {
       
    68         context: SparseConfigContext,
       
    69     },
       
    70     EntryOutsideSection {
       
    71         context: SparseConfigContext,
       
    72         line: Vec<u8>,
       
    73     },
       
    74     #[from]
       
    75     HgError(HgError),
       
    76     #[from]
       
    77     PatternError(PatternError),
       
    78 }
       
    79 
       
    80 /// Parse sparse config file content.
       
    81 fn parse_config(
       
    82     raw: &[u8],
       
    83     context: SparseConfigContext,
       
    84 ) -> Result<SparseConfig, SparseConfigError> {
       
    85     let mut includes = vec![];
       
    86     let mut excludes = vec![];
       
    87     let mut profiles = HashSet::new();
       
    88     let mut warnings = vec![];
       
    89 
       
    90     #[derive(PartialEq, Eq)]
       
    91     enum Current {
       
    92         Includes,
       
    93         Excludes,
       
    94         None,
       
    95     };
       
    96 
       
    97     let mut current = Current::None;
       
    98     let mut in_section = false;
       
    99 
       
   100     for line in raw.split(|c| *c == b'\n') {
       
   101         let line = line.trim();
       
   102         if line.is_empty() || line[0] == b'#' {
       
   103             // empty or comment line, skip
       
   104             continue;
       
   105         }
       
   106         if line.starts_with(b"%include ") {
       
   107             let profile = line[b"%include ".len()..].trim();
       
   108             if !profile.is_empty() {
       
   109                 profiles.insert(profile.into());
       
   110             }
       
   111         } else if line == b"[include]" {
       
   112             if in_section && current == Current::Includes {
       
   113                 return Err(SparseConfigError::IncludesAfterExcludes {
       
   114                     context,
       
   115                 });
       
   116             }
       
   117             in_section = true;
       
   118             current = Current::Includes;
       
   119             continue;
       
   120         } else if line == b"[exclude]" {
       
   121             in_section = true;
       
   122             current = Current::Excludes;
       
   123         } else {
       
   124             if current == Current::None {
       
   125                 return Err(SparseConfigError::EntryOutsideSection {
       
   126                     context,
       
   127                     line: line.into(),
       
   128                 });
       
   129             }
       
   130             if line.trim().starts_with(b"/") {
       
   131                 warnings.push(SparseWarning::RootWarning {
       
   132                     context,
       
   133                     line: line.into(),
       
   134                 });
       
   135                 continue;
       
   136             }
       
   137             match current {
       
   138                 Current::Includes => {
       
   139                     includes.push(b'\n');
       
   140                     includes.extend(line.iter());
       
   141                 }
       
   142                 Current::Excludes => {
       
   143                     excludes.push(b'\n');
       
   144                     excludes.extend(line.iter());
       
   145                 }
       
   146                 Current::None => unreachable!(),
       
   147             }
       
   148         }
       
   149     }
       
   150 
       
   151     Ok(SparseConfig {
       
   152         includes,
       
   153         excludes,
       
   154         profiles,
       
   155         warnings,
       
   156     })
       
   157 }
       
   158 
       
   159 fn read_temporary_includes(
       
   160     repo: &Repo,
       
   161 ) -> Result<Vec<Vec<u8>>, SparseConfigError> {
       
   162     let raw = repo.hg_vfs().try_read("tempsparse")?.unwrap_or(vec![]);
       
   163     if raw.is_empty() {
       
   164         return Ok(vec![]);
       
   165     }
       
   166     Ok(raw.split(|c| *c == b'\n').map(ToOwned::to_owned).collect())
       
   167 }
       
   168 
       
   169 /// Obtain sparse checkout patterns for the given revision
       
   170 fn patterns_for_rev(
       
   171     repo: &Repo,
       
   172     rev: Revision,
       
   173 ) -> Result<Option<SparseConfig>, SparseConfigError> {
       
   174     if !repo.has_sparse() {
       
   175         return Ok(None);
       
   176     }
       
   177     let raw = repo.hg_vfs().try_read("sparse")?.unwrap_or(vec![]);
       
   178 
       
   179     if raw.is_empty() {
       
   180         return Ok(None);
       
   181     }
       
   182 
       
   183     let mut config = parse_config(&raw, SparseConfigContext::Sparse)?;
       
   184 
       
   185     if !config.profiles.is_empty() {
       
   186         let mut profiles: Vec<Vec<u8>> = config.profiles.into_iter().collect();
       
   187         let mut visited = HashSet::new();
       
   188 
       
   189         while let Some(profile) = profiles.pop() {
       
   190             if visited.contains(&profile) {
       
   191                 continue;
       
   192             }
       
   193             visited.insert(profile.to_owned());
       
   194 
       
   195             let output =
       
   196                 cat(repo, &rev.to_string(), vec![HgPath::new(&profile)])
       
   197                     .map_err(|_| {
       
   198                         HgError::corrupted(format!(
       
   199                             "dirstate points to non-existent parent node"
       
   200                         ))
       
   201                     })?;
       
   202             if output.results.is_empty() {
       
   203                 config.warnings.push(SparseWarning::ProfileNotFound {
       
   204                     profile: profile.to_owned(),
       
   205                     rev,
       
   206                 })
       
   207             }
       
   208 
       
   209             let subconfig = parse_config(
       
   210                 &output.results[0].1,
       
   211                 SparseConfigContext::Sparse,
       
   212             )?;
       
   213             if !subconfig.includes.is_empty() {
       
   214                 config.includes.push(b'\n');
       
   215                 config.includes.extend(&subconfig.includes);
       
   216             }
       
   217             if !subconfig.includes.is_empty() {
       
   218                 config.includes.push(b'\n');
       
   219                 config.excludes.extend(&subconfig.excludes);
       
   220             }
       
   221             config.warnings.extend(subconfig.warnings.into_iter());
       
   222             profiles.extend(subconfig.profiles.into_iter());
       
   223         }
       
   224 
       
   225         config.profiles = visited;
       
   226     }
       
   227 
       
   228     if !config.includes.is_empty() {
       
   229         config.includes.extend(b"\n.hg*");
       
   230     }
       
   231 
       
   232     Ok(Some(config))
       
   233 }
       
   234 
       
   235 /// Obtain a matcher for sparse working directories.
       
   236 pub fn matcher(
       
   237     repo: &Repo,
       
   238 ) -> Result<(Box<dyn Matcher + Sync>, Vec<SparseWarning>), SparseConfigError> {
       
   239     let mut warnings = vec![];
       
   240     if !repo.requirements().contains(SPARSE_REQUIREMENT) {
       
   241         return Ok((Box::new(AlwaysMatcher), warnings));
       
   242     }
       
   243 
       
   244     let parents = repo.dirstate_parents()?;
       
   245     let mut revs = vec![];
       
   246     let p1_rev =
       
   247         repo.changelog()?
       
   248             .rev_from_node(parents.p1.into())
       
   249             .map_err(|_| {
       
   250                 HgError::corrupted(format!(
       
   251                     "dirstate points to non-existent parent node"
       
   252                 ))
       
   253             })?;
       
   254     if p1_rev != NULL_REVISION {
       
   255         revs.push(p1_rev)
       
   256     }
       
   257     let p2_rev =
       
   258         repo.changelog()?
       
   259             .rev_from_node(parents.p2.into())
       
   260             .map_err(|_| {
       
   261                 HgError::corrupted(format!(
       
   262                     "dirstate points to non-existent parent node"
       
   263                 ))
       
   264             })?;
       
   265     if p2_rev != NULL_REVISION {
       
   266         revs.push(p2_rev)
       
   267     }
       
   268     let mut matchers = vec![];
       
   269 
       
   270     for rev in revs.iter() {
       
   271         let config = patterns_for_rev(repo, *rev);
       
   272         if let Ok(Some(config)) = config {
       
   273             warnings.extend(config.warnings);
       
   274             let mut m: Box<dyn Matcher + Sync> = Box::new(AlwaysMatcher);
       
   275             if !config.includes.is_empty() {
       
   276                 let (patterns, subwarnings) = parse_pattern_file_contents(
       
   277                     &config.includes,
       
   278                     Path::new(""),
       
   279                     Some(b"relglob:".as_ref()),
       
   280                     false,
       
   281                 )?;
       
   282                 warnings.extend(subwarnings.into_iter().map(From::from));
       
   283                 m = Box::new(IncludeMatcher::new(patterns)?);
       
   284             }
       
   285             if !config.excludes.is_empty() {
       
   286                 let (patterns, subwarnings) = parse_pattern_file_contents(
       
   287                     &config.excludes,
       
   288                     Path::new(""),
       
   289                     Some(b"relglob:".as_ref()),
       
   290                     false,
       
   291                 )?;
       
   292                 warnings.extend(subwarnings.into_iter().map(From::from));
       
   293                 m = Box::new(DifferenceMatcher::new(
       
   294                     m,
       
   295                     Box::new(IncludeMatcher::new(patterns)?),
       
   296                 ));
       
   297             }
       
   298             matchers.push(m);
       
   299         }
       
   300     }
       
   301     let result: Box<dyn Matcher + Sync> = match matchers.len() {
       
   302         0 => Box::new(AlwaysMatcher),
       
   303         1 => matchers.pop().expect("1 is equal to 0"),
       
   304         _ => Box::new(UnionMatcher::new(matchers)),
       
   305     };
       
   306 
       
   307     let matcher =
       
   308         force_include_matcher(result, &read_temporary_includes(repo)?)?;
       
   309     Ok((matcher, warnings))
       
   310 }
       
   311 
       
   312 /// Returns a matcher that returns true for any of the forced includes before
       
   313 /// testing against the actual matcher
       
   314 fn force_include_matcher(
       
   315     result: Box<dyn Matcher + Sync>,
       
   316     temp_includes: &[Vec<u8>],
       
   317 ) -> Result<Box<dyn Matcher + Sync>, PatternError> {
       
   318     if temp_includes.is_empty() {
       
   319         return Ok(result);
       
   320     }
       
   321     let forced_include_matcher = IncludeMatcher::new(
       
   322         temp_includes
       
   323             .into_iter()
       
   324             .map(|include| {
       
   325                 IgnorePattern::new(PatternSyntax::Path, include, Path::new(""))
       
   326             })
       
   327             .collect(),
       
   328     )?;
       
   329     Ok(Box::new(UnionMatcher::new(vec![
       
   330         Box::new(forced_include_matcher),
       
   331         result,
       
   332     ])))
       
   333 }