mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-07 13:25:28 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f9446f447 | |||
| 3f9cd67b1c | |||
| dbcbf2c5ca | |||
| eed0fca8d3 | |||
| 05e031b90b | |||
| bec767c13f | |||
| 8046a777af | |||
| 0c58b0c513 | |||
| b66e227a02 | |||
| af6df84b5d |
@@ -148,6 +148,20 @@ Hence, the new feature should be implemented as follows:
|
||||
- **Service Hub** (`src/modules/services/`): Central service registry using dependency injection
|
||||
- **Common Library** (`src/lib/`): Platform-independent sync logic, shared with other tools
|
||||
|
||||
### Conflict Merge Policy
|
||||
|
||||
Markdown conflict auto-merge should behave like a conservative three-way merge. The guiding rule is to merge changes when they touch non-overlapping regions, and to keep a manual conflict when the edits overlap semantically.
|
||||
|
||||
When in doubt, prefer the safer outcome: preserve data, keep the conflict visible, and ask the user rather than silently discarding content or choosing one side.
|
||||
|
||||
- If one side deletes a line and the other side leaves that same line unchanged, treat it as a safe deletion. The deleted line must not be reintroduced into the merged result.
|
||||
- If one side inserts new content in a different region while the other side deletes an unchanged old region, preserve the insertion and the deletion.
|
||||
- If one side deletes a line and the other side modifies that same line, keep the conflict for user resolution.
|
||||
- If both sides insert different content at the same position, keep both insertions in a deterministic order unless the surrounding deletion context indicates that they are competing replacements.
|
||||
- Avoid resolving conflicts by simply choosing the newest revision unless the user has explicitly selected that behaviour.
|
||||
|
||||
This policy is intentionally aligned with the conflict checkboxes and compatibility settings: automatic merge should remove avoidable prompts, but it must not silently choose between overlapping user intentions.
|
||||
|
||||
### File Structure Conventions
|
||||
|
||||
- **Platform-specific code**: Use `.platform.ts` suffix (replaced with `.obsidian.ts` in production builds via esbuild)
|
||||
|
||||
@@ -51,6 +51,7 @@ import { EVENT_SETTING_SAVED, eventHub } from "@/common/events.ts";
|
||||
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
import { tryGetFilePath } from "@lib/common/utils.doc.ts";
|
||||
import { configureHiddenFileSyncMode } from "./configureHiddenFileSyncMode.ts";
|
||||
type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce";
|
||||
|
||||
declare global {
|
||||
@@ -1832,43 +1833,35 @@ ${messageFetch}${messageOverwrite}${messageMerge}
|
||||
}
|
||||
|
||||
async configureHiddenFileSync(mode: keyof OPTIONAL_SYNC_FEATURES) {
|
||||
if (
|
||||
mode != "FETCH" &&
|
||||
mode != "OVERWRITE" &&
|
||||
mode != "MERGE" &&
|
||||
mode != "DISABLE" &&
|
||||
mode != "DISABLE_HIDDEN"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == "DISABLE" || mode == "DISABLE_HIDDEN") {
|
||||
// await this.core.$allSuspendExtraSync();
|
||||
await this.core.services.setting.applyPartial(
|
||||
{
|
||||
syncInternalFiles: false,
|
||||
},
|
||||
true
|
||||
);
|
||||
// this.core.settings.syncInternalFiles = false;
|
||||
// await this.core.saveSettings();
|
||||
return;
|
||||
}
|
||||
this._log("Gathering files for enabling Hidden File Sync", LOG_LEVEL_NOTICE);
|
||||
if (mode == "FETCH") {
|
||||
await this.initialiseInternalFileSync("pullForce", true);
|
||||
} else if (mode == "OVERWRITE") {
|
||||
await this.initialiseInternalFileSync("pushForce", true);
|
||||
} else if (mode == "MERGE") {
|
||||
await this.initialiseInternalFileSync("safe", true);
|
||||
}
|
||||
await this.core.services.setting.applyPartial(
|
||||
{
|
||||
useAdvancedMode: true,
|
||||
syncInternalFiles: true,
|
||||
const result = await configureHiddenFileSyncMode(mode, {
|
||||
disable: async () => {
|
||||
// await this.core.$allSuspendExtraSync();
|
||||
await this.core.services.setting.applyPartial(
|
||||
{
|
||||
syncInternalFiles: false,
|
||||
},
|
||||
true
|
||||
);
|
||||
// this.core.settings.syncInternalFiles = false;
|
||||
// await this.core.saveSettings();
|
||||
},
|
||||
true
|
||||
);
|
||||
enable: async () => {
|
||||
this._log("Gathering files for enabling Hidden File Sync", LOG_LEVEL_NOTICE);
|
||||
await this.core.services.setting.applyPartial(
|
||||
{
|
||||
useAdvancedMode: true,
|
||||
syncInternalFiles: true,
|
||||
},
|
||||
true
|
||||
);
|
||||
},
|
||||
initialise: async (direction) => {
|
||||
await this.initialiseInternalFileSync(direction, true);
|
||||
},
|
||||
});
|
||||
if (result == "ignored" || result == "disabled") {
|
||||
return;
|
||||
}
|
||||
// this.plugin.settings.useAdvancedMode = true;
|
||||
// this.plugin.settings.syncInternalFiles = true;
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
type HiddenFileSyncMode = "FETCH" | "OVERWRITE" | "MERGE" | "DISABLE" | "DISABLE_HIDDEN";
|
||||
type HiddenFileSyncDirection = "pullForce" | "pushForce" | "safe";
|
||||
|
||||
type ConfigureHiddenFileSyncHandlers = {
|
||||
disable: () => Promise<void>;
|
||||
enable: () => Promise<void>;
|
||||
initialise: (direction: HiddenFileSyncDirection) => Promise<void>;
|
||||
};
|
||||
|
||||
export type ConfigureHiddenFileSyncResult = "ignored" | "disabled" | "enabled";
|
||||
|
||||
function getInitialiseDirection(mode: keyof OPTIONAL_SYNC_FEATURES): HiddenFileSyncDirection | false {
|
||||
if (mode == "FETCH") return "pullForce";
|
||||
if (mode == "OVERWRITE") return "pushForce";
|
||||
if (mode == "MERGE") return "safe";
|
||||
return false;
|
||||
}
|
||||
|
||||
function isDisableMode(mode: keyof OPTIONAL_SYNC_FEATURES): boolean {
|
||||
return mode == "DISABLE" || mode == "DISABLE_HIDDEN";
|
||||
}
|
||||
|
||||
function isHiddenFileSyncMode(mode: keyof OPTIONAL_SYNC_FEATURES): mode is HiddenFileSyncMode {
|
||||
return mode == "FETCH" || mode == "OVERWRITE" || mode == "MERGE" || isDisableMode(mode);
|
||||
}
|
||||
|
||||
export async function configureHiddenFileSyncMode(
|
||||
mode: keyof OPTIONAL_SYNC_FEATURES,
|
||||
handlers: ConfigureHiddenFileSyncHandlers
|
||||
): Promise<ConfigureHiddenFileSyncResult> {
|
||||
if (!isHiddenFileSyncMode(mode)) {
|
||||
return "ignored";
|
||||
}
|
||||
if (isDisableMode(mode)) {
|
||||
await handlers.disable();
|
||||
return "disabled";
|
||||
}
|
||||
const direction = getInitialiseDirection(mode);
|
||||
if (direction === false) {
|
||||
return "ignored";
|
||||
}
|
||||
await handlers.enable();
|
||||
await handlers.initialise(direction);
|
||||
return "enabled";
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { configureHiddenFileSyncMode } from "./configureHiddenFileSyncMode.ts";
|
||||
|
||||
describe("configureHiddenFileSyncMode", () => {
|
||||
it.each([
|
||||
["FETCH", "pullForce"],
|
||||
["OVERWRITE", "pushForce"],
|
||||
["MERGE", "safe"],
|
||||
] as const)("enables hidden file sync before initialising %s", async (mode, direction) => {
|
||||
const calls: string[] = [];
|
||||
|
||||
const result = await configureHiddenFileSyncMode(mode, {
|
||||
disable: vi.fn(async () => {
|
||||
calls.push("disable");
|
||||
}),
|
||||
enable: vi.fn(async () => {
|
||||
calls.push("enable");
|
||||
}),
|
||||
initialise: vi.fn(async (actualDirection) => {
|
||||
calls.push(`init:${actualDirection}`);
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toBe("enabled");
|
||||
expect(calls).toEqual(["enable", `init:${direction}`]);
|
||||
});
|
||||
|
||||
it.each(["DISABLE", "DISABLE_HIDDEN"] as const)("disables hidden file sync immediately for %s", async (mode) => {
|
||||
const calls: string[] = [];
|
||||
|
||||
const result = await configureHiddenFileSyncMode(mode, {
|
||||
disable: vi.fn(async () => {
|
||||
calls.push("disable");
|
||||
}),
|
||||
enable: vi.fn(async () => {
|
||||
calls.push("enable");
|
||||
}),
|
||||
initialise: vi.fn(async (direction) => {
|
||||
calls.push(`init:${direction}`);
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toBe("disabled");
|
||||
expect(calls).toEqual(["disable"]);
|
||||
});
|
||||
});
|
||||
+1
-1
Submodule src/lib updated: cf9810dec5...a0efb7274e
+14
@@ -3,6 +3,20 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
|
||||
|
||||
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved Markdown conflict auto-merge so that non-overlapping edits are merged while overlapping delete-and-edit cases remain visible for manual resolution (#993).
|
||||
- Behaviour change:
|
||||
- When one side deletes an unchanged line and the other side edits a different region, the deleted line is no longer reintroduced into the merged result.
|
||||
- When one side deletes a line and the other side modifies that same line, the conflict is preserved instead of silently choosing one side.
|
||||
- Fixed an issue where applying a newer database entry to storage could incorrectly preserve an older local file as a conflict (#994).
|
||||
- Behaviour change:
|
||||
- Local storage is preserved as a conflict when it may contain unsynchronised changes that are not represented in the revision history. A newer incoming text entry is applied without creating a conflict only when it clearly extends the existing local text.
|
||||
- Fixed an issue where choosing Disable and then Overwrite in Hidden File Sync could silently skip hidden files, because the overwrite setup ran while hidden file synchronisation was still disabled (#989, PR #992).
|
||||
- Hidden File Sync is now re-enabled before the Fetch, Overwrite, or Merge initialisation runs, instead of after it completes. If that initialisation fails, the setting may remain enabled.
|
||||
|
||||
## 0.25.79
|
||||
|
||||
29th June, 2026
|
||||
|
||||
Reference in New Issue
Block a user