<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>emre şahin's digital garden 🍃 - gitignore</title>
    <link>https://emresahin.net/tags/gitignore/</link>
    <description>Posts in the gitignore tag</description>
    <language>en</language>
    <managingEditor>contact@emresahin.net (Emre Şahin)</managingEditor>
    <lastBuildDate>Tue, 15 Sep 2026 19:46:32 +0000</lastBuildDate>
    <atom:link href="https://emresahin.net/tags/gitignore/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>Xvc Devlog - 221107</title>
      <published>2022-11-08T09:57:00+00:00</published>
      <updated>2022-11-08T09:57:00+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Tue, 08 Nov 2022 09:57:00 +0000</pubDate>
      <link>https://emresahin.net/xvc-devlog---221107/</link>
      <guid isPermaLink="true">https://emresahin.net/xvc-devlog---221107/</guid>
      <description>🐇 Welcome to the November 7th issue of Xvc Devlog. In the previous devlog , we began to implement Git integration. How is it going, Mr. Tortoise? 🐢 It looks like we don’t have many architectural problems. 🐇 You’re forgetting how Exec::cmd works, though. You have those kinds of problems. 🐢 Yeah, I...</description>
      <category>devlog</category>
      <category>Software Development</category>
      <category>xvc</category>
      <category>architecture</category>
      <category>shell</category>
      <category>git</category>
      <category>gitignore</category>
      <category>rust-analyzer</category>
      <category>LSP</category>
      <content:encoded><![CDATA[<p>🐇 Welcome to the November 7th issue of Xvc Devlog. In the <a href="https://emresahin.net/xvc-devlog-221105">previous devlog</a>, we began to implement Git integration. How is it going, Mr. Tortoise?</p>
<p>🐢 It looks like we don’t have many architectural problems.</p>
<p>🐇 You’re forgetting how <code>Exec::cmd</code> works, though. You have those kinds of problems.</p>
<p>🐢 Yeah, I’m figuring that out. <code>Exec::shell</code> requires a string to run on the shell, while <code>Exec::cmd</code> just needs a command name. You have to supply <code>args</code> with another function. I’m writing a closure now to handle this.</p>
<hr>
<p>🐢 It started working, but it revealed a much bigger problem: <code>.xvc/</code> is not added to <code>.gitignore</code> in <code>xvc init</code>.</p>
<p>🐇 Wow, that’s a showstopper.</p>
<p>🐢 Yup. I think I should fix it as well.</p>
<p>🐇 On closer glance, I think the problem is not <code>xvc init</code> <em>not modifying</em> <code>.gitignore</code>; it modifies it incorrectly. Maybe putting certain files on a whitelist is a better idea than trying to blacklist everything.</p>
<p>🐢 Yeah, we should define a set of <em>Git-tracked</em> files and directories, whitelist them, and let all other files be ignored.</p>
<p>🐇 Go ahead, then.</p>
<p>🐢 I think I’ve fixed it. There were two problems: the initial gitignore content was wrong, and it was placed in the root of the repository instead of <code>.xvc</code>.</p>
<p>🐇 Maybe putting it directly in the root is better than hiding it in <code>.xvc</code>. What do you think?</p>
<p>🐢 There seems to be an assumption in <code>file track</code> to have a <code>.gitignore</code> file somewhere. I think we should also handle changes in <code>.gitignore</code> files throughout the repository.</p>
<p>🐇 Umm, yes. We should handle <code>.gitignore</code> files as well. But not all <code>.gitignore</code> files are changed by Xvc. How can we make sure they were modified by Xvc?</p>
<p>🐢 I think there are ways to do that, like tracking them somewhere. But I believe we shouldn’t try. We should just get a list of <code>.gitignore</code> files, <code>git add</code> them, and include them in the commit.</p>
<p>🐇 Ok. Let’s write a test for this as well. No <code>.gitignore</code> should appear in <code>git status -s</code>.</p>
<p>🐢 I wrote the test. It fails now. Do you think we should check the output of Git status to determine which files to add?</p>
<p>🐇 That may be a good idea. Git status should already know which files need to be added. We can use that information.</p>
<p>🐢 <a href="https://css-tricks.com/git-pathspecs-and-how-to-use-them/">It looks like <code>pathspec</code></a> is enough to modify <code>git add</code> behavior. We should be able to write <code>*.gitignore</code> and let all gitignore files be added.</p>
<p>🐇 Let’s try this manually.</p>
<p>🐢 Yep, it works. <code>git add '*.gitignore'</code> adds all <code>.gitignore</code> files in the subdirectories, too.</p>
<p>🐇 Congrats. 👏🥳</p>
<hr>
<p>🐢 I’m writing the missing documentation for the crates. There are <code>proc_macro not expanded</code> errors all over the code.</p>
<p>🐇 I think you should update rust-analyzer and everything. There must be a command for this.</p>
<p>🐢 I reinstalled RA with <code>:LspInstall</code> and restarted the LSP. It seems to work correctly now.</p>]]></content:encoded>
    </item>
    <item>
      <title>Developing a gitignore crate</title>
      <published>2022-06-09T02:48:03+00:00</published>
      <updated>2022-06-09T02:48:03+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Thu, 09 Jun 2022 02:48:03 +0000</pubDate>
      <link>https://emresahin.net/developing-a-gitignore-crate/</link>
      <guid isPermaLink="true">https://emresahin.net/developing-a-gitignore-crate/</guid>
      <description>I needed a file system ignore library for a utility I was writing. This is similar to Git’s .gitignore , but the files containing the rules can have different names, and ignore rules may be defined programmatically. I was using burntsushi’s ignore crate , which in turn uses globset by the same au...</description>
      <category>Rust</category>
      <category>Software Development</category>
      <category>rust</category>
      <category>gitignore</category>
      <category>architecture</category>
      <category>cli</category>
      <category>xvc</category>
      <content:encoded><![CDATA[<p>I needed a file system <em>ignore</em> library for a utility I was writing. This is similar
to Git’s <code>.gitignore</code>, but the files containing the rules can have different
names, and ignore rules may be defined programmatically.</p>
<p>I was using burntsushi’s <a href="https://crates.io/crates/ignore">ignore crate</a>, which in turn uses <a href="https://crates.io/crates/globset">globset</a> by the
same author. In a directory hierarchy, I was first collecting all <code>.ignore</code>
files, then trying to decide which files are ignored. Ripgrep’s <code>from</code>
parameter led me to believe this.</p>
<p>However, it looks like it has a bug that doesn’t consider the source directory of
the ignore file. Git has different semantics when <code>a/.gitignore</code> and
<code>a/b/.gitignore</code> have a line <code>mydir/</code>. In the second case, Git
doesn’t ignore <code>a/mydir/</code>, but Ripgrep seems to consider all elements from all
files as if they are from the root directory.</p>
<p>In practice, most people may be using ignore files in their root directory, but
there are valid use cases (like moving around directories with ignore patterns
in them) that are affected by such behavior.</p>
<p>I decided to tackle this. Ripgrep’s <a href="https://crates.io/crates/globset">globset</a> seems nice and fast, so
it was better to base the solution on top of that. My basic use case is walking
a directory hierarchy. There are solutions to this like <a href="https://docs.rs/walkdir/latest/walkdir/">walkdir</a> or <a href="https://docs.rs/jwalk/latest/jwalk/">jwalk</a>,
but they use ripgrep’s gitignore; hence, they have the same bug.</p>
<p>I usually begin by setting up data structures to understand the problem. This
allows me to understand the domain formally.</p>
<p>First, what are we trying to accomplish? Basically, we want to decide if a path
should be ignored (a) in the results and (b) in the directory traversal.
The gitignore specification not only allows <em>ignores</em>, but also has a way to add
paths to a whitelist to <em>unignore</em>. Hence, deciding whether to ignore a path is
like:</p>
<pre><code class="language-rust">let include = |path| {
    if whitelist_globs.match(path) {
       true
    } else if ignore_globs.match(path) {
       false
    } else {
       true }
}</code></pre>
<p>We also need to decide whether to traverse a directory:</p>
<pre><code class="language-rust">let traverse = |path| {
    if path.is_dir() &amp;&amp; !include(path) { false } else { true }
}</code></pre>
<p>We have the following possible <code>MatchResult</code>s between the ignore rules and a
path:</p>
<pre><code class="language-rust">pub enum MatchResult {
    NoMatch,
    Ignore,
    Whitelist,
}</code></pre>
<p>We need a comparison function to get a <code>MatchResult</code>. One side of the
comparison is <code>path</code>, and the other is a set of <code>GlobSet</code>s from the <a href="https://crates.io/crates/globset">globset</a>
crate. We’ll have two globsets: one for <code>ignores</code> and the other for
<code>whitelists</code>.</p>
<pre><code class="language-rust">pub struct IgnoreRules {
    root: PathBuf,

    patterns: Arc&lt;Vec&lt;Pattern&lt;Glob&gt;&gt;&gt;,

    whitelist_set: Arc&lt;GlobSet&gt;,
    ignore_set: Arc&lt;GlobSet&gt;,
}</code></pre>
<p>The data types for the sets are <code>Arc&lt;GlobSet&gt;</code>, as I planned to make the traversal
concurrent for each directory. However, as only one thread will be modifying
the rules (one <code>.gitignore</code> or <code>.ignore</code> file per directory), I felt no need to
wrap the sets with <code>Mutex</code>.</p>
<p>We also keep track of each pattern separately. <code>patterns</code> is a <code>Pattern</code> vector
that contains all globs individually. One reason is to rebuild the <code>GlobSet</code> as
we traverse directories: since we cannot add any more rules to the sets once they
have been built, we keep the patterns separately to add them to the sets of the
child directories.</p>
<p>Another reason is to keep track of the source, as this can be used for
debugging or reporting, similar to <a href="https://git-scm.com/docs/git-check-ignore"><code>git check-ignore</code></a>.</p>
<p><code>root</code> is the point where this traversal began.</p>
<p>Patterns are defined in a generic way to be instantiated with globs, strings,
regexes, or any other type.</p>
<pre><code class="language-rust">pub struct Pattern&lt;T&gt; {
    pattern: T,
    original: String,
    source: Source,
    effect: PatternEffect,
    relativity: PatternRelativity,
    path_kind: PathKind,
}</code></pre>
<p>The rationale for this generic approach is that the patterns need some kind of
transformation. They can be read from files as strings, then compiled into
globs or regexes.</p>
<pre class="mermaid">flowchart LR
     String--&gt;Ok(Glob)--&gt;Glob--&gt;Regex
</pre>

<p>The <code>source</code> field shows where this pattern was retrieved. It’s an enum with two
possible values for the time being:</p>
<pre><code class="language-rust">enum Source {
    File { path: PathBuf, line: usize },
    Global,
}</code></pre>
<p>The <a href="https://git-scm.com/docs/gitignore">gitignore specification</a> says:</p>
<blockquote>
<p>An optional prefix “!” which negates the pattern; any matching file excluded
by a previous pattern will become included again.</p>
</blockquote>
<p>We use the <code>effect</code> field to tag the pattern as either an <code>Ignore</code> or a
<code>Whitelist</code> pattern.</p>
<pre><code class="language-rust">pub enum PatternEffect {
    Ignore,
    Whitelist,
}</code></pre>
<p>The spec also makes a distinction between files and directories:</p>
<blockquote>
<p>If there is a separator at the end of the pattern then the pattern will only
match directories, otherwise the pattern can match both files and
directories.</p>
</blockquote>
<p>So we use <code>path_kind</code> to reflect this distinction:</p>
<pre><code class="language-rust">pub enum PathKind {
    Any,
    Directory,
}</code></pre>
<p>Some patterns can be relative to the directory they are found in:</p>
<blockquote>
<p>If there is a separator at the beginning or middle (or both) of the pattern,
then the pattern is relative to the directory level of the particular
.gitignore file itself. Otherwise the pattern may also match at any level
below the .gitignore level.</p>
</blockquote>
<p>And we show this with the <code>relativity</code> field:</p>
<pre><code class="language-rust">pub enum PatternRelativity {
    Anywhere,
    RelativeTo { directory: String },
}</code></pre>
<p>Note that we are converting from the specification to Rust types. In the future,
these enums can be extended to cover cases where these patterns are used in
other places, or we can merge some of them to reduce complexity. Currently, this
level of abstraction seems adequate.</p>
<p>To convert <code>Pattern&lt;String&gt;</code> to <code>Pattern&lt;Glob&gt;</code> (or <code>Pattern&lt;Result&lt;Glob, Error&gt;&gt;</code>), or any other type, we can use a <code>map</code> function similar to
<code>Option::map</code>:</p>
<pre><code class="language-rust">    fn map&lt;U, F&gt;(self, f: F) -&gt; Pattern&lt;U&gt;
    where
        F: FnOnce(T) -&gt; U,
    {
        let pat = Pattern::&lt;U&gt; {
            pattern: f(self.pattern),
            original: self.original,
            source: self.source,
            effect: self.effect,
            relativity: self.relativity,
            path_kind: self.path_kind,
        };
        pat
    }</code></pre>
<p><code>map</code> will make it easy to convert between various types of patterns.</p>
<p>Now, we change the perspective to a higher level. Let’s begin by writing a <em>file tree walker.</em></p>
<pre><code class="language-rust">pub fn walk_serial(
    given: IgnoreRules,
    dir: &amp;Path,
    walk_options: &amp;WalkOptions,
    sender: &amp;Sender&lt;WalkerResult&lt;PathMetadata&gt;&gt;,
) -&gt; WalkerResult&lt;()&gt; { todo!() }</code></pre>
<p>This function uses the given <code>IgnoreRules</code> and returns files and directories
via the channel. <code>PathMetadata</code> is just a bundle of <code>PathBuf</code> and its
<code>Metadata</code>.</p>
<pre><code class="language-rust">pub struct PathMetadata {
    path: PathBuf,
    metadata: Metadata,
}</code></pre>
<p><code>WalkOptions</code> will contain various options about this traversal. Currently, it only
has <code>ignore_filename</code> and <code>include_dirs</code>, which set whether only files are sent
through the channel, or if directories should be included too.</p>
<pre><code class="language-rust">pub struct WalkOptions&lt;'a&gt; {
    ignore_filename: Option&lt;&amp;'a str&gt;,
    include_dirs: bool,
}</code></pre>
<p>Initially, <code>IgnoreRules</code> contains only the files that should be ignored (or
whitelisted) globally. When a directory contains a <code>.gitignore</code> file (or any
file name that was set in <code>WalkOptions</code>), it checks for new ignore rules and sends
all files through the channel. If a directory is not ignored, it then calls
<code>walk_serial</code> recursively with it.</p>
<p>The final lines of the function are as follows:</p>
<pre><code class="language-rust">    for child_dir in child_dirs {
        walk_serial(dir_with_ignores.clone(), &amp;child_dir, walk_options, sender)?;
    }</code></pre>
<p>As it calls itself as a final operation after collecting and sending all child
files, this is an example of <em>tail recursion.</em></p>
<p>The output is sent through a channel because our final goal is to make all
this concurrent. Each new directory will create a new thread, and all files
will be collected from the channel. At this point, we are creating a serial
version of the walker with identical data structures to test it easily.</p>
<p>One gotcha using the channels in the serial version is that the channel size must
be enough to contain all file names. Effectively, this means it will be
<code>crossbeam_channel::unbounded</code> for <code>walk_serial</code>. We will (ab)use the channel’s
<code>Sender&lt;PathMetadata&gt;</code> as a <code>Vec&lt;PathMetadata&gt;</code> in <em>no parallel</em> mode. In
parallel settings, using <code>unbounded</code> channels is not recommended; if the
consumers of the channel don’t work as quickly, you may fill up the memory,
especially for longer-running processes.</p>
<p><code>walk_serial</code> will basically do four things:</p>
<ul>
<li>List all the elements in the given directory.</li>
<li>Check if there is a new ignore file and update the rules if necessary.</li>
<li>Filter the elements by ignore rules and send them to the channel.</li>
<li>Run <code>walk_serial</code> recursively for child directories if they are not ignored.</li>
</ul>
<h3 id="read-the-directory">Read the directory</h3>
<p>The first is done using <code>read_dir</code> of <code>Path</code>. It returns a set of elements.
Then the list is processed, and elements’ metadata is retrieved. This requires a
bit of error handling: <code>read_dir</code> may return an <code>Error</code>, each element in the
result can be an <code>Error</code>, and getting <code>metadata</code> may also result in an <code>Error</code>.</p>
<pre><code class="language-rust">    let elements = dir
        .read_dir()
        .map_err(|e| anyhow!("Error reading directory: {:?}, {:?}", dir, e))?;
    let mut child_dirs = Vec::&lt;PathBuf&gt;::new();
    let mut child_paths = Vec::&lt;PathMetadata&gt;::new();

    for entry in elements {
        match entry {
            Err(err) =&gt; sender.send(Err(WalkerError::from(anyhow!(
                "Error reading entry in dir {:?} {:?}",
                dir,
                err
            ))))?,
            Ok(entry) =&gt; match entry.metadata() {
                Err(err) =&gt; sender.send(Err(WalkerError::from(anyhow!(
                    "Error getting metadata {:?} {}",
                    entry,
                    err
                ))))?,
                Ok(md) =&gt; {
                    child_paths.push(PathMetadata {
                        path: entry.path(),
                        metadata: md.clone(),
                    });
                    if md.is_dir() {
                        child_dirs.push(entry.path());
                    }
                }
            },
        }
    }</code></pre>
<p>Note that we collect <code>PathMetadata</code> objects in the <code>child_paths</code> vector. If
there happens to be an error while retrieving, it’s sent to the caller to ignore or
to report to the user.</p>
<p>I usually handle errors first in <code>match</code> expressions because they are usually
more straightforward.</p>
<h3 id="check-if-there-are-new-ignore-rules">Check if there are new ignore rules</h3>
<p>After reading all the files in the directory, it checks if there actually is an
ignore filename in <code>given</code> and that a file exists with that name in the list we
just created. If there is no such file, we can use <code>given</code> rules to decide the
ignored elements.</p>
<p>If there is a file that may change the ignore rules, it reads it and updates
<code>patterns</code>. Then, if there are <em>new</em> ignore or whitelist rules, the respective
globsets are recreated. It checks whether we have new rules because compiling
globs is possibly an expensive operation.</p>
<h3 id="filtering-directory-elements">Filtering directory elements</h3>
<p>Filtering files and directories from the list checks two globsets. If an
element matches a whitelist rule, it’s included in the results without checking
if it <em>also</em> matches ignore rules. Hence, whitelisting has a priority. Otherwise,
it checks the ignore rules, and if they don’t match, the element is included in
the results.</p>
<p>If there are directories matching the ignore rules, they are not traversed, and
any possible <em>whitelisting</em> rule that was specified <em>within</em> that directory is
not taken into consideration.</p>
<h3 id="listing-child-directories">Listing child directories</h3>
<p>We saw how <code>walk_serial</code> calls itself for child directories above. After
sending all results to the channel, it calls itself for the child directories
and repeats.</p>
<h2 id="converting-patterns-from-string-to-glob">Converting patterns from String to Glob</h2>
<p>The <em>interesting</em> part about ignoring files is that we should modify patterns
into globs to add to a <code>GlobSet</code>. We cannot add the patterns directly to a <code>GlobSet</code>
because some of the rules make them <em>context dependent.</em> We parsed the patterns
to get <code>PatternRelativity</code> and <code>PathKind</code>, and these also affect how we add the
patterns to a <code>GlobSet</code>.</p>
<p>The heart of the conversion is a function that receives a <code>String</code>, filters it,
and transforms it to a list of <code>Pattern&lt;Glob&gt;</code>s.</p>
<pre><code class="language-rust">fn content_to_patterns(
    ignore_root: &amp;Path,
    source: Option&lt;&amp;Path&gt;,
    contents: &amp;str,
) -&gt; Vec&lt;Pattern&lt;WalkerResult&lt;Glob&gt;&gt;&gt; {</code></pre>
<p>As we also need to report the line number for patterns, we use <code>enumerate</code> on
the iterator:</p>
<pre><code class="language-rust">    let patterns: Vec&lt;Pattern&lt;WalkerResult&lt;Glob&gt;&gt;&gt; = content
        .lines()
        .enumerate()
        // A line starting with # serves as a comment. Put a backslash ("\") in front of the first hash for patterns that begin with a hash.
        .filter(|(_, line)| !(line.trim().is_empty() || line.starts_with("#")))</code></pre>
<p>The <code>filter</code> line is used to check for blank and comment lines. Then we trim the
trailing space unless the pattern ends with <code>\</code>.</p>
<pre><code class="language-rust">        .map(|(i, line)| {
            if !line.ends_with("\\ ") {
                (i, line.trim_end())
            } else {
                (i, line)
            }
        })</code></pre>
<p>The function receives the source file name as a parameter. We create the
<code>Source</code> using it and the line number:</p>
<pre><code class="language-rust">        .map(|(i, line)| {
            (
                line,
                match source {
                    Some(p) =&gt; Source::File {
                        path: p
                            .strip_prefix(ignore_root)
                            .expect("path must be within ignore_root")
                            .to_path_buf(),
                        line: (i + 1).into(),
                    },
                    None =&gt; Source::Global,
                },
            )
        })</code></pre>
<p>Then we build the pattern object, update the pattern to add it before the globset,
and build the globs. Building the glob may cause errors if it’s not well-formed.
They are handled as well.</p>
<pre><code class="language-rust">        .map(|(line, source)| build_pattern(source, line))
        .map(transform_pattern_for_glob)
        .map(|pc| pc.map(|s| Glob::new(&amp;s).map_err(WalkerError::from)))</code></pre>
<p>Two functions are left that may need attention. One is <code>build_pattern</code> that
converts a pattern line to a <code>Pattern&lt;String&gt;</code> object. The other is the
<code>transform_pattern_for_glob</code> function that modifies <code>Pattern&lt;String&gt;</code> before
building the glob.</p>
<p>After writing <code>walk_serial</code>, we’ll use all the machinery to write a parallel
version of it.</p>
<h3 id="parsing-pattern-strings-for-pattern-objects">Parsing pattern strings for pattern objects</h3>
<p>We create pattern objects from pattern strings following the <a href="https://git-scm.com/docs/gitignore">spec</a>. There are
three basic rules that we will follow:</p>
<ul>
<li>If the pattern starts with <code>!</code>, it’s a whitelist rule.</li>
<li>If the pattern ends with <code>/</code>, it matches only to directories.</li>
<li>If the pattern contains a non-final <code>/</code>, it’s relative to the directory.</li>
</ul>
<p>We’ll also check whether the line starts with <code>/</code> and remove that to prevent
double-slashes <code>//</code> in patterns. The resulting pattern in the object won’t
contain any of the artifacts that change its semantics, and we’ll add them when
building the globsets.</p>
<p>Let’s start by checking whether the line starts with <code>!</code>.</p>
<pre><code class="language-rust">    let begin_exclamation = original.starts_with("!");
    let line = if begin_exclamation || original.starts_with("\\!") {
        original[1..].to_owned()
    } else {
        original.to_owned()
    };</code></pre>
<p>Checking the presence of <code>/</code> at the beginning, at the end, and in intermediate
positions is also straightforward:</p>
<pre><code class="language-rust">    let end_slash = line.ends_with("/");
    let line = if end_slash {
        &amp;line[..line.len() - 1]
    } else {
        line
    };

    let begin_slash = line.starts_with("/");
    let non_final_slash = if line.len() &gt; 0 {
        line[..line.len() - 1].chars().find(|c| *c == '/').is_some()
    } else {
        false
    };</code></pre>
<p>We check the non-final slash separately from the beginning slash because we’ll
remove the initial slash if it exists.</p>
<pre><code class="language-rust">    let line = if begin_slash { &amp;line[1..] } else { line };</code></pre>
<p>Although there is almost 1-1 correspondence between the boolean conditions and
the enums, I create enums in separate statements for <em>self-documenting</em> code.</p>
<pre><code class="language-rust">    let effect = if begin_exclamation {
        PatternEffect::Whitelist
    } else {
        PatternEffect::Ignore
    };

    let path_kind = if end_slash {
        PathKind::Directory
    } else {
        PathKind::Any
    };

    let relativity = if non_final_slash {
        PatternRelativity::RelativeTo {
            directory: current_dir.to_owned(),
        }
    } else {
        PatternRelativity::Anywhere
    };</code></pre>
<p>After building these attributes of the pattern and stripping the pattern from the
initial and final <code>/</code>, and other semantic-changing elements, we build a pattern:</p>
<pre><code class="language-rust">
    let pattern = Pattern::&lt;String&gt; {
        pattern: line.to_owned(),
        original: original.to_owned(),
        source,
        effect,
        relativity,
        path_kind,
    };
</code></pre>
<h2 id="transforming-a-pattern-string-to-a-suitable-glob">Transforming a pattern string to a suitable glob</h2>
<p>Now, another transformation is needed to convert <code>Pattern&lt;String&gt;</code> to
<code>Pattern&lt;Glob&gt;</code>. In this case, we’ll need to recreate the pattern based on the
rules.</p>
<p>The transformation considers <code>relativity</code> and <code>path_kind</code> to generate a glob
that can be added to a <code>GlobSet</code>. Note that the function returns not a
compiled <code>Pattern&lt;Glob&gt;</code>, but a <code>Pattern&lt;String&gt;</code>, because after this
transformation, building the glob requires <code>Glob::new</code>.</p>
<pre><code class="language-rust">fn transform_pattern_for_glob(pattern: Pattern&lt;String&gt;) -&gt; Pattern&lt;String&gt; {
    let anything_anywhere = |p| format!("**/{p}");
    let anything_relative = |p, directory| format!("{directory}/**/{p}");
    let directory_anywhere = |p| format!("**{p}/");
    let directory_relative = |p, directory| format!("{directory}/**/{p}/");

    let transformed_pattern = match (&amp;pattern.path_kind, &amp;pattern.relativity) {
        (PathKind::Any, PatternRelativity::Anywhere) =&gt; anything_anywhere(pattern.pattern),
        (PathKind::Any, PatternRelativity::RelativeTo { directory }) =&gt; {
            anything_relative(pattern.pattern, directory)
        }
        (PathKind::Directory, PatternRelativity::Anywhere) =&gt; directory_anywhere(pattern.pattern),
        (PathKind::Directory, PatternRelativity::RelativeTo { directory }) =&gt; {
            directory_relative(pattern.pattern, directory)
        }
    };

    Pattern {
        pattern: transformed_pattern,
        ..pattern
    }
}</code></pre>
<p>I could merge the closures into the <code>match</code> and lower the number of lines, but adding
the closures increased <em>self-documentation</em>. Instead of multiple <code>if/else</code>
branches, I tend to use <code>match</code> with tuples to have a clearer picture. The
<em>corresponding</em> <code>if/else</code> code without closures would be like:</p>
<pre><code class="language-rust">
let transformed_pattern = if pattern.path_kind == PathKind::Any {
    if pattern.relativity == PatternRelativity::Anywhere {
        format!("**/{p}")
        } else if ...
        }
...</code></pre>
<p>And in my opinion, this latter format is more error-prone and less readable.</p>
<h2 id="combining-it-all-together">Combining it all together</h2>
<p>At this point, we can revisit <code>content_to_patterns</code> again:</p>
<pre><code class="language-rust linenos hl_lines=[&quot;13-14&quot;]">fn content_to_patterns(
    ignore_root: &amp;Path,
    source: Option&lt;&amp;Path&gt;,
    content: &amp;str,
) -&gt; Vec&lt;Pattern&lt;Result&lt;Glob&gt;&gt;&gt; {
    watch!(source);
    let patterns: Vec&lt;Pattern&lt;Result&lt;Glob&gt;&gt;&gt; = content
        .lines()
        .enumerate()
        ...
        .map(|(line, source)| build_pattern(source, line))
        .map(transform_pattern_for_glob)
        .map(|pc| pc.map(|s|
            Glob::new(&amp;s).map_err(Error::from)))
        .collect();

    patterns
}
</code></pre>
<p>The last step in the transformation is creating the <code>Glob</code> object, as in the
highlighted lines above. It runs <code>Pattern&lt;String&gt;.map</code> with <code>Glob::new</code> to get
a <code>Pattern&lt;Result&lt;Glob&gt;&gt;</code>. <code>Error</code> and <code>Result</code> are custom types defined within
the crate using the <a href="https://crates.io/crates/thiserror/latest/thiserror/"><code>thiserror</code></a> crate.</p>
<p>After we have a vector of <code>Pattern&lt;Result&lt;Glob&gt;&gt;</code>, we report the errors in
<code>clear_glob_errors</code> to get <code>Vec&lt;Pattern&lt;Glob&gt;&gt;</code> from this vector. The function is defined as:</p>
<pre><code class="language-rust linenos">fn clear_glob_errors(
    sender: &amp;Sender&lt;Result&lt;PathMetadata&gt;&gt;,
    new_patterns: Vec&lt;Pattern&lt;Result&lt;Glob&gt;&gt;&gt;,
) -&gt; Vec&lt;Pattern&lt;Glob&gt;&gt; {
    let new_glob_patterns: Vec&lt;Pattern&lt;Glob&gt;&gt; = new_patterns
        .into_iter()
        .filter_map(|p| match p.transpose() {
            Ok(p) =&gt; Some(p),
            Err(e) =&gt; {
                sender
                    .send(Err(Error::from(anyhow!("Error in glob pattern: {:?}", e))))
                    .expect("Error in channel");
                None
            }
        })
        .collect();
    new_glob_patterns
}</code></pre>
<p>The <code>sender</code> is the same channel that we use to send the results. This
communication channel is used to report <em>only the errors</em>, not <code>PathMetadata</code>;
hence, when we encounter an <code>Err(e)</code> in <code>new_patterns</code>, it’s reported to the same
channel that the results are expected. Handling the errors first seems like a
common theme here as well.</p>
<p>This is also one of the reasons <code>walk_serial</code> receives a channel instead of a
vector. Our usual use case is traversing all the directories in parallel, and
without channel-based communication in the serial version, we would need to
duplicate this function.</p>
<p>At this point, I completed the <code>walk_serial</code> functionality. I wrote tests. I’m using the
<a href="https://crates.io/crates/test-case"><code>test-case</code></a> crate for writing many tests encompassing different use cases.
I’m omitting them here for brevity.</p>
<h2 id="writing-a-parallel-walker">Writing a Parallel Walker</h2>
<p>Converting <code>walk_serial</code> to a parallel one is quite trivial. As we already used
channels in the serial version, we have all the components of a parallel
walker. For the sake of simplicity, I won’t use a thread pool and instead spawn a new
thread for each <em>task.</em> One question is what’s the unit task that we’ll use to
create tasks.</p>
<p><a href="https://docs.rs/jwalk/latest/jwalk/">jwalk</a> uses directories as points to spawn new tasks. That’s a sensible
approach, and I’ll use this as well. This means a single directory will be
processed by a single thread, even if it contains one million files, and 10
directories will be processed by 10 threads even if each contains one file. In
practice, I think we can assume <em>directories</em> are natural chunks for this
problem.</p>
<p>The interface for the parallel walker is identical to the serial version. This is
to replace one with the other when the need arises.<sup class="footnote-reference" id="fr-1-1"><a href="#footnote-1">1</a></sup></p>
<p>The only difference is in the final loop. <code>walk_serial</code> calls itself
recursively at the end, while <code>walk_parallel</code> calls itself in <em>separate threads</em>.</p>
<pre><code class="language-rust">
pub fn walk_parallel(
    given: IgnoreRules,
    dir: &amp;Path,
    walk_options: WalkOptions,
    sender: Sender&lt;Result&lt;PathMetadata&gt;&gt;,
) -&gt; Result&lt;()&gt; {
    ....
    crossbeam::scope(|s| {
        for child_dir in child_dirs {
            let dwi = dir_with_ignores.clone();
            let walk_options = walk_options.clone();
            let sender = sender.clone();
            s.spawn(move |_| walk_parallel(dwi, &amp;child_dir.path, walk_options, sender));
        }
    })
    .expect("Error in crossbeam scope in walk_parallel");

    Ok(())
}

</code></pre>
<h2 id="code">Code</h2>
<p>The code is soon to be released, and I’ll add a link to it. You can check <a href="https://github.com/iesahin">my
GitHub profile</a> if I forget.</p>
<hr>
<ol class="footnote-definition">
<li id="footnote-1">
<p>Update: I decided to change the <code>walk_serial</code> interface to use <code>Vec</code>
instead of <code>Sender</code> for simplicity. <a href="#fr-1-1">↩</a></p>
</li>
</ol>]]></content:encoded>
    </item>
  </channel>
</rss>
