Skip to content

Hello everyone 👋

Over time, Zettel Notes, a markdown note-taking app for Android, has steadily grown and is now used on 50K+ devices. One question that often comes up from users is:

How exactly does Git sync work inside the app?

In this post, I’ll explain the mechanism in simple terms — focusing on what actually happens behind the scenes when you tap Sync.

Quick Overview

Whenever a sync is triggered, the app runs a fixed sequence of steps:

  1. Check host connectivity
  2. Login and validate the local repository
  3. Commit local changes
  4. Pull changes from remote
  5. Push updates to remote

Each step must complete successfully before the next begins.

How Sync Works in the App

Every sync run follows this pipeline:

Connectivity Check → Login → Commit → Pull → Push → Notify Result

Each stage acts as a checkpoint. If something fails, the process stops and reports the issue instead of continuing.

Sync Session

The app starts by creating a sync session using GitSyncManager:

Java
1
GitSyncManager manager = new GitSyncManager(mContext, repoModel);

It manages:

  • Repository configuration
  • Credentials
  • Git operations
  • Logs and error handling

Connectivity Check

Java
1
if (manager.isHostReachable())

Before touching the repository, the app checks whether the Git host is reachable. This avoids unnecessary processing when the network is unavailable.

Login and Local Repository Validation

Java
1
if (manager.login())

During this phase the app:

  • Reads remote URL and credentials from settings
  • Opens the local Git repository
  • Removes stale .index.lock files (which can appear after crashes)
  • Ensures the selected branch exists
  • Updates the origin remote if the URL has changed
  • Calls ls-remote to confirm remote access

Commit Phase

Java
1
commit = manager.commitAll();

Before pulling anything from the server, the app commits all local changes.

What happens here:

  1. If nothing changed → skip
  2. Stage modified files
  3. Stage deletions
  4. Add new (untracked) files
  5. Create a commit with the configured author and message

Pull Phase

Java
1
pull = manager.pullChanges();

If the commit succeeds, the app fetches changes from the remote repository.

Default approach: Rebase

Bash
1
git pull --rebase

Rebasing keeps history clean and linear, which works well for syncing notes.

Conflict Handling

If rebase encounters conflicts:

  1. The app detects the conflicting files
  2. The rebase is aborted
  3. Sync falls back to a merge

Merge Fallback

During fallback, the merge strategy depends on the configured conflict preference:

  • OURS (client wins) → keep local version
  • THEIRS (server wins) → keep remote version

Push Phase — Sending Changes to Remote

Java
1
push = manager.pushChanges();

Push runs only if both commit and pull were successful.

Logging and Result Notification

After the pipeline finishes, the app writes a sync log:

Java
1
2
String syncLog = manager.getSyncLog();
mSyncLogManager.writeSyncLog(repoModel.getUri(), syncLog);