Handle multiple conflict revisions deterministically

This commit is contained in:
vorotamoroz
2026-07-23 06:38:16 +00:00
parent d784799969
commit f8f11f358a
15 changed files with 576 additions and 97 deletions
@@ -22,9 +22,10 @@ import type { Editor, MarkdownFileInfo, MarkdownView } from "@/deps.ts";
export class ModuleInteractiveConflictResolver extends AbstractObsidianModule {
private postponedConflictEpisodes = new Set<FilePathWithPrefix>();
private async isConflicted(filename: FilePathWithPrefix): Promise<boolean | undefined> {
private async getConflictVersionCount(filename: FilePathWithPrefix): Promise<number | undefined> {
try {
return (await this.core.databaseFileAccess.getConflictedRevs(filename)).length > 0;
const conflictCount = (await this.core.databaseFileAccess.getConflictedRevs(filename)).length;
return conflictCount === 0 ? 0 : conflictCount + 1;
} catch (error) {
this._log(`Could not inspect the conflict state of ${filename}`, LOG_LEVEL_VERBOSE);
this._log(error, LOG_LEVEL_VERBOSE);
@@ -35,19 +36,26 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule {
private async getActiveConflictMessages(): Promise<string[]> {
const filename = this.services.vault.getActiveFilePath();
if (!filename) return [];
const conflicted = await this.isConflicted(filename);
if (conflicted === false) {
const versionCount = await this.getConflictVersionCount(filename);
if (versionCount === 0) {
this.postponedConflictEpisodes.delete(filename);
return [];
}
if (conflicted === true || this.postponedConflictEpisodes.has(filename)) {
if (versionCount !== undefined && versionCount >= 3) {
return [
$msg("This file has ${COUNT} unresolved versions. They will be reviewed one pair at a time.", {
COUNT: `${versionCount}`,
}),
];
}
if (versionCount === 2 || this.postponedConflictEpisodes.has(filename)) {
return [$msg("This file has unresolved conflicts.")];
}
return [];
}
private async refreshConflictState(filename: FilePathWithPrefix): Promise<void> {
if ((await this.isConflicted(filename)) === false) {
if ((await this.getConflictVersionCount(filename)) === 0) {
this.postponedConflictEpisodes.delete(filename);
}
eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR);
@@ -115,8 +123,21 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule {
this._log(`Merge: Could not read ${filename} from the local database`, LOG_LEVEL_VERBOSE);
return false;
}
if (!testDoc._conflicts) {
if (!testDoc._conflicts || testDoc._conflicts.length === 0) {
this._log(`Merge: Nothing to do ${filename}`, LOG_LEVEL_VERBOSE);
await this.refreshConflictState(filename);
return false;
}
if (
testDoc._rev !== conflictCheckResult.left.rev ||
!testDoc._conflicts.includes(conflictCheckResult.right.rev)
) {
this._log(
`Merge: The compared revisions changed while the dialogue was open: ${filename}`,
LOG_LEVEL_INFO
);
await this.refreshConflictState(filename);
await this.services.conflict.queueCheckFor(filename);
return false;
}
const toDelete = selected;
@@ -125,7 +146,7 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule {
// 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];
const delRev = conflictCheckResult.right.rev;
if (!(await this.core.databaseFileAccess.storeContent(filename, p))) {
this._log(`Concatenated content cannot be stored:${filename}`, LOG_LEVEL_NOTICE);
return false;
@@ -141,7 +162,10 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule {
);
return false;
}
} else if (typeof toDelete === "string") {
} else if (
typeof toDelete === "string" &&
(toDelete === conflictCheckResult.left.rev || toDelete === conflictCheckResult.right.rev)
) {
// Select one of the conflicted revision to delete.
if (
(await this.services.conflict.resolveByDeletingRevision(filename, toDelete, "UI Selected")) ==
@@ -151,7 +175,7 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule {
return false;
}
} else {
this._log(`Merge: Something went wrong: ${filename}, (${toDelete as string})`, LOG_LEVEL_NOTICE);
this._log(`Merge: Something went wrong: ${filename}, (${String(toDelete)})`, LOG_LEVEL_NOTICE);
return false;
}
// In here, some merge has been processed.
@@ -166,10 +190,13 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule {
});
}
async allConflictCheck() {
while (await this.pickFileForResolve());
let notifyIfEmpty = true;
while (await this.pickFileForResolve(notifyIfEmpty)) {
notifyIfEmpty = false;
}
}
async pickFileForResolve() {
async pickFileForResolve(notifyIfEmpty = true) {
const notes: { id: DocumentID; path: FilePathWithPrefix; dispPath: string; mtime: number }[] = [];
for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) {
if (!("_conflicts" in doc)) continue;
@@ -183,7 +210,9 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule {
notes.sort((a, b) => b.mtime - a.mtime);
const notesList = notes.map((e) => e.dispPath);
if (notesList.length == 0) {
this._log("There are no conflicted documents", LOG_LEVEL_NOTICE);
if (notifyIfEmpty) {
this._log("There are no conflicted documents", LOG_LEVEL_NOTICE);
}
return false;
}
const target = await this.core.confirm.askSelectString("File to resolve conflict", notesList);