Skip to content

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

Draft
koppor wants to merge 39 commits into
mainfrom
file-sync-main
Draft

koppor wants to merge 39 commits into
mainfrom
file-sync-main

Conversation

@koppor

@koppor koppor commented Sep 2, 2026 •

Copy link
Copy Markdown
Member

Summary

🤖 With "Automatically merge external changes" enabled (per library, or as the global default in Preferences → General → Saving), changes made to the library file by another program are 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. Accepting an external change to an entry now edits the entry in place, so it keeps its table position, selection, and open entry editor.

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. Open a .bib file and set Library → Library properties → Saving → "Automatically merge external changes" to On (or enable the global default of the same name in Preferences → General → Saving; both are off by default).

  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.

    Review notification

    Review of the conflicting 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

Before: no such option; every external change was reported for review.

After (Preferences → General → Saving, global defaults):

Preferences after

After (Library properties → Saving, per library):

Library properties after

Related issues and pull requests

Closes #8431

Supersedes #16802, which was based on #11879 without needing it.

Supersedes JabRef#442. Implements the idea of #5669: a baseline of the file decides whether an external change is merged silently or offered for review.

The save side relies on autosave, which is broken on main and fixed in #16826. Follow-up in the same stack: #16827 (conflicted copies of sync clients).

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 5 commits September 2, 2026 09:18
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
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWmXyN3GUNhfF9fSo1UNrt
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
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Automatically merge external changes into open local libraries

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add opt-in automatic merging of external file changes, with global defaults and per-library
 overrides.
• Merge nonconflicting entry fields automatically; offer review only for conflicting changes.
• Preserve entry identity during merges and guard scans against incomplete writes and stale results.
Diagram

graph TD
  Prefs["Sync settings"] --> Monitor["File monitor"] --> Scanner["Change scanner"] --> Triage{"Change origin"} --> Apply["Apply merge"]
  Baseline["Library baseline"] --> Triage --> Review["Conflict review"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Build a direct three-way database diff
  • ➕ Could avoid translating two-way changes and heuristically pairing renamed entries.
  • ➕ Could classify metadata and groups at finer granularity.
  • ➖ Would duplicate existing database-diff and review integration.
  • ➖ Requires a substantially larger change to the GUI change model.

Recommendation: The baseline plus existing change-scanner approach is the better incremental choice: it reuses established diff, review, and Git field-merge rules. Review the conservative renamed-entry matching and baseline advancement particularly closely.

Files changed (35) +2020 / -26

Enhancement (18) +1137 / -20
ChangeScanner.javaMake file scans cancellable and expose change triage +22/-5

Make file scans cancellable and expose change triage

• Adds a readiness check before parsing and distinguishes abandoned scans from read failures. Exposes classification against a library baseline.

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

ChangeTriage.javaClassify and merge scanned external changes +263/-0

Classify and merge scanned external changes

• Sorts database changes into disk-only, memory-only, and conflicting groups, including field-level entry merges. Pairs split entry changes and maintains baseline ancestry across scans and reviews.

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

DatabaseChangeMonitor.javaCoordinate safe automatic file synchronization +270/-14

Coordinate safe automatic file synchronization

• Tracks synchronization settings and baselines, waits for stable file writes, and discards overtaken scans. Applies nonconflicting changes, offers conflict-only review, maintains dirty state, and withdraws obsolete notifications.

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

SavingPropertiesView.javaPresent per-library synchronization choices +27/-0

Present per-library synchronization choices

• Adds a selector for On, Off, or the displayed global default and binds it to library properties.

jabgui/src/main/java/org/jabref/gui/libraryproperties/saving/SavingPropertiesView.java

SavingPropertiesViewModel.javaLoad and store the library synchronization override +16/-0

Load and store the library synchronization override

• Represents an absent override as following the global preference and persists explicit On or Off choices in library metadata.

jabgui/src/main/java/org/jabref/gui/libraryproperties/saving/SavingPropertiesViewModel.java

GeneralTab.javaExpose the global automatic-merge preference +1/-0

Expose the global automatic-merge preference

• Adds the default external-change merge checkbox to General preferences.

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

GeneralTabViewModel.javaBind the global synchronization default +7/-0

Bind the global synchronization default

• Loads, stores, and exposes the new library preference to the General settings UI.

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

SavingProperties.fxmlAdd external-change controls to library properties +6/-0

Add external-change controls to library properties

• Introduces a labeled synchronization selector in the Saving tab.

jabgui/src/main/resources/org/jabref/gui/libraryproperties/saving/SavingProperties.fxml

LibraryPreferences.javaAdd the opt-in global synchronization default +17/-0

Add the opt-in global synchronization default

• Introduces an observable, off-by-default preference with accessors for automatic external-change merging.

jablib/src/main/java/org/jabref/logic/LibraryPreferences.java

MetaDataSerializer.javaWrite the library synchronization override +1/-0

Write the library synchronization override

• Serializes an explicitly set per-library synchronization value into library metadata.

jablib/src/main/java/org/jabref/logic/exporter/MetaDataSerializer.java

MetaDataParser.javaRead the library synchronization override +2/-0

Read the library synchronization override

• Parses the persisted per-library boolean from BibTeX metadata.

jablib/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java

LibraryBaseline.javaTrack the common ancestor for local-file merges +419/-0

Track the common ancestor for local-file merges

• Snapshots entries and other library content, identifies which side changed, and merges nonconflicting entry fields using existing Git merge rules. Supports entry matching and item-level ancestry updates after scans and reviews.

jablib/src/main/java/org/jabref/logic/sync/LibraryBaseline.java

BibChangeDescriber.javaName new undoable entry changes +6/-0

Name new undoable entry changes

• Adds descriptions for entry-comment edits and changed-flag updates in undo reporting.

jablib/src/main/java/org/jabref/logic/undo/BibChangeDescriber.java

UndoManager.javaExpose whether an undo step exists +3/-0

Expose whether an undo step exists

• Adds a canUndo method to the undo-manager interface.

jablib/src/main/java/org/jabref/logic/undo/UndoManager.java

MetaData.javaStore a nullable per-library synchronization choice +27/-0

Store a nullable per-library synchronization choice

• Adds an optional synchronization override, change notifications, overwrite support, and equality handling. Absence means the library follows the global preference.

jablib/src/main/java/org/jabref/model/metadata/MetaData.java

BibChange.javaPermit new undoable entry operations +1/-1

Permit new undoable entry operations

• Extends the sealed BibChange hierarchy with comment changes and changed-flag changes.

jablib/src/main/java/org/jabref/model/undo/BibChange.java

UndoableCommentsChange.javaMake external entry-comment edits undoable +40/-0

Make external entry-comment edits undoable

• Adds a guarded, invertible comment change that marks the entry as changed for correct serialization.

jablib/src/main/java/org/jabref/model/undo/UndoableCommentsChange.java

JabRef_en.propertiesLocalize synchronization controls and feedback +9/-0

Localize synchronization controls and feedback

• Adds English strings for settings, merge notifications, and the new undo operations.

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

Bug fix (2) +101 / -4
EntryChange.javaApply external entry changes in place +33/-4

Apply external entry changes in place

• Replaces entry removal and reinsertion with undoable edits to type, fields, comments, and changed state. This preserves entry identity, table position, selection, and open editors.

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

UndoableChangedFlag.javaUndo entry changed-state updates safely +68/-0

Undo entry changed-state updates safely

• Records the entry's changed flag and original content so undo restores an unmarked state only when later edits have not made that unsafe.

jablib/src/main/java/org/jabref/model/undo/UndoableChangedFlag.java

Tests (8) +747 / -2
ChangeTriageTest.javaTest change classification and baseline advancement +386/-0

Test change classification and baseline advancement

• Covers field merges, conflicts, additions, deletions, renamed entries, comments, metadata, and preservation of ancestors across scans and reviews.

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

DatabaseChangeMonitorTest.javaTest monitor cleanup and dirty-state preservation +57/-1

Test monitor cleanup and dirty-state preservation

• Checks that unregistering withdraws a pending review and that applying an external change does not clear unrelated unsaved edits. Updates monitor test fixtures.

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

GroupTreeSharedDatabaseProfileTest.javaAdapt shared-group test preference fixture +1/-1

Adapt shared-group test preference fixture

• Supplies the new synchronization argument to the LibraryPreferences constructor.

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

GroupTreeViewModelTest.javaAdapt group-tree test preference fixture +1/-0

Adapt group-tree test preference fixture

• Supplies the new synchronization argument to the LibraryPreferences constructor.

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

BibtexParserTest.javaTest parsing of the library synchronization setting +8/-0

Test parsing of the library synchronization setting

• Verifies that BibTeX metadata retains an explicit false override.

jablib/src/test/java/org/jabref/logic/importer/fileformat/BibtexParserTest.java

LibraryBaselineTest.javaTest three-way baseline and field-merge rules +265/-0

Test three-way baseline and field-merge rules

• Exercises side classification, field and type conflicts, comments, entry identity, string renames, and conservative matching of ambiguous entries.

jablib/src/test/java/org/jabref/logic/sync/LibraryBaselineTest.java

MetaDataOverwriteWithTest.javaCover synchronization setting in metadata overwrite tests +1/-0

Cover synchronization setting in metadata overwrite tests

• Populates the new per-library override in the existing metadata fixture.

jablib/src/test/java/org/jabref/model/metadata/MetaDataOverwriteWithTest.java

BibChangeTest.javaTest changed-flag undo safeguards +28/-0

Test changed-flag undo safeguards

• Adds the new change type to shared undo tests and verifies that undo restores the unmarked flag only when entry content permits it.

jablib/src/test/java/org/jabref/model/undo/BibChangeTest.java

Documentation (5) +31 / -0
CHANGELOG.mdAnnounce automatic merging and entry-position fix +2/-0

Announce automatic merging and entry-position fix

• Adds user-facing notes for the opt-in external-change merge and for preserving an accepted entry's table position, selection, and editor.

CHANGELOG.md

ux.mdSpecify automatic external-change merge behavior +9/-0

Specify automatic external-change merge behavior

• Defines opt-in defaults, silent merging, conflict-only review, and preservation of unsaved in-memory edits.

docs/requirements/ux.md

BibFileMerger.javaDocument the related local-file merge strategy +3/-0

Document the related local-file merge strategy

• Clarifies how baseline-based synchronization relates to Git's merge-base approach.

jablib/src/main/java/org/jabref/logic/git/merge/BibFileMerger.java

DBMSSynchronizer.javaDistinguish shared-database and local-file synchronization +3/-0

Distinguish shared-database and local-file synchronization

• Documents why local file changes use a baseline-based three-way merge rather than shared-database synchronization.

jablib/src/main/java/org/jabref/logic/shared/DBMSSynchronizer.java

package-info.javaDocument the synchronization package +14/-0

Document the synchronization package

• Describes the division between baseline merge logic and GUI change detection, and null-marks the package.

jablib/src/main/java/org/jabref/logic/sync/package-info.java

Other (2) +4 / -0
module-info.javaExport the synchronization logic package +1/-0

Export the synchronization logic package

• Makes LibraryBaseline available to the GUI module.

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

JabRefCliPreferences.javaPersist the global synchronization preference +3/-0

Persist the global synchronization preference

• Adds a preference key and binds the new LibraryPreferences property to preference storage.

jablib/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java

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

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

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Old reviews can overwrite newer edits ✓ Resolved
Description
synchronize leaves an existing conflict notification actionable when a later scan applies
disk-only changes without offering another review. If the file reverts the conflicted entry to its
baseline value while changing another entry, accepting the old review can apply its obsolete disk
value over the unsaved in-memory edit and rebase against that obsolete value.
Code

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[R420-422]

+        if (!triage.bothSides().isEmpty()) {
+            listeners.forEach(listener -> listener.databaseChanged(triage.bothSides()));
+        }
Evidence
Rule 2 prohibits silently discarding unsaved edits. The new synchronization path publishes a review
only for both-sides changes, while the review action applies its captured changes without checking
whether a later scan has superseded them.

Protect unsaved edits during external file changes
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[403-422]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[140-150]

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 conflict review remains actionable after a newer synchronization scan that offers no replacement review. Accepting it can apply changes from an obsolete file state.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[403-437]
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[140-150]
## Recommended Fix
Invalidate pending reviews when a newer scan supersedes them, and check that a review is still current before applying its result. Add a test covering a conflict followed by a disk-only scan.

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


2. External edits can erase migrated groups ✓ Resolved
Description
onSynchronizingChanged treats !undoManager.canUndo() as proof that a modified library matches
its file and captures the in-memory state as the disk baseline. Search-group migrations,
save-then-undo, and rejected external changes can all leave memory divergent with no undo step, so
an immediate or later scan can classify file differences as disk-only and silently apply them over
those changes.
Code

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[R228-231]

+            } else if (baseline == null && (!libraryTab.isModified() || !undoManager.canUndo())) {
+                // A modified tab without an undoable step was only dirtied by a setting, such as the one just
+                // switched on; its entries still match the file
+                baseline = captureBaseline();
Evidence
JabRefUndoManager.canUndo() checks only the undo stack, so it cannot establish equality with the
file: save-then-undo can leave the tab modified with steps on the redo stack, migration loading
marks changed group metadata without an undo step, and applyResolvedChanges marks denied changes
without adding one. The new condition captures the divergent in-memory state as the baseline in each
case; the ensuing scan can then classify differences from the file as disk-only and apply them
without review.

Safely handle externally modified bibliography files
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[220-232]
jabgui/src/main/java/org/jabref/gui/LibraryTab.java[430-442]
jabgui/src/main/java/org/jabref/gui/importer/actions/SearchGroupsMigrationAction.java[46-52]
jablib/src/main/java/org/jabref/logic/sync/LibraryBaseline.java[103-116]
jabgui/src/main/java/org/jabref/gui/collab/ChangeTriage.java[76-99]
jablib/src/main/java/org/jabref/logic/undo/JabRefUndoManager.java[416-418]
jablib/src/main/java/org/jabref/logic/undo/JabRefUndoManager.java[522-547]
jabgui/src/main/java/org/jabref/gui/LibraryTab.java[437-442]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[218-243]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[401-418]
jablib/src/main/java/org/jabref/logic/undo/JabRefUndoManager.java[410-418]
jablib/src/main/java/org/jabref/logic/undo/JabRefUndoManager.java[516-525]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[368-385]

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 modified library can have no undo step even when its in-memory state differs from the file. Re-enabling synchronization can capture that state as the disk baseline, causing an immediate or later scan to silently apply file content over a migration, an undone state, or a rejected external change.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[218-244]
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[401-418]
## Recommended Fix
Do not use undo availability to establish disk consistency. Capture the baseline immediately only when the library is known to match the file—for example, when it is unmodified or after comparing it with the file—and otherwise retain the review path until a successful save establishes a baseline. Add a test that rejects an external change, disables synchronization, and re-enables it.

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


3. Reviewing one entry can erase another edit ✓ Resolved
Description
rebaseAfterReview rebuilds the baseline from memory but preserves old ancestors only for rejected
changes in the dialog’s resolved list, discarding ancestors retained for memory-only edits. If A
has an unsaved edit while B needs review, accepting B makes A’s edited value its ancestor; a later
disk edit to A is classified as disk-only and overwrites A without review.
Code

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[R393-395]

+            LibraryBaseline updated = captureBaseline();
+            if (updated != null && previous != null) {
+                ChangeTriage.keepUnresolved(updated, previous, resolved.stream().filter(change -> !change.isAccepted()).toList());
Evidence
synchronize preserves the ancestors of both memory-only and conflicting changes, but sends only
conflicts to the review dialog. rebaseAfterReview passes only rejected dialog changes to
keepUnresolved, so A’s memory-only ancestor is replaced by its edited state; sideOf then
classifies a subsequent file change to A as DISK.

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[368-395]
jabgui/src/main/java/org/jabref/gui/collab/ChangeTriage.java[96-105]
jablib/src/main/java/org/jabref/logic/sync/LibraryBaseline.java[292-300]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[369-386]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[390-399]

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

## Issue description
Review rebasing discards preserved ancestors for memory-only edits that were not shown in the review. A later disk edit to one of those entries can overwrite its unsaved value without review.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[368-399]
- jabgui/src/main/java/org/jabref/gui/collab/ChangeTriage.java[110-139]
## Recommended Fix
Carry forward the previous ancestors of memory-only edits and outstanding conflicts when rebasing, and update ancestors only for items actually resolved. This can be done by starting from the previous baseline and updating accepted items that now match the file, or by retaining the last triage’s memory-only changes and passing them to `keepUnresolved` alongside rejected changes. Test an unsaved memory-only edit to A alongside a conflict in B, then accept B and edit A on disk.

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


View high (4)
4. Undo can omit later edits from a save ✓ Resolved
Description
EntryChange.applyChange places UndoableChangedFlag before the content edits, so undo restores
the clean flag last even when an intervening field or comment edit causes a content inversion to
refuse. The entry can then hold the intervening edit with hasChanged() false, causing
BibEntryWriter to save its stale parsed serialization instead of its current contents.
Code

jabgui/src/main/java/org/jabref/gui/collab/entrychange/EntryChange.java[R50-52]

+        CompoundEdit entryEdit = new CompoundEdit(getName());
+        // First in the compound, so that undo restores it after the field edits have marked the entry changed again
+        entryEdit.applyEdit(new UndoableChangedFlag(oldEntry, oldEntry.hasChanged(), true));
Evidence
ChangeSet.inverted() reverses the edits, and ChangeSet.apply() continues after a refused
inversion. Field and comment inversions can refuse stale values, but the final changed-flag
inversion independently sets the flag to false; the writer uses that flag to select parsed
serialization.

jabgui/src/main/java/org/jabref/gui/collab/entrychange/EntryChange.java[49-68]
jablib/src/main/java/org/jabref/model/undo/ChangeSet.java[27-32]
jablib/src/main/java/org/jabref/model/undo/ChangeSet.java[43-60]
jablib/src/main/java/org/jabref/model/undo/UndoableChangedFlag.java[19-25]
jablib/src/main/java/org/jabref/model/undo/UndoableCommentsChange.java[18-25]
jablib/src/main/java/org/jabref/logic/bibtex/BibEntryWriter.java[67-78]

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 partially refused undo of an external entry merge can still mark the entry unchanged. The writer then saves old parsed text instead of a later field or comment edit.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/entrychange/EntryChange.java[49-68]
- jablib/src/main/java/org/jabref/model/undo/UndoableChangedFlag.java[12-25]
## Recommended Fix
Restore the unchanged flag only if the entry's entire content was successfully restored to the original parsed state; otherwise leave it changed so saving serializes current values. Add a test that edits a merged field again, undoes the merge, and saves.

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


5. Merged-in edits are reverted by the next file change ✓ Resolved
Description
After applying accepted changes, synchronize and rebaseAfterReview rebuild the baseline with
captureBaseline() from memory, so an accepted change whose result differs from the file (a
field-level merge, or a review where the user kept their value) gets memory as its ancestor. The
next external write then makes the file's older value look like a disk-only change, and triage
silently applies it over the user's edit.
Code

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[R376-381]

+        synchronized (database) {
+            LibraryBaseline updated = captureBaseline();
+            if (updated != null) {
+                ChangeTriage.keepUnresolved(updated, scannedBaseline, unresolved);
+            }
+            baseline = updated;
Evidence
mergeEntry produces local+remote, which is applied, and then captureBaseline snapshots memory.
The merged entry is not in unresolved, so keepUnresolved does not preserve it. On the next scan,
sideOf(base=merged, local=merged, remote=disk) returns DISK, and the accepted change reverts the
in-memory field.

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[369-399]
jablib/src/main/java/org/jabref/logic/sync/LibraryBaseline.java[293-300]
jabgui/src/main/java/org/jabref/gui/collab/ChangeTriage.java[150-154]

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

## Issue description
After a merge or review, the baseline is taken from memory. For accepted changes whose applied result differs from the file, the file's value is then classified as a DISK change on the next scan and overwrites the in-memory edit.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[376-398]
- jabgui/src/main/java/org/jabref/gui/collab/ChangeTriage.java[112-139]
## Recommended Fix
For every accepted EntryChange (and similar changes) whose applied entry differs from the parsed disk entry, record the disk version as the baseline ancestor for that entry id. You could add a baseline method that stores a snapshot of a given BibEntry under the local id, and call it with the original disk entry. Leftover memory edits then stay classified as memory-only.

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


6. Closed tabs can apply stale scans ✓ Resolved
Description
unregister() increments volatile scanGeneration outside synchronized (database), while scan
startup increments the same counter under that lock. A file callback racing tab closure can lose one
non-atomic increment, leaving a queued success callback current so it can synchronize changes or
publish a review after disposal.
Code

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[405]

+        scanGeneration++;
Evidence
The lifecycle rule requires disposal to invalidate queued work and make stale callbacks no-ops. Scan
startup and result handling use a generation token, but disposal modifies its volatile counter with
a non-atomic increment outside the lock shared by scan startup.

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[266-305]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[404-412]
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
Tab disposal does not atomically invalidate queued scans because `scanGeneration++` can race scan startup and lose an increment.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[404-412]
## Recommended Fix
Increment `scanGeneration` under the same `synchronized (database)` lock used when scans are started, or replace the counter with an atomic generation mechanism. Ensure disposal always changes the generation observed by every previously queued callback before listener cleanup completes.

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


7. A read failure hides external changes ✓ Resolved
Description
ChangeScanner.scanForChanges(BooleanSupplier) catches an IOException from parsing and returns
Optional.of(List.of()), making a failed scan indistinguishable from a successful scan with no
changes. After scanIfFileChanged records that file snapshot, a transient read failure reaches
neither retry nor review, and later events for the same snapshot are skipped.
Code

jabgui/src/main/java/org/jabref/gui/collab/ChangeScanner.java[R54-57]

+            return Optional.of(getDatabaseChanges(database.getDatabasePath().get()));
} catch (IOException e) {
LOGGER.warn("Error while parsing changed file.", e);
-            return List.of();
+            return Optional.of(List.of());
Evidence
The external-file safety rule requires disk changes to be loaded or offered for review rather than
silently skipped. The scanner converts a parsing failure to an empty successful result, while the
monitor has already stored the matching snapshot and consequently suppresses subsequent scans of
that state.

Safely handle externally modified bibliography files
jabgui/src/main/java/org/jabref/gui/collab/ChangeScanner.java[45-57]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[255-282]

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 bibliography read failure is converted into a successful empty scan after the disk snapshot has already been recorded, preventing the same external file state from being scanned again.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/ChangeScanner.java[53-57]
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[255-282]
## Recommended Fix
Represent parsing failure separately from an empty change list and handle it in the monitor by invalidating the recorded disk snapshot and scheduling a bounded retry. Keep abandoned stale-generation scans distinct so they remain no-ops rather than being retried.

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



Remediation recommended

8. Unrelated setting edits force a review 🐞 Bug ≡ Correctness
Description
ChangeTriage.triage classifies the entire serialized metadata as one item and assigns that result
to its accompanying group change. When memory changes one setting, such as encoding, and the file
changes only the groups, both changes go to review even though they affect different items.
Code

jabgui/src/main/java/org/jabref/gui/collab/ChangeTriage.java[R89-94]

+                    keepLocalSettings(local.getMetaData(), metadataChange.getMetaDataDiff().getNewMetaData());
+                    metaDataSide = baseline.sideOfMetaData(local.getMetaData(), metadataChange.getMetaDataDiff().getNewMetaData());
+                    yield metaDataSide;
+                }
+                case GroupChange _ ->
+                        metaDataSide;
Evidence
The baseline compares complete serialized metadata maps, while the two-way diff represents groups as
a metadata change followed by a group change. The added test explicitly sets encoding in memory and
groups on disk, then observes both changes in bothSides, demonstrating that the new triage
requires review for this disjoint pair.

jablib/src/main/java/org/jabref/logic/sync/LibraryBaseline.java[246-248]
jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeList.java[32-40]
jabgui/src/main/java/org/jabref/gui/collab/ChangeTriage.java[88-94]
jabgui/src/test/java/org/jabref/gui/collab/ChangeTriageTest.java[354-375]

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

## Issue description
Independent in-memory and on-disk metadata edits are classified together, so unrelated external group changes require review.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/ChangeTriage.java[88-94]
- jablib/src/main/java/org/jabref/logic/sync/LibraryBaseline.java[246-248]
## Recommended Fix
Compare metadata items independently against their baseline values. Apply disk-only items without replacing memory-only settings, and send only genuinely conflicting items to review; cover a local encoding edit paired with a disk group edit.

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


9. Accepting a review can switch auto-merge off 🐞 Bug ≡ Correctness
Description
scanToVerifyMatch sends the raw scanned changes to offerReview without going through
ChangeTriage.triage, so keepLocalSettings never copies the in-memory sync setting into the
parsed disk metadata. When the user accepts the MetadataChange, MetaData.overwriteWith (which
now copies synchronizeWithFile) puts back the file's absent or false value;
onSynchronizingChanged(false) then clears the baseline, and the review dialog never showed this
difference.
Code

jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[R263-266]

+                          if (ChangeTriage.matchesFile(scanned, preferences.getCitationKeyPatternPreferences().getKeyPatterns())) {
+                              captureIfCurrent(generation);
+                          } else {
+                              offerReview(generation, scanned);
Evidence
Only triage calls keepLocalSettings. The verify path goes straight to review, and applying a
MetadataChange overwrites the in-memory metadata, including the newly copied synchronizeWithFile.

jabgui/src/main/java/org/jabref/gui/collab/ChangeTriage.java[88-92]
jablib/src/main/java/org/jabref/model/metadata/MetaData.java[482-482]
jabgui/src/main/java/org/jabref/gui/collab/metedatachange/MetadataChange.java[24-33]

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 review offered after switching sync on for a modified library applies the file's metadata as-is. That replaces the user's unsaved per-library synchronizeWithFile value and disables synchronization again.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[258-271]
- jabgui/src/main/java/org/jabref/gui/collab/ChangeTriage.java[205-209]
## Recommended Fix
Make keepLocalSettings accessible (package-private). In offerReview, before notifying listeners, call it for every MetadataChange in the list, passing database.getMetaData() and the change's getMetaDataDiff().getNewMetaData().

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



Informational

10. New undo method in shared interface is unused 🐞 Bug ⚙ Maintainability
Description
canUndo() was added to the logic-layer UndoManager interface, but no production code calls it
through that interface, because the switch-on check now uses libraryTab.isModified(). Every future
implementation must still provide it, and its Javadoc suggests the undo stack shows whether the
library matches its file, which the monitor's own comment says it does not.
Code

jablib/src/main/java/org/jabref/logic/undo/UndoManager.java[R64-65]

+    /// Whether there is an undoable step, i.e. whether the library was edited through the undo manager
+    boolean canUndo();
Evidence
A search finds no production caller of canUndo through the logic-layer UndoManager. GuiUndoManager
already declares it.

jabgui/src/main/java/org/jabref/gui/undo/GuiUndoManager.java[32-32]

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

## Issue description
`canUndo()` in the logic-layer UndoManager interface has no production caller through that interface.
## Fix Focus Areas
- jablib/src/main/java/org/jabref/logic/undo/UndoManager.java[64-65]
## Recommended Fix
Delete the declaration. GuiUndoManager already declares canUndo and JabRefUndoManager implements it, so existing callers and tests keep working.

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


Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

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/test/java/org/jabref/gui/collab/LibraryBaselineTest.java Outdated
Comment thread jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java Outdated
Comment thread jabgui/src/main/java/org/jabref/gui/collab/LibraryBaseline.java Outdated
@jabref-machine jabref-machine added the status: changes-required Pull requests that are not yet complete label Sep 2, 2026
koppor and others added 2 commits September 2, 2026 10:35
A save, switching synchronization off, or closing the tab now discards the result of a scan still running. Baseline entries found for disk entries are kept under the id of the in-memory entry they came from, and among entries sharing a citation key the one with identical content is preferred.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWmXyN3GUNhfF9fSo1UNrt
@jabref-machine jabref-machine added status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers and removed status: changes-required Pull requests that are not yet complete labels Sep 2, 2026
Comment on lines +41 to +42
/// Changes the entry in place rather than replacing it, so it keeps its identity: table position, selection, and
/// an open entry editor stay as they are.

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.

Very nice change IMHO

Sync clients and editors may write the file in several steps; a half-written library parses fine and would look like every later entry had been deleted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWmXyN3GUNhfF9fSo1UNrt
public void applyChange(CompoundEdit undoEdit) {
undoEdit.applyEdit(new UndoableRemoveEntries(databaseContext.getDatabase(), oldEntry));
undoEdit.applyEdit(new UndoableInsertEntries(databaseContext.getDatabase(), newEntry));
if (!oldEntry.getType().equals(newEntry.getType())) {

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.

Also use Objects.equals for null safety?

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

Done in 5d60bd3.

LOGGER.error("Error while trying to monitor {}", path, e);
}
if (database.getLocation() == DatabaseLocation.LOCAL) {
preferences.getLibraryPreferences().autoSaveProperty().addListener(synchronizingListener);

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.

Is it possible that we add the listener multiple times (e.g. this code get exeucted multiple times)

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

It runs once per monitor instance (constructor), and a tab replacing its monitor calls unregister() on the old one first, which removes the listener again, so the preference does not accumulate listeners. Added a comment saying so (5d60bd3).

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

Follow-up on the unregister side: unregister() (called on tab close and when the tab replaces its monitor) removes the listener, but did so only while the library was still local. A library converted to a shared one in the meantime would have kept it. Now removed unconditionally (0d21f34).

// A group change is always accompanied by the metadata change it is part of, which precedes it in the list
Side metaDataSide = Side.BOTH;
for (DatabaseChange change : pairSplitEntries(changes, local, resolverFactory)) {
Side side = switch (change) {

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.

I wonder if we should put that into a new method. Together with the for loop, this gets complex quick.

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

Done in 5d60bd3: the entry-change arm is now classifyEntryChange, returning side and (possibly merged) change; the switch arm is three lines.

@github-actions github-actions Bot added status: changes-required Pull requests that are not yet complete and removed status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers labels Sep 4, 2026
Comment on lines +244 to +251
if (generation != scanGeneration) {
LOGGER.debug("Discarding result of a scan overtaken by a newer file change");
return;
}
// Sorting the changes on the FX thread right before applying them leaves no window
// for a user edit to slip in between classification and application
synchronize(scannedBaseline, scanner.triage(scannedBaseline, changes));
})

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.

big lambda, maybe a new method instead?

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

Done in 5d60bd3: extracted to onScannedForSynchronization.

calixtus and others added 2 commits September 4, 2026 23:26
Collecting them in a CompoundEdit named after the change lets the warning
logged by a failing ChangeSet name the entry, not just the enclosing merge.
Undo granularity is unchanged: nested sets are folded into the one step the
caller opens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P4cHLmHHsmaTJmBP6JnZat

@calixtus calixtus left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There might be scaling problems. Claude says

Details
  1. find() is a linear scan, called per added entry — on the FX thread

LibraryBaseline.java:246 streams the entire entriesById map for every lookup:

List<Map.Entry<String, BibEntry>> candidates = entriesById.entrySet().stream()
        .filter(entry -> entry.getValue().getCitationKey().filter(key::equals).isPresent())
        .toList();
...
return byKey.or(() -> entriesById.entrySet().stream()
        .filter(entry -> sameContentExceptKey(entry.getValue(), remote))
        .findFirst());

It's called once per EntryAdd in triage (:110), again per EntryAdd in pairSplitEntries (:196), and again in keepUnresolved (:160). So it's O(A · N) in adds × library size.

The fallback path makes it worse: sameContentExceptKey (:257) copies both field maps into fresh HashMaps on every single comparison, just to drop one key. For 5 000 added entries against a 100 000-entry library that's 5·10⁸ comparisons and 10⁹ throwaway HashMaps.

And this is not on the background thread. scanForChanges (DatabaseChangeMonitor.java:252) deliberately moves triage into onSuccess:

// Sorting the changes on the FX thread right before applying them leaves no window
// for a user edit to slip in between classification and application
synchronize(scannedBaseline, scanner.triage(scannedBaseline, changes));

The reasoning for that placement is sound, freezes the UI. Is a large A realistic?Yes: a Dropbox/Nextcloud client replacing r's bulk import, or — worst — a formatting
pass that changes citation keys, which droimilarity threshold and turns them into
add/delete pairs. That last one is exactlyists for, so the code's own worst case is
the one that triggers the quadratic path.
Fix: build the index once per triage inste Map<String, List> by citation key,plus a Map<contentHash, Entry> for the keyless fallback. That turns O(A·N) into O(N + A). And rewrite sameContentExceptKey to compare in place (oring KEY_FIELD, then iterate) instead of copying two maps.

  1. pairSplitEntries is quadratic in the change LibraryBaseline.java:198:
paired.remove(entryDelete);
paired.set(paired.indexOf(entryAdd), new EntryChange(...));                                                        

ArrayList.remove(Object) and indexOf are both O(C), inside a loop over all changes → O(A · C). In the key-change scenario C ≈ 2N, so this is quadratic in lendent of the find() cost.

Fix: one pass building a new list, with ange, DatabaseChange> of replacements and a set of deletes to skip. O(C).

  1. The baseline deep-copies every entry, a

LibraryBaseline.of (:73) does new BibEntry. The field strings are shared by reference, so the bibliographic data isn't duplicatedblem is the per-object overhead: every copy allocates a Guava EventBus, a ConcurrentHaeMap, fieldsAsWords, latexFreeFields, a MultiKeyMap, a SimpleObjectProperty and a java:106-163). Order of a kilobyte per entry before any content — call it ~100 MB for aong as the tab is open, and briefly doubled
while captureBaseline() builds the replaceopped (DatabaseChangeMonitor.java:302).
That's an estimate from the field list, no

It also re-runs on every save — resetChangWithDisk → captureBaseline (:192), andsynchronizing requires autosave, so that's every 31 s while editing. Autosave at least runs off the FX thread
(AutosaveUiManager is on the autosave execture at :302 is on the FX thread.

Fix: snapshot only what is actually read. needs just the type and the field map — arecord EntrySnapshot(EntryType type, Map<Field, String> fields) would cut the per-entry cost to one map. mergeFields does need real BibEntry objecthComputer, but that's the rare both-sidescase, so build them on demand there.

@koppor

koppor commented Sep 28, 2026

Copy link
Copy Markdown
Member Author

🤖 Generated with Claude Code
Fixed: option now "Automatically merge external changes" (General tab + Library properties), changelog reworded per suggestion. f1beda8

@koppor
koppor marked this pull request as ready for review September 28, 2026 15:51
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java Outdated
Comment thread jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java Outdated
Comment thread jabgui/src/main/java/org/jabref/gui/collab/entrychange/EntryChange.java Outdated
Comment on lines +122 to +124
case MetadataChange _,
GroupChange _ ->
updated.keepMetaData(previous);

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

16. Mixed review decisions recur on next scan 🐞 Bug ≡ Correctness

keepUnresolved restores the entire previous metadata baseline when either a metadata change or its
accompanying group change is rejected. Because groups occupy a key in that same serialized metadata,
accepting one change and rejecting the other also restores the accepted item's old ancestor, so the
next scan offers the settled decision for review again.
Agent Prompt
## Issue description
Metadata and groups are separate review choices but share one baseline snapshot. Rejecting either choice resets the accepted choice's ancestor, causing repeated reviews.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/ChangeTriage.java[112-139]
- jablib/src/main/java/org/jabref/logic/sync/LibraryBaseline.java[319-322]
## Recommended Fix
Preserve or advance the serialized group-tree key independently of other metadata keys according to each review decision. Test accepting a metadata edit while rejecting its accompanying group edit, and the reverse, across a subsequent scan.

ⓘ 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: metadata and groups advance their ancestors separately, per judged item. ff34175

Comment on lines +150 to +153
// Neither key nor content match: an entry deleted in memory may still be the origin, with key and a
// field changed on disk; then the deletion and the change need a review
List<String> goneFromMemory = entriesById.keySet().stream().filter(id -> !existsInMemory.test(id)).toList();
return closestOf(goneFromMemory, remote).isPresent() ? Side.BOTH : Side.DISK;

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

17. Bulk file additions can freeze the table 🐞 Bug ➹ Performance

Lookup.sideOfAddedEntry scans every baseline entry to rebuild goneFromMemory for each unmatched
added disk entry, even when no entry was deleted in memory. A large external import therefore
performs work proportional to existing entries times additions inside synchronization's JavaFX
success callback, delaying table updates and input.
Agent Prompt
## Issue description
Every new disk entry triggers a full scan of existing baseline entries during triage. Bulk external additions can therefore stall the JavaFX thread.
## Fix Focus Areas
- jablib/src/main/java/org/jabref/logic/sync/LibraryBaseline.java[119-154]
- jabgui/src/main/java/org/jabref/gui/collab/ChangeTriage.java[50-75]
## Recommended Fix
Compute baseline IDs absent from memory once per triage pass and reuse them for added-entry classification; skip closest-match work entirely when that collection is empty. Test or benchmark a large library receiving many new disk entries.

ⓘ 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: entries gone from memory are collected once per pass; closest-match skipped when there are none. ff34175

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

Copy link
Copy Markdown
Contributor

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

@github-actions github-actions Bot added the status: changes-required Pull requests that are not yet complete label Sep 28, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions github-actions Bot removed the status: changes-required Pull requests that are not yet complete label Sep 28, 2026
…chronizer

Javadoc cross-links explaining where the three-way ancestor comes from in each case, and a changelog entry for the in-place entry change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions github-actions Bot added component: shared-database component: git component: diff-merge Merging of BibTeX entries, merge entry dialog, diffing ... labels Sep 28, 2026
@koppor
koppor marked this pull request as draft September 28, 2026 17:50
koppor and others added 2 commits September 28, 2026 20:05
A merged entry and every reviewed item get the disk version as ancestor, so an unsaved edit is neither reverted by the next file change nor forgotten by a review that did not cover it. Enabling synchronization on a modified library compares it with the file instead of trusting the undo journal. The unchanged mark is not restored over a later edit, and the entries gone from memory are collected once per pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…sewhere

The review covers the differences to the file only; accepting all of them does not make an unsaved edit outside the review disappear, so neither the clean mark nor a fresh baseline from memory is warranted then.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@koppor
koppor marked this pull request as ready for review September 28, 2026 18:13
Comment on lines +89 to +94
keepLocalSettings(local.getMetaData(), metadataChange.getMetaDataDiff().getNewMetaData());
metaDataSide = baseline.sideOfMetaData(local.getMetaData(), metadataChange.getMetaDataDiff().getNewMetaData());
yield metaDataSide;
}
case GroupChange _ ->
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

8. Unrelated setting edits force a review 🐞 Bug ≡ Correctness

ChangeTriage.triage classifies the entire serialized metadata as one item and assigns that result
to its accompanying group change. When memory changes one setting, such as encoding, and the file
changes only the groups, both changes go to review even though they affect different items.
Agent Prompt
## Issue description
Independent in-memory and on-disk metadata edits are classified together, so unrelated external group changes require review.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/ChangeTriage.java[88-94]
- jablib/src/main/java/org/jabref/logic/sync/LibraryBaseline.java[246-248]
## Recommended Fix
Compare metadata items independently against their baseline values. Apply disk-only items without replacing memory-only settings, and send only genuinely conflicting items to review; cover a local encoding edit paired with a disk group edit.

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

Comment on lines +263 to +266
if (ChangeTriage.matchesFile(scanned, preferences.getCitationKeyPatternPreferences().getKeyPatterns())) {
captureIfCurrent(generation);
} else {
offerReview(generation, scanned);

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

9. Accepting a review can switch auto-merge off 🐞 Bug ≡ Correctness

scanToVerifyMatch sends the raw scanned changes to offerReview without going through
ChangeTriage.triage, so keepLocalSettings never copies the in-memory sync setting into the
parsed disk metadata. When the user accepts the MetadataChange, MetaData.overwriteWith (which
now copies synchronizeWithFile) puts back the file's absent or false value;
onSynchronizingChanged(false) then clears the baseline, and the review dialog never showed this
difference.
Agent Prompt
## Issue description
The review offered after switching sync on for a modified library applies the file's metadata as-is. That replaces the user's unsaved per-library synchronizeWithFile value and disables synchronization again.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/collab/DatabaseChangeMonitor.java[258-271]
- jabgui/src/main/java/org/jabref/gui/collab/ChangeTriage.java[205-209]

## Recommended Fix
Make keepLocalSettings accessible (package-private). In offerReview, before notifying listeners, call it for every MetadataChange in the list, passing database.getMetaData() and the change's getMetaDataDiff().getNewMetaData().

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

Comment on lines +64 to +65
/// Whether there is an undoable step, i.e. whether the library was edited through the undo manager
boolean canUndo();

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.

Informational

10. New undo method in shared interface is unused 🐞 Bug ⚙ Maintainability

canUndo() was added to the logic-layer UndoManager interface, but no production code calls it
through that interface, because the switch-on check now uses libraryTab.isModified(). Every future
implementation must still provide it, and its Javadoc suggests the undo stack shows whether the
library matches its file, which the monitor's own comment says it does not.
Agent Prompt
## Issue description
`canUndo()` in the logic-layer UndoManager interface has no production caller through that interface.

## Fix Focus Areas
- jablib/src/main/java/org/jabref/logic/undo/UndoManager.java[64-65]

## Recommended Fix
Delete the declaration. GuiUndoManager already declares canUndo and JabRefUndoManager implements it, so existing callers and tests keep working.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should not be in the libs undomanager

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

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 792e502

@github-actions github-actions Bot added the status: changes-required Pull requests that are not yet complete label Sep 28, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions github-actions Bot added status: changes-required Pull requests that are not yet complete and removed status: changes-required Pull requests that are not yet complete labels Sep 28, 2026
@calixtus

Copy link
Copy Markdown
Member

Stop messing around with undo. I had it fixed. Do not introduce new patterns and do not modify the interfaces.

import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

/// Records an entry's "changed since parsing" flag around an in-place edit, so that undoing the edit also restores

@calixtus calixtus Sep 28, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is weird. Marker for changes on a file is the undo stack.

koppor commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

🤖 Generated with Claude Code

Merged main into this branch to resolve the merge conflict.

Conflicting file: docs/requirements/ux.md. main renamed the heading of req~ux.large-library.bulk-entry-removal~1, and this PR inserts req~ux.external-library-changes.automatic-merge~1 immediately before it. Kept both: this PR's new section, then that requirement under its renamed heading.

CHANGELOG.md, DBMSSynchronizer.java and JabRef_en.properties auto-merged (both sides' keys retained). The PR's CI is the compile check.


Generated by Claude Code

@github-actions github-actions Bot added status: changes-required Pull requests that are not yet complete and removed status: changes-required Pull requests that are not yet complete labels Sep 29, 2026
@koppor
koppor marked this pull request as draft October 3, 2026 21:30

This branch has not been deployed

No deployments
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.

Enhancements to handling of externally modified files

5 participants