@@ -22,17 +22,16 @@ use std::fs::File;
22
22
use std:: io;
23
23
use std:: io:: BufRead ;
24
24
use std:: io:: BufReader ;
25
- use std:: io:: ErrorKind ;
26
25
use std:: iter;
27
26
use std:: path:: Path ;
28
27
29
- use ignore:: gitignore;
30
28
use itertools:: Itertools as _;
31
29
use thiserror:: Error ;
32
30
use tracing:: instrument;
33
31
34
32
use crate :: repo_path:: RepoPath ;
35
33
use crate :: repo_path:: RepoPathComponentBuf ;
34
+ use crate :: tree:: Tree ;
36
35
37
36
#[ derive( PartialEq , Eq , Debug ) ]
38
37
pub enum Visit {
@@ -514,14 +513,23 @@ impl<V: Debug> Debug for RepoPathTree<V> {
514
513
}
515
514
}
516
515
516
+ #[ derive( Clone , Debug ) ]
517
+ struct IgnoreWrapper ( ignore:: gitignore:: Gitignore ) ;
518
+
519
+ impl Default for IgnoreWrapper {
520
+ fn default ( ) -> Self {
521
+ Self ( ignore:: gitignore:: Gitignore :: empty ( ) )
522
+ }
523
+ }
524
+
517
525
/// Matches file paths with glob patterns.
518
526
///
519
527
/// Patterns are provided as `(dir, pattern)` pairs, where `dir` should be the
520
528
/// longest directory path that contains no glob meta characters, and `pattern`
521
529
/// will be evaluated relative to `dir`.
522
530
#[ derive( Clone , Debug ) ]
523
531
pub struct GitAttributesMatcher {
524
- matcher : gitignore :: Gitignore ,
532
+ tree : RepoPathTree < IgnoreWrapper > ,
525
533
}
526
534
527
535
#[ derive( Debug , Error ) ]
@@ -534,10 +542,9 @@ pub enum GitAttributesMatcherError {
534
542
InvalidPattern ( String ) ,
535
543
}
536
544
537
- fn parse_gitignore (
545
+ fn parse_gitattributes (
538
546
f : File ,
539
- root : impl AsRef < Path > ,
540
- builder : & mut gitignore:: GitignoreBuilder ,
547
+ builder : & mut ignore:: gitignore:: GitignoreBuilder ,
541
548
) -> Result < ( ) , GitAttributesMatcherError > {
542
549
let reader = BufReader :: new ( f) ;
543
550
@@ -549,6 +556,11 @@ fn parse_gitignore(
549
556
None => continue ,
550
557
} ;
551
558
559
+ // lines starting with # are ignored
560
+ if pattern. starts_with ( '#' ) {
561
+ continue ;
562
+ }
563
+
552
564
// Simple heuristic to detect LFS files
553
565
let is_lfs = parts. any ( |p| p == "filter=lfs" ) ;
554
566
if !is_lfs {
@@ -563,80 +575,64 @@ fn parse_gitignore(
563
575
"negative patterns are not allowed in .gitattributes files" . to_string ( ) ,
564
576
) ) ;
565
577
}
578
+
579
+ // git somehow accepts this but ignores it
566
580
if pattern. ends_with ( "/" ) {
567
- return Err ( GitAttributesMatcherError :: InvalidPattern ( format ! (
568
- "trailing slash does not match a directory in .gitattributes, try {}** instead" ,
569
- pattern
570
- ) ) ) ;
581
+ continue ;
571
582
}
572
-
573
- builder. add_line ( Some ( root. as_ref ( ) . to_path_buf ( ) ) , pattern) ?;
583
+ builder. add_line ( None , pattern) ?;
574
584
}
575
585
576
586
Ok ( ( ) )
577
587
}
578
588
579
589
impl GitAttributesMatcher {
580
590
/// Create a new matcher for the contents of a .gitattributes file
581
- pub fn new ( ) -> Result < Self , GitAttributesMatcherError > {
582
- // TODO(brandon): We shouldn't assume `jj` is being run from the root.
583
- let root = Path :: new ( "." ) ;
584
- let mut builder = gitignore:: GitignoreBuilder :: new ( & root) ;
585
-
586
- let gitattributes_path = root. join ( ".gitattributes" ) ;
587
-
588
- match File :: open ( gitattributes_path) {
589
- Ok ( f) => {
590
- parse_gitignore ( f, & root, & mut builder) ?;
591
- }
592
- Err ( e) => {
593
- // If the file doesn't exist, we'll create a blank Gitignore,
594
- // equivalent to Gitignore::empty()
595
- if e. kind ( ) != ErrorKind :: NotFound {
596
- return Err ( GitAttributesMatcherError :: ReadError ( e) ) ;
597
- }
591
+ pub fn new ( tree : & Tree , repo_root : & Path ) -> Result < Self , GitAttributesMatcherError > {
592
+ let git_attributes_matcher = FileGlobsMatcher :: new ( [ (
593
+ tree. dir ( ) ,
594
+ glob:: Pattern :: new ( "**/.gitattributes" )
595
+ . expect ( "This is a statically known valid pattern" ) ,
596
+ ) ] ) ;
597
+ let entries = tree. entries_matching ( & git_attributes_matcher) ;
598
+ let mut tree = RepoPathTree :: default ( ) ;
599
+ for ( path, _) in entries {
600
+ let gitattributes_path = path
601
+ . to_fs_path ( repo_root)
602
+ . expect ( "The file matcher only returns valid paths" ) ;
603
+
604
+ if gitattributes_path. exists ( ) {
605
+ let relative_path = path
606
+ . parent ( )
607
+ . expect ( "There is always a parent directory for files" )
608
+ . to_owned ( ) ;
609
+ let location = repo_root. join ( relative_path. to_internal_dir_string ( ) ) ;
610
+ let mut builder = ignore:: gitignore:: GitignoreBuilder :: new ( location) ;
611
+ let file = File :: open ( & gitattributes_path) ?;
612
+
613
+ parse_gitattributes ( file, & mut builder) ?;
614
+ tree. add ( & relative_path) . value = IgnoreWrapper ( builder. build ( ) ?) ;
598
615
}
599
616
}
600
-
601
- Ok ( GitAttributesMatcher {
602
- matcher : builder. build ( ) ?,
603
- } )
617
+ Ok ( GitAttributesMatcher { tree } )
604
618
}
605
619
}
606
620
607
621
impl Matcher for GitAttributesMatcher {
608
622
fn matches ( & self , file : & RepoPath ) -> bool {
609
- // TODO(brandon): Figure out if we need to handle is_dir
610
- let path = match file. to_fs_path ( self . matcher . path ( ) ) {
611
- Ok ( p) => p,
612
- Err ( _) => return false ,
613
- } ;
614
- let m_match = self . matcher . matched ( & path, false ) ;
615
- match m_match {
616
- ignore:: Match :: None => false ,
617
- ignore:: Match :: Ignore ( _) => {
618
- tracing:: debug!( ?m_match, ?path, "matching a file" ) ;
619
- true
620
- }
621
- ignore:: Match :: Whitelist ( _) => false ,
622
- }
623
+ self . tree
624
+ . walk_to ( file)
625
+ . take_while ( |( _, tail_path) | !tail_path. is_root ( ) )
626
+ . any ( |( sub, tail_path) | {
627
+ let Ok ( path) = tail_path. to_fs_path ( sub. value . 0 . path ( ) ) else {
628
+ return false ;
629
+ } ;
630
+ matches ! ( sub. value. 0 . matched( path, false ) , ignore:: Match :: Ignore ( _) )
631
+ } )
623
632
}
624
633
625
- fn visit ( & self , dir : & RepoPath ) -> Visit {
626
- let dir = match dir. to_fs_path ( self . matcher . path ( ) ) {
627
- Ok ( p) => p,
628
- Err ( _) => return Visit :: Nothing ,
629
- } ;
630
- // TODO(brandon): Try to understand if these make sense
631
- let m_match = self . matcher . matched ( & dir, true ) ;
632
- match m_match {
633
- ignore:: Match :: None => Visit :: Nothing ,
634
- ignore:: Match :: Ignore ( _) => {
635
- tracing:: debug!( ?m_match, ?dir, "visiting a dir" ) ;
636
- Visit :: AllRecursively
637
- }
638
- ignore:: Match :: Whitelist ( _) => Visit :: Nothing ,
639
- }
634
+ fn visit ( & self , _dir : & RepoPath ) -> Visit {
635
+ Visit :: Nothing
640
636
}
641
637
}
642
638
0 commit comments