Git-shaped sync between Notion database views and folders of markdown files
Project description
n-commit
Git-shaped sync between Notion database views and local folders of markdown files. Notion is the source of truth; you decide when to pull and push.
Install
pip install n-commit
First call opens a browser for OAuth. Tokens cache at ~/.config/n-commit/tokens/.
Onboarding
mkdir my-notion && cd my-notion
# one-shot: folder name is derived from the Notion title
n-commit add "https://www.notion.so/…?v=…"
n-commit add "https://www.notion.so/…?v=…" --as crm/people # explicit folder
# batch: paste URLs in a loop (blank line or Ctrl-C ends)
n-commit add -i
# the first `add` creates n-commit.toml + .gitignore
To use the same workspace on another machine: copy n-commit.toml to a folder and run n-commit pull. (Git, scp, Dropbox — anything that moves a 1KB text file.)
Mental model
A workspace is a directory tree containing one n-commit.toml manifest at its root and one subfolder per binding. Each binding mirrors one Notion database view.
~/my-notion/
├── n-commit.toml ← the manifest (committed)
├── .gitignore ← ignores **/.notion/
├── interviews/
│ ├── _FIELDS.md ← cheat sheet, regenerated on bind
│ ├── jane.md ← row ↔ file
│ └── .notion/ ← cache (gitignored)
│ ├── schema.json
│ ├── fingerprints.json
│ └── mirror/jane.md ← last-pulled bytes
└── crm/people/
├── _FIELDS.md
├── alice.md
└── .notion/
Manifest:
[folders."interviews"]
view = "https://www.notion.so/…?v=…"
[folders."crm/people"]
view = "https://www.notion.so/…?v=…"
Workspace is found by walking up from cwd, like git finds .git/. One file describes every binding.
Verbs
add, remove, status, pull, push, restore. Run n-commit <verb> --help for the per-verb reference. API-call cost per verb:
| Verb | Reads remote | Writes remote | Writes local | Default API calls |
|---|---|---|---|---|
add |
yes (one-time) | no | yes | 1 list_rows + 1 fetch_schema (+ N fetch_row if --init=push) |
status |
only --remote |
no | no | 0 (or 1 list_rows per binding) |
pull |
yes | no | yes (clean only) | 1 list_rows per binding + N fetch_row (cache misses only) |
push |
yes (drift check) | yes | yes (echo) | 3/file (drift+update+echo); 2 with --force |
restore |
no | no | yes | 0 |
Conflict / drift playbook
pull→ConflictError(dirty file + remote also changed):- keep local:
push <file>(willDriftErrorif remote also moved →--force) - take remote:
restore <file>then re-pull - both diverged: open file, mirror at
.notion/mirror/<rel>, hand-merge,push
- keep local:
push→DriftError(mirror diverged from current remote):- keep local:
pull --force, re-merge,push. Orpush --force. - take remote:
restore <file>
- keep local:
status --remotesurfaces conflicts early — cheapest place to catch them.
Layout
| File | Role |
|---|---|
n_commit/core.py |
The five primitives + CLI entrypoint. All filesystem IO. |
n_commit/manifest.py |
n-commit.toml read/write/find. Pure, no Notion deps. |
n_commit/frontmatter.py |
Pure transforms: envelope parse, props ↔ YAML, body normalize, row fingerprint, slug. |
n_commit/client.py |
Typed Notion MCP wrapper. |
n_commit/auth.py |
Generic MCP OAuth (PKCE) with fcntl.flock to serialize refresh-token rotation. |
n_commit/mcp_client.py |
Generic MCP transport + dynamic tool proxy. |
Invariants
fetch_rowis the single source of truth for row bytes. Never mixlist_rowsprops with envelope-fetch props in the same path — they disagree on edge keys.- Mirror is always updated on pull, even on conflict. Working files are only overwritten if clean (or
--force). Conflicts surface loudly viaConflictError. - Canonicalization for hashing.
normalize_bodystrips presigned-S3 query strings so attachment URLs don't cause drift. CRLF → LF. Trailing whitespace stripped. Triple-newlines collapsed. - Read-only types are stripped on push:
created_time,last_edited_time,rollup,formula. notion_idis always 32-char undashed hex. Normalize at boundaries viaurl_to_id.- Comments don't bump
Last edited time. Usepull <file>(always fetches) when you want comments. query-database-viewcaps at 100 rows. Larger views truncate silently —pull <file>is the escape hatch for the tail.- Empty
list_rowsis a no-op. Not "everything was deleted". - OAuth refresh tokens are single-use.
FileTokenStorageusesfcntl.flockso concurrent sessions don't race. create_rowvalidates the parent. Rejected property bags silently produce orphan pages on Notion's side;create_rowre-fetches and verifies the parent matches.
Practices
frontmatter.pyandmanifest.pyare pure. No IO outside their declared file.core.py's primitives takeview_urlas an argument; only the CLI reads the manifest.- No imports inside functions. Module top only.
- Explicit over compact. Readable loops beat dense comprehensions.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file n_commit-0.4.0.tar.gz.
File metadata
- Download URL: n_commit-0.4.0.tar.gz
- Upload date:
- Size: 29.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b10f2693ff8a51dac37450d45631be3253a9f399a5c39bc39350048d864d6664
|
|
| MD5 |
3c6904373107f2cf1607f1fad995d337
|
|
| BLAKE2b-256 |
830d852abd3acddb99071a965c9c4fd65515c283bd2ba15fa7e8a7c8b32b973f
|
File details
Details for the file n_commit-0.4.0-py3-none-any.whl.
File metadata
- Download URL: n_commit-0.4.0-py3-none-any.whl
- Upload date:
- Size: 33.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
36ad6f73bd8e86d60004ebe3eae480a6fe7aa37d9fb3c7e5e8a0a041e41fafb6
|
|
| MD5 |
21e0dc55d3f0de4437db35753efa7e31
|
|
| BLAKE2b-256 |
62d4c47a9231a71f23dfd3b113fac991aec8271188f4f075869356f0dd7e71bc
|