Skip to content

Interval sync creates empty commits when no vault content changed #63

Description

@dennisrongo

Problem summary

When the plugin runs with the interval sync strategy (syncStrategy: "interval"), a new commit is pushed to the remote repository on every sync tick, even when no vault content has changed. The commit's only change is the internal metadata file (.obsidian/github-sync-metadata.json), whose lastSync timestamp is refreshed on every run — so the metadata update itself manufactures the diff.

Over time this produces an enormous number of effectively empty "Sync" commits. On a large vault synced at a short interval I observed on the order of 1,000–2,700 commits per day during a one-month period, each changing only the metadata file, and 4,000+ such commits accumulating in the repository history.

Steps to reproduce

  1. Install the plugin and configure a GitHub token + repository.
  2. Set syncStrategy to "interval" with a short sync interval (e.g. 1 minute) and let Obsidian stay open.
  3. Make no edits to the vault.
  4. Watch the remote repository: a new "Sync" commit appears after every interval tick, and git show on each commit shows that the only changed file is .obsidian/github-sync-metadata.json (only the lastSync field differs).

Evidence

From a repository synced with this plugin (vault anonymized) — every commit changes only the metadata file:

$ git log --oneline -3
a1b2c3d Sync
d4e5f6a Sync
789abcd Sync

$ git show --stat a1b2c3d
 .obsidian/github-sync-metadata.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

$ git show a1b2c3d -- .obsidian/github-sync-metadata.json | grep lastSync
-  "lastSync": 1756100000000,
+  "lastSync": 1756100060000,

Commit-frequency stats over one month of interval syncing showed 1,000–2,700 such commits per day, with no other file changing.

Root cause analysis

The sync flow in src/sync-manager.ts (as of 2ebd201, v1.0.7) has no has-changes guard before committing:

  1. commitSync() (L818) stamps Date.now() into the metadata and saves it (L824–826):
    const syncTime = Date.now();
    this.metadataStore.data.lastSync = syncTime;
    this.metadataStore.save();
  2. It then unconditionally injects the updated metadata file into the new tree (L883–885):
    delete treeFiles[`${this.vault.configDir}/${MANIFEST_FILE_NAME}`].sha;
    treeFiles[`${this.vault.configDir}/${MANIFEST_FILE_NAME}`].content =
      JSON.stringify(this.metadataStore.data);
  3. Finally it creates a commit and moves the branch head unconditionally (L901–908):
    const commitSha = await this.client.createCommit({
      // TODO: Make this configurable or find a nicer commit message
      message: "Sync",
      treeSha: newTreeSha,
      parent: branchHeadSha,
    });
    await this.client.updateBranchHead({ sha: commitSha, retry: true });

syncImpl() does have an early return when no sync actions are found (if (actions.length === 0) at L485), but that guard fires before the metadata file is re-stamped and injected — so it doesn't help in practice, because on a tick where any action is produced (or where the freshly-uploaded metadata from the previous tick makes determineSyncActions produce a spurious action), the metadata write at L883–885 guarantees the new tree always differs from the base tree. The result is a self-sustaining loop: each commit changes lastSync, which makes the next tick see the metadata file as changed, which produces another commit.

Suggested fix sketch

Skip the commit when the only difference between newTreeFiles and the base tree is the metadata file. Roughly, just before creating the tree at L887–897:

const changedFiles = Object.keys(treeFiles).filter(
  (filePath) =>
    filePath !== `${this.vault.configDir}/${MANIFEST_FILE_NAME}` &&
    treeFiles[filePath].sha === null || // delete_remote
    treeFiles[filePath].content !== undefined, // upload
);

if (changedFiles.length === 0) {
  // Only the metadata file changed — nothing real to commit.
  // Persist lastSync locally without pushing, and return.
  return;
}

i.e.:

  • Compute the set of real content changes (uploads and delete_remote entries), excluding the metadata/manifest file.
  • If that set is empty, skip createTree / createCommit / updateBranchHead entirely (the local lastSync can still be saved so the next tick doesn't re-detect anything).
  • Optionally, while touching this code path, the existing TODO at L902 about making the commit message configurable would be a nice companion fix.

This would make interval mode safe to leave on for large vaults; today the only workaround is switching to manual sync (which is what I had to do) or periodically squashing the history on the remote.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions