3939#include <sys/stat.h>
4040#include <sys/types.h>
4141
42+ #if BFS_WITH_LIBGIT2
43+ # include <git2.h>
44+ #endif
45+
4246/** Initialize a bftw_stat cache. */
4347static void bftw_stat_init (struct bftw_stat * bufs , struct bfs_stat * stat_buf , struct bfs_stat * lstat_buf ) {
4448 bufs -> stat_buf = stat_buf ;
@@ -247,6 +251,8 @@ struct bftw_file {
247251
248252 /** Cached bfs_stat() info. */
249253 struct bftw_stat stat_bufs ;
254+ /** Structured path metadata. */
255+ struct bfs_path path ;
250256
251257 /** The offset of this file in the full path. */
252258 size_t nameoff ;
@@ -755,6 +761,7 @@ static struct bftw_file *bftw_file_new(struct bftw_cache *cache, struct bftw_fil
755761
756762 file -> namelen = namelen ;
757763 memcpy (file -> name , name , namelen + 1 );
764+ bfs_path_init (& file -> path , parent ? & parent -> path : NULL , file -> name , namelen );
758765
759766 return file ;
760767}
@@ -795,6 +802,7 @@ static void bftw_file_free(struct bftw_cache *cache, struct bftw_file *file) {
795802 bftw_file_close (cache , file );
796803 }
797804
805+ bfs_path_reset (& file -> path );
798806 bftw_stat_recycle (cache , file );
799807
800808 varena_free (& cache -> files , file , file -> namelen + 1 );
@@ -845,6 +853,8 @@ struct bftw_state {
845853 struct bftw_file * file ;
846854 /** The previous file. */
847855 struct bftw_file * previous ;
856+ /** Temporary path metadata for the current callback. */
857+ struct bfs_path pathinfo ;
848858
849859 /** The currently open directory. */
850860 struct bfs_dir * dir ;
@@ -970,6 +980,7 @@ static int bftw_state_init(struct bftw_state *state, const struct bftw_args *arg
970980 state -> path = NULL ;
971981 state -> file = NULL ;
972982 state -> previous = NULL ;
983+ bfs_path_init (& state -> pathinfo , NULL , NULL , 0 );
973984
974985 state -> dir = NULL ;
975986 state -> de = NULL ;
@@ -1723,6 +1734,19 @@ static void bftw_init_ftwbuf(struct bftw_state *state, enum bftw_visit visit) {
17231734 ftwbuf -> nameoff = xbaseoff (ftwbuf -> path );
17241735 }
17251736
1737+ struct bfs_path * ftw_path ;
1738+ if (de || !file ) {
1739+ const struct bfs_path * parent_path = file ? & file -> path : NULL ;
1740+ const char * name = ftwbuf -> path + ftwbuf -> nameoff ;
1741+ size_t namelen = strlen (name );
1742+ bfs_path_init (& state -> pathinfo , parent_path , name , namelen );
1743+ ftw_path = & state -> pathinfo ;
1744+ } else {
1745+ ftw_path = & file -> path ;
1746+ }
1747+
1748+ ftwbuf -> pathinfo = ftw_path ;
1749+
17261750 ftwbuf -> stat_flags = bftw_stat_flags (state , ftwbuf -> depth );
17271751
17281752 if (ftwbuf -> error != 0 ) {
@@ -2082,6 +2106,7 @@ static void bftw_drain(struct bftw_state *state, struct bftw_queue *queue) {
20822106 */
20832107static int bftw_state_destroy (struct bftw_state * state ) {
20842108 dstrfree (state -> path );
2109+ bfs_path_reset (& state -> pathinfo );
20852110
20862111 struct ioq * ioq = state -> ioq ;
20872112 if (ioq ) {
@@ -2102,6 +2127,184 @@ static int bftw_state_destroy(struct bftw_state *state) {
21022127 return state -> error ? -1 : 0 ;
21032128}
21042129
2130+ #if BFS_WITH_LIBGIT2
2131+
2132+ /** Data for the gitignore feature that gets attached to a bfs_path. */
2133+ struct bfs_git_data {
2134+ /** The repository this path is in. */
2135+ struct git_repository * repo ;
2136+ /** The git data for the root of the repository. */
2137+ struct bfs_git_data * root_data ;
2138+ /** The path associated with this data. */
2139+ const struct bfs_path * path ;
2140+ /** Whether this path is the owner of the repo pointer. */
2141+ bool is_owner ;
2142+ /** Whether this path is ignored. */
2143+ bool is_ignored ;
2144+ /** Whether we have checked the git status of this path. */
2145+ bool is_checked ;
2146+ };
2147+
2148+ /** Destructor for bfs_git_data. */
2149+ static void bfs_git_data_free (void * ptr ) {
2150+ struct bfs_git_data * data = ptr ;
2151+ if (!data ) {
2152+ return ;
2153+ }
2154+ if (data -> is_owner && data -> repo ) {
2155+ git_repository_free (data -> repo );
2156+ }
2157+ free (data );
2158+ }
2159+
2160+ /** Get the git data for a path, allocating if necessary. */
2161+ static struct bfs_git_data * bfs_git_data (const struct bfs_path * path ) {
2162+ if (!path ) {
2163+ return NULL ;
2164+ }
2165+
2166+ struct bfs_git_data * data = bfs_path_data (path );
2167+ if (!data ) {
2168+ data = ZALLOC (struct bfs_git_data );
2169+ if (data ) {
2170+ data -> path = path ;
2171+ bfs_path_set_data (path , data , bfs_git_data_free );
2172+ }
2173+ }
2174+ return data ;
2175+ }
2176+
2177+ /** Build the full path string for a bfs_path. */
2178+ static dchar * bfs_path_str (const struct bfs_path * path ) {
2179+ if (!path ) {
2180+ return NULL ;
2181+ }
2182+
2183+ dchar * str = bfs_path_str (path -> parent );
2184+ if (!str ) {
2185+ return dstrndup (path -> name , path -> namelen );
2186+ }
2187+
2188+ if (dstrapp (& str , '/' ) != 0 ) {
2189+ dstrfree (str );
2190+ return NULL ;
2191+ }
2192+ if (dstrncat (& str , path -> name , path -> namelen ) != 0 ) {
2193+ dstrfree (str );
2194+ return NULL ;
2195+ }
2196+
2197+ return str ;
2198+ }
2199+
2200+ /** Get the path of a file relative to the git repo root. */
2201+ static dchar * bfs_git_relative_path (const struct bfs_path * path , const struct bfs_git_data * data ) {
2202+ dchar * repo_root_str = bfs_path_str (data -> root_data -> path );
2203+ if (!repo_root_str ) {
2204+ return NULL ;
2205+ }
2206+
2207+ dchar * full_path = bfs_path_str (path );
2208+ if (!full_path ) {
2209+ dstrfree (repo_root_str );
2210+ return NULL ;
2211+ }
2212+
2213+ const char * relative_path_start = full_path ;
2214+ size_t repo_root_len = strlen (repo_root_str );
2215+
2216+ // Strip the repository root directory from the full path to make it relative.
2217+ if (strcmp (repo_root_str , "." ) != 0 && strncmp (full_path , repo_root_str , repo_root_len ) == 0 ) {
2218+ relative_path_start += repo_root_len ;
2219+ if (* relative_path_start == '/' ) {
2220+ relative_path_start ++ ;
2221+ }
2222+ }
2223+
2224+ // Skip "./" prefix if present.
2225+ if (strncmp (relative_path_start , "./" , 2 ) == 0 ) {
2226+ relative_path_start += 2 ;
2227+ }
2228+
2229+ dchar * relative_path = dstrdup (relative_path_start );
2230+ dstrfree (full_path );
2231+ dstrfree (repo_root_str );
2232+ return relative_path ;
2233+ }
2234+
2235+ /** Update the is_ignored and is_checked values for a path. Returns is_ignored. */
2236+ static bool bfs_update_gitignore (const struct bfs_path * path , struct bfs_git_data * data , const struct bfs_ctx * ctx ) {
2237+ dchar * git_path = bfs_git_relative_path (path , data );
2238+ if (!git_path ) {
2239+ return false;
2240+ }
2241+
2242+ int ignored = 0 ;
2243+ if (git_ignore_path_is_ignored (& ignored , data -> repo , git_path ) == 0 && ignored ) {
2244+ data -> is_ignored = true;
2245+ }
2246+
2247+ dstrfree (git_path );
2248+ return data -> is_ignored ;
2249+ }
2250+
2251+ /** Recursively check git status for a path and its parents. */
2252+ static void bfs_path_update_git_status (const struct bfs_path * path , const struct bfs_ctx * ctx ) {
2253+ if (!path ) {
2254+ return ;
2255+ }
2256+
2257+ struct bfs_git_data * data = bfs_git_data (path );
2258+ if (!data || data -> is_checked ) {
2259+ return ;
2260+ }
2261+
2262+ // Recurse to ensure parent is checked first.
2263+ bfs_path_update_git_status (path -> parent , ctx );
2264+
2265+ // Inherit from parent
2266+ if (path -> parent ) {
2267+ struct bfs_git_data * parent_data = bfs_path_data (path -> parent );
2268+ if (parent_data ) {
2269+ data -> repo = parent_data -> repo ;
2270+ data -> root_data = parent_data -> root_data ;
2271+ }
2272+ }
2273+
2274+ if (data -> repo && bfs_update_gitignore (path , data , ctx )) {
2275+ return ;
2276+ }
2277+
2278+ // Check for a new repository at the current path.
2279+ dchar * search_path_str = bfs_path_str (path );
2280+ if (search_path_str ) {
2281+ struct git_repository * opened_repo = NULL ;
2282+ // Only the roots of the search need to check for git repos above the search dir.
2283+ unsigned int repo_open_flags = path -> parent ? GIT_REPOSITORY_OPEN_NO_SEARCH : 0 ;
2284+ if (git_repository_open_ext (& opened_repo , search_path_str , repo_open_flags , NULL ) != 0 ) {
2285+ opened_repo = NULL ;
2286+ }
2287+
2288+ if (opened_repo ) {
2289+ data -> repo = opened_repo ;
2290+ data -> is_owner = true;
2291+ data -> root_data = data ;
2292+ }
2293+ }
2294+
2295+ dstrfree (search_path_str );
2296+
2297+ data -> is_checked = true;
2298+ }
2299+
2300+ bool bftw_is_gitignored (const struct BFTW * ftwbuf , const struct bfs_ctx * ctx ) {
2301+ bfs_path_update_git_status (ftwbuf -> pathinfo , ctx );
2302+ struct bfs_git_data * data = bfs_git_data (ftwbuf -> pathinfo );
2303+ return data && data -> is_ignored ;
2304+ }
2305+
2306+ #endif // BFS_WITH_LIBGIT2
2307+
21052308/**
21062309 * Shared implementation for all search strategies.
21072310 */
@@ -2293,7 +2496,7 @@ static int bftw_ids(const struct bftw_args *args) {
22932496 }
22942497 }
22952498
2296- done :
2499+ done :
22972500 return bftw_ids_destroy (& state );
22982501}
22992502
@@ -2325,7 +2528,7 @@ static int bftw_eds(const struct bftw_args *args) {
23252528 bftw_impl (& state .nested );
23262529 }
23272530
2328- done :
2531+ done :
23292532 return bftw_ids_destroy (& state );
23302533}
23312534
0 commit comments