GitSyncMarks — Sync Logic
Overview
GitSyncMarks implements bidirectional bookmark synchronization using a three-way merge algorithm. Each bookmark is stored as an individual JSON file. The sync engine compares three states — base (last sync), local (browser), and remote (GitHub) — to automatically merge non-conflicting changes.
Core Concept: Three-Way Merge
Sync Operations
Push (Local → GitHub)
Full push of all local bookmarks as individual files using atomic commit.
Pull (GitHub → Local)
Fetch remote file map, convert to bookmark tree, replace local bookmarks.
Sync (Bidirectional Three-Way Merge)
Diff Computation
computeDiff(base, current) compares two file maps (path → content) and produces:
| Category | Meaning |
|---|---|
| added | Files in current but not in base |
| removed | Files in base but not in current |
| modified | Files in both but with different content |
Generated/meta files (README.md, _index.json, bookmarks.html, feed.xml, dashy-conf.yml, settings.enc) are excluded from diff via DIFF_IGNORE_SUFFIXES. Individual settings files (settings-{id}.enc) are excluded via SETTINGS_ENC_PATTERN.
Merge Rules
mergeDiffs(localDiff, remoteDiff) applies these rules per file path:
| Local | Remote | Action |
|---|---|---|
| Added | — | Push to GitHub |
| — | Added | Create locally |
| Modified | — | Push to GitHub |
| — | Modified | Apply locally |
| Removed | — | Delete on GitHub |
| — | Removed | Delete locally |
| Same change | Same change | No action needed |
| Different change | Different change | Conflict |
| Removed | Removed | No action needed |
Conflict Detection and Resolution
When mergeDiffs finds conflicts (same file changed differently on both sides):
hasConflictflag is set inchrome.storage.local- Popup shows conflict warning with resolution buttons:
- Local → GitHub (force push) — overwrites remote
- GitHub → Local (force pull) — overwrites local
- The chosen operation clears the conflict flag
First Sync Special Cases
When no base state exists (first sync ever):
| Local | Remote | Action |
|---|---|---|
| Has bookmarks | Empty repo | Push |
| Empty | Has data | Pull |
| Has bookmarks | Has data | Conflict (user must choose) |
| Empty | Empty | Nothing to do |
Auto-Sync and Debounce
Bookmark event → triggerAutoSync() → debouncedSync(5000ms)
↓
clearTimeout (if pending)
↓
setTimeout(sync, 5000ms)
- Default delay: 5 seconds
- Each new event resets the timer
- Uses
sync()(three-way merge), not just push - Suppressed for 10 seconds after a pull (to ignore bookmark events from
replaceLocalBookmarks)
Re-Entrancy Guard
A module-level isSyncing boolean prevents concurrent operations. background.js also checks isSyncInProgress() and isAutoSyncSuppressed() before triggering auto-sync.
The replaceLocalBookmarks Algorithm
Uses role-based mapping for cross-browser compatibility:
- Get local bookmark tree via
chrome.bookmarks.getTree() - Detect each root folder's role via
detectRootFolderRole()(uses browser-specific IDs with title fallback) - For each local root folder, get its role and the corresponding remote data:
a. Only roles in
SYNC_ROLES(toolbar, other) are processed; menu and mobile are ignored. b. GitHub Repos preservation: WhengithubReposEnabledis on and the target role matchesgithubReposParent, and Git data does not contain a folder titledGitHubRepos (username)(or anyGitHubRepos (prefix when username is unknown), the local GitHubRepos folder is preserved and merged into the data before replacement c. Remove all existing children (reverse order) d. Recursively recreate from merged remote data - Result: All bookmarks appear in both browsers; GitHubRepos folder is kept on pull when not in Git
Stale-Fetch Guard (Path 8)
When only remote changes exist (path 8), the API response may be cached or eventually consistent. To avoid overwriting local state with stale data (e.g. right after our own push), we re-fetch getLatestCommitSha() before applying. If it differs from the commit we fetched, we skip the pull and treat as "all in sync".
GitHub API requests use cache: no-store to reduce cache-related staleness.
Optimized Remote Fetching
fetchRemoteFileMap() minimizes API calls:
getLatestCommitSha()— 1 callgetCommit()+getTree(recursive=1)— 2 calls → full file list with SHAs- For each file: compare blob SHA with stored base SHA
- SHA matches → use cached content from base state (0 calls)
- SHA differs →
getBlob()(1 call per changed file)
In the common case (few files changed), this is 3 + N calls where N is the number of changed files.