Skip to content

Synchronize local libraries with their files (merge external changes without a dialog) - #16802

Closed
koppor wants to merge 7 commits into
fix-syncfrom
file-sync
Closed

koppor wants to merge 7 commits into
fix-syncfrom
file-sync

Conversation

@koppor

@koppor koppor commented Sep 1, 2026 •

Copy link
Copy Markdown
Member

Summary

🤖 Stacked on #11879 (base branch fix-sync); only the last commits belong to this PR.

With "Synchronize local libraries with their files" (the former autosave option) enabled, changes made to the library file by another program are now merged into the open library automatically, including field-level merges of entries also edited in JabRef. The "External changes detected" review is only offered when the same item was changed differently in memory and on disk, or deleted on one side and changed on the other.

Analogies: like honey, external edits now flow in smoothly instead of sticking in a dialog; like chocolate, the merge takes the best of both sides; like the moon, the review still shows up, but only when the two sides truly eclipse each other.

jabref-contrib-policy:4.2:reviewed​:ok

Steps to test

  1. Enable "Synchronize local libraries with their files" in Preferences → General → Saving.

    Preference

  2. Open a .bib file in JabRef and edit it in a text editor: change a field of one entry, add a new entry, save.

  3. Observe: the table updates within a few seconds, a "Merged 2 change(s) from the library file" notification appears, no review dialog.

  4. Change a field of an entry in JabRef, then change the same field to a different value in the text editor.

  5. Observe: "External changes detected" with "Review changes", offering the usual merge dialog for that entry only.

  6. Delete an entry in JabRef, then change that entry in the text editor: the review is offered; delete it in the text editor instead: nothing is asked.

Merged external changes without a dialog

Related issues and pull requests

Closes #8431

Stacked on #11879.

User documentation: JabRef/user-documentation#672

AI usage

Claude Code (model claude-fable-5-1), AIL4: implementation, tests, and manual GUI test driven by the AI, directed and reviewed by the author.

AI CHECKLIST.md walkthrough

1. Code self-review

Nullability and control flow

  • [/] No == null / != null checks — absence of an item on one side of the three-way comparison is modelled as null, as in the Git merge rules being reused.
  • No Objects.requireNonNull(...).
  • New classes annotated with @NullMarked.
  • Optional consumed with ifPresent / map / orElseThrow.
  • [/] StringUtil.isBlank(...).

Exceptions

  • No catch (Exception e).
  • No throw new RuntimeException(...).
  • Logged exceptions passed as the last logger argument.

Style and idioms

  • New BibEntry objects built with withers.
  • Modern Java used.
  • [/] Regexes.
  • Background work uses BackgroundTask.
  • No commented-out code, no trivial comments, no AI-disclosure comments.
  • Markdown Javadoc uses Markdown syntax.

User-facing text

  • All user-facing text localized.
  • Sentence case; no trailing !; labels do not end with :.
  • Variance expressed with placeholders.

Security

  • [/] HTML escaping.

Tests

  • Behavior changes have added tests (LibraryBaselineTest).
  • Tests assert object contents with plain JUnit asserts, no @DisplayName, no caught exceptions.
  • [/] Fetcher tests.

2. Verification commands

  • :jabgui:test --tests org.jabref.gui.collab.* and LocalizationConsistencyTest pass.
  • checkstyleMain checkstyleTest.
  • modernizer.
  • rewriteRun reports no changes.
  • [/] javadoc.
  • markdownlint-cli2 on the changed Markdown.
  • IntelliJ formatter (idea-2026.2.1 container) reports nothing to reformat.

3. Documentation

4. Pull request

  • PR body built from the template, every section filled.
  • All checklist items kept and marked.
  • All HTML comments removed.
  • Created with gh pr create --body-file.
  • [/] TODO placeholder.

Checklist

  • I own the copyright of the code submitted and I license it under the MIT license
  • If AI tools were used, I disclosed them in the "AI usage" section and reviewed, understood, and take full ownership of all AI-generated code
  • I manually tested my changes in running JabRef (always required)
  • I added JUnit tests for changes (if applicable)
  • I added screenshots in the PR description (if change is visible to the user)
  • I added one sentence (max 20 words) to CHANGELOG.md describing the change from the user's point of view (if the change is visible to the user)
  • I checked the user documentation for up to dateness and submitted a pull request to our user documentation repository

🤖 Generated with Claude Code

koppor and others added 2 commits September 1, 2026 22:32
External changes to a library file are merged into the open library without asking; the review dialog only appears for an item that was changed differently in memory and on disk. The autosave preference is relabeled accordingly, since both directions together keep library and file the same.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWmXyN3GUNhfF9fSo1UNrt
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWmXyN3GUNhfF9fSo1UNrt
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Synchronize local libraries with external file changes

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Automatically merge non-conflicting external library changes while preserving unsaved in-memory
 edits.
• Restrict review dialogs to true three-way conflicts, including conflicting edits and delete-modify
 cases.
• Relabel autosave as synchronization and document and test the new behavior.
Diagram

graph TD
  A["Library file"] --> B["Change monitor"] --> C["Change scanner"] --> D{"Baseline triage"} --> E["Automatic merge"]
  D --> F["Conflict review"]
  D --> G["Memory changes"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use the complete Git merge planner
  • ➕ Centralizes three-way merge semantics in one library-level engine.
  • ➕ Could reduce duplicated classification rules for entries and metadata.
  • ➖ Requires adapting GUI collaboration changes to Git planner result types.
  • ➖ Risks a broader architectural change for file synchronization.
  • ➖ May not preserve existing resolver, undo, and notification workflows directly.
2. Snapshot full database contexts
  • ➕ Avoids custom maps and serialized metadata snapshots.
  • ➕ Retains a uniform representation of every library element.
  • ➖ Introduces mutable object-identity and defensive-copying concerns.
  • ➖ Consumes more memory and still needs stable entry matching.
  • ➖ Does not eliminate specialized conflict triage logic.

Recommendation: Keep the PR's focused LibraryBaseline approach. It integrates with existing DatabaseChange resolvers and undo handling while reusing Git conflict and field-patch utilities where semantics overlap; adopting the complete Git planner would be substantially more invasive.

Files changed (11) +658 / -11

Enhancement (6) +418 / -8
ChangeScanner.javaExpose baseline triage for scanned changes +5/-0

Expose baseline triage for scanned changes

• Adds a delegation method that classifies scanner results against a LibraryBaseline using the active database and resolver factory.

jabgui/src/main/java/org/jabref/gui/collab/ChangeScanner.java

DatabaseChangeMonitor.javaAutomatically synchronize non-conflicting file changes +89/-2

Automatically synchronize non-conflicting file changes

• Tracks a common library baseline while synchronization is enabled, applies disk-only changes, and sends true conflicts to the existing review flow. Generation checks discard stale overlapping scans, while preference listeners manage baseline lifecycle and cleanup.

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java

LibraryBaseline.javaImplement three-way library change classification +300/-0

Implement three-way library change classification

• Introduces snapshots and triage for entries, metadata, preambles, groups, and BibTeX strings. It performs field-level entry merges, reconciles split add/delete pairs, and preserves unresolved ancestors across scans.

jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java

EntryChange.javaApply entry merges without replacing entry identity +21/-4

Apply entry merges without replacing entry identity

• Changes entry type and fields through granular undo edits instead of removing and reinserting the entry. This preserves table position, selection, and open editor state during synchronization.

jabgui/src/main/java/org/jabref/gui/collab/entrychange/EntryChange.java

GeneralTab.javaRelabel autosave as library-file synchronization +1/-1

Relabel autosave as library-file synchronization

• Renames the saving preference to describe its new bidirectional synchronization behavior while retaining the existing preference property.

jabgui/src/main/java/org/jabref/gui/preferences/general/GeneralTab.java

JabRef_en.propertiesLocalize synchronization preference and merge notification +2/-1

Localize synchronization preference and merge notification

• Replaces the autosave label with synchronization terminology and adds the notification shown after external changes merge automatically.

jablib/src/main/resources/l10n/JabRef_en.properties

Tests (2) +229 / -3
LibraryBaselineTest.javaCover three-way synchronization and conflict triage +226/-0

Cover three-way synchronization and conflict triage

• Tests disk-only and memory-only changes, field-level merges, citation-key matching, additions, deletions, metadata encoding, strings, conflicts, and unresolved-baseline retention.

jabgui/src/test/java/org/jabref/gui/collab/LibraryBaselineTest.java

GroupTreeSharedDatabaseProfileTest.javaNormalize shared database test imports +3/-3

Normalize shared database test imports

• Reorders imports to satisfy project and CI formatting rules without changing test behavior.

jabgui/src/test/java/org/jabref/gui/groups/GroupTreeSharedDatabaseProfileTest.java

Documentation (2) +10 / -0
CHANGELOG.mdDocument automatic external-change synchronization +1/-0

Document automatic external-change synchronization

• Adds a user-facing changelog entry explaining that autosave now merges non-conflicting external file changes automatically.

CHANGELOG.md

ux.mdDefine library-file synchronization requirements +9/-0

Define library-file synchronization requirements

• Specifies baseline-based synchronization behavior, field-level merging, and the conflict conditions that still require review.

docs/requirements/ux.md

Other (1) +1 / -0
module-info.javaExport Git merge utility package +1/-0

Export Git merge utility package

• Exports the merge-planning utility package so the GUI synchronization implementation can reuse conflict detection and field patch computation.

jablib/src/main/java/module-info.java

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWmXyN3GUNhfF9fSo1UNrt
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (5) 📎 Requirement gaps (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Queued scans survive synchronization 📘 Rule violation ☼ Reliability ⭐ New
Description
Disabling synchronization or unregistering the monitor does not invalidate scans already queued on
the executor, because their callbacks check only the scan generation and retain the previously
captured baseline. A stale callback can therefore automatically apply disk changes, update dirty
state, and notify listeners after synchronization was disabled, the tab was closed, the monitor was
replaced, or the component was disposed, rather than routing the changes through external-change
review.
Code

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[R202-203]

+            if (!enabled) {
+                baseline = null;
Evidence
Rule 51 requires disposal or rebinding to invalidate queued work so stale callbacks cannot mutate
components. isSynchronizing() is checked only when the task is submitted, preference changes
merely clear baseline, and unregister() removes listeners without incrementing scanGeneration
or recording an inactive state; consequently, the callback—which validates only scanGeneration—can
call synchronize with its captured baseline, immediately apply disk-only changes, update dirty
state, and invoke listeners even though LibraryTab has unregistered the monitor during close or
replacement.

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[200-206]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[237-245]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[301-307]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[230-277]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[301-308]
jabgui/src/main/java/org/jabref/gui/LibraryTab.java[697-715]
jabgui/src/main/java/org/jabref/gui/LibraryTab.java[799-810]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[192-207]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Queued synchronization scans remain valid after synchronization is disabled or the monitor is unregistered. Their asynchronous callbacks can consequently mutate the library after the user disabled synchronization, the tab closed, the monitor was replaced, or the component was disposed.

## Issue Context
The existing generation check detects only a newer scan. Disabling synchronization clears the current baseline but leaves the callback's captured baseline intact, while `unregister()` removes listeners without invalidating pending work or recording an inactive lifecycle state.

Invalidate pending scans when synchronization preferences or monitor lifecycle state change, or add an active/current-mode guard that every asynchronous callback checks before performing work. If synchronization has been disabled, do not perform the automatic merge; route the changes through the external-change review path instead.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[198-207]
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[230-257]
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[260-277]
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[301-308]
- jabgui/src/main/java/org/jabref/gui/LibraryTab.java[697-715]
- jabgui/src/main/java/org/jabref/gui/LibraryTab.java[799-810]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Merged entries use mutators 📘 Rule violation ⚙ Maintainability ⭐ New
Description
mergeFields constructs a new BibEntry and populates it with setField, contrary to the required
wither-style construction pattern. This makes construction look like an observable edit and deviates
from the repository's value-update convention.
Code

jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[R242-245]

+            if (value == null) {
+                merged.clearField(field);
+            } else {
+                merged.setField(field, value);
Evidence
Rule 10 explicitly requires newly created BibEntry instances to use withField-style updates
where applicable. The new merge method copies an entry and then invokes `merged.setField(field,
value)` while assembling that result.

AGENTS.md: Prefer Immutable Collection and Value Patterns
jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[237-247]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The newly constructed merged `BibEntry` is populated through `setField` instead of the required `withField` construction pattern.

## Issue Context
Retain the existing handling for removed fields, but use the construction-oriented wither for non-null field values.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[237-247]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Invalid scan erases baseline 🐞 Bug ≡ Correctness ⭐ New
Description
An invalid, empty, or unreadable external file produces an empty change list, after which
synchronize still replaces the baseline with the current in-memory library. This records unsaved
edits as already synchronized, so a later valid disk change to the same field is classified as
disk-only and can overwrite the edit without review.
Code

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[R268-273]

+        synchronized (database) {
+            LibraryBaseline updated = captureBaseline();
+            if (updated != null) {
+                updated.keepUnresolved(scannedBaseline, unresolved);
+            }
+            baseline = updated;
Evidence
ChangeScanner returns List.of() both for invalid/empty parser results and caught I/O failures,
making those failures indistinguishable from a successful no-change scan. synchronize then
captures the current in-memory library as the new baseline, while LibraryBaseline.sideOf later
treats differences from that new baseline as disk-only.

jabgui/src/main/java/org/jabref/gui/collab/ChangeScanner.java[36-63]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[261-274]
jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[71-84]
jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[210-219]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Invalid, empty, or unreadable file parses return an empty change list, but synchronization treats that result as successful and advances the baseline. Unsaved in-memory divergence can consequently be forgotten and overwritten by a later disk update.

## Issue Context
Distinguish a successful comparison with no changes from a scan that could not produce a valid database. Only advance the synchronization baseline after a valid comparison.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/ChangeScanner.java[36-63]
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[236-247]
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[261-274]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View high (6)
4. String rename collision ignored 🐞 Bug ≡ Correctness ⭐ New
Description
Disk-side string renames are classified as disk-only by checking only whether the destination name
existed in the baseline, not whether that name was added in memory afterward. The rename is then
automatically accepted despite colliding with the unsaved in-memory string, potentially leaving
duplicate names or an unapplied merge reported as successful.
Code

jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[R131-133]

+                case BibTexStringRename stringRename -> {
+                    boolean oldUntouchedInMemory = Objects.equals(strings.get(stringRename.getOldString().getName()), stringRename.getOldString().getContent());
+                    yield oldUntouchedInMemory && !strings.containsKey(stringRename.getNewString().getName()) ? Side.DISK : Side.BOTH;
Evidence
Rename triage uses strings.containsKey(newName), where strings is the baseline snapshot, and
never checks the current database supplied to triage. BibTexStringRename.applyChange detects a
current-name collision but still proceeds to apply the name change, while synchronization has
already marked disk-only changes accepted.

jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[71-84]
jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[125-145]
jabgui/src/main/java/org/jabref/gui/collab/stringrename/BibTexStringRename.java[28-36]
jablib/src/main/java/org/jabref/logic/bibtex/comparator/BibStringDiff.java[44-88]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A disk rename to a string name independently added in memory is classified as disk-only because rename triage checks only the baseline namespace. Automatic application then collides with the current string.

## Issue Context
Classify a rename as disk-only only when the old string is unchanged in memory and the destination name is absent from both the baseline and the current in-memory database. Add coverage for same-content and different-content destination strings added after the baseline.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[125-134]
- jabgui/src/main/java/org/jabref/gui/collab/stringrename/BibTexStringRename.java[28-36]
- jabgui/src/test/java/org/jabref/gui/collab/LibraryBaselineTest.java[202-225]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Scans can complete out-of-order ✓ Resolved 📎 Requirement gap ☼ Reliability
Description
Each file event starts an independent asynchronous synchronization using a shared mutable baseline,
with no cancellation, serialization, ordering, or generation check before applying results. Because
concurrent scans can complete out of order, an older disk snapshot may be applied after a newer one,
leaving the in-memory library stale and risking loss or misclassification of external changes.
Code

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[R212-215]

+            BackgroundTask.wrap(() -> scanner.scanForChanges(scannedBaseline))
+                          .onSuccess(triage -> synchronize(scannedBaseline, triage))
+                          .onFailure(e -> LOGGER.error("Error while synchronizing with the library file", e))
+                          .executeWith(taskExecutor);
Evidence
Compliance rule 2 requires external data to be protected during reconciliation. The monitor submits
a background scan for each changed snapshot at lines 212-215, but it neither passes an immutable
event snapshot into the scanner nor validates the result on completion; because the production
executor has five worker threads, scans can overlap, and the callbacks at lines 229-245 can change
the database and replace the shared baseline with an older result after a newer scan has completed.

Protect external changes when unsaved in-memory changes exist
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[209-215]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[229-245]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[194-245]
jabgui/src/main/java/org/jabref/gui/collab/ChangeScanner.java[36-64]
jabgui/src/main/java/org/jabref/gui/util/UiTaskExecutor.java[28-37]
jabgui/src/main/java/org/jabref/gui/util/UiTaskExecutor.java[115-128]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Overlapping synchronization scans can finish out of order and apply stale external file contents after newer contents, potentially leaving the in-memory library stale and causing external changes to be lost or misclassified.
## Issue Context
Every detected file event submits an independent background scan that captures shared mutable baseline state. The executor permits concurrent tasks, scans read from the mutable path rather than an immutable event snapshot, and completion callbacks modify the database and replace the shared baseline without cancellation, serialization, ordering, or validation that the result belongs to the latest event.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[194-245]
- jabgui/src/main/java/org/jabref/gui/collab/ChangeScanner.java[36-64]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Synchronization label translations missing 📘 Rule violation ≡ Correctness
Description
The English Autosave local libraries key was renamed, but translated bundles still contain the
obsolete key and lack the new synchronization label. Non-English users therefore fall back to
English instead of receiving a synchronized translation key.
Code

jablib/src/main/resources/l10n/JabRef_en.properties[716]

+Synchronize\ local\ libraries\ with\ their\ files=Synchronize local libraries with their files
Evidence
Rule 44 requires renamed localization keys to be updated consistently in every bundle. The English
bundle introduces the new synchronization key, while translated bundles such as German still expose
Autosave local libraries at line 717 and have no matching new key.

jablib/src/main/resources/l10n/JabRef_en.properties[716-716]
jablib/src/main/resources/l10n/JabRef_de.properties[717-717]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The renamed synchronization preference key is not aligned across localization bundles.
## Issue Context
Use the repository-supported localization workflow rather than manually maintaining generated translations. Remove or migrate the obsolete autosave key and ensure the new key exists consistently across bundles.
## Fix Focus Areas
- jablib/src/main/resources/l10n/JabRef_en.properties[716-716]
- jablib/src/main/resources/l10n/JabRef_de.properties[717-717]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Merge notification translations missing 📘 Rule violation ≡ Correctness
Description
The new Merged %0 change(s) from the library file key exists only in the English bundle. All
translated bundles consequently lack the notification key required by the new synchronization
workflow.
Code

jablib/src/main/resources/l10n/JabRef_en.properties[3520]

+Merged\ %0\ change(s)\ from\ the\ library\ file=Merged %0 change(s) from the library file
Evidence
Rule 44 requires every code-used key and its placeholders to exist across all bundles. The new %0
notification is added only to JabRef_en.properties; for example, the German notification region
contains the existing external-change key but no merged-change key.

jablib/src/main/resources/l10n/JabRef_en.properties[3520-3520]
jablib/src/main/resources/l10n/JabRef_de.properties[3386-3386]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[232-235]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new merged-changes notification key is absent from translated localization bundles.
## Issue Context
Preserve the `%0` placeholder exactly and use the repository-supported localization synchronization workflow to add the key consistently.
## Fix Focus Areas
- jablib/src/main/resources/l10n/JabRef_en.properties[3520-3520]
- jablib/src/main/resources/l10n/JabRef_de.properties[3386-3386]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Concurrent edits get overwritten ✓ Resolved 🐞 Bug ≡ Correctness
Description
A change classified as disk-only on the background thread is applied later without revalidating the
live database. If the user edits the same field before the success callback runs, the accepted
EntryChange silently replaces that newer edit with the scanned disk value.
Code

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[R212-214]

+            BackgroundTask.wrap(() -> scanner.scanForChanges(scannedBaseline))
+                          .onSuccess(triage -> synchronize(scannedBaseline, triage))
+                          .onFailure(e -> LOGGER.error("Error while synchronizing with the library file", e))
Evidence
BackgroundTask performs triage asynchronously and invokes its success callback later, while
synchronize applies accepted changes without a state/version check. Database diffs retain live
local entry references, and EntryChange.applyChange mutates those entries to the previously
captured disk values.

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[209-245]
jablib/src/main/java/org/jabref/logic/util/BackgroundTask.java[183-186]
jablib/src/main/java/org/jabref/logic/bibtex/comparator/BibDatabaseDiff.java[47-58]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeList.java[79-92]
jabgui/src/main/java/org/jabref/gui/collab/entrychange/EntryChange.java[41-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Automatic synchronization applies background-scan results without checking for in-memory edits made after triage, allowing newer user edits to be overwritten.
## Issue Context
The diff retains references to live entries, while classification and application occur at different times and on different threads.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[209-245]
- jabgui/src/main/java/org/jabref/gui/collab/ChangeScanner.java[49-52]
- jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[85-139]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Citation renames remain unpaired 🐞 Bug ≡ Correctness
Description
pairSplitEntries cannot pair a delete/add caused by a disk-side citation-key rename because find
requires citation-key equality whenever the added entry has a key. Such a rename can consequently be
applied as a separate deletion and insertion, losing entry identity or creating a duplicate while
the original deletion awaits review.
Code

jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[R242-245]

+                          .filter(base -> remote.getCitationKey()
+                                                .map(key -> base.getCitationKey().filter(key::equals).isPresent())
+                                                .orElseGet(() -> sameContent(base, remote)))
+                          .findFirst();
Evidence
DatabaseChangeList represents unmatched entries as separate additions and deletions, and
pairSplitEntries only combines them when find succeeds. For an added entry with a renamed citation
key, find examines only baseline entries with the new key, so it cannot identify the old baseline
entry and the separate changes remain.

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeList.java[79-92]
jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[166-191]
jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[239-245]
jabgui/src/main/java/org/jabref/gui/collab/entryadd/EntryAdd.java[22-25]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Split entry changes caused by citation-key renames cannot be paired because lookup requires the renamed key to equal the baseline key.
## Issue Context
The two-way diff may emit an EntryDelete and EntryAdd when similarity is too low. Pairing must identify the common baseline entry even when its citation key changed.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[166-191]
- jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[239-245]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

10. Merge logic lives in GUI 📘 Rule violation ⚙ Maintainability
Description
The new LibraryBaseline class implements three-way comparison, conflict classification,
field-level merging, and baseline reconciliation directly in org.jabref.gui. This substantial
non-visual business logic is not reusable outside the GUI layer and requires exporting an internal
logic utility package.
Code

jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[R82-85]

+    /// Sorts the differences between the in-memory library and its file, as computed by
+    /// [DatabaseChangeList#compareAndGetChanges], by which side changed. Entries modified on both sides in different
+    /// fields are merged field by field (with the Git merge rules) into a new, accepted [EntryChange].
+    public Triage triage(List<DatabaseChange> changes, BibDatabaseContext local, @Nullable DatabaseChangeResolverFactory resolverFactory) {
Evidence
Rule 21 requires complex non-visual operations to reside under org.jabref.logic. The new GUI-layer
class performs change triage and field-level merging, and the PR exposes
org.jabref.logic.git.merge.planning.util from jablib so that GUI code can invoke its merge
primitives.

AGENTS.md: Keep Non-GUI Logic Out of the GUI Layer: AGENTS.md: Keep Non-GUI Logic Out of the GUI Layer: AGENTS.md: Keep Non-GUI Logic Out of the GUI Layer: AGENTS.md: Keep Non-GUI Logic Out of the GUI Layer
jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[82-140]
jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[212-233]
jablib/src/main/java/module-info.java[120-120]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Substantial library synchronization and merge logic has been implemented in the GUI module.
## Issue Context
The GUI should delegate three-way comparison and merge planning to reusable logic-layer services. Keep presentation-specific `DatabaseChange` adaptation in `jabgui` while moving baseline representation, conflict classification, and field merging under `org.jabref.logic`.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[33-269]
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[209-245]
- jablib/src/main/java/module-info.java[117-121]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Encoding changes are discarded 🐞 Bug ≡ Correctness
Description
Metadata triage compares serialized maps that omit MetaData.encoding, although MetaDataDiff
detects encoding changes. An encoding-only disk change is therefore classified as memory-only and
silently dropped instead of being synchronized.
Code

jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[R112-114]

+                case MetadataChange metadataChange -> {
+                    metaDataSide = sideOf(metaData, serialize(local.getMetaData(), citationKeyPatterns), serialize(metadataChange.getMetaDataDiff().getNewMetaData(), citationKeyPatterns), Objects::equals);
+                    yield metaDataSide;
Evidence
MetaDataDiff explicitly emits an encoding difference, while MetaDataSerializer serializes numerous
metadata properties but not encoding. Since sideOf sees no remote divergence in that serialized
representation, it returns MEMORY and synchronization does not apply the MetadataChange.

jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[112-114]
jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[194-203]
jablib/src/main/java/org/jabref/logic/bibtex/comparator/MetaDataDiff.java[98-115]
jablib/src/main/java/org/jabref/logic/exporter/MetaDataSerializer.java[40-87]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Encoding-only external metadata changes disappear because the baseline representation does not include encoding.
## Issue Context
MetaDataDiff compares encoding, but MetaDataSerializer's map omits it, causing both baseline and remote values used by sideOf to appear unchanged.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[112-114]
- jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[266-268]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Runtime enablement stays inactive ✓ Resolved 🐞 Bug ≡ Correctness
Description
A monitor created while synchronization is disabled retains a null baseline, and enabling the
preference does not recreate the monitor or initialize that baseline. External changes therefore
continue through the review-dialog path until a later manual save or tab reload establishes a
baseline.
Code

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[R209-210]

+        LibraryBaseline scannedBaseline = baseline;
+        if (scannedBaseline != null && isSynchronizing()) {
Evidence
GeneralTabViewModel only persists the preference, and LibraryTab installs autosave management during
library setup rather than in response to preference changes. A monitor constructed while autosave is
off stores a null baseline, and the new scan gate requires that baseline to be non-null before
synchronization can run.

jabgui/src/main/java/org/jabref/gui/preferences/general/GeneralTabViewModel.java[282-286]
jabgui/src/main/java/org/jabref/gui/LibraryTab.java[386-403]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[181-218]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Enabling synchronization for an already-open library does not initialize its monitor baseline, so the feature remains inactive.
## Issue Context
The preference update only stores the flag. Existing monitors and autosave managers are not recreated, while the synchronization branch requires a baseline captured earlier.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[181-218]
- jabgui/src/main/java/org/jabref/gui/preferences/general/GeneralTabViewModel.java[282-286]
- jabgui/src/main/java/org/jabref/gui/LibraryTab.java[386-403]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources

Grey Divider

Tip of the day
💡 Did you know, you can describe a rule in plain language on the Rules page and Qodo drafts it for you

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java Outdated
Comment thread jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java
Comment thread jablib/src/main/resources/l10n/JabRef_en.properties
Comment thread jablib/src/main/resources/l10n/JabRef_en.properties
Comment on lines +212 to +214
BackgroundTask.wrap(() -> scanner.scanForChanges(scannedBaseline))
.onSuccess(triage -> synchronize(scannedBaseline, triage))
.onFailure(e -> LOGGER.error("Error while synchronizing with the library file", e))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

5. Concurrent edits get overwritten 🐞 Bug ≡ Correctness

A change classified as disk-only on the background thread is applied later without revalidating the
live database. If the user edits the same field before the success callback runs, the accepted
EntryChange silently replaces that newer edit with the scanned disk value.
Agent Prompt
## Issue description
Automatic synchronization applies background-scan results without checking for in-memory edits made after triage, allowing newer user edits to be overwritten.

## Issue Context
The diff retains references to live entries, while classification and application occur at different times and on different threads.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[209-245]
- jabgui/src/main/java/org/jabref/gui/collab/ChangeScanner.java[49-52]
- jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[85-139]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Generated with Claude Code

Fixed in c7bb88b: only the parse and the two-way diff run in the background; the classification against the baseline now happens on the FX thread immediately before the accepted changes are applied, so an edit made in the meantime is seen as an in-memory change and either merged field by field or sent to review instead of being overwritten.

Comment on lines +242 to +245
.filter(base -> remote.getCitationKey()
.map(key -> base.getCitationKey().filter(key::equals).isPresent())
.orElseGet(() -> sameContent(base, remote)))
.findFirst();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

6. Citation renames remain unpaired 🐞 Bug ≡ Correctness

pairSplitEntries cannot pair a delete/add caused by a disk-side citation-key rename because find
requires citation-key equality whenever the added entry has a key. Such a rename can consequently be
applied as a separate deletion and insertion, losing entry identity or creating a duplicate while
the original deletion awaits review.
Agent Prompt
## Issue description
Split entry changes caused by citation-key renames cannot be paired because lookup requires the renamed key to equal the baseline key.

## Issue Context
The two-way diff may emit an EntryDelete and EntryAdd when similarity is too low. Pairing must identify the common baseline entry even when its citation key changed.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[166-191]
- jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[239-245]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Generated with Claude Code

Fixed in c7bb88b: when no baseline entry carries the added entry's citation key, the lookup falls back to matching the remaining content, so a key renamed on disk is paired with its in-memory entry and applied as one change. Test citationKeyChangedOnDiskIsMergedIntoTheSameEntry added.

Comment on lines +112 to +114
case MetadataChange metadataChange -> {
metaDataSide = sideOf(metaData, serialize(local.getMetaData(), citationKeyPatterns), serialize(metadataChange.getMetaDataDiff().getNewMetaData(), citationKeyPatterns), Objects::equals);
yield metaDataSide;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

7. Encoding changes are discarded 🐞 Bug ≡ Correctness

Metadata triage compares serialized maps that omit MetaData.encoding, although MetaDataDiff
detects encoding changes. An encoding-only disk change is therefore classified as memory-only and
silently dropped instead of being synchronized.
Agent Prompt
## Issue description
Encoding-only external metadata changes disappear because the baseline representation does not include encoding.

## Issue Context
MetaDataDiff compares encoding, but MetaDataSerializer's map omits it, causing both baseline and remote values used by sideOf to appear unchanged.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[112-114]
- jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[266-268]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Generated with Claude Code

Fixed in c7bb88b: the baseline's metadata snapshot now includes the encoding, so an encoding change on disk is classified and applied like any other metadata change. Test encodingChangedOnDiskIsAccepted added.

A scan overtaken by a newer file change discards its result, and changes are sorted on the FX thread right before they are applied, so no edit can slip in between. Entries whose citation key changed on disk are paired with their in-memory entry, encoding changes count as metadata changes, and switching synchronization on for an open, unmodified library takes effect immediately.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWmXyN3GUNhfF9fSo1UNrt
@koppor
koppor marked this pull request as ready for review September 1, 2026 23:41
Comment on lines +202 to +203
if (!enabled) {
baseline = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Queued scans survive synchronization 📘 Rule violation ☼ Reliability

Disabling synchronization or unregistering the monitor does not invalidate scans already queued on
the executor, because their callbacks check only the scan generation and retain the previously
captured baseline. A stale callback can therefore automatically apply disk changes, update dirty
state, and notify listeners after synchronization was disabled, the tab was closed, the monitor was
replaced, or the component was disposed, rather than routing the changes through external-change
review.
Agent Prompt
## Issue description
Queued synchronization scans remain valid after synchronization is disabled or the monitor is unregistered. Their asynchronous callbacks can consequently mutate the library after the user disabled synchronization, the tab closed, the monitor was replaced, or the component was disposed.

## Issue Context
The existing generation check detects only a newer scan. Disabling synchronization clears the current baseline but leaves the callback's captured baseline intact, while `unregister()` removes listeners without invalidating pending work or recording an inactive lifecycle state.

Invalidate pending scans when synchronization preferences or monitor lifecycle state change, or add an active/current-mode guard that every asynchronous callback checks before performing work. If synchronization has been disabled, do not perform the automatic merge; route the changes through the external-change review path instead.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[198-207]
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[230-257]
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[260-277]
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[301-308]
- jabgui/src/main/java/org/jabref/gui/LibraryTab.java[697-715]
- jabgui/src/main/java/org/jabref/gui/LibraryTab.java[799-810]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +242 to +245
if (value == null) {
merged.clearField(field);
} else {
merged.setField(field, value);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Merged entries use mutators 📘 Rule violation ⚙ Maintainability

mergeFields constructs a new BibEntry and populates it with setField, contrary to the required
wither-style construction pattern. This makes construction look like an observable edit and deviates
from the repository's value-update convention.
Agent Prompt
## Issue description
The newly constructed merged `BibEntry` is populated through `setField` instead of the required `withField` construction pattern.

## Issue Context
Retain the existing handling for removed fields, but use the construction-oriented wither for non-null field values.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[237-247]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +268 to +273
synchronized (database) {
LibraryBaseline updated = captureBaseline();
if (updated != null) {
updated.keepUnresolved(scannedBaseline, unresolved);
}
baseline = updated;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Invalid scan erases baseline 🐞 Bug ≡ Correctness

An invalid, empty, or unreadable external file produces an empty change list, after which
synchronize still replaces the baseline with the current in-memory library. This records unsaved
edits as already synchronized, so a later valid disk change to the same field is classified as
disk-only and can overwrite the edit without review.
Agent Prompt
## Issue description
Invalid, empty, or unreadable file parses return an empty change list, but synchronization treats that result as successful and advances the baseline. Unsaved in-memory divergence can consequently be forgotten and overwritten by a later disk update.

## Issue Context
Distinguish a successful comparison with no changes from a scan that could not produce a valid database. Only advance the synchronization baseline after a valid comparison.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/ChangeScanner.java[36-63]
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[236-247]
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[261-274]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +131 to +133
case BibTexStringRename stringRename -> {
boolean oldUntouchedInMemory = Objects.equals(strings.get(stringRename.getOldString().getName()), stringRename.getOldString().getContent());
yield oldUntouchedInMemory && !strings.containsKey(stringRename.getNewString().getName()) ? Side.DISK : Side.BOTH;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. String rename collision ignored 🐞 Bug ≡ Correctness

Disk-side string renames are classified as disk-only by checking only whether the destination name
existed in the baseline, not whether that name was added in memory afterward. The rename is then
automatically accepted despite colliding with the unsaved in-memory string, potentially leaving
duplicate names or an unapplied merge reported as successful.
Agent Prompt
## Issue description
A disk rename to a string name independently added in memory is classified as disk-only because rename triage checks only the baseline namespace. Automatic application then collides with the current string.

## Issue Context
Classify a rename as disk-only only when the old string is unchanged in memory and the destination name is absent from both the baseline and the current in-memory database. Add coverage for same-content and different-content destination strings added after the baseline.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java[125-134]
- jabgui/src/main/java/org/jabref/gui/collab/stringrename/BibTexStringRename.java[28-36]
- jabgui/src/test/java/org/jabref/gui/collab/LibraryBaselineTest.java[202-225]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit c7bb88b

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

This pull requests was closed without merging. You have been unassigned from the respective issue #8431. In case you closed the PR for yourself, you can re-open it. Please also check After submission of a pull request in CONTRIBUTING.md.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant