## 0.24.31

### Fixed

- The description of `Enable Developers' Debug Tools.` has been refined.
- Automatic conflict checking and resolution has been improved.
- Resolving conflicts dialogue will not be shown for the multiple files at once.
This commit is contained in:
vorotamoroz
2025-07-10 11:12:44 +01:00
parent 7535999388
commit 52b02f3888
3 changed files with 68 additions and 65 deletions

View File

@@ -37,13 +37,17 @@ export class ModuleConflictChecker extends AbstractModule implements ICoreModule
// TODO-> Move to ModuleConflictResolver? // TODO-> Move to ModuleConflictResolver?
conflictResolveQueue = new QueueProcessor( conflictResolveQueue = new QueueProcessor(
async (filenames: FilePathWithPrefix[]) => { async (filenames: FilePathWithPrefix[]) => {
await this.core.$$resolveConflict(filenames[0]); const filename = filenames[0];
return await this.core.$$resolveConflict(filename);
}, },
{ {
suspended: false, suspended: false,
batchSize: 1, batchSize: 1,
concurrentLimit: 1, // No need to limit concurrency to `1` here, subsequent process will handle it,
delay: 10, // And, some cases, we do not need to synchronised. (e.g., auto-merge available).
// Therefore, limiting global concurrency is performed on resolver with the UI.
concurrentLimit: 10,
delay: 0,
keepResultUntilDownstreamConnected: false, keepResultUntilDownstreamConnected: false,
} }
).replaceEnqueueProcessor((queue, newEntity) => { ).replaceEnqueueProcessor((queue, newEntity) => {
@@ -57,19 +61,13 @@ export class ModuleConflictChecker extends AbstractModule implements ICoreModule
new QueueProcessor( new QueueProcessor(
(files: FilePathWithPrefix[]) => { (files: FilePathWithPrefix[]) => {
const filename = files[0]; const filename = files[0];
// const file = await this.core.storageAccess.isExists(filename);
// if (!file) return [];
// if (!(file instanceof TFile)) return;
// if ((file instanceof TFolder)) return [];
// Check again?
return Promise.resolve([filename]); return Promise.resolve([filename]);
// this.conflictResolveQueue.enqueueWithKey(filename, { filename, file });
}, },
{ {
suspended: false, suspended: false,
batchSize: 1, batchSize: 1,
concurrentLimit: 5, concurrentLimit: 10,
delay: 10, delay: 0,
keepResultUntilDownstreamConnected: true, keepResultUntilDownstreamConnected: true,
pipeTo: this.conflictResolveQueue, pipeTo: this.conflictResolveQueue,
totalRemainingReactiveSource: this.core.conflictProcessQueueCount, totalRemainingReactiveSource: this.core.conflictProcessQueueCount,

View File

@@ -13,6 +13,7 @@ import { ConflictResolveModal } from "./InteractiveConflictResolving/ConflictRes
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts"; import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
import { displayRev, getPath, getPathWithoutPrefix } from "../../common/utils.ts"; import { displayRev, getPath, getPathWithoutPrefix } from "../../common/utils.ts";
import { fireAndForget } from "octagonal-wheels/promises"; import { fireAndForget } from "octagonal-wheels/promises";
import { serialized } from "../../lib/src/concurrency/lock.ts";
export class ModuleInteractiveConflictResolver extends AbstractObsidianModule implements IObsidianModule { export class ModuleInteractiveConflictResolver extends AbstractObsidianModule implements IObsidianModule {
$everyOnloadStart(): Promise<boolean> { $everyOnloadStart(): Promise<boolean> {
@@ -34,67 +35,71 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule im
} }
async $anyResolveConflictByUI(filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise<boolean> { async $anyResolveConflictByUI(filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise<boolean> {
this._log("Merge:open conflict dialog", LOG_LEVEL_VERBOSE); // UI for resolving conflicts should one-by-one.
const dialog = new ConflictResolveModal(this.app, filename, conflictCheckResult); return await serialized(`conflict-resolve-ui`, async () => {
dialog.open(); this._log("Merge:open conflict dialog", LOG_LEVEL_VERBOSE);
const selected = await dialog.waitForResult(); const dialog = new ConflictResolveModal(this.app, filename, conflictCheckResult);
if (selected === CANCELLED) { dialog.open();
// Cancelled by UI, or another conflict. const selected = await dialog.waitForResult();
this._log(`Merge: Cancelled ${filename}`, LOG_LEVEL_INFO); if (selected === CANCELLED) {
return false; // Cancelled by UI, or another conflict.
} this._log(`Merge: Cancelled ${filename}`, LOG_LEVEL_INFO);
const testDoc = await this.localDatabase.getDBEntry(filename, { conflicts: true }, false, true, true);
if (testDoc === false) {
this._log(`Merge: Could not read ${filename} from the local database`, LOG_LEVEL_VERBOSE);
return false;
}
if (!testDoc._conflicts) {
this._log(`Merge: Nothing to do ${filename}`, LOG_LEVEL_VERBOSE);
return false;
}
const toDelete = selected;
// const toKeep = conflictCheckResult.left.rev != toDelete ? conflictCheckResult.left.rev : conflictCheckResult.right.rev;
if (toDelete === LEAVE_TO_SUBSEQUENT) {
// Concatenate both conflicted revisions.
// Create a new file by concatenating both conflicted revisions.
const p = conflictCheckResult.diff.map((e) => e[1]).join("");
const delRev = testDoc._conflicts[0];
if (!(await this.core.databaseFileAccess.storeContent(filename, p))) {
this._log(`Concatenated content cannot be stored:${filename}`, LOG_LEVEL_NOTICE);
return false; return false;
} }
// 2. As usual, delete the conflicted revision and if there are no conflicts, write the resolved content to the storage. const testDoc = await this.localDatabase.getDBEntry(filename, { conflicts: true }, false, true, true);
if ( if (testDoc === false) {
(await this.core.$$resolveConflictByDeletingRev(filename, delRev, "UI Concatenated")) == this._log(`Merge: Could not read ${filename} from the local database`, LOG_LEVEL_VERBOSE);
MISSING_OR_ERROR
) {
this._log(
`Concatenated saved, but cannot delete conflicted revisions: ${filename}, (${displayRev(delRev)})`,
LOG_LEVEL_NOTICE
);
return false; return false;
} }
} else if (typeof toDelete === "string") { if (!testDoc._conflicts) {
// Select one of the conflicted revision to delete. this._log(`Merge: Nothing to do ${filename}`, LOG_LEVEL_VERBOSE);
if ( return false;
(await this.core.$$resolveConflictByDeletingRev(filename, toDelete, "UI Selected")) == MISSING_OR_ERROR }
) { const toDelete = selected;
// const toKeep = conflictCheckResult.left.rev != toDelete ? conflictCheckResult.left.rev : conflictCheckResult.right.rev;
if (toDelete === LEAVE_TO_SUBSEQUENT) {
// Concatenate both conflicted revisions.
// Create a new file by concatenating both conflicted revisions.
const p = conflictCheckResult.diff.map((e) => e[1]).join("");
const delRev = testDoc._conflicts[0];
if (!(await this.core.databaseFileAccess.storeContent(filename, p))) {
this._log(`Concatenated content cannot be stored:${filename}`, LOG_LEVEL_NOTICE);
return false;
}
// 2. As usual, delete the conflicted revision and if there are no conflicts, write the resolved content to the storage.
if (
(await this.core.$$resolveConflictByDeletingRev(filename, delRev, "UI Concatenated")) ==
MISSING_OR_ERROR
) {
this._log(
`Concatenated saved, but cannot delete conflicted revisions: ${filename}, (${displayRev(delRev)})`,
LOG_LEVEL_NOTICE
);
return false;
}
} else if (typeof toDelete === "string") {
// Select one of the conflicted revision to delete.
if (
(await this.core.$$resolveConflictByDeletingRev(filename, toDelete, "UI Selected")) ==
MISSING_OR_ERROR
) {
this._log(`Merge: Something went wrong: ${filename}, (${toDelete})`, LOG_LEVEL_NOTICE);
return false;
}
} else {
this._log(`Merge: Something went wrong: ${filename}, (${toDelete})`, LOG_LEVEL_NOTICE); this._log(`Merge: Something went wrong: ${filename}, (${toDelete})`, LOG_LEVEL_NOTICE);
return false; return false;
} }
} else { // In here, some merge has been processed.
this._log(`Merge: Something went wrong: ${filename}, (${toDelete})`, LOG_LEVEL_NOTICE); // So we have to run replication if configured.
// TODO: Make this is as a event request
if (this.settings.syncAfterMerge && !this.core.$$isSuspended()) {
await this.core.$$replicateByEvent();
}
// And, check it again.
await this.core.$$queueConflictCheck(filename);
return false; return false;
} });
// In here, some merge has been processed.
// So we have to run replication if configured.
// TODO: Make this is as a event request
if (this.settings.syncAfterMerge && !this.core.$$isSuspended()) {
await this.core.$$replicateByEvent();
}
// And, check it again.
await this.core.$$queueConflictCheck(filename);
return false;
} }
async allConflictCheck() { async allConflictCheck() {
while (await this.pickFileForResolve()); while (await this.pickFileForResolve());

View File

@@ -367,7 +367,7 @@ export const SettingInformation: Partial<Record<keyof AllSettings, Configuration
}, },
enableDebugTools: { enableDebugTools: {
name: "Enable Developers' Debug Tools.", name: "Enable Developers' Debug Tools.",
desc: "Requires restart of Obsidian", desc: "While enabled, it causes very performance impact but debugging replication testing and other features will be enabled. Please disable this if you have not read the source code. Requires restart of Obsidian.",
}, },
suppressNotifyHiddenFilesChange: { suppressNotifyHiddenFilesChange: {
name: "Suppress notification of hidden files change", name: "Suppress notification of hidden files change",