Documentation Menu

Sync a repository

8 min read

quillet init, push and pull keep chosen folders of a repository in step with an environment, so engineers can write content as files, review it in a pull request and publish it from a pipeline.

Why some content belongs in the repository

Some content is written next to code: a component's JSON Schema, the guides for a feature, a landing page an engineer owns. Writing that content in the dashboard means copying text between two places and reviewing it in neither. With sync, the repository holds it as plain files. A change to them is a pull request, and a merge pushes them to the CMS.

The CMS is still a place where people edit, too. A content manager may fix a typo in the dashboard while an engineer edits the same file. Sync compares the file, the server's copy and the last state the two agreed on. That is how it tells "I changed this" from "somebody else did", and why it never overwrites either side without being told to.

How sync tells your edit from someone else's

Sync rests on a scope in the configuration file that says which folders the repository manages, a lock file that remembers what both sides last agreed on, and a verdict for each document that decides what a push and a pull do with it.

The scope and the layout

The sync section of .quilletrc names the content directory and the folders this repository manages:

"sync": {
  "contentDir": "content",
  "scope": [{ "prefix": "docs/" }, { "prefix": "landing/", "locales": ["en-US", "de-DE"] }]
}

A prefix is a folder in the CMS's own key grammar, ending in /. _schemas/ and _singletons/ are prefixes like any other, so components and singletons sync too. An entry without locales covers every locale of the project. A file that sits under the content directory but outside the scope is left alone, with a warning.

Each document is one file, <contentDir>/<key>.<locale>.<ext>, with .json, .md or .txt according to its type. A JSON file holds the body and nothing else. The metadata, title, order, tags, schema and searchable, lives in a sidecar file beside it, so a Markdown file stays plain Markdown. This guide, in this site's own repository, is two files:

content/docs/cli/sync.en-GB.md
content/docs/cli/sync.en-GB.meta.json

A folder's settings live in <folder>/_index.<locale>.json, and a component lives in _schemas/<name>.<locale>.json. Documents, keys and folders explains the keys.

The lock

quillet.lock sits beside .quilletrc. It records the version and the content hash of every document the checkout manages, as the server answered after the last pull or push. Commit it. The lock is what lets the CLI tell a local edit from a remote one, and its diff in a pull request shows which documents moved.

The verdicts

diff hashes every file in the scope and asks the server to compare those hashes with its own copies and with the lock. Each document gets one verdict, and the verdict decides what a push and a pull do with it:

diff says Meaning push pull
Changed on both sides you and the server both moved since the lock refuses the whole push keeps your file
Changed locally you edited the file ~ update keeps your file
New locally a file the server does not have + create keeps your file
Deleted locally the lock knows it, the folder does not . skipped, or - delete with --allow-delete writes the file back
Changed on the server someone edited it elsewhere . skipped writes the server's copy
New on the server a document this checkout never had . skipped writes it

status prints the same counts in one line. Both commands exit with code 3 when anything changed on both sides, so a pipeline stops there.

What a push sends

A push writes drafts, not published versions. Components go first, and alone, so that a document bound to a new component validates against it. Documents follow in batches of up to 500. Each batch is a transaction, so a stale base anywhere in a batch refuses all of it. A document's base is the version and content hash the lock recorded for it after the last pull or push; it goes stale when someone saves a newer version on the server, so the server's hash no longer matches the lock's. --publish then publishes exactly the documents the push created or updated, and nothing else.

The CMS hashes a document's body, not its metadata, so an edit to a sidecar alone is invisible to the server's comparison. The CLI compares the sidecar with the lock instead, and says so: the sidecar changed — push sends it with the body. A field you remove from a sidecar is cleared on the server.

How to

Start from a repository with a .quilletrc that names your CMS, project and environment, as described in Configure. Then set up sync for one folder. init writes the section, creates the directory and pulls whatever the server already holds there:

npx quillet init --scope <folder>/ --content-dir content
Set up .quilletrc. Wrote 0, kept 0, removed 0.

Write a page as two files, the body and its sidecar:

mkdir -p content/<folder>
printf '# Hello\n\nThe first page this repository manages.\n' > content/<folder>/hello.en-US.md
printf '{ "title": "Hello" }\n' > content/<folder>/hello.en-US.meta.json

Ask what has moved:

npx quillet status
1 changed locally, 0 on the server, 0 conflicts, 0 unchanged.

Print the plan. A dry run sends nothing and changes nothing:

npx quillet push --dry-run
+ create docs/hello (en-US)
1 document to write, 0 to delete, 0 to resolve.

Push the page, and publish what the push wrote:

npx quillet push --publish
created docs/hello (en-US)
Pushed 1 document, published 1.

Commit the configuration, the lock and the content together:

git add .quilletrc quillet.lock content
git commit -m "Sync <folder>/ with Quillet"

When someone edits a synced document in the dashboard, status reports 1 on the server. Bring their edit into your working copy:

npx quillet pull

When both sides changed

Suppose you edited the file and someone else saved the document after your last pull. diff lists the document and exits with code 3, and push refuses everything it was going to send:

Changed on both sides:
  docs/hello (en-US)
0 changed locally, 0 on the server, 1 conflict, 0 unchanged.
1 document changed on both sides. Pull, resolve, or push --force.

A plain pull keeps your file and reports kept docs/hello (en-US) — conflict. To see what the other person changed, read the server's copy and its history:

npx quillet content get <folder>/hello
npx quillet content versions <folder>/hello

Then choose a side. Both commands below act on the whole sync scope, not only on the one conflicting document. To keep your file, merge their change into it by hand and push. push --force sends every local change in the scope with no base, so each one replaces the server's copy, conflict or not; npx quillet push --dry-run --force lists them first:

npx quillet push --force

Or throw your edit away and take the server's copy. pull --force overwrites every locally changed file in the scope. It also deletes the files of documents the server no longer holds, except files you never pushed:

npx quillet pull --force

Either way, npx quillet status reads 0 conflicts afterwards. Nothing is lost on the server, because the copy you replaced is still a version in its history.

Deleting a document

Removing a file does not delete the document. A push skips it. You have to ask for the delete:

rm content/<folder>/hello.en-US.md content/<folder>/hello.en-US.meta.json
npx quillet push --dry-run --allow-delete
npx quillet push --allow-delete

The dry run prints - delete docs/hello (en-US) before the real push sends it.

Limits

  • The dashboard does not show which folders a repository manages. A folder whose syncSource is cli refuses changes made as a person, in the dashboard or with a personal token. A content manager can read a synced document there but cannot save it; to change it, ask an engineer to edit the file and push it.
  • There is no merge. You settle a conflict by choosing a side, pull --force or push --force, and either choice applies to every document in the scope. Combining the two edits is done by hand.
  • A folder whose syncSource is cli accepts only a CLI key. Signed in as a person, with quillet login and a personal token, a push to it is refused with 403: "Folder / accepts changes only from the CLI."
  • Files are not synced. Upload them with quillet assets upload, and reference them from Markdown by URL.
  • A scope names folders, so the root's own settings, _index, are never in it. Write that document with quillet content put _index.
  • One comparison carries at most 5,000 documents. Narrow the scope, or give an entry only the locales it needs.
  • A push of more than 500 documents is several transactions. If a later batch is refused, the earlier ones stay written, and the lock records them.
  • Nothing runs on its own. There is no watch mode and no push on commit, and the CLI never runs git for you.

Last updated