Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@uiw/react-codemirror": "^4.23.8",
"@zip.js/zip.js": "^2.7.60",
"codemirror": "^6.0.1",
"ignore": "5.3.2",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 24 additions & 10 deletions src/events-listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import MetadataStore, { MANIFEST_FILE_NAME } from "./metadata-store";
import { GitHubSyncSettings } from "./settings/settings";
import Logger, { LOG_FILE_NAME } from "./logger";
import GitHubSyncPlugin from "./main";
import { isGitignored, loadGitignoreMatcher } from "./gitignore";

/**
* Tracks changes to local sync directory and updates files metadata.
Expand All @@ -27,7 +28,7 @@ export default class EventsListener {

private async onCreate(file: TAbstractFile) {
await this.logger.info("Received create event", file.path);
if (!this.isSyncable(file.path)) {
if (!(await this.isSyncable(file.path))) {
// The file has not been created in directory that we're syncing with GitHub
await this.logger.info("Skipped created file", file.path);
return;
Expand Down Expand Up @@ -66,7 +67,7 @@ export default class EventsListener {
// Skip folders
return;
}
if (!this.isSyncable(filePath)) {
if (!(await this.isSyncable(filePath))) {
// The file was not in directory that we're syncing with GitHub
return;
}
Expand All @@ -79,7 +80,7 @@ export default class EventsListener {

private async onModify(file: TAbstractFile) {
await this.logger.info("Received modify event", file.path);
if (!this.isSyncable(file.path)) {
if (!(await this.isSyncable(file.path))) {
// The file has not been create in directory that we're syncing with GitHub
await this.logger.info("Skipped modified file", file.path);
return;
Expand Down Expand Up @@ -112,30 +113,33 @@ export default class EventsListener {
// Skip folders
return;
}
if (!this.isSyncable(file.path) && !this.isSyncable(oldPath)) {
const newFileIsSyncable = await this.isSyncable(file.path);
const oldFileIsSyncable = await this.isSyncable(oldPath);

if (!newFileIsSyncable && !oldFileIsSyncable) {
// Both are not in directory that we're syncing with GitHub
return;
}

if (this.isSyncable(file.path) && this.isSyncable(oldPath)) {
if (newFileIsSyncable && oldFileIsSyncable) {
// Both files are in the synced directory
// First create the new one
await this.onCreate(file);
// Then delete the old one
await this.onDelete(oldPath);
return;
} else if (this.isSyncable(file.path)) {
} else if (newFileIsSyncable) {
// Only the new file is in the local directory
await this.onCreate(file);
return;
} else if (this.isSyncable(oldPath)) {
} else if (oldFileIsSyncable) {
// Only the old file was in the local directory
await this.onDelete(oldPath);
return;
}
}

private isSyncable(filePath: string) {
private async isSyncable(filePath: string) {
if (filePath === `${this.vault.configDir}/${MANIFEST_FILE_NAME}`) {
// Manifest file must always be synced
return true;
Expand All @@ -153,10 +157,20 @@ export default class EventsListener {
filePath.startsWith(this.vault.configDir)
) {
// Sync configs only if the user explicitly wants to
return true;
return !(await this.isIgnored(filePath));
} else {
// All other files can be synced
return true;
return !(await this.isIgnored(filePath));
}
}

private async isIgnored(filePath: string): Promise<boolean> {
if (!this.settings.useGitignore) {
return false;
}

// The file can change during the session, so events always read
// the current version before deciding whether to update local metadata.
return isGitignored(await loadGitignoreMatcher(this.vault), filePath);
}
}
43 changes: 43 additions & 0 deletions src/gitignore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Vault, normalizePath } from "obsidian";
import ignore, { Ignore } from "ignore";

export const GITIGNORE_FILE_NAME = ".gitignore" as const;

export function createGitignoreMatcher(patterns = ""): Ignore {
return ignore().add(patterns);
}

/**
* Loads root vault rules and returns a matcher equivalent to gitignore.
*/
export async function loadGitignoreMatcher(vault: Vault): Promise<Ignore> {
const matcher = createGitignoreMatcher();
const gitignorePath = normalizePath(GITIGNORE_FILE_NAME);

if (await vault.adapter.exists(gitignorePath)) {
matcher.add(await vault.adapter.read(gitignorePath));
}

return matcher;
}

/**
* Normalizes Obsidian paths to the relative format expected by the parser.
*/
export function isGitignored(
matcher: Ignore | null,
filePath: string,
isDirectory = false,
): boolean {
if (!matcher || filePath === "") {
return false;
}

const normalizedPath = normalizePath(filePath);
const pathToTest =
isDirectory && !normalizedPath.endsWith("/")
? `${normalizedPath}/`
: normalizedPath;

return matcher.ignores(pathToTest);
}
2 changes: 2 additions & 0 deletions src/settings/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface GitHubSyncSettings {
githubOwner: string;
githubRepo: string;
githubBranch: string;
useGitignore: boolean;
syncStrategy: "manual" | "interval";
syncInterval: number;
syncOnStartup: boolean;
Expand All @@ -22,6 +23,7 @@ export const DEFAULT_SETTINGS: GitHubSyncSettings = {
githubOwner: "",
githubRepo: "",
githubBranch: "main",
useGitignore: false,
syncStrategy: "manual",
syncInterval: 1,
syncOnStartup: false,
Expand Down
12 changes: 12 additions & 0 deletions src/settings/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ export default class GitHubSyncSettingsTab extends PluginSettingTab {
}),
);

new Setting(containerEl)
.setName("Use .gitignore")
.setDesc("Ignore files as defined in .gitignore")
.addToggle((toggle) => {
toggle
.setValue(this.plugin.settings.useGitignore)
.onChange(async (value) => {
this.plugin.settings.useGitignore = value;
await this.plugin.saveSettings();
});
});

new Setting(containerEl).setName("Sync").setHeading();

const syncStrategies = {
Expand Down
Loading