(* Copyright (C) 2008 Mauricio Fernandez http//eigenclass.org * See README.txt and LICENSE for the redistribution and modification terms *) open Util open Unix module type IGNORE = sig type t val init : string -> t val update : t -> base:string -> path:string -> t val is_ignored : ?debug:bool -> t -> string -> bool end let join a b = if a <> "" && b <> "" then a ^ "/" ^ b else a ^ b type 'a fold_acc = Continue of 'a | Prune of 'a module type S = sig type ignore_info val fold_directory : ?debug:bool -> ?sorted:bool -> ('a -> string -> Unix.stats -> 'a fold_acc) -> 'a -> string -> ?ign_info:ignore_info -> string -> 'a end module Make(M : IGNORE) : S with type ignore_info = M.t = struct type ignore_info = M.t let rec fold_directory ?(debug=false) ?(sorted=false) f acc base ?(ign_info = M.init base) path = let readd d = let l = ref [] in try while true do l := readdir d :: !l done; assert false with End_of_file -> !l in let acc = ref acc in let ign_info = M.update ign_info ~base ~path in let dir = join base path in try do_finally (opendir dir) closedir (fun d -> List.iter (function "." | ".." -> () | n when M.is_ignored ~debug ign_info n -> () | n -> let n = join path n in let stat = lstat (join base n) in match f !acc n stat with | Continue x -> acc := x; if stat.st_kind = S_DIR then acc := fold_directory ~debug ~sorted f ~ign_info !acc base n | Prune x -> acc := x) (let l = readd d in if sorted then List.sort compare l else l)); !acc with Unix.Unix_error _ -> !acc end module Ignore_none : IGNORE = struct type t = unit let init _ = () let update () ~base ~path = () let is_ignored ?debug () _ = false end module Gitignore : IGNORE = struct open Printf type glob_type = Accept | Deny (* Simple: no wildcards, no slash * Simple_local: leading slash, otherwise no slashes, no wildcards * Endswith: *.whatever, no slashes * Endswith_local: *.whatever, leading slash only * Startswith_local: whatever*, leading slash only * Noslash: wildcards, no slashes * Nowildcard: non-prefix slashes, no wildcards * Complex: non-prefix slashes, wildcards * *) type patt = Simple of string | Noslash of string | Complex of string | Simple_local of string | Endswith of string | Endswith_local of string | Startswith_local of string | Nowildcard of string * int type glob = glob_type * patt type t = (string * glob list) list external fnmatch : bool -> string -> string -> bool = "perform_fnmatch" "noalloc" let string_of_patt = function Simple s | Noslash s | Complex s | Nowildcard (s, _) -> s | Simple_local s -> "/" ^ s | Endswith s -> "*." ^ s | Endswith_local s -> "/*." ^ s | Startswith_local s -> "/" ^ s ^ "*" let has_wildcard s = let rec loop s i max = if i < max then match String.unsafe_get s i with '*' | '?' | '[' | '{' -> true | _ -> loop s (i+1) max else false in loop s 0 (String.length s) let suff1 s = String.sub s 1 (String.length s - 1) let pref1 s = String.sub s 0 (String.length s - 1) let patt_of_string s = try match String.rindex s '/' with 0 -> let s = suff1 s in if not (has_wildcard s) then Simple_local s else let suff = suff1 s in if s.[0] = '*' && not (has_wildcard suff) then Endswith_local suff else let pref = pref1 s in if s.[String.length s - 1] = '*' && not (has_wildcard pref) then Startswith_local pref else Complex s | _ -> if not (has_wildcard s) then let l = String.length s in Nowildcard (s, if s.[0] = '/' then l - 1 else l) else Complex s with Not_found -> let suff = suff1 s in if s.[0] = '*' && not (has_wildcard suff) then Endswith suff else if has_wildcard s then Noslash s else Simple s let glob_of_string s = match s.[0] with '!' -> (Accept, (patt_of_string (suff1 s))) | _ -> (Deny, patt_of_string s) let collect_globs l = let rec aux acc = function [] -> acc | line::tl -> if line = "" || line.[0] = '#' then aux acc tl else aux (glob_of_string line :: acc) tl in aux [] l let read_gitignore path = try collect_globs (do_finally (open_in (join path ".gitignore")) close_in (fun is -> let l = ref [] in try while true do l := input_line is :: !l done; assert false with End_of_file -> !l) ) with Sys_error _ -> [] let init path = [] let update t ~base ~path = let rec remove_local = function [] -> [] | ((_, (Simple _ | Noslash _ | Complex _ | Endswith _ | Nowildcard _)) as x)::tl -> x :: remove_local tl | (_, (Simple_local _ | Endswith_local _ | Startswith_local _))::tl -> remove_local tl in let t = match t with (f, l)::tl -> (f, remove_local l)::tl | [] -> [] in (Filename.basename path, read_gitignore (join base path)) :: t type path = { basename : string; length : int; full_name : string Lazy.t } let path_of_string s = { basename = s; length = String.length s; full_name = lazy s } let string_of_path p = Lazy.force p.full_name let path_length p = p.length let push pref p = { basename = p.basename; length = p.length + 1 + String.length pref; full_name = lazy (String.concat "/" [pref; string_of_path p]) } let basename p = p.basename let check_ending suff path = let fname = basename path in let l1 = String.length suff in let l2 = String.length fname in if l2 < l1 then false else strneq l1 suff 0 fname (l2 - l1) let check_start pref path = let fname = basename path in let l1 = String.length pref in let l2 = String.length fname in if l2 < l1 then false else strneq l1 pref 0 fname 0 let glob_matches local patt path = match patt with Simple s -> s = basename path | Simple_local s -> if local then s = basename path else false | Endswith s -> check_ending s path | Endswith_local s -> if local then check_ending s path else false | Startswith_local s -> if local then check_start s path else false | Noslash s -> fnmatch false s (basename path) | Complex s -> fnmatch true s (string_of_path path) | Nowildcard (s, l) -> if l = path_length path then fnmatch true s (string_of_path path) else false let path_of_ign_info t = String.concat "/" (List.rev (List.map fst t)) let is_ignored ?(debug=false) t fname = let rec aux local path = function | [] -> false | (dname, globs)::tl as t -> let ign = List.fold_left (fun s (ty, patt) -> if glob_matches local patt path then (match ty with Accept -> if debug then eprintf "ACCEPT %S (matched %S) at %S\n" (string_of_path path) (string_of_patt patt) (path_of_ign_info t); `Kept | Deny -> if debug then eprintf "DENY %S (matched %S) at %S\n" (string_of_path path) (string_of_patt patt) (path_of_ign_info t); `Ignored) else s) `Dontknow globs in match ign with | `Dontknow -> aux false (push dname path) tl | `Ignored -> true | `Kept -> false in fname = ".git" || aux true (path_of_string fname) t end