From 923a811a4a1ba2d40f780594e4dc35d10fc2593e Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Thu, 23 Jan 2025 10:49:22 +0100 Subject: [PATCH] Handle first sync with completely empty repository --- src/github/client.ts | 20 ++++++++++++++++++++ src/sync-manager.ts | 45 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/src/github/client.ts b/src/github/client.ts index 1a59ba5..c08a249 100644 --- a/src/github/client.ts +++ b/src/github/client.ts @@ -131,4 +131,24 @@ export default class GithubClient { }); return res.json; } + + /** + * Create a new file in the repo, the content must be base64 encoded or the request will fail. + * + * @param message commit message + * @param path path to create in the repo + * @param content base64 encoded content of the file + */ + async createFile(path: string, content: string, message: string) { + await requestUrl({ + url: `https://api.github.com/repos/${this.owner}/${this.repo}/contents/${path}`, + headers: this.headers(), + method: "PUT", + body: JSON.stringify({ + message: message, + content: content, + branch: this.branch, + }), + }); + } } diff --git a/src/sync-manager.ts b/src/sync-manager.ts index 6dab185..b8a7f65 100644 --- a/src/sync-manager.ts +++ b/src/sync-manager.ts @@ -2,6 +2,7 @@ import { Vault, normalizePath } from "obsidian"; import GithubClient, { GetTreeResponseItem, NewTreeRequestItem, + RepoContent, } from "./github/client"; import MetadataStore, { FileMetadata, Metadata } from "./metadata-store"; import EventsListener from "./events-listener"; @@ -85,7 +86,41 @@ export default class SyncManager { * This fails neither remote nor local folders are empty. */ async firstSync() { - const { files, sha: treeSha } = await this.client.getRepoContent(); + let repositoryIsBare = false; + let res: RepoContent; + let files: { + [key: string]: GetTreeResponseItem; + } = {}; + let treeSha: string = ""; + try { + res = await this.client.getRepoContent(); + files = res.files; + treeSha = res.sha; + } catch (err) { + if (err.status !== 409) { + throw err; + } + // The repository is bare, meaning it has no tree, no commits and no branches + repositoryIsBare = true; + } + + if (repositoryIsBare) { + // Since the repository is completely empty we need to create a first commit. + // We can't create that by going throught the normal sync process since the + // API doesn't let us create a new tree when the repo is empty. + // So we create a the manifest file as the first commit, since we're going + // to create that in any case right after this. + await this.client.createFile( + `${this.vault.configDir}/github-sync-metadata.json`, + "", + "Initial commit", + ); + // Now get the repo content again cause we know for sure it will return a + // valid sha that we can use to create the first sync commit. + res = await this.client.getRepoContent(); + files = res.files; + treeSha = res.sha; + } const remoteContentDirIsEmpty = this.remoteContentDirIsEmpty(files); const localContentDirIsEmpty = await this.localContentDirIsEmpty(); @@ -93,7 +128,7 @@ export default class SyncManager { if (!remoteContentDirIsEmpty && !localContentDirIsEmpty) { // Both have files, we can't sync, show error throw new Error("Both remote and local have files, can't sync"); - } else if (remoteContentDirIsEmpty) { + } else if (remoteContentDirIsEmpty || repositoryIsBare) { // Remote has no files and no manifest, let's just upload whatever we have locally. // This is fine even if the local content dir is empty. // The most important thing at this point is that the remote manifest is created. @@ -624,7 +659,11 @@ export default class SyncManager { let remotePath = filePath; if (!filePath.startsWith(this.vault.configDir)) { if (this.settings.localContentDir === "") { - remotePath = `${this.settings.repoContentDir}/${filePath}`; + if (this.settings.repoContentDir === "") { + remotePath = filePath; + } else { + remotePath = `${this.settings.repoContentDir}/${filePath}`; + } } else { remotePath = filePath.replace( this.settings.localContentDir,