<?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 🍃 - Software Development</title>
    <link>https://emresahin.net/categories/software-development/</link>
    <description>Posts in the Software Development category</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/categories/software-development/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>devlog 9</title>
      <published>2025-01-04T13:03:16+00:00</published>
      <updated>2025-01-04T13:03:16+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Sat, 04 Jan 2025 13:03:16 +0000</pubDate>
      <link>https://emresahin.net/devlog-9/</link>
      <guid isPermaLink="true">https://emresahin.net/devlog-9/</guid>
      <description>🐢 Today I’m planning to add cross-compilation to the Xvc 0.6.13 branch to provide more platform support. 🐇 It looks like you first need to turn off this ghost text from the blink output. It makes writing insufferable. 🐢 Yep, let’s do that first. 🐇 Now, let’s restart Neovim. 🐢 I don’t know why bli...</description>
      <category>devlog</category>
      <category>Software Development</category>
      <category>blink-cmp</category>
      <category>github-cli</category>
      <category>github-actions</category>
      <category>xvc</category>
      <category>reflinks</category>
      <category>cross-compilation</category>
      <category>rust</category>
      <category>ci-cd</category>
      <content:encoded><![CDATA[<p>🐢 Today I’m planning to add cross-compilation to the Xvc 0.6.13 branch to provide more platform support.</p>
<p>🐇 It looks like you first need to turn off this ghost text from the <code>blink</code> output. It makes writing insufferable.</p>
<p>🐢 Yep, let’s do that first.</p>
<p>🐇 Now, let’s restart Neovim.</p>
<p>🐢 I don’t know why <code>blink.cmp</code> doesn’t prioritize the emojis I use. Maybe we can just use <code>#tor</code> and <code>#rab</code> for ourselves.</p>
<p>🐇 I think over time it will learn that the emojis I defined in the snippets file should have higher priority, but let’s skip this for now. What do we need to do to add cross-compilation?</p>
<p>🐢 Maybe we can just make the completion menu wait a bit longer. It shows up almost instantly, and I want it to wait for a few more milliseconds.</p>
<p>🐇 Okay, let’s look at the config.</p>
<p>🐢 The configuration file doesn’t seem to have a key for this. Let’s search: <code>blink.cmp</code>.</p>
<p>🐇 I think the culprit is typo resistance; it causes better options to be pushed down: <a href="https://cmp.saghen.dev/configuration/reference#fuzzy">https://cmp.saghen.dev/configuration/reference#fuzzy</a></p>
<p>🐢 Let’s turn that off.</p>
<p>🐇 There is an error in the configuration file. I don’t know why it’s failing.</p>
<p>🐢 I added emojis to Espanso and checked the error message. The configuration I copied from their docs seems to be broken. The message is:</p>
<pre><code class="language-text">...share/nvim/lazy/blink.cmp/lua/blink/cmp/config/utils.lua:14: fuzzy.max_items: unexpected field found in configuration
</code></pre>
<p>I’ll just delete that line.</p>
<p>🐇 It still doesn’t prioritize snippets, but we can look into this later. Espanso seems to be a better tool for this anyway.</p>
<p>🐢 Yep. Let’s look into cross-compilation support for Rust.</p>
<p>🐇 The well-known option is <code>cross.rs</code>: <a href="https://github.com/cross-rs/cross">https://github.com/cross-rs/cross</a></p>
<p>🐢 We can start with that. Installation is from the Git repository:</p>
<pre><code class="language-bash">$ cargo install cross --git https://github.com/cross-rs/cross
    Updating git repository `https://github.com/cross-rs/cross`
    Updating git submodule `https://github.com/cross-rs/cross-toolchains.git`
  Installing cross v0.2.5 (https://github.com/cross-rs/cross#4090beca)
...
   Installed package `cross v0.2.5 (https://github.com/cross-rs/cross#4090beca)` (executables `cross`, `cross-util`)
</code></pre>
<p>🐇 Now we have the <code>cross</code> and <code>cross-util</code> commands. Cross-compilation requires Podman on Linux or Docker on macOS. Do we have Docker?</p>
<p>🐢 It looks like we don’t. Maybe we can just set up a remote build using Podman or GitHub Actions. I saw a crate for that yesterday; I remember saving it somewhere but can’t find it now. Searching again seems easier, which says something about my archival and retrieval habits.</p>
<p>🐇 Maybe later you can add some vector search capabilities to your archive—semantic search.</p>
<p>🐢 Yep, <em>sometime</em> later.</p>
<p>🐇 Now let’s search for “adding rust cross compilation to github actions.”</p>
<p>🐢 I found a link to the action: <a href="https://github.com/marketplace/actions/build-rust-projects-with-cross">https://github.com/marketplace/actions/build-rust-projects-with-cross</a></p>
<p>Let’s look at the example:</p>
<pre><code class="language-yaml">jobs:
  release:
    name: Release - ${{ matrix.platform.os-name }}
    strategy:
      matrix:
        platform:
          - os-name: FreeBSD-x86_64
            runs-on: ubuntu-20.04
            target: x86_64-unknown-freebsd
            skip_tests: true

          - os-name: Linux-x86_64
            runs-on: ubuntu-20.04
            target: x86_64-unknown-linux-musl

          - os-name: Linux-aarch64
            runs-on: ubuntu-20.04
            target: aarch64-unknown-linux-musl

          - os-name: Linux-riscv64
            runs-on: ubuntu-20.04
            target: riscv64gc-unknown-linux-gnu

          - os-name: Windows-x86_64
            runs-on: windows-latest
            target: x86_64-pc-windows-msvc

          - os-name: macOS-x86_64
            runs-on: macOS-latest
            target: x86_64-apple-darwin

          # more targets here ...

    runs-on: ${{ matrix.platform.runs-on }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Build binary
        uses: houseabsolute/actions-rust-cross@v0
        with:
           command: ${{ matrix.platform.command }}
          target: ${{ matrix.platform.target }}
          args: "--locked --release"
          strip: true
      - name: Publish artifacts and release
        uses: houseabsolute/actions-rust-release@v0
        with:
          executable-name: ubi
          target: ${{ matrix.platform.target }}
</code></pre>
<p>🐇 We can just convert the current configuration to this and see if it works.</p>
<p>🐢 Let’s do that. I created <code>.github/workflows/release.yml</code> and will update it.</p>
<p>🐇 Let’s check the results with <code>gh</code>:</p>
<pre><code class="language-bash">$ gh -R iesahin/xvc run list
completed	failure	v0.6.13	Release	v0.6.13	pull_request	12525070531	34s	2024-12-28T08:17:28Z
</code></pre>
<p>🐢 It says the release has completed.</p>
<pre><code class="language-bash">$ gh -R iesahin/xvc run view 12525070531

X v0.6.13 Release iesahin/xvc#263 · 12525070531
Triggered via pull_request about 3 minutes ago

JOBS
X Release - FreeBSD-x86_64 in 12s (ID 34936257653)
  ✓ Set up job
  ✓ Checkout
  X Build binary
  - Publish artifacts and release
  ✓ Post Build binary
  ✓ Post Checkout
  ✓ Complete job
...
</code></pre>
<p>Now let’s look at the failure:</p>
<pre><code class="language-bash">$ gh -R iesahin/xvc run view 12525070531 --log-failed
</code></pre>
<p>🐇 Change the order and see what happens. Maybe we should first succeed in a non-cross-compilation build.</p>
<pre><code class="language-bash">$ gh -R iesahin/xvc run list
completed	failure	v0.6.13	Release	v0.6.13	pull_request	12525178664	35s	2024-12-28T08:34:18Z
...
$ gh -R iesahin/xvc run view   12525178664 --log-failed
...
</code></pre>
<p>🐢 It’s the same error. The <a href="https://github.com/houseabsolute/ubi/blob/master/.github/workflows/ci.yml">usage example</a> is actually much more sophisticated.</p>
<p>🐇 The issue is that we’re asking for <code>command</code> from the matrix, but the matrix doesn’t define it. That’s the second time today an example from the documentation has failed. I’ve added <code>command</code> for each platform. We can also add separate features this way to make platform-specific functionality work.</p>
<p>🐢 Agreed. Let’s look at it once more.</p>
<pre><code class="language-bash">$ gh -R iesahin/xvc run list | rg Release | head -n 1
completed	failure	v0.6.13	Release	v0.6.13	pull_request	12525253982	41s	2024-12-28T08:46:23Z

$ gh -R iesahin/xvc run view 12525253982 --log-failed
...
Release - macOS-x86_64	Build binary	2024-12-28T08:46:44.8464010Z  [1m [31merror [0m [1m: [0m the lock file /Users/runner/work/xvc/xvc/Cargo.lock needs to be updated but --locked was passed to prevent this
</code></pre>
<p>🐇 Ah, that’s a different error. Let’s remove the <code>--locked</code> flag and retry.</p>
<pre><code class="language-bash">$ gh -R iesahin/xvc run list | rg Release | head -n 1
completed	failure	v0.6.13	Release	v0.6.13	pull_request	12525289176	1m8s	2024-12-28T08:53:23Z
</code></pre>
<p>🐢 We’re finally starting to get some good news. Now we’re getting OpenSSL errors. We need feature flags for these platforms or to specify where OpenSSL is. Let’s add <code>bundled-openssl</code> to the failed ones. We also need <code>bundled-sqlite</code> for Windows binaries.</p>
<pre><code class="language-bash">gh -R iesahin/xvc run list | rg Release | head -n 1
completed	failure	v0.6.13	Release	v0.6.13	pull_request	12525354179	3m9s	2024-12-28T09:04:55Z
</code></pre>
<p>🐇 It looks like the <code>Changes.md</code> file is missing, and it can’t upload the binaries as a release because of this. I’ll set the changes file to <code>CHANGELOG.md</code>.</p>
<p>🐢 It’s weird to fail because of that. I think we should report these errors and possibly send a PR to make that file optional.</p>
<pre><code class="language-bash">gh -R iesahin/xvc run list | rg Release | head -n 1
in_progress		v0.6.13	Release	v0.6.13	pull_request	12525449368	1m38s	2024-12-28T09:18:40Z
...
✓ Release - macOS-x86_64 in 2m39s (ID 34937017695)
✓ Release - macOS-aarch64 in 2m42s (ID 34937017787)
..
ARTIFACTS
xvc-macOS-x86_64.tar.gz
xvc-macOS-arm64.tar.gz
</code></pre>
<p>🐢 It looks like <code>Linux-riscv64</code> has OpenSSL compilation errors. I think we can skip this platform for now. The goal was to add <code>aarch64</code> for macOS, and that seems to have succeeded.</p>
<pre><code class="language-bash">gh -R iesahin/xvc run list | rg Release | head -n 1
completed	failure	v0.6.13	Release	v0.6.13	pull_request	12525504079	3m24s	2024-12-28T09:26:43Z
...
X Release - Linux-x86_64	Build binary	2024-12-28T09:29:16.2262518Z  [0m [1m [38;5;9merror[E0308] [0m [0m [1m: mismatched types [0m
...
</code></pre>
<p>🐢 It looks like <code>Linux-x86_64</code> doesn’t support reflinks. Maybe we can make reflinks an optional feature and add it specifically to macOS and Windows targets.</p>
<p>🐇 I’ve removed <code>reflink</code> from the default features. This is a breaking change, but <em>fortunately</em> we don’t have many users who will be affected by it.</p>
<p>Let’s check the results once more.</p>
<pre><code class="language-bash">gh -R iesahin/xvc run list | rg Release | head -n 1
completed	failure	v0.6.13	Release	v0.6.13	pull_request	12525597866	3m39s	2024-12-28T09:42:44Z
</code></pre>
<p>🐢 Now turn off the <code>NetBSD</code> target as well.</p>
<p>🐇 <code>FreeBSD</code> and <code>Linux-x86_64</code> targets are building. Let’s see why the ARM Linux targets are failing.</p>
<pre><code class="language-bash">gh -R iesahin/xvc run list | rg Release | head -n 1
completed	failure	v0.6.13	Release	v0.6.13	pull_request	12525643284	4m46s	2024-12-28T09:52:04Z
</code></pre>
<p>🐇 It looks like those targets don’t have <code>libsqlite3</code> installed. Let’s add <code>bundled-sqlite</code> to these targets too.</p>
<pre><code class="language-bash">gh -R iesahin/xvc run list | rg Release | head -n 1
completed	failure	v0.6.13	Release	v0.6.13	pull_request	12525734325	4m11s	2024-12-28T10:07:36Z
</code></pre>
<p>🐢 Adding Android as a target didn’t work. Let’s remove it for now; it seems to require more work.</p>
<p>🐇 I think we’ll eventually move to using Xvc to distribute binaries. We could build the Android binary on Termux and link it on the releases page or push it as a release artifact.</p>
<p>🐢 I need to learn more about GitHub releases. If we can add artifacts to the release, maybe we can do some of this work locally.</p>
<p>🐇 We can start by looking at the capabilities of <code>gh</code> commands.</p>
<p>🐢 It looks like it’s possible to upload assets to releases. Let’s check how that works.</p>
<pre><code class="language-bash">gh release upload --help
</code></pre>
<p>🐇 So basically, we can just upload files to tags. We can list releases and work with them like anything else.</p>
<pre><code class="language-bash">gh -R iesahin/xvc release list
</code></pre>
<p>And we can delete releases:</p>
<pre><code class="language-bash">for r in v0.4.2-alpha.8 v0.4.2-alpha.7 v0.4.2-alpha.6 v0.4.2-alpha.5 v0.4.2-alpha.0 v0.4.1-alpha.0; do
  gh -R iesahin/xvc release delete "${r}"
done
</code></pre>
<p>🐢 Now let’s list them again.</p>
<pre><code class="language-bash">gh -R iesahin/xvc release list
</code></pre>
<p>🐇 The latest one has failed again.</p>
<pre><code class="language-bash">X Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
</code></pre>
<p>🐢 It looks like we’re coming to the end of this session. Under what conditions will we create a release?</p>
<p>🐇 I think it’s better to release only non-alpha tags.</p>
<p>🐢 Then the rule in the workflow will be something like:</p>
<pre><code class="language-yaml">on:
  workflow_dispatch:
  push:
    tags:
      - "v*.*.*"
      - "!v.*.*-alpha.*"
</code></pre>
<p>🐇 And now we have this result:</p>
<pre><code class="language-bash">✓ v0.6.13 Release iesahin/xvc#263 · 12533498361
Triggered via pull_request about 21 minutes ago

JOBS
✓ Release - FreeBSD-x86_64 in 4m14s
✓ Release - Linux-x86_64 in 4m3s
✓ Release - Linux-aarch64 in 4m50s
✓ Release - Windows-x86_64 in 8m47s
✓ Release - Windows-aarch64 in 6m42s
✓ Release - macOS-x86_64 in 2m37s
✓ Release - macOS-aarch64 in 2m36s
</code></pre>
<p>🐢 Nice! Let’s check the release list.</p>
<pre><code class="language-bash">gh -R iesahin/xvc release list
</code></pre>
<p>🐇 Why is there no “latest” release?</p>
<p>🐢 We have to tag it first.</p>
<p>🐇 Ah, right. Let’s tag it then.</p>
<p>🐢 I’ve pushed the changes and tagged them with <code>v0.6.13-alpha.5</code>.</p>
<p>🐇 I think it’s possible to make a release today.</p>
<p>🐢 It looks like it, yes.</p>
<p>🐇 We can merge the PR and tag it. Then everything should work.</p>
<p>🐢 We need some cleanup in the YAML files, though.</p>
<p>🐇 We can leave that for the next release.</p>
<pre><code class="language-bash">gh -R iesahin/xvc run list
in_progress		v0.6.13	Rust-CI	v0.6.13	pull_request	12533690474	9m18s	2024-12-29T08:02:29Z
completed	success	Release	Release	v0.6.13-alpha.5	push	12533689099	9m11s	2024-12-29T08:02:19Z
</code></pre>
<p>🐢 Now we have another failure in the regular CI. Let’s look at it.</p>
<p>🐇 The issue seems to be in the doc tests:</p>
<pre><code class="language-diff">- Total #: 8 Workspace Size:         276 Cached Size:          19
+ Total #: 8 Workspace Size:         278 Cached Size:          19
</code></pre>
<p>🐢 These tests are brittle, but they provide valuable information. Let’s fix it and push again.</p>
<p>🐇 Done. We can also remove some of the watches that produce so many logs.</p>
<p>🐢 I’m a bit ambivalent about them. I thought we could use these watches when debugging, but experience has shown that we need more granular watches during debugging and almost never use these otherwise. Let’s remove some of them.</p>
<p>🐇 We can increase the output for certain commands, but the tracing output doesn’t help much in regular runs. If Xvc gets popular enough that we can’t cope with bug reports, we can always add more watches.</p>
<p>🐢 Another option is to exclude the watch code from the release build, but that won’t change anything for our debug cycles.</p>
<p>🐇 I think removing them is a fair trial. We can always put them back when debugging.</p>
<p>🐢 Watches could also produce regular output instead of tracing. That way we won’t forget to remove them.</p>
<p>🐇 Ah, yep, that’s a good option too.</p>
<p>🐢 We can have a <code>trace!</code> macro similar to the current one for user consumption, and a <code>watch!</code> macro that sends output to <code>stderr</code>.</p>
<p>🐇 Good idea. Let’s do that in the next release.</p>
<p>🐢 Let’s check the tests before pushing this cleanup.</p>
<pre><code class="language-bash">gh -R iesahin/xvc run list
completed	success	v0.6.13	Rust-CI	v0.6.13	pull_request	12533993607	13m49s	2024-12-29T08:47:50Z
</code></pre>
<p>🐇 CI succeeded, and the release didn’t run. Let’s do some more cleanup.</p>
<pre><code class="language-bash">gh -R iesahin/xvc run list
in_progress		v0.6.13	Rust-CI	v0.6.13	pull_request	12534416152	43s	2024-12-29T09:57:02Z
</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>TIL 12: Modern C Features</title>
      <published>2025-01-04T12:58:23+00:00</published>
      <updated>2025-01-04T12:58:23+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Sat, 04 Jan 2025 12:58:23 +0000</pubDate>
      <link>https://emresahin.net/til-12/</link>
      <guid isPermaLink="true">https://emresahin.net/til-12/</guid>
      <description>🐢 There are some new features in C since I learned it in the 90s. One is variable-length arrays (VLAs). You can now set the length of an array at runtime. This makes some uses of pointers moot, but Linus Torvalds is reported to have said that the Linux kernel does not contain any VLAs. 🐇 It may b...</description>
      <category>Software Development</category>
      <category>C</category>
      <category>arrays</category>
      <category>variable length arrays</category>
      <category>complex numbers</category>
      <category>initialization</category>
      <category>dialog</category>
      <category>TIL</category>
      <content:encoded><![CDATA[<p>🐢 There are some new features in C since I learned it in the 90s. One is variable-length arrays (VLAs). You can now set the length of an array at runtime. This makes some uses of pointers moot, but Linus Torvalds is reported to have said that the Linux kernel does not contain any VLAs.</p>
<p>🐇 It may be useful, though. It looks like syntactic sugar for a <code>const</code> pointer plus <code>malloc</code>, but implicitness is usually not ideal. It also appears to use the stack.</p>
<p>🐢 I haven’t looked into the details, but it may be useful in some cases. I don’t think it can replace pointer usage if it uses the stack, though. Stack sizes are usually small, and having arrays that can fill them up is not a good approach. Another feature is complex number support. It seems from the lecture that this is also a bit of a half-baked feature.</p>
<p>🐇 What is the difference between this and using a struct with two float fields?</p>
<p>🐢 There are operators (+, -, *, or ==) that support these. Since there is no operator overloading in C, having a separate complex number type may be useful.</p>
<p>🐇 The example looks like <code>double complex cx = 1.0 + 3.0*I</code>; and yes, this may be useful if you’re frequently using complex numbers in your code. But I think it’s unnecessary in most cases. There shouldn’t be such a frequent need for complex numbers, right?</p>
<p>🐢 A good complex number library will probably provide more than the built-in type, such as vectors for these numbers. A dedicated library will still be needed in most cases, I believe.</p>
<p>🐇 Another feature added to C is that struct members can now be initialized by name or index. You can initialize an array like <code>int a[6] = { [3] = 29, [2] = 14 };</code>, and it will initialize only those specific members.</p>
<p>🐢 Ah, this is much more useful than complex numbers for the general case.</p>
<p>🐇 There is also a span syntax: <code>int a[10] = {1, 2, [2 ... 4] = 3, [5] = 30};</code>.</p>
<p>🐢 That is really neat.</p>
<p>🐇 It’s also possible to omit the initial length, so the initializer values determine the size of the array. For example, <code>int a[] = {1, 3, [30] = 55, [9090] = 1010};</code> will result in an array of length 9091.</p>
<p>🐢 How is this used with structs?</p>
<p>🐇 Instead of <code>[]</code>, in struct initializers, we use the dot notation, like <code>struct Point p1 = { .x=0, .y=10 };</code>.</p>
<p>🐢 This is really useful and would keep much of the initialization code simpler.</p>
<p>🐇 There is also a way to initialize arrays of structs, like:</p>
<pre><code class="language-c">struct points pts[5] = { [0].x = 10, [0].y = 20, [3].x = 100 };
</code></pre>
<p>🐢 This feature is a nice addition to C; I really like it.</p>]]></content:encoded>
    </item>
    <item>
      <title>devlog 8</title>
      <published>2025-01-04T12:41:16+00:00</published>
      <updated>2025-01-04T12:41:16+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Sat, 04 Jan 2025 12:41:16 +0000</pubDate>
      <link>https://emresahin.net/devlog-8/</link>
      <guid isPermaLink="true">https://emresahin.net/devlog-8/</guid>
      <description>🐢 The only failure was the patch coverage in Xvc. Let’s see what needs to be done. 🐇 It looks, from the coverage page , that our additions to HStore don’t have any tests. Maybe we can add some unit tests to the new joins. 🐢 I don’t find unit tests particularly useful, but let’s use GitHub Copilot...</description>
      <category>devlog</category>
      <category>Software Development</category>
      <category>xvc</category>
      <category>rust</category>
      <category>coverage</category>
      <category>cargo-publish</category>
      <category>github-copilot</category>
      <category>claude</category>
      <category>dufs</category>
      <category>neovim</category>
      <category>testing</category>
      <content:encoded><![CDATA[<p>🐢 The only failure was the patch coverage in Xvc. Let’s see what needs to be done.</p>
<p>🐇 It looks, from the <a href="https://app.codecov.io/gh/iesahin/xvc/pull/263?src=pr&amp;el=tree&amp;utm_medium=referral&amp;utm_source=github&amp;utm_content=comment&amp;utm_campaign=pr+comments&amp;utm_term=Emre+Sahin">coverage page</a>, that our additions to HStore don’t have any tests. Maybe we can add some unit tests to the new joins.</p>
<p>🐢 I don’t find unit tests particularly useful, but let’s use GitHub Copilot to add some for us.</p>
<p>🐇 I added a unit test and a doc test for <code>full_join</code>. I think doc tests have more value; they provide documentation, and we can readily see how to use a function from its docs. It’s better to increase coverage with doc tests.</p>
<p>🐢 There are things I should keep in mind while writing doc tests. Imports must use the full path, not <code>crate</code>. Also, the tested struct doesn’t have implicit imports.</p>
<p>🐇 The ceremony for adding keys and values is a bit too much. It might be worthwhile to add an <code>insert</code> method for anything that implements <code>Into&lt;XvcEntity&gt;</code>.</p>
<p>🐢 That would certainly save time—no more typing <code>.into()</code> for each key! :)</p>
<p>🐇 Pushed to test again. Should we have some way to test coverage locally?</p>
<p>🐢 I don’t think we need to check coverage locally. It’s not worth our time right now.</p>
<p>🐇 Now, while waiting for the tests to complete, what else can we do?</p>
<pre><code class="language-bash">gh -R iesahin/xvc run list
completed failure v0.6.13 Rust-CI v0.6.13 pull_request 12543831360 3m54s 2024-12-30T08:15:06Z
...
</code></pre>
<p>🐢 That didn’t take too long. Let’s view the results:</p>
<pre><code class="language-bash">gh -R iesahin/xvc run view 12543831360
...
  X Run Current Dev Tests
...
To see what failed, try: 
View this run on GitHub: https://github.com/iesahin/xvc/actions/runs/12543831360
</code></pre>
<p>🐢 The current dev tests are failing for some reason. Let’s run them locally.</p>
<p>🐇 We’re missing <code>llvm-tools-preview</code> locally. How do I install this?</p>
<p>🐢 The command is <code>rustup component add llvm-tools-preview</code>:</p>
<pre><code class="language-text">info: component 'llvm-tools' for target 'aarch64-apple-darwin' is up to date
</code></pre>
<p>🐇 It’s already installed. We just need to set the environment variables.</p>
<p>🐢 Instead, we can just turn off dev tests for the time being. We don’t really need them; our local tests pass.</p>
<p>🐇 Yeah, okay. We don’t need to solve every single bit of these issues right now.</p>
<pre><code class="language-bash">gh -R iesahin/xvc run list
...
</code></pre>
<p>🐇 Okay, let’s take a look at the run again.</p>
<pre><code class="language-bash">gh -R iesahin/xvc run list
completed failure v0.6.13 Rust-CI v0.6.13 pull_request 12544020064 3m39s 2024-12-30T08:33:25Z

gh -R iesahin/xvc run view 12544020064
...
  X Test and Coverage
...

gh run view 12544020064 --log-failed
...
Test and Coverage (stable) Test and Coverage 2024-12-30T08:36:58.8911690Z Error: ProcessError { stdout: "", stderr: "  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\ncurl: (7) Failed to connect to e1.xvc.dev port 80 after 160 ms: Couldn't connect to server\n" }
...
</code></pre>
<p>🐢 We need to start Nginx on the server. We forgot to do that yesterday.</p>
<p>🐇 Ah, right. After that <code>dufs</code> installation. Okay.</p>
<p>🐢 We also need to add a reverse proxy for <code>dufs</code> somehow, but that’s for later.</p>
<p>🐇 For this use case, I don’t think it’s necessary. We can just adjust the port to a non-standard one if we need 443 for something else. Let’s look at the tests again.</p>
<p>🐢 Let’s add another doc test, this time to <code>XvcStore</code>.</p>
<pre><code class="language-bash">cargo test -p xvc-ecs --doc
...
test result: ok. 8 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 2.87s
</code></pre>
<p>🐇 Sent the files again. Waiting for the tests to finish.</p>
<p>🐢 Let’s check the keymaps file in the meantime.</p>
<p>🐇 I tried reading the documentation but didn’t see an error. Maybe we should just set it to non-lazy and disallow remaps.</p>
<p>🐢 We’ve already spent too much time on this.</p>
<p>🐇 Yes, let’s check the tests again.</p>
<pre><code class="language-bash">gh -R iesahin/xvc run list
completed success v0.6.13 Rust-CI v0.6.13 pull_request 12544291068 8m10s 2024-12-30T09:00:42Z
...
</code></pre>
<p>🐢 Oh, nice, the merge is ready.</p>
<p>🐇 Patch coverage is still behind the target, though.</p>
<p>🐢 Yeah, but let’s release this one and ensure the next one is better covered. Also, I’m not sure if the doc tests actually affected the coverage report.</p>
<p>🐇 If we look at the <a href="https://app.codecov.io/gh/iesahin/xvc/pull/263?src=pr&amp;el=tree&amp;utm_medium=referral&amp;utm_source=github&amp;utm_content=comment&amp;utm_campaign=pr+comments&amp;utm_term=Emre+Sahin">coverage page</a> again, we can see.</p>
<p>🐢 It seems codecov.io doesn’t consider coverage for doc tests. That’s a bit weird, but let’s not spend more time on it.</p>
<p>🐇 Sure, let’s merge.</p>
<p>🐢 I think we forgot to bump the versions in the <code>Cargo.toml</code> files. We’ll have to do that in <code>main</code>.</p>
<p>🐇 Oh, yeah. Let’s bump them and tag the release as well.</p>
<p>🐢 Now we can wait for all the files to be produced. What’s next?</p>
<p>🐇 We can release the Python version too. It shouldn’t need any changes.</p>
<p>🐢 Right. Maybe we can add a few tests there as well.</p>
<p>🐇 Let’s bump the version first and see.</p>
<p>🐢 I’ve bumped the versions in <code>Cargo.toml</code> and run <code>maturin develop</code>.</p>
<p>🐇 It seems ready now.</p>
<p>🐢 The main branch is failing, though. The “Publish crates” action is looking for <code>libsqlite3</code>. Let’s investigate.</p>
<pre><code class="language-bash">gh -R iesahin/xvc run list
completed failure Release v0.6.13 Publish Crates v0.6.13 push 12544753359 6m1s 2024-12-30T09:41:56Z
</code></pre>
<p>🐢 The issue is that the VM doesn’t have <code>libsqlite3-dev</code>. Let’s add it.</p>
<p>🐇 We need to restart the job manually. Let’s skip tagging this time.</p>
<p>🐢 Some of the packages were already published, and now they’re causing failures because crates.io says they already exist. Maybe we can check if a package is already published before trying.</p>
<p>🐇 Let’s see if we can make <code>cargo publish</code> more forgiving.</p>
<p>🐢 There doesn’t seem to be an easy option. Let’s search for “how to skip published packages in workspace to avoid errors with cargo publish.”</p>
<p>🐇 Claude is hallucinating again. Let’s try a manual approach: how to skip already published packages?</p>
<p>🐢 It might be easier to just add a check. How do we get that info?</p>
<p>🐇 Or we can just move on to the next package if one is already available.</p>
<p>🐢 Let’s push the missing packages manually this time.</p>
<p>🐢 We should have a key to open garden files quickly. What does <code>Fzf-Lua files</code> receive as arguments?</p>
<p>🐇 Let’s check the help page: <code>fzf-lua</code>.</p>
<p>🐢 Before that, maybe we can try to fix why selected lines are not searched in Visual-Line mode?</p>
<p>🐇 Yep, let’s fix our search first.</p>
<p>🐢 How do you set a key in visual line mode in Neovim Lua?</p>
<p>🐇 The abbreviation is <code>V</code>. Let’s try that.</p>
<p>🐢 Looks like we need to restart the session.</p>
<p>🐇 It’s still not working. Let’s check <code>:map</code>.</p>
<p>🐢 It seems it needs more care; I’ll check it later.</p>
<p>👨🏾‍🦲
🐢 Bence kendini biraz daha anlamlı bir işle uğraştırmalısın.
🐇 Ne gibi?
🐢 Belki biraz daha yayın yapmalısın, biraz daha işe başvurmalısın.
🐇 “Meli”, “malı” ile geçiyor ömrümüz.</p>
<p>⌚
🐢 It looks like we can move most of the daily template to links or commands. We can call it <code>ref/daily</code>. My idea is to fill the page intentionally, without templates.</p>
<p>🐇 It might be better to start with a blank page, yeah. The template makes me a bit nervous. There are too many things to fill in, and most of them aren’t things I like being pushed into doing.</p>
<p>🐢 Let’s start by moving the daily templates to <code>ref/daily</code>. No more daily template.</p>
<p>🐇 Now we can delete the rest of this page. We’ll use the daily links page and maybe have reminders at the end of these sessions.</p>
<p>👨🏽‍⚕️</p>]]></content:encoded>
    </item>
    <item>
      <title>devlog 7</title>
      <published>2024-08-06T06:43:01+00:00</published>
      <updated>2024-08-06T06:43:01+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Tue, 06 Aug 2024 06:43:01 +0000</pubDate>
      <link>https://emresahin.net/devlog-7/</link>
      <guid isPermaLink="true">https://emresahin.net/devlog-7/</guid>
      <description>I noticed that I often forget to update the Xvc CHANGELOG. To fix this, I added a pre-push hook that checks the files I’m pushing. If the CHANGELOG is not among them and there are changes to Rust files, it prevents the push. I hope this will remind me to update the logs more frequently. #!/bin/ba...</description>
      <category>devlog</category>
      <category>Software Development</category>
      <category>Git</category>
      <category>git-hooks</category>
      <category>automation</category>
      <category>changelog</category>
      <category>xvc</category>
      <category>bash</category>
      <content:encoded><![CDATA[<p>I noticed that I often forget to update the Xvc CHANGELOG. To fix this, I added
a pre-push hook that checks the files I’m pushing. If the CHANGELOG is not among
them and there are changes to Rust files, it prevents the push. I hope this will
remind me to update the logs more frequently.</p>
<pre><code class="language-bash">#!/bin/bash

# Git pre-push hook to check if CHANGELOG.md is included in the push and if the branch is develop

# Get the current branch name

current_branch=$(git rev-parse --abbrev-ref HEAD)

# Check if the branch is develop

if [ "$current_branch" == "main" ]; then
	echo "You are on the main branch. Skipping CHANGELOG.md check."
	exit 0
fi

# remote="$1"
# url="$2"

# Get the list of commits to be pushed
commits=$(git rev-list '@{u}..HEAD')

has_rust_files=$(false)

# TODO: We can iterate to get file list only once
# Get the list of files that are going to be pushed
for commit in $commits; do
	if git diff-tree --no-commit-id --name-only -r "${commit}" | grep -q "\\.rs$"; then
		has_rust_files=$(true)
	fi
done

if [[ ! $has_rust_files ]]; then
	echo "No .rs files in the push, no need to check CHANGELOG"
	exit 0
fi

# Check if CHANGELOG.md is among the files in the commits
for commit in $commits; do
	if git diff-tree --no-commit-id --name-only -r "${commit}" | grep -q "CHANGELOG.md"; then
		echo "CHANGELOG.md is included in the push."
		exit 0
	fi
done

echo "ERROR: CHANGELOG.md is not included in the push."
exit 1
</code></pre>
<p><strong>Edit (2025-02-08):</strong> Added a check for when only code files are changed.</p>]]></content:encoded>
    </item>
    <item>
      <title>devlog 6</title>
      <published>2024-07-12T08:53:14+00:00</published>
      <updated>2024-07-12T08:53:14+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Fri, 12 Jul 2024 08:53:14 +0000</pubDate>
      <link>https://emresahin.net/devlog-6/</link>
      <guid isPermaLink="true">https://emresahin.net/devlog-6/</guid>
      <description>I have a habit of testing against the CLI’s help string output. It allows me to keep the documentation up to date and makes me aware of any undocumented options. When new features are added, the help text changes and the test fails, which prompts me to add those options to the documentation. I tr...</description>
      <category>devlog</category>
      <category>Software Development</category>
      <category>xvc</category>
      <category>Python</category>
      <category>pytest</category>
      <category>clap</category>
      <category>CLI</category>
      <category>Testing</category>
      <content:encoded><![CDATA[<p>I have a habit of testing against the CLI’s help string output. It allows me to
keep the documentation up to date and makes me aware of any undocumented options.
When new features are added, the help text changes and the test fails, which
prompts me to add those options to the documentation.</p>
<p>I tried the same approach when testing Python bindings with Pytest:</p>
<pre><code class="language-python">def test_pipeline_step_dependency(empty_xvc_repo):
    dep_help = empty_xvc_repo.pipeline().step().dependency(help=True)
    expected = """
Usage: xvc pipeline step dependency [OPTIONS] --step-name &lt;STEP_NAME&gt;

Options:
  -s, --step-name &lt;STEP_NAME&gt;
          Name of the step to add the dependency to
"""
    assert dep_help == expected
</code></pre>
<p>This doesn’t work because the help text is generated by <a href="https://docs.rs/clap/latest/clap/">clap</a> and skips the usual
thread-based output handler. All command output and errors in Xvc are returned
as strings from the command, except for the help text that’s generated by <a href="https://docs.rs/clap/latest/clap/">clap</a>
automatically.</p>
<p>There are probably workarounds for this, but I won’t pursue them further as I don’t
test the actual functionality in the Python bindings anyway.</p>]]></content:encoded>
    </item>
    <item>
      <title>TIL 11: Unhide Markdown Code Block Types in LazyVim</title>
      <published>2024-02-26T10:22:29+00:00</published>
      <updated>2024-02-26T10:22:29+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Mon, 26 Feb 2024 10:22:29 +0000</pubDate>
      <link>https://emresahin.net/til-11/</link>
      <guid isPermaLink="true">https://emresahin.net/til-11/</guid>
      <description>To unhide Markdown code block types in LazyVim, you can set the conceallevel to 0 . Add the following to your configuration: vim.opt.conceallevel = 0</description>
      <category>Software Development</category>
      <category>Vim</category>
      <category>LazyVim</category>
      <category>Markdown</category>
      <category>Lua</category>
      <category>Neovim</category>
      <content:encoded><![CDATA[<p>To unhide Markdown code block types in LazyVim, you can set the <code>conceallevel</code> to <code>0</code>.</p>
<p>Add the following to your configuration:</p>
<pre><code class="language-lua">vim.opt.conceallevel = 0
</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>devlog</title>
      <published>2023-06-12T15:04:19+00:00</published>
      <updated>2023-06-12T15:04:19+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Mon, 12 Jun 2023 15:04:19 +0000</pubDate>
      <link>https://emresahin.net/devlog/</link>
      <guid isPermaLink="true">https://emresahin.net/devlog/</guid>
      <description>I’ve read a few interesting ideas here . Identifying whether an uploaded document is a template could be a useful feature. This is a basic classification task. We can also find ways to extract named entities from the documents and remove them to create a template. Then, we can ask the user to pro...</description>
      <category>devlog</category>
      <category>Software Development</category>
      <category>legalops</category>
      <category>contract</category>
      <category>negotiation</category>
      <category>nlp</category>
      <category>document-analysis</category>
      <category>templates</category>
      <content:encoded><![CDATA[<ul>
<li>I’ve read a few interesting ideas <a href="http://sourcinginnovation.com/wordpress/2023/06/06/source-to-pay-is-extensive-p22-time-for-contract-management-but-its-a-nag-lets-start-with-negotiation/">here</a>.</li>
<li>Identifying whether an uploaded document is a template could be a useful feature.
<ul>
<li>This is a basic classification task. We can also find ways to extract named entities from the documents and remove them to create a template. Then, we can ask the user to provide values for these entities to generate a complete document.</li>
</ul>
</li>
</ul>]]></content:encoded>
    </item>
    <item>
      <title>Is it possible to combine Word and Outlook add-ins?</title>
      <published>2023-03-21T21:04:00+00:00</published>
      <updated>2023-03-21T21:04:00+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Tue, 21 Mar 2023 21:04:00 +0000</pubDate>
      <link>https://emresahin.net/is-it-possible-to-combine-word-and-outlook-add-ins-/</link>
      <guid isPermaLink="true">https://emresahin.net/is-it-possible-to-combine-word-and-outlook-add-ins-/</guid>
      <description>We’re currently working on a Microsoft Word add-in in my day job. The company already has an Outlook add-in and wanted to use the same manifest file for the Word add-in. The manifest files have some keys that look similar. Although much of the content appears similar, merging them is not possible...</description>
      <category>til</category>
      <category>Software Development</category>
      <category>Microsoft Word</category>
      <category>Outlook</category>
      <category>add-in</category>
      <category>Office</category>
      <category>Microsoft 365</category>
      <category>Manifest</category>
      <content:encoded><![CDATA[<p>We’re currently working on a Microsoft Word add-in in my day job. The company already has an Outlook add-in and wanted to use the same manifest file for the Word add-in. <a href="https://learn.microsoft.com/en-us/office/dev/add-ins/develop/add-in-manifests?tabs=tabid-1#required-elements-by-office-add-in-type">The manifest files</a> have some keys that look similar. Although much of the content appears similar, merging them is not possible.</p>
<p>There are three types of Office add-ins: Task pane, Content, and Mail. These have different schema definitions at the top of the file. Consequently, it’s not possible to combine these three different types of Office add-ins. If an add-in needs mail permissions, it cannot work in Word, and if it requires document permissions, it cannot work in Outlook.</p>]]></content:encoded>
    </item>
    <item>
      <title>Xvc Devlog - 221108</title>
      <published>2022-11-09T10:42:00+00:00</published>
      <updated>2022-11-09T10:42:00+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Wed, 09 Nov 2022 10:42:00 +0000</pubDate>
      <link>https://emresahin.net/xvc-devlog---221108/</link>
      <guid isPermaLink="true">https://emresahin.net/xvc-devlog---221108/</guid>
      <description>🐇 I think we can begin by checking GitHub PRs . What do you have today? 🐢 The tests for yesterday’s Git integration PR failed. I’ll begin by checking the logs. 🐇 Maybe run the tests locally to see. They might fail on your machine as well. It might be a simple thing. 🐢 Probably, yes. I’ve started ...</description>
      <category>devlog</category>
      <category>Software Development</category>
      <category>xvc</category>
      <category>git</category>
      <category>testing</category>
      <category>trace</category>
      <category>rsync</category>
      <category>storage</category>
      <category>github actions</category>
      <category>automation</category>
      <content:encoded><![CDATA[<p>🐇 I think we can begin by checking <a href="https://github.com/pulls">GitHub PRs</a>. What do you have today?</p>
<p>🐢 The tests for yesterday’s <a href="https://github.com/iesahin/xvc/pull/105">Git integration PR</a> failed. I’ll begin by checking the logs.</p>
<p>🐇 Maybe run the tests locally to see. They might fail on your machine as well. It might be a simple thing.</p>
<p>🐢 Probably, yes. I’ve started the tests now. I shouldn’t forget to run them before testing the PR; it may save some time.</p>
<p>🐇 Ah, yeah, maybe. Yesterday you were already at the end of the workday, so it didn’t matter much. But you can shorten the testing time by reducing the number of files, etc. I think a separate benchmark suite might be good to have. Tests should be shorter; benchmarks should only run for tags.</p>
<p>🐢 Yup. I should shorten the tests. I should make them use a shorter list of files, maybe.</p>
<p>🐇 Local tests have passed. You’ll have to check the logs.</p>
<p>🐢 It seems <code>git diff --name-only --cached</code> returns files not yet added. That’s causing an error in new repositories with <code>stash</code>. Stashing shouldn’t run at all when there are no staged files.</p>
<p>🐇 Git behavior between the local version and the remote version seems different.</p>
<p>🐢 I think the trace should also show the Git version string.</p>
<p>🐇 You should also require a minimum Git version for the commands. You’re using some newer options, and not all users may have them.</p>
<p>🐢 Yep. Let’s see the version on the CI now.</p>
<hr>
<p>🐢 Ah, I see—the problem is not about versions. It’s that the CI doesn’t configure <code>git config --global user.email</code> and <code>user.name</code>. Commits don’t work without these.</p>
<p>🐇 You should remove the Git version report, then. It’s one more process call for no reason.</p>
<p>🐢 I think it should be in <code>trace!</code>; I can check the verbosity level and call it only if it’s trace.</p>
<hr>
<p>🐢 I completed the xvc-config docs as well. I think I can merge it before the tests finish, as it’s just a documentation update.</p>
<p>🐇 You seem to be rushing for the release.</p>
<p>🐢 Yeah, I want to really start testing this on the servers.</p>
<p>🐇 Then go ahead; let’s release a new version.</p>
<hr>
<p>🐇 It looks like you’re back for an evening session. I think it’s time to start using Xvc on your torrent server.</p>
<p>🐢 Ah, yeah. Let’s see how it goes.</p>
<p>🐇 Create a repository for torrents. Then you can add files to it with cache-type = symlink or cache-type = hardlink.</p>
<p>🐢 I think creating a local storage may also help. I can use it to store files to retrieve them later.</p>
<p>🐇 Umm. We don’t have a “garbage collection” facility yet, you know. There are no file deletions at the moment. It may be better to have a repository-to-repository transfer feature, using SSH.</p>
<p>🐢 Yeah, we don’t have SSH storage either. I think this highlights a lack of features.</p>
<p>🐇 It’s possible to mimic Rsync storage with <code>xvc storage new generic</code>, but it doesn’t feel quite the same.</p>
<p>🐢 Then I’m adding these three tickets.</p>
<p>🐇 I think for 0.3.4, the command we add might be <code>xvc file delete</code>. You can work on this and add <code>rsync</code> support as well.</p>
<p>🐢 There is a <a href="https://docs.rs/librsync/latest/librsync/">librsync</a> bindings library for Rust. But it looks like it doesn’t allow transferring file contents between hosts. There is also <a href="https://github.com/your-tools/rusync">rusync</a>, which is similar to rsync and implemented in Rust. There is also <a href="https://lib.rs/crates/fast_rsync">fast_rsync</a> in pure Rust. It uses MD4 to calculate deltas, though, I think.</p>
<p>🐇 None of these seem to have network capability, though.</p>
<p>🐢 I think it’s better just to use the process for now. We are trying to come up with the simplest solution <em>for now.</em> We are just trying to be more general.</p>
<p>🐇 Yes, I think we can just use the process for the time being.</p>
<p>🐢 Then let’s start by adding it.</p>
<p>🐇 We should also begin to create release notes. GitHub can generate them from PRs. <a href="https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuring-automatically-generated-release-notes">Automatically generated release notes</a></p>
<p>🐢 Created an issue for that. Now we’re starting <a href="https://github.com/iesahin/xvc/issues/111"><code>xvc storage new rsync</code> #111</a>.</p>
<p>🐇 Ok. Let’s do this.</p>]]></content:encoded>
    </item>
    <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>Xvc Devlog - 221105</title>
      <published>2022-11-07T10:04:00+00:00</published>
      <updated>2022-11-07T10:04:00+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Mon, 07 Nov 2022 10:04:00 +0000</pubDate>
      <link>https://emresahin.net/xvc-devlog---221105/</link>
      <guid isPermaLink="true">https://emresahin.net/xvc-devlog---221105/</guid>
      <description>It’s Saturday, November 5th. The best part of free software development seems to be being able to work whenever you want, including Saturdays. Ah yeah, when you work for free, you can do so at any time you want, perhaps. You also don’t have team members, and that means when you sit in front of th...</description>
      <category>devlog</category>
      <category>Software Development</category>
      <category>xvc</category>
      <category>git</category>
      <category>dvc</category>
      <category>git-lfs</category>
      <category>git-annex</category>
      <category>shell</category>
      <category>process</category>
      <category>which</category>
      <category>VCS</category>
      <content:encoded><![CDATA[<p>It’s Saturday, November 5th. The best part of free software development seems to be being able to work whenever you want, including Saturdays.</p>
<p>Ah yeah, when you work for free, you can do so at any time you want, perhaps. You also don’t have team members, and that means when you sit in front of this, you can move it.</p>
<p>Umm, right. Let’s take a look at <a href="https://github.com/iesahin/xvc/pulls">the outstanding PRs</a>.</p>
<p>You have a <a href="https://github.com/iesahin/xvc/pull/93">documentation PR#93</a>. You also have work that you’ve <a href="https://github.com/iesahin/xvc/issues/74">begun to integrate Git into.</a> I think it’s better to focus on the latter today.</p>
<p>Right. Let’s think about the relationship between Git and Xvc. I believe we should identify a general relation to avoid ending up in a mess like DVC and Git.</p>
<p>Why do you think the DVC and Git relationship is a mess?</p>
<p>They don’t automate common Git operations like a commit after <code>dvc add</code>. There is only <em>auto-stage</em>, and that’s turned off by default. This makes it seem that DVC wants to intervene as little as possible with the user’s Git workflow. That’s understandable. I support this. But on the other hand, they use the <code>.git/</code> directory itself to store and manage experiments in a custom way that creates custom stash objects for experiments. This is against the principle of minimum intervention.</p>
<p>So this makes it a mess?</p>
<p>The mess, in my opinion, is caused by the second factor. If DVC doesn’t perform any Git operations, that’s alright. It was intended to be VCS-agnostic. Then experiments came and used Git internals in a way that no other similar tool uses.</p>
<p>Git-LFS and Git-Annex seem to use some non-standard mechanisms as well.</p>
<p>Ok. Not <em>no other tool</em> uses, but in a way that no other tool has used.</p>
<p>You know, GitHub PRs are also stored in a similar way. They also use non-standard machinery.</p>
<p>Yeah, but these tools are all Git-specific tools. They accept the <em>dominion of Git</em>, and don’t try to bring any VCS-agnosticism.</p>
<p>And Xvc tries to have this agnosticism?</p>
<p>I believe the initial design of DVC, which aims to be VCS-agnostic or being able to run without a VCS, is valuable. I like the idea behind Git, but the interface and implementation show that it’s a <em>gradual development.</em> There is no library behind it.</p>
<p>Libgit?</p>
<p><a href="https://libgit2.org">Libgit2</a> is something different. Although it’s said to have some common code, it doesn’t support all features. Git is command-line software with a mix of scripts and compiled executables, and not all code seems to be written in a way that could be used by external tools.</p>
<p>Hmm. <a href="https://github.com/iesahin/xvc/issues/74#issuecomment-1302531362">The comment</a> you added to the issue says <code>git stash push --staged</code> is not available in libgit2. Can’t you mimic it like DVC does for branches?</p>
<p>I don’t want to depend on Git at that level.</p>
<p>So, you’ll be using the CLI and shell for Git?</p>
<p>Yes, I believe, at the moment, before any performance tests, that this doesn’t matter much. Running Git commands once in a while using the shell shouldn’t make much difference in overall performance.</p>
<p>Then you’ll use it like a command-line tool, like the user?</p>
<p>Yes, and I’ll make it run outside of the usual threads. All Git will be like a sandwich, wrapping around Xvc operations. If there are <code>--git-ref</code> instructions in an <code>xvc</code> command, it will be run before Xvc performs the command, and if there are any changes in Xvc metafiles, they will be committed to the current branch.</p>
<p>Like</p>
<pre class="mermaid">graph LR

co["git checkout"] --&gt; xvc
xvc --&gt; cm["git commit"]

</pre>

<p>The first could be a branch as well. So we have:</p>
<pre class="mermaid">graph LR

br["git branch"] --&gt; xvc
co["git checkout"] --&gt; xvc
xvc --&gt; cm["git commit"]

</pre>

<p>Looks sensible. How will you reflect these in the command line?</p>
<p>With something like <code>xvc --git-checkout my-branch file list</code></p>
<p>Hmm, and for a branch?</p>
<p>I think instead of different options for <code>branch</code>, <code>checkout</code> or <code>tag</code>, we can have a <code>git-ref</code> option that marks the option as a git reference. It will be checked out, or created as a branch from the current one if it doesn’t exist.</p>
<p>I think creating a branch is not a good idea. It should be explicit. You can just send the <code>--git-ref</code> value to <code>git checkout</code> and perform the Xvc operation. If the user wants to create a branch, I think they can do it themselves.</p>
<p>What about storing the results in a branch? After adding a bunch of files, they may want to store them in another branch, maybe?</p>
<p>That’s sensible. We can have another option, like <code>--to-branch</code> in certain operations.</p>
<p>Or in the <code>xvc</code> command as a general option. In that case, we can change the option names to <code>--from-ref</code> and <code>--to-branch</code>. It will be like:</p>
<pre class="mermaid">graph LR

fr["git checkout $(--from-ref)"] --&gt; xvc
xvc --&gt; tb["git checkout --branch $(--to-branch)"]
tb --&gt; co["git add .xvc &amp;&amp; git commit -m 'xvc cmd'"]

</pre>

<p>If no such options are given, xvc will run without branching, right?</p>
<p>Yep. <code>--from-ref</code> and <code>--to-branch</code> options are just shortcuts for user behavior. Any other VCS tool could be used this way. We don’t need to integrate Git at the library level.</p>
<p>This brings up the question of portability, though. When you aim for the software to be portable, you can’t rely on the existence of Git on the host, right?</p>
<p>I think a <code>git.command</code> option in the configuration is a good idea. Xvc will issue a warning if it can’t run the commands.</p>
<p>Will you use the shell to run this command? Otherwise no <code>$PATH</code> configuration is possible, you know.</p>
<p>I believe that could be another option: <code>git.use_shell</code>. If <code>git.command</code> is set to an absolute path, Xvc may use it without the shell. Otherwise, it can use the shell. Running the process directly will make it faster and more secure.</p>
<p>There is also this option to run Xvc in another process. Because we may access Git in the shell that runs Xvc, and if we can access it, maybe we don’t need shell execution in the process.</p>
<p>That’s a cool idea. But I wouldn’t add that extra complexity. Instead, we can try to find the <code>git</code> executable if <code>git.command</code> is not an absolute path. If <code>git.command = /usr/bin/git</code> in the configuration, we use it as is. Otherwise, we can get <code>$PATH</code> or <code>%PATH%</code> from the environment and search for <code>git.command</code> in that to find the exact executable.</p>
<p>It looks like there is a crate called <a href="https://crates.io/crates/which">which</a> that does exactly what we are looking for.</p>
<p>Ah, cool. Then we can just use that to find the executable and run it. We don’t need to drop to a shell.</p>
<p>Yep. Let’s go back to implementation now.</p>]]></content:encoded>
    </item>
    <item>
      <title>Xvc Devlog 221030</title>
      <published>2022-10-31T08:57:00+00:00</published>
      <updated>2022-10-31T08:57:00+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Mon, 31 Oct 2022 08:57:00 +0000</pubDate>
      <link>https://emresahin.net/xvc-devlog-221030/</link>
      <guid isPermaLink="true">https://emresahin.net/xvc-devlog-221030/</guid>
      <description>🐇 We’ll start by checking the repository today. What are the most important issues? 🐢 I’ve merged PR#66 . Let’s list the outstanding issues now. 65 OPEN Add Mermaid Support to Netlify Installation 64 OPEN Fix `xvc-storage` compilation warnings bug 54 OPEN Fix all references to `xvc data` in the d...</description>
      <category>devlog</category>
      <category>Software Development</category>
      <category>github actions</category>
      <category>arc42</category>
      <category>debugging</category>
      <category>xvc storage</category>
      <category>tests</category>
      <category>s3cmd</category>
      <content:encoded><![CDATA[<p>🐇 We’ll start by checking <a href="https://github.com/iesahin/xvc">the repository</a> today. What are the most important issues?</p>
<p>🐢 I’ve merged <a href="https://github.com/iesahin/xvc/pull/66">PR#66</a>. Let’s list the outstanding issues now.</p>
<pre><code>65	OPEN	Add Mermaid Support to Netlify Installation
64	OPEN	Fix `xvc-storage` compilation warnings	bug
54	OPEN	Fix all references to `xvc data` in the documentation
51	OPEN	Add a benchmark script to compare Xvc with other tools
50	OPEN	Consider changing `xvc file push` to `xvc file send` and `xvc file pull` to `xvc file retrieve`
49	OPEN	Add `--generic-command` as a dependency type to `xvc pipeline dependency`
48	OPEN	Write rsync example for `xvc storage generic`	documentation
47	OPEN	Write rclone example for `xvc storage generic`	documentation
46	OPEN	Allow to skip init in remotes with `--skip-init` option
45	OPEN	Update `VStore::to_store` to use some internal mechanism to avoid `XvcStore::insert`
43	OPEN	Update to `clap 4.0`
42	OPEN	Clean up xvc crate dependencies
36	OPEN	Add version to released file binaries
33	OPEN	Update `arch/remotes.md` for new naming	documentation
32	OPEN	Remove arc42 sections from the documentation	documentation
29	OPEN	Add documentation for `xvc storage new gcs`	documentation
28	OPEN	`xvc storage new yandex`	enhancement
24	OPEN	Create a logo for Xvc	documentation
23	OPEN	Add a Github action to upload new versions to crates.io	automation
20	OPEN	ref: Add rsync example for `xvc storage new generic`
18	OPEN	Add a new workflow for remote tests	automation
12	OPEN	Create a website for Xvc
5	OPEN	Add storage tests to Github Actions	automation
4	OPEN	fix clippy warnings	bug
1	OPEN	`xvc storage new` for all S3 compatible cloud services supported by `rust-s3`
</code></pre>
<p>🐇 As far as I know, you removed the arc42 stubs from the docs yesterday. You can close issue #32.</p>
<p>🐢 I closed it via <code>gh issue close</code>.</p>
<p>🐇 You should add a comment to that.</p>
<p>🐢 Added the comment.</p>
<hr>
<p>🐇 It’s October 30th. Looking at the logs, you’ve <a href="https://github.com/iesahin/xvc/issues?q=is%3Aissue+is%3Aclosed">closed some more issues since we last talked.</a></p>
<p>🐢 I’ve been productive over the last few days. Now, let’s take a look at the <a href="https://github.com/iesahin/xvc/issues?q=is%3Aopen+is%3Aissue">open</a> issues.</p>
<p>🐇 I think you have a more pressing problem. <a href="https://github.com/iesahin/xvc/actions/runs/3352401264">Your tests</a> are failing. You need to fix them first before moving to another task.</p>
<p>🐢 Created a new <a href="https://github.com/iesahin/xvc/issues/81">issue</a>. The logs say:</p>
<pre><code>81Z error: 7 targets failed:
2022-10-29T18:18:36.8969467Z ##[error]    `-p xvc-workflow-tests --test test_storage_new_digital_ocean`
2022-10-29T18:18:36.8970697Z     `-p xvc-workflow-tests --test test_storage_new_gcp`
2022-10-29T18:18:36.8971279Z     `-p xvc-workflow-tests --test test_storage_new_generic_rsync`
2022-10-29T18:18:36.8971840Z     `-p xvc-workflow-tests --test test_storage_new_minio`
2022-10-29T18:18:36.8972387Z     `-p xvc-workflow-tests --test test_storage_new_r2`
2022-10-29T18:18:36.8973035Z     `-p xvc-workflow-tests --test test_storage_new_s3`
2022-10-29T18:18:36.8973577Z     `-p xvc-workflow-tests --test test_storage_new_wasabi`
2022-10-29T18:18:36.9368079Z ##[error]The process '/home/runner/.cargo/bin/cargo' failed with exit code 101
</code></pre>
<p>🐇 Looking at the logs, you should clean up those older branches.</p>
<p>🐢 Let’s do it now.</p>
<hr>
<p>🐢 Done. I’ve deleted all branches except <code>main</code>.</p>
<p>🐇 Good. I think the best way is to delete them as soon as you merge them.</p>
<p>🐢 I’ve activated that setting. These were older branches.</p>
<p>🐇 Cool. Now, will you be checking the failing tests?</p>
<p>🐢 I think remote tests should never run if there are other errors. We can run coverage and remote tests as a second step after the first succeed.</p>
<p>🐇 That seems like a neat idea. You want to split the current job into two: one for compiling and testing the non-remote parts, and the second for coverage and storage tests. It won’t waste CI minutes that way?</p>
<p>🐢 On second thought, I think at the moment, that’s not a pressing issue. We should start fixing these tests at once. We can split them later.</p>
<p>🐇 Ok. It looks from the logs like secrets are not being made available to your jobs.</p>
<pre><code>2022-10-29T18:18:17.5039311Z test test_storage_new_digital_ocean ... FAILED
2022-10-29T18:18:17.5039550Z
2022-10-29T18:18:17.5039652Z failures:
2022-10-29T18:18:17.5039784Z
2022-10-29T18:18:17.5040197Z ---- test_storage_new_digital_ocean stdout ----
2022-10-29T18:18:17.5040558Z Error: VarError { source: NotPresent }
2022-10-29T18:18:17.5040758Z
2022-10-29T18:18:17.5040765Z
</code></pre>
<p>🐢 I added them now. Let’s wait until the job ends to get a new set of logs.</p>
<p>🐇 You can write some documentation in the meantime.</p>
<p>🐢 I think <a href="https://github.com/iesahin/xvc/issues/82">#82</a> is a good candidate for this. There must not be too many missing docs in the ECS crate.</p>
<p>🐇 It was about <code>walker</code> and you fixed the <code>ecs</code>. You’re the most absentminded developer here, I believe.</p>
<p>🐢 Ooops, you’re right. I’ll add them together in a <a href="https://github.com/iesahin/xvc/pull/93">single PR.</a></p>
<p>🐇 In the meantime, the storage testing job has ended with 🔴.</p>
<p>🐢 Checking the raw logs. It looks like we didn’t update the tests to match the current options:</p>
<pre><code>2022-10-30T14:45:50.1058348Z error: Found argument '--storage-prefix' which wasn't expected, or isn't valid in this context
2022-10-30T14:45:50.1059809Z ##[error]Found argument '--storage-prefix' which wasn't expected, or isn't valid in this context
2022-10-30T14:45:50.1061740Z 	If you tried to supply `--storage-prefix` as a value rather than a flag, use `-- --storage-prefix`
2022-10-30T14:45:50.1063220Z
2022-10-30T14:45:50.1063394Z USAGE:
2022-10-30T14:45:50.1064302Z     xvc storage new digital-ocean --name &lt;NAME&gt; --bucket-name &lt;BUCKET_NAME&gt; --region &lt;REGION&gt;
</code></pre>
<p>🐇 Is it <code>--storage-prefix</code> or <code>--remote-prefix</code>? Which one is clearer?</p>
<p>🐢 I think updating the tests to conform to the current options is better for now. We can update the options later if desired.</p>
<p>🐇 Another failure is <code>rg</code>. You assume it exists on the testing system.</p>
<p>🐢 <code>ripgrep</code> is available in Ubuntu 20.04, so we can just update the initial package list.</p>
<p>🐇 The same <code>--storage-prefix</code> failure appears in the <code>minio</code> tests.</p>
<p>🐢 Ok. Fixing it.</p>
<p>🐇 S3 tests want to run the <code>new-s3</code> subcommand. I think we now see why we need these tests in the first place.</p>
<p>🐢 Yeah. Fixing the prefix option, too.</p>
<p>🐇 <code>s3cmd</code> is also required. You should add it, too.</p>
<p>🐢 Ok. I think we’ve added all missing dependencies. I’ll have to convert the <code>mc</code> tests for Minio to use <code>s3cmd</code>.</p>
<p>🐇 Then we can try again. Now, we can get back to documentation.</p>
<p>🐢 Added some more documentation to PR#93. I think it’s better to return to the storage test errors now.</p>
<p>🐇 It looks like you have missing DigitalOcean credentials.</p>
<p>🐢 Let’s take a look.</p>
<hr>
<p>🐢 I’ve updated the tests to use a config file instead of command-line arguments that would be visible in the logs.</p>
<p>🐇 Good practice. You also need to remove previous logs.</p>
<p>🐢 Maybe there is an option for that.</p>
<p>🐇 It looks like there isn’t. There are masking options; when we add them to secrets, they are masked. But you shouldn’t use them in calls anyway.</p>]]></content:encoded>
    </item>
    <item>
      <title>Feature flags in Rust</title>
      <published>2022-10-01T14:09:00+00:00</published>
      <updated>2022-10-01T14:09:00+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Sat, 01 Oct 2022 14:09:00 +0000</pubDate>
      <link>https://emresahin.net/Feature-flags-in-Rust/</link>
      <guid isPermaLink="true">https://emresahin.net/Feature-flags-in-Rust/</guid>
      <description>Rust allows you to define feature flags to compile certain parts of a binary conditionally. The Cargo Book has an extensive section on them. The Cargo.toml file can have a [features] section. In this section, features can be defined, and they can enable other features. [features] my-feature = [] ...</description>
      <category>Rust</category>
      <category>Software Development</category>
      <category>Rust</category>
      <category>Cargo</category>
      <category>Feature Flags</category>
      <category>Conditional Compilation</category>
      <category>Configuration</category>
      <content:encoded><![CDATA[<p>Rust allows you to define feature flags to compile certain parts of a binary conditionally.
The Cargo Book has an extensive <a href="https://doc.rust-lang.org/cargo/reference/features.html">section</a> on them.</p>
<p>The <code>Cargo.toml</code> file can have a <code>[features]</code> section.
In this section, features can be defined, and they can enable other features.</p>
<pre><code class="language-toml">[features]
my-feature = []
another-feature = ["my-feature"]
</code></pre>
<p>These markers are used in the code as follows:</p>
<pre><code class="language-rust">
#[cfg(feature = "my-feature")]
pub mod my_module;
</code></pre>
<p>The default features are listed under the <code>default</code> key of the <code>[features]</code> section.
If it’s not defined, it’s considered empty, so <code>cargo build</code> supplies no features to the build system by default.</p>
<p>If you don’t want <code>default</code> features added automatically, you can use the <code>--no-default-features</code> option with <code>cargo</code>.</p>
<p>Starting from Rust 1.60, dependencies can be tied to features.
When you define a dependency in the <code>[dependencies]</code> section, add <code>optional = true</code> to its options. Then, in <code>[features]</code>, use the <code>dep:package</code> syntax.</p>
<pre><code class="language-toml">
[dependencies]

pack = { version = "1.0", optional = true }

[features]

my-feature = ["dep:pack"]
</code></pre>
<p>This way, Cargo doesn’t compile <code>pack</code> if <code>my-feature</code> is not enabled.</p>
<p>A caveat to using features is that they should be additive.
There shouldn’t be a <code>no-std</code> feature to remove the standard library dependency; instead, you should define a <code>std</code> feature and enable it by default.
The reason behind this is that Cargo ensures there is a single copy of a package with <em>all</em> the requested features enabled for the entire dependency graph.
If you have a <code>no-x</code> feature that removes functionality, it breaks the additive logic of Cargo’s feature resolution.</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>
    <item>
      <title>Factory Constructors in Dart</title>
      <published>2020-12-26T01:03:56+00:00</published>
      <updated>2020-12-26T01:03:56+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Sat, 26 Dec 2020 01:03:56 +0000</pubDate>
      <link>https://emresahin.net/factory-constructors-in-dart/</link>
      <guid isPermaLink="true">https://emresahin.net/factory-constructors-in-dart/</guid>
      <description>Dart supports factory constructors, which can return an instance of a subtype or even a cached instance. To create a factory constructor, use the factory keyword: class Square extends Shape {} class Circle extends Shape {} class Shape { Shape(); factory Shape.fromTypeName(String typeName) { if (t...</description>
      <category>Dart</category>
      <category>Programming</category>
      <category>Software Development</category>
      <category>oop</category>
      <category>factory-pattern</category>
      <category>constructors</category>
      <category>dart-lang</category>
      <category>design-patterns</category>
      <content:encoded><![CDATA[<p>Dart supports factory constructors, which can return an instance of a subtype or even a cached instance.
To create a factory constructor, use the <code>factory</code> keyword:</p>
<pre><code class="language-dart">class Square extends Shape {}

class Circle extends Shape {}

class Shape {
  Shape();

  factory Shape.fromTypeName(String typeName) {
    if (typeName == 'square') return Square();
    if (typeName == 'circle') return Circle();

    throw ArgumentError('I don\'t recognize $typeName');
  }
}
</code></pre>]]></content:encoded>
    </item>
    <item>
      <title>Should we expect a software crisis?</title>
      <published>2019-02-23T09:51:51+00:00</published>
      <updated>2019-02-23T09:51:51+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Sat, 23 Feb 2019 09:51:51 +0000</pubDate>
      <link>https://emresahin.net/should-we-expect-a-software-crisis-14467-71541/</link>
      <guid isPermaLink="true">https://emresahin.net/should-we-expect-a-software-crisis-14467-71541/</guid>
      <description>I read a blog post titled The Quiet Crisis unfolding in Software Development that mainly says current software building practices lead to the accumulation of technical debt and that legacy software becomes unmanageable over time. It warns about highly skilled developers : “These kinds of high per...</description>
      <category>Software Development</category>
      <category>Opinion</category>
      <category>Technical Debt</category>
      <category>Software Crisis</category>
      <category>Code Quality</category>
      <category>Feedback Loops</category>
      <content:encoded><![CDATA[<p>I read a blog post titled <a href="http://ift.tt/1TNLzfm"><em>The Quiet Crisis unfolding in Software
Development</em></a> that mainly says current software
building practices lead to the accumulation of <em>technical debt</em> and that legacy
software becomes unmanageable over time.</p>
<p>It warns about <em>highly skilled developers</em>:</p>
<blockquote>
<p>“These kinds of high performers are actually low performers when
TCO is factored in. Unless you’re a startup where time to market is
the highest priority, keep these kinds of developers under close
scrutiny with extensive design and code reviews.”</p>
</blockquote>
<p>and says continual improvement should be an innate element of all
software building practices.</p>
<blockquote>
<p>“Continual improvement isn’t a stand alone project. If you make it a
stand alone project you’ll eventually find a reason to abandon it
entirely because it’s impacting deadlines. It should be something
developers are tasked to do all the time during their normal
development activities. This is not unlike the power of compound
interest.”</p>
</blockquote>
<p>In my experience, the following holds too:</p>
<blockquote>
<p>“Some employees aren’t aware of the impact they have on other team
members and interrupt them frequently with one quick question or
worse. Encourage your team to prefer communication in roughly the
following order so that interruptions are minimized: e-mail, chat room
(if your team isn’t using a chat room yet, they should be), instant
message, phone call/dropping by in person.”</p>
</blockquote>
<p>But in general, I would not expect a <em>crisis</em> in software development.
Software is not like a building or a tool; it’s rather like an orchard
you gather value from over time. It surely needs maintenance, but the cost of
maintenance and the value it provides should be considered together.</p>
<p>I believe that most errors are preventable in software development, but
the (communication/technical/financial) cost of <em>perfect software</em> is usually
higher than the value it provides. It’s true that we should aim for particular
practices that reduce such cost, like not interrupting team members when they
are working, but striving for perfect software does not automatically bring it.</p>
<p>What brings it? Receiving and listening to feedback. A software
developer should be open to feedback, be it from the tests they write or
users’ bug reports. Our foremost aim should be opening as many
feedback channels as possible, in the form of unit tests, integration tests, and
shipping results to the users frequently. We can only evaluate
the technical debt on top of this feedback.</p>]]></content:encoded>
    </item>
    <item>
      <title>zsh'de dosya seçim operatörleri</title>
      <published>2014-03-02T22:00:00+00:00</published>
      <updated>2014-03-02T22:00:00+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Sun, 02 Mar 2014 22:00:00 +0000</pubDate>
      <link>https://emresahin.net/zsh-dosya-operasyonlari/</link>
      <guid isPermaLink="true">https://emresahin.net/zsh-dosya-operasyonlari/</guid>
      <description>zsh, dosya seçmek için bash’ten biraz daha gelişmiş operatörlere sahip. Bunlar sayesinde bir defada bir dizindeki dosyaların tamamına erişip onlar üzerinde işlemler yapmak mümkün. Burada kısa bazı örnekler vereceğim. Bir dizindeki tüm dosyalar: ls * Bulunduğumuz dizin ve tüm alt dizinlerdeki dosy...</description>
      <category>Software Development</category>
      <category>zsh</category>
      <category>shell</category>
      <category>linux</category>
      <category>tutorial</category>
      <content:encoded><![CDATA[<p>zsh, dosya seçmek için bash’ten biraz daha gelişmiş operatörlere sahip. Bunlar sayesinde bir defada bir dizindeki dosyaların tamamına erişip onlar üzerinde işlemler yapmak mümkün. Burada kısa bazı örnekler vereceğim.</p>
<p>Bir dizindeki tüm dosyalar: <code>ls *</code></p>
<p>Bulunduğumuz dizin ve tüm alt dizinlerdeki dosyalar: <code>ls **/*</code></p>
<p>Son iki haftada değiştirilmiş dosyalar: <code>ls **/*(.mw-2)</code></p>
<p>Boyutu 100 MB’tan büyük olan dosyalar: <code>ls **/*(.Lm+100)</code></p>
<p>Sadece okunabilir olan dosyalar: <code>ls **/*(.R)</code></p>
<p><code>/etc</code> dizininde dünya tarafından yazılabilir olan dosyalar: <code>ls /etc/**/*(.W)</code></p>
<p><code>/etc</code> dizininde son bir haftada değiştirilmiş ve dünya tarafından yazılabilir olan dosyalar: <code>ls /etc/**/*(.Wmw-1)</code></p>
<p>zsh’in sağladığı bu gibi seçeneklerin yanında dosya adını parçalamak da kolay. Örneğin dosyanın uzantıdan önceki kısmını almak için <code>*(:r)</code> yazıyoruz. Dizindeki <em>jpg</em> dosyalarını ImageMagick’in <code>convert</code> programıyla aynı isimli png dosyalarına çevirmek şöyle mümkün:</p>
<pre><code class="language-bash">for f in **/*.jpg(.) ; do
   convert ${f:r}.jpg ${f:r}.png
done
</code></pre>
<p>Burada anlatılanları <code>find</code> yoluyla yapmak da mümkün ancak çok daha uzun sürebiliyor. Bilhassa dosya adlarını parçalamak, dönüştürmek gibi işlemleri yaparken zsh’in nimetleri çok. Tek satırlık bir operasyonla çok güçlü bazı dönüşümler yapabilmek mümkün.</p>]]></content:encoded>
    </item>
    <item>
      <title>This Site's RSS Generator</title>
      <published>2013-04-22T21:13:06+00:00</published>
      <updated>2013-04-22T21:13:06+00:00</updated>
      <author>Emre Şahin</author>
      <pubDate>Mon, 22 Apr 2013 21:13:06 +0000</pubDate>
      <link>https://emresahin.net/this-sites-rss-generator/</link>
      <guid isPermaLink="true">https://emresahin.net/this-sites-rss-generator/</guid>
      <description>This is an ancient post from 2013. I’m not using any of these now. Previously with Pandoc , I was using a simple setup to create RSS feeds. Markdown files were converted to plain, headerless HTML, and they were collected together to build an XML file. The obvious drawback is that all HTML files s...</description>
      <category>Software Development</category>
      <category>Python</category>
      <category>RSS</category>
      <category>automation</category>
      <category>static site generator</category>
      <category>web development</category>
      <content:encoded><![CDATA[<p><strong>This is an ancient post from 2013. I’m not using any of these now.</strong></p>
<p>Previously with <a href="http://johnmacfarlane.net/pandoc/">Pandoc</a>, I was using a simple setup to create RSS feeds. <code>Markdown</code> files were converted to plain, headerless HTML, and they were collected together to build an XML file. The obvious drawback is that all HTML files should be generated by Pandoc; anything that doesn’t fit that route does not appear in the feeds.</p>
<p>However, when I began to use <a href="http://orgmode.org">Org Mode</a> for data analysis and other tasks, I stopped using Pandoc. Org Mode has extensive facilities for exporting into HTML and other document formats, so I would not mess with Pandoc for this.</p>
<p>I thought RSS could be produced by parsing HTML files after they are produced. This requires parsing the HTML file, but it’s simple, and there are parsers for all programming languages out there. My previous RSS generator was in Python, and I decided to modify it to fit my needs. I think producing RSS for a static HTML site is a common need, and I tried to solve this problem as simply as possible.</p>
<p>Let’s begin with the ubiquitous shebang line. This tells the system that the script is in Python.</p>
<pre><code class="language-python">#!/usr/bin/env python
</code></pre>
<p>The following are the imports for this script. Apart from <a href="http://www.dalkescientific.com/Python/PyRSS2Gen.html">PyRSS2Gen</a>, all modules are present in Python 2.7.</p>
<pre><code class="language-python">import argparse
import codecs
import os
import datetime
from HTMLParser import HTMLParser
import PyRSS2Gen as rssgen
import operator as op
import re
import subprocess as proc
</code></pre>
<p>I use <a href="https://mercurial.selenic.com">Mercurial</a> to track the site’s files. I once thought about using the Mercurial public API to check the status of files, but it proved to be overkill because only the modification time of files is necessary, and retrieving them using a standard command-line call is much simpler. Hence, I removed the following imports for the time being.</p>
<pre><code class="language-python"># from mercurial import commands as cmd
# from mercurial import hg
# from mercurial import ui as hgui
</code></pre>
<p>The following function returns a valid HTML tag string, given the tag and its attributes in a list. <code>HTMLParser</code> sends the tags in a list form, and I use this function to reconvert them to usual HTML tags.</p>
<pre><code class="language-python">def make_tag(tag, attrs):
    content_list = [ tag ]
    content_list += [ "%s=\"%s\"" % (k, v) for (k, v) in attrs]
    return "&lt;" + " ".join(content_list) + "&gt;"
</code></pre>
<p><code>TitleBodyExtractor</code> is an <code>HTMLParser</code> subclass. It collects the body of a page in a string and also keeps the title. These two are the only requirements. It might be possible to parse meta tags to get publish date and author information as well, but I prefer to keep simple things simple.</p>
<pre><code class="language-python">class TitleBodyExtractor(HTMLParser):

    def __init__(self):
        HTMLParser.__init__(self)
        self.in_body = False
        self.in_title = False
        self.body = ""
        self.title = ""


    def handle_data(self, data):
        if self.in_body:
            self.body += data
        if self.in_title:
            self.title += data

    def handle_starttag(self, tag, attrs):
        if self.in_body:
            self.body += make_tag(tag, attrs)

        if tag == "body":
            self.in_body = True
        if tag == "title":
            self.in_title = True

    def handle_endtag(self, tag):
        if tag == "body":
            self.in_body = False

        if tag == "title":
            self.in_title = False

        if self.in_body:
            self.body += "&lt;/%s&gt;" % (tag)
</code></pre>
<p>Getting contents of a file in <em>UTF-8</em> encoding is a common task. The following two functions retrieve and store the contents in UTF-8 using the <code>codecs</code> module.</p>
<pre><code class="language-python">def get_content(filename):
    f = codecs.open(filename, "r", "utf-8")
    cont = f.read()
    f.close()
    return cont

def write_content(filename, content):
    f = codecs.open(filename, "w", "utf-8")
    f.write(content)
    f.close()
</code></pre>
<p>Mercurial allows running commands for a repository outside of that repository with the <code>-R</code> command-line switch. However, it requires the exact path of the repository and does not accept a child path. The following function finds the repository path of a file by recursively checking whether parent paths contain an <code>.hg/</code> directory.</p>
<pre><code class="language-python">def get_repo_path(dir):
    if dir == "/" or dir == "":
        return ""
    if os.path.exists(os.path.join(dir, ".hg")):
        return dir
    else:
        return get_repo_path(os.path.dirname(dir))
</code></pre>
<p>The <code>FileObject</code> class keeps the required data of an HTML file. It stores the path, modification time, body, and title.</p>
<pre><code class="language-python">class FileObject:
    def __init__(self, path, mtime):
        self.path = path
        self.mtime = int(mtime)
        self._body = ""
        self._title = ""

    def parse(self):
        content = get_content(self.path)
        tbe = TitleBodyExtractor()
        tbe.feed(content)
        self._body = tbe.body
        self._title = tbe.title

    def body(self):
        if self._body == "":
            self.parse()
        return self._body

    def title(self):
        if self._title == "":
            self.parse()
        return self._title

    def __str__(self):
        return str(self.path) + " " + str(self.mtime)
</code></pre>
<p>The modification time of a file should be retrieved from the Mercurial repository. The following function calls <code>hg log</code> with a specific template, then parses the date to get the last commit time of a file. If the file is not registered to a repository, it simply returns the filesystem modification time.</p>
<pre><code class="language-python">def get_mtime(full_path):
    if os.path.exists(full_path):
        repo_path = get_repo_path(full_path)
        if repo_path != "":
            logcmd = "/usr/bin/hg log -R %s --template='{date|hgdate}' -l 1 %s " % (repo_path, full_path)
            # print logcmd

            proc_res = proc.check_output(logcmd, shell=True).split()
            if len(proc_res) &gt; 0:
                filetime = int(proc_res[0])
            else:
                filetime = os.path.getmtime(full_path)
        else:
            filetime = os.path.getmtime(full_path)
        return filetime
    else:
        return 0
</code></pre>
<p>We need a list of files as <code>FileObject</code> objects, given the directory name and extension. The function also takes a repository path and excludes filenames that match a given regex.</p>
<pre><code class="language-python">
    def file_list(dirname, extension, repo_path, exclude_regex=None):
        results = []
        for root, dirs, files in os.walk(dirname):
            # print "Dirs:", dirs
            for d in dirs:
                if exclude_regex == None or (not re.match(exclude_regex, d)):
                    results += file_list(os.path.join(root, d), extension, repo_path)
                else:
                    print "Skipping", d
            # print "Files:", files
            for f in files:
                if (exclude_regex == None or (not re.match(exclude_regex, f))) and f.endswith(extension):
                    fullname = os.path.join(root, f)
                    filetime = get_mtime(fullname)
                    results.append(FileObject(fullname, filetime))
        return results
</code></pre>
<p>Given a local file in the site, we need to create a link that shows the URL of that file relative to the site’s URL. The following function finds the relative path with respect to an input directory and returns the complete URL by concatenating it to the site’s URL.</p>
<pre><code class="language-python">
    def make_link(site_url, input_dir, file_path):
        rel_path = os.path.relpath(file_path, input_dir)
        return site_url + rel_path
</code></pre>
<p>Given a <code>FileObject</code> that points to an HTML file, we need a function that builds an RSS item from it. It obtains the URL of the file and fills the rest using the attributes of the <code>FileObject</code>.</p>
<pre><code class="language-python">
    def get_rss_item(file_object, input_dir, site_url):
        the_link = make_link(site_url, input_dir, file_object.path)
        item = rssgen.RSSItem(title=file_object.title(),
                              link=the_link,
                              description=file_object.body(),
                              guid=rssgen.Guid(the_link),
                              pubDate=datetime.datetime.fromtimestamp(file_object.mtime))
        return item
</code></pre>
<p>The function that creates an RSS file from the files in a given directory is the topmost function. It takes all variables that are set from the command line and returns the RSS object.</p>
<p>It first lists all files with the given extension (default being <code>.html</code>) in the input directory. Then it compares the modification time of the RSS file with the modification times of these listed files. If there is no previous RSS file or there are newer HTML files, the RSS is generated again.</p>
<p>Note that a certain amount of <em>edit time</em> can be set, so that the script doesn’t consider files as <em>new</em> if they are modified within <em>edit time</em> minutes. This can be set to prevent too frequent generation of files during an edit session.</p>
<p>To generate the RSS, each file is supplied to the previous function and an RSS item is obtained; then these are fed into the <code>RSS2</code> function of <code>PyRSS2Gen</code> to get the resulting object.</p>
<pre><code class="language-python">def generate_rss(input_dir, extension = ".html", output = "rss/rss.xml", site_title = "Title", site_description = "Description", max_items = 20, site_url = "http://example.com", edit_time=0, exclude_regex = None):
        if not site_url.endswith("/"):
            site_url += "/"
        files = file_list(input_dir, extension, get_repo_path(input_dir), exclude_regex)
        rssmtime = get_mtime(output)
        files_up = [f for f in files if f.mtime &gt;= (rssmtime + edit_time)]
        if len(files_up) &gt; 0:
            print "Before", files_up
            files_up.sort(key=lambda x: x.mtime, reverse=True)
            print "After", files_up
            rss_items = [get_rss_item(fo, input_dir, site_url) for fo in files_up[:max_items]]
            rssobj = rssgen.RSS2(title = site_title,
                                 link = site_url,
                                 description = site_description,
                                 lastBuildDate = datetime.datetime.now(),
                                 items = rss_items)
            return rssobj
        return None
</code></pre>
<p>The main function uses <code>argparse</code> to handle options. The input directory, site title, site URL, and number of items are mandatory; other options have sensible default values.</p>
<p>The function also builds the <code>exclude_regex</code> object to supply to file listings. The regex is built here from the supplied string, and all other functions use this compiled regex.</p>
<p>After generating the RSS, it writes the file with the <code>write_xml</code> function.</p>
<pre><code class="language-python">
    def main():

        parser = argparse.ArgumentParser(description='Generate RSS feed from a set of HTML files')

        parser.add_argument('--input-dir', help="input directory", required=True)
        parser.add_argument("--extension", help="file extension to collect", default=".html")
        parser.add_argument("--output", help="output filename to write the results", default="rss/rss.xml")
        parser.add_argument('--title', help="title of the RSS feed", required=True)
        parser.add_argument("--description", help="site description", default="")
        parser.add_argument("--items", help="max items included in the feed", required=True, type=int)
        parser.add_argument("--site-url", help="site url of items", required=True)
        parser.add_argument("--exclude-regex", help="regex to set skipped files", default="")
        parser.add_argument("--edit-time", help="minutes to wait before putting an item into rss", default=0, type=int)

        args = vars(parser.parse_args())

        if args["exclude_regex"] == "":
            exclude_regex = None
        else:
            exclude_regex = re.compile(args["exclude_regex"])

        rssresults = generate_rss(args["input_dir"],
                                  args["extension"],
                                  args["output"],
                                  args["title"],
                                  args["description"],
                                  args["items"],
                                  args["site_url"],
                                  args["edit_time"],
                                  exclude_regex)

        if rssresults != None:
            rssresults.write_xml(open(args["output"], "w"))



    if __name__ == "__main__":
        main()
</code></pre>
<p>You can get the resulting Python script from <code>rss-generator.py</code>.</p>]]></content:encoded>
    </item>
  </channel>
</rss>
