Skip to content

Paused

Go back

Keeping Obsidian and My Git Repos in Sync

I want all of my knowledge in one place, on laptop and on mobile. I already write almost everything in Markdown, so Obsidian is the obvious place.1

The problem is that some of this writing lives in git repos, for example the posts of this blog, the documentation for my projects, and the notes site for RPG campaigns. For my blog for instance all the content is plain Markdown in a folder ~/dev/blog/src/content/posts.

I wanted to see and edit this content inside my main vault without copying files around by hand. Changes on one side should appear on the other. For the blog it is also handy: I can read my old posts anywhere and update them from the phone.

It took a few tries. This post explains what I ended up with, what I rejected, and why.

The constraint that decides everything

My vault lives in iCloud Drive, so it syncs to my iPhone. This one detail rules out most of the simple answers.

The first idea, which I had already tried some time ago, was a symlink: put a link in the vault that points to the folder in the repo. Obsidian on the Mac follows symlinks, so the files show up, and there is only one copy of each file. Nothing to sync.

The problem is iCloud. It does not follow symlinks, it uploads the link itself. On the iPhone the link points to /Users/me/dev/..., a path that does not exist there. So the folders were missing on the phone, and I wanted to read my posts there too.

The opposite is to keep the real files in the vault and put a symlink in the repo. Now iCloud is happy, but git is not: git stores the symlink, not the files it points to. The repo would contain a link to my iCloud folder instead of my posts, and the site would not build anywhere else.

Rejected: Obsidian plugins

If links don’t work, the files must be copied. There are a few community plugins for this:

There is also a deeper issue with plugins: they only run while Obsidian is open on the Mac.

Rejected: writing my own plugin

I thought about it. But two-way sync is a hard problem. You need to tell a deleted file from a new one, notice when both sides changed, and handle changes made while the plugin was not running. Getting this wrong means losing writing. That is probably why the existing plugins are buggy, and mine would be too.

Chosen: Unison in the background

While searching Homebrew, I found Unison, a command-line file sync tool that has existed since the late 90s. It solves exactly this problem. The important part is that it keeps a record of the last sync, called the archive. With it, Unison knows:

When a file changed on both sides, it does not guess. With my settings, the newer version wins and the other one is saved next to it, as a copy with conflict_on_ and the date in its name. Nothing is lost.

Installing it is one command:

brew install unison

Each folder pair is a profile in ~/.unison. The shared settings go in common.prf:

ignore = Name .DS_Store
ignore = Name *.icloud
ignore = Name .trash
batch = true
auto = true
times = true
perms = 0
prefer = newer
copyonconflict = true
confirmbigdel = true
silent = true
log = true
logfile = /Users/me/Library/Logs/unison-obsidian.log~/.unison/common.prf

What these do:

Then one profile per repo:

include common
root = /Users/me/dev/blog/src/content/posts
root = /Users/me/Library/Mobile Documents/iCloud~md~obsidian/Documents/Vault/Blog~/.unison/blog.prf

unison blog syncs the blog once. To run it every minute I use launchd, the macOS service that starts jobs on a schedule. The job is a file in ~/Library/LaunchAgents/com.me.unison-obsidian.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>com.me.unison-obsidian</string>
  <key>ProgramArguments</key>
  <array>
    <string>/opt/homebrew/bin/unison</string><string>blog</string>
  </array>
  <key>StartInterval</key><integer>60</integer>
  <key>RunAtLoad</key><true/>
  <key>ProcessType</key><string>Background</string>
  <key>StandardErrorPath</key><string>/Users/me/Library/Logs/unison-obsidian.err.log</string>
</dict>
</plist>

Before starting it, in Finder, right-click your vault folders and pick Keep Downloaded. Otherwise iCloud can remove the local copy of files to save space, and Unison would see them as missing and delete them from the repo too. Then start the job:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.me.unison-obsidian.plist

It uses almost no resources. After the first sync, Unison only compares file sizes and dates, finishes in a fraction of a second and exits. Nothing stays running between runs, and it works whether Obsidian is open or not.

The full flow is: repo ↔ Unison ↔ vault folder ↔ iCloud ↔ iPhone.

There is one limit: an edit made on the iPhone reaches the repo only when the Mac is on. That is fine for me, because I commit and publish from the Mac anyway.

Maintaining it

To check that the last run worked:

launchctl print gui/$(id -u)/com.me.unison-obsidian | grep 'last exit'

last exit code = 0 means the last run was fine. Don’t worry if launchctl also says state = not running: each sync takes less than a second, so between runs there is nothing to run.

To see what it copies, as it happens:

tail -f ~/Library/Logs/unison-obsidian.log

To find conflicts, which also show up in git status as new files:

find ~/dev/blog/src/content/posts -name '*conflict_on_*'

When there is one, I merge what I need into the original file and delete the copy.

A few things to remember:

A script for all of this

After setting it up by hand twice, I wrote a small script, obsidian-sync, and put it in my dotfiles. It creates the plist, and adding a new folder is one command:

obsidian-sync add blog ~/dev/blog/src/content/posts Blog

obsidian-sync status shows in one place when the last sync ran, when it last changed something, and any conflicts and errors. Its log only records the syncs that copied or deleted files, so it stays short and easy to read.

The script uses its own names: the job is local.obsidian-sync, and the logs are obsidian-sync.log and obsidian-sync.err.log in ~/Library/Logs. So with the script, the commands in “Maintaining it” become obsidian-sync status, obsidian-sync log and obsidian-sync uninstall.

Was it worth it?

I traded a simple idea, a symlink, for a small background job. In return I can read and edit my posts and notes on the phone, git stays the source of truth, and a conflict gives me two files instead of a lost edit. The setup is two small config files and one plist, and Unison does the hard part.

It has worked well in the days I have been testing it, and it fixed a problem I had for a long time. I hope it means more documents, and more posts here.

Footnotes

  1. Sorry, Microsoft Office. ↩


How do you keep your notes and repos in sync? Did I miss a simpler way? Reply via email


Next500 Hours at FoodCycle