Skip to content

Allow updating entries from multi merge results#15562

Draft
deepgupta6 wants to merge 4 commits into
JabRef:mainfrom
deepgupta6:fix-issue-15265
Draft

Allow updating entries from multi merge results#15562
deepgupta6 wants to merge 4 commits into
JabRef:mainfrom
deepgupta6:fix-issue-15265

Conversation

@deepgupta6

@deepgupta6 deepgupta6 commented Apr 15, 2026

Copy link
Copy Markdown
jabref.15265.mp4

Related issues and pull requests

Closes #15265

PR Description

This PR makes the web-based bibliographic update flow persist the selected merge result back to the original entry and records the change as a single undoable action, so the feature now matches the expected behavior. It also keeps a more specific entry type when possible instead of falling back to Misc, which improves merge accuracy.

Steps to test

  1. Open example library
  2. Choose an entry for example Garcia_2018
  3. Lookup -> Update with bibliographic information via entry data
  4. Choose any field that differs from the original entry for example ISSN
  5. Click on Merge entries
  6. See the changes reflect to original entry

Checklist

  • I own the copyright of the code submitted and I license it under the MIT license
  • 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 a screenshot in the PR description showing a library with a single entry with me as author and as title the issue number
  • [.] I described the change in CHANGELOG.md in a way that can be understood by the average user (if 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

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

qodo-free-for-open-source-projects Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

(Agentic_describe updated until commit c69403b)

Persist merge results and improve entry type handling

✨ Enhancement 🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Refactor merge logic into reusable UpdateOriginalEntry class
• Apply merged results to original entry as single undoable action
• Preserve specific entry types instead of defaulting to Misc
• Add guards against empty merge results to prevent field clearing
• Add regression tests for merge initialization and empty result handling
Diagram
flowchart LR
  A["User selects merge result"] --> B["UpdateOriginalEntry processes merge"]
  B --> C["Apply field changes"]
  B --> D["Update entry type if needed"]
  C --> E["Record as single undo action"]
  D --> E
  E --> F["Notify user of update"]
  G["Empty merge result"] --> H["Guard prevents field clearing"]
  H --> F
Loading

Grey Divider

File Changes

1. jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java ✨ Enhancement +1/-1

Add undoManager dependency to web fetcher action

• Pass undoManager parameter to UpdateWithBibliographicInformationByWebFetchers constructor

jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java


2. jabgui/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java Refactoring +2/-61

Extract merge logic to UpdateOriginalEntry class

• Remove inline merge logic from showMergeDialog method
• Delegate merge result application to new UpdateOriginalEntry class
• Remove unused imports related to undo operations and field management

jabgui/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java


3. jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java ✨ Enhancement +96/-0

New class for applying merge results to entries

• New class encapsulating entry merge and update logic
• Handles field updates, entry type changes, and field removal
• Records changes as single NamedCompoundEdit for undo support
• Guards against empty merged entries to prevent unintended field clearing
• Supports both single-source and multi-source merge scenarios

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java


View more (5)
4. jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java ✨ Enhancement +12/-3

Enable undo support for multi-source merges

• Add undoManager parameter to constructor
• Change addSource call to pass BibEntry directly instead of lambda
• Capture merge result and apply via UpdateOriginalEntry
• Enable undo support for multi-source merge operations

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java


5. jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModel.java ✨ Enhancement +5/-0

Preserve specific entry types during merge

• Add logic to preserve specific entry types instead of defaulting to Misc
• When merged entry is Misc and source entry has specific type, adopt source type
• Improves merge accuracy by maintaining more specific entry classifications

jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModel.java


6. jabgui/src/test/java/org/jabref/gui/mergeentries/UpdateOriginalEntryTest.java 🧪 Tests +70/-0

Add regression tests for merge result handling

• New test class with regression tests for UpdateOriginalEntry
• Test empty merged entry does not modify original entry
• Test canceled merge does not modify original entry or undo manager

jabgui/src/test/java/org/jabref/gui/mergeentries/UpdateOriginalEntryTest.java


7. jabgui/src/test/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModelTest.java 🧪 Tests +31/-18

Add test for immediate merge initialization

• Reorder test methods for better organization
• Add test for immediate entry initialization when adding source with BibEntry
• Verify merged entry is populated with original entry fields synchronously

jabgui/src/test/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModelTest.java


8. jablib/src/main/resources/l10n/JabRef_en.properties 📝 Documentation +2/-0

Add localization strings for merge operations

• Add localization key for generic merge operation without specific fetcher
• Add localization key for multi-source merge notification message

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


Grey Divider

Qodo Logo

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

qodo-free-for-open-source-projects Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (6)

Grey Divider


Action required

1. Passing null to UndoableFieldChange 📘 Rule violation ☼ Reliability ⭐ New
Description
New code passes null as an argument to UndoableFieldChange, extending a null-based contract
instead of using explicit nullability/safer alternatives. This violates the project rule to avoid
passing null and can lead to null-related bugs and unclear contracts.
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[R64-79]

+                if (originalString.isEmpty() || !originalString.equals(mergedString)) {
+                    originalEntry.setField(field, mergedString.get()); // mergedString always present
+                    compoundEdit.addEdit(new UndoableFieldChange(originalEntry, field, originalString.orElse(null),
+                            mergedString.get()));
+                    edited = true;
+                }
+            }
+
+            // Remove fields which are not in the merged entry, unless they are internal fields
+            for (Field field : originalFields) {
+                if (!jointFields.contains(field) && !FieldFactory.isInternalField(field)) {
+                    Optional<String> originalString = originalEntry.getField(field);
+                    originalEntry.clearField(field);
+                    compoundEdit.addEdit(new UndoableFieldChange(originalEntry, field, originalString.get(), null)); // originalString always present
+                    edited = true;
+                }
Evidence
PR Compliance ID 8 forbids introducing/expanding patterns of passing null as an argument. The new
implementation explicitly passes null as the old/new field value when creating
UndoableFieldChange edits.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[64-79]

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

## Issue description
`UpdateOriginalEntry.update()` constructs `UndoableFieldChange` edits passing `null` for old/new values (`originalString.orElse(null)` and `..., null`). This violates the project's null-safety rule.

## Issue Context
Prefer an API that encodes absence explicitly (e.g., Optional) or a dedicated constant/sentinel, or adjust `UndoableFieldChange` usage so that `null` is not passed.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[64-79]

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


2. EntryType identity comparison 🐞 Bug ≡ Correctness ⭐ New
Description
MultiMergeEntriesViewModel.updateFields() uses '=='/'!=' to compare EntryType values against
StandardEntryType.Misc, which is not reliable because EntryType is an interface and not all
implementations are enums. This can cause the merged entry type to remain Misc even when a source
entry has a more specific type, reducing merge accuracy.
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModel.java[R54-56]

+        if (mergedEntry.get().getType() == StandardEntryType.Misc
+                && entry.getType() != StandardEntryType.Misc) {
+            mergedEntry.get().setType(entry.getType());
Evidence
The PR introduces identity comparisons against StandardEntryType.Misc in updateFields(). Since
EntryType is an interface and the codebase contains non-enum implementations (e.g., UnknownEntryType
with value-based equals), identity comparison can misclassify an EntryType that is logically “misc”
(or not) and therefore skip the intended type upgrade.

jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModel.java[50-57]
jablib/src/main/java/org/jabref/model/entry/types/EntryType.java[1-10]
jablib/src/main/java/org/jabref/model/entry/types/UnknownEntryType.java[13-52]

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

### Issue description
`MultiMergeEntriesViewModel.updateFields` compares `EntryType` values using `==`/`!=` against `StandardEntryType.Misc`. Since `EntryType` is an interface and can be implemented by non-enum types, identity comparison can fail even when types are logically equal, preventing the merged entry type from being upgraded away from `Misc`.

### Issue Context
This logic is part of the PR’s goal to keep a more specific entry type when possible.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModel.java[50-57]

### Suggested change
Replace identity checks with logical equality, e.g.:
```java
if (StandardEntryType.Misc.equals(mergedEntry.get().getType())
       && !StandardEntryType.Misc.equals(entry.getType())) {
   mergedEntry.get().setType(entry.getType());
}
```
(Equivalent `.equals(...)` forms are fine; the key is to avoid `==`/`!=` for `EntryType`.)

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


3. Early merge wipes fields 🐞 Bug ≡ Correctness
Description
If the user clicks “Merge entries” before sources finish loading, the dialog can return the
still-empty default merged entry, and the new apply logic will then clear nearly all non-internal
fields on the original entry. This can cause severe, silent data loss in the selected BibEntry (only
recoverable if the user immediately uses Undo).
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[R77-119]

+        Optional<BibEntry> mergedEntry = dialogService.showCustomDialogAndWait(mergedEntriesView);
+
+        if (mergedEntry.isPresent()) {
+            NamedCompoundEdit compoundEdit = new NamedCompoundEdit(Localization.lang("Merge entry with information"));
+
+            // Updated the original entry with the new fields
+            Set<Field> jointFields = new TreeSet<>(Comparator.comparing(Field::getName));
+            jointFields.addAll(mergedEntry.get().getFields());
+            Set<Field> originalFields = new TreeSet<>(Comparator.comparing(Field::getName));
+            originalFields.addAll(originalEntry.getFields());
+            boolean edited = false;
+
+            // entry type
+            EntryType oldType = originalEntry.getType();
+            EntryType newType = mergedEntry.get().getType();
+
+            if (!oldType.equals(newType)) {
+                originalEntry.setType(newType);
+                compoundEdit.addEdit(new UndoableChangeType(originalEntry, oldType, newType));
+                edited = true;
+            }
+
+            // fields
+            for (Field field : jointFields) {
+                Optional<String> originalString = originalEntry.getField(field);
+                Optional<String> mergedString = mergedEntry.get().getField(field);
+                if (originalString.isEmpty() || !originalString.equals(mergedString)) {
+                    originalEntry.setField(field, mergedString.get()); // mergedString always present
+                    compoundEdit.addEdit(new UndoableFieldChange(originalEntry, field, originalString.orElse(null),
+                            mergedString.get()));
+                    edited = true;
+                }
+            }
+
+            // Remove fields which are not in the merged entry, unless they are internal fields
+            for (Field field : originalFields) {
+                if (!jointFields.contains(field) && !FieldFactory.isInternalField(field)) {
+                    Optional<String> originalString = originalEntry.getField(field);
+                    originalEntry.clearField(field);
+                    compoundEdit.addEdit(new UndoableFieldChange(originalEntry, field, originalString.get(), null)); // originalString always present
+                    edited = true;
+                }
+            }
Evidence
The merge view model starts with an empty merged entry (new BibEntry) and only populates it once
background-loaded sources complete. In this action, even the original entry is added as an
asynchronously-loaded source, so there is a window where the user can confirm the dialog while
mergedEntry still has no fields; the new code then treats “fields absent from mergedEntry” as
“should be removed from originalEntry”. The dialog sets OK/Cancel but does not disable the OK action
while sources are loading.

jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModel.java[28-47]
jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModel.java[98-113]
jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesView.java[91-106]
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[52-75]
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[77-130]

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 merge dialog’s result can be the default, still-empty `mergedEntry` if the user confirms before sources finish loading (the original entry is currently loaded via `BackgroundTask` too). The new apply logic then clears all original fields not present in that empty result, causing severe data loss.
## Issue Context
`MultiMergeEntriesViewModel` initializes `mergedEntry` as `new BibEntry()` and only fills it after each `EntrySource` finishes loading. `UpdateWithBibliographicInformationByWebFetchers` now applies the dialog result back to `originalEntry` and clears fields missing from the merged result.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[55-75]
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[77-130]
- jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesView.java[91-141]
## Suggested fix
1. Ensure the original entry is added synchronously so the merged result is at least initialized with the original fields immediately (e.g., use the `addSource(String, BibEntry)` overload for the original entry).
2. Additionally (recommended), disable the OK/"Merge entries" button until at least the original source has loaded and/or until `mergedEntry` has been populated (e.g., bind the OK button’s `disableProperty` to a loading indicator / mergedEntry fields emptiness).
3. As a safety net, before applying changes, guard against an empty merged result (e.g., if `mergedEntry.getFields().isEmpty()` then treat as cancel and do not mutate `originalEntry`).

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


View more (2)
4. Type-only merge skipped 🐞 Bug ≡ Correctness
Description
UpdateOriginalEntry.update() treats a merge result with no fields as "Canceled" and returns without
applying any changes, which drops valid type-only merges. In the 3-way merge dialog, changing only
the entry type updates the merged entry via setType (not via setField), so mergedEntry.getFields()
can remain empty even though the user confirmed the merge.
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[R36-41]

+    public void update() {
+        if (mergedEntry.isPresent() && !mergedEntry.get().getFields().isEmpty()) {
+            String editName = fetcher
+                    .map(value -> Localization.lang("Merge entry with %0 information", value.getName()))
+                    .orElseGet(() -> Localization.lang("Merge entry with information"));
+            NamedCompoundEdit compoundEdit = new NamedCompoundEdit(editName);
Evidence
UpdateOriginalEntry only proceeds when mergedEntry is present AND mergedEntry.getFields() is
non-empty; otherwise it notifies "Canceled" and performs no update. However, BibEntry.getFields() is
derived only from the fields map (it does not include the entry type), and the 3-way merge view
writes type changes via mergedEntry.setType(...) when the special TYPE_HEADER row changes.
Therefore, a user-confirmed merge that changes only the type yields a merged entry with no fields,
causing UpdateOriginalEntry to skip persisting the type change and to emit a misleading "Canceled"
notification.

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[36-94]
jablib/src/main/java/org/jabref/model/entry/BibEntry.java[430-433]
jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/FieldRowViewModel.java[80-86]
jabgui/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java[108-117]

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

## Issue description
`UpdateOriginalEntry.update()` currently short-circuits when `mergedEntry.get().getFields().isEmpty()`. This prevents persisting type-only merges coming from the 3-way merge dialog (type changes are applied via `BibEntry.setType`, which does not add anything to `getFields()`). It also misreports a confirmed merge as "Canceled".
### Issue Context
- `BibEntry.getFields()` only returns keys from the internal `fields` map; entry type is stored separately.
- 3-way merge UI sets type through `mergedEntry.setType(...)` when the `TYPE_HEADER` row changes.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[36-94]
### What to change
1. Treat only `mergedEntry.isEmpty()` as cancel.
2. Allow proceeding when the merge result contains *either* (a) field changes or (b) a type change.
3. Avoid destructive field clearing when `mergedEntry.get().getFields()` is empty:
 - Apply type change if needed.
 - Skip field update/removal loops when there are no merged fields.
4. Ensure notifications reflect the action:
 - If user confirmed but nothing changed: "No information added".
 - If type changed only: record undo + show updated notification.

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


5. mergedEntry uses isPresent()+get() 📘 Rule violation ⚙ Maintainability
Description
UpdateOriginalEntry.update() uses mergedEntry.isPresent() and then calls mergedEntry.get()
multiple times, which is discouraged and less safe/idiomatic than fluent Optional handling. This
violates the Optional control-flow compliance requirement and increases the chance of future
misuse/maintenance errors.
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[R36-38]

+    public void update() {
+        if (mergedEntry.isPresent() && !mergedEntry.get().getFields().isEmpty()) {
+            String editName = fetcher
Evidence
PR Compliance ID 26 forbids using isPresent()/isEmpty() followed by get() in new or modified
code. The newly added UpdateOriginalEntry.update() method checks mergedEntry.isPresent() and
then immediately dereferences it with mergedEntry.get().

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[36-38]
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
`UpdateOriginalEntry.update()` uses `mergedEntry.isPresent()` and then dereferences with `mergedEntry.get()`. This violates the project’s Optional style rule and is less readable/robust than fluent Optional control flow.
## Issue Context
This is newly introduced code in `UpdateOriginalEntry` and should follow JabRef’s Optional conventions.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[36-95]

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



Remediation recommended

6. UndoManager Swing import added 📘 Rule violation ⛨ Security
Description
New code introduces javax.swing.undo.UndoManager usage in the GUI command, which adds Java Swing
API usage. This violates the requirement to use JavaFX-only UI technology.
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[8]

+import javax.swing.undo.UndoManager;
Evidence
PR Compliance ID 18 forbids introducing Java Swing usage. The change adds an explicit import of the
Swing UndoManager type in the GUI code.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[8-8]

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 GUI command introduces a Swing API dependency via `javax.swing.undo.UndoManager`, conflicting with the JavaFX-only UI technology requirement.
## Issue Context
This PR adds `UndoManager` as a constructor dependency and stores it in the command for undo handling.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[8-46]
- jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java[309-312]

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


7. Manual Optional isPresent() branching 📘 Rule violation ⚙ Maintainability
Description
The new code uses mergedEntry.isPresent() followed by repeated mergedEntry.get() calls instead
of ifPresentOrElse/fluent Optional control flow. This reduces clarity and violates the project's
Optional usage conventions.
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[R77-91]

+        Optional<BibEntry> mergedEntry = dialogService.showCustomDialogAndWait(mergedEntriesView);
+
+        if (mergedEntry.isPresent()) {
+            NamedCompoundEdit compoundEdit = new NamedCompoundEdit(Localization.lang("Merge entry with information"));
+
+            // Updated the original entry with the new fields
+            Set<Field> jointFields = new TreeSet<>(Comparator.comparing(Field::getName));
+            jointFields.addAll(mergedEntry.get().getFields());
+            Set<Field> originalFields = new TreeSet<>(Comparator.comparing(Field::getName));
+            originalFields.addAll(originalEntry.getFields());
+            boolean edited = false;
+
+            // entry type
+            EntryType oldType = originalEntry.getType();
+            EntryType newType = mergedEntry.get().getType();
Evidence
PR Compliance ID 10 requires using Optional’s fluent APIs (e.g., ifPresentOrElse) and discourages
manual isPresent() branching. The new code branches on isPresent() and then repeatedly calls
get() on the Optional.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[77-91]

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 code uses manual `Optional.isPresent()` + `get()` patterns rather than `Optional.ifPresentOrElse(...)`, which is discouraged by the compliance checklist.
## Issue Context
`showCustomDialogAndWait(...)` returns an `Optional<BibEntry>`. The code branches with `isPresent()` and then repeatedly accesses `mergedEntry.get()`.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[77-130]

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


8. Test uses orElse("") 📘 Rule violation ⚙ Maintainability
Description
The test asserts Optional values by converting absence into an empty-string sentinel via
orElse(""). This violates the Optional-idiom guideline and can weaken the intent of assertions
compared to asserting on the Optional directly or using orElseThrow().
Code

jabgui/src/test/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModelTest.java[55]

+        assertEquals("2015", viewModel.mergedEntryProperty().get().getField(StandardField.YEAR).orElse(""));
Evidence
The checklist explicitly calls out avoiding orElse("") patterns for Optional handling. The added
assertion uses orElse("") when asserting the Optional field value.

AGENTS.md
jabgui/src/test/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModelTest.java[55-55]

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 newly added unit test uses `Optional.orElse("")` as a sentinel in assertions.
## Issue Context
Project guidelines prefer `orElseThrow()` or `assertEquals(Optional.of(expected), optionalActual)` to keep Optional semantics explicit.
## Fix Focus Areas
- jabgui/src/test/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModelTest.java[51-56]

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



Advisory comments

9. Redundant comment in update() 📘 Rule violation ⚙ Maintainability
Description
UpdateOriginalEntry.update() adds comments that restate the code (e.g., "Updated the original
entry with the new fields") without explaining rationale. This increases noise and violates the
requirement that comments explain "why" rather than "what".
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[43]

+            // Updated the original entry with the new fields
Evidence
The checklist forbids trivial comments that merely paraphrase nearby code. The added comment
provides no rationale beyond what the code already expresses.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[43-43]

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

## Issue description
New comments in `UpdateOriginalEntry.update()` restate behavior without adding rationale.
## Issue Context
Per style guidance, comments should explain intent/why, not narrate obvious steps.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[43-73]

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


Grey Divider

Previous review results

Review updated until commit c69403b

Results up to commit 7032a25


Details

🐞 Bugs (1)   📘 Rule violations (2)   📎 Requirement gaps (0)
🐞\ ≡ Correctness (1)
📘\ ⛨ Security (1) ⚙ Maintainability (1)

Action required
1. Early merge wipes fields 🐞
Description
If the user clicks “Merge entries” before sources finish loading, the dialog can return the
still-empty default merged entry, and the new apply logic will then clear nearly all non-internal
fields on the original entry. This can cause severe, silent data loss in the selected BibEntry (only
recoverable if the user immediately uses Undo).
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[R77-119]

+        Optional<BibEntry> mergedEntry = dialogService.showCustomDialogAndWait(mergedEntriesView);
+
+        if (mergedEntry.isPresent()) {
+            NamedCompoundEdit compoundEdit = new NamedCompoundEdit(Localization.lang("Merge entry with information"));
+
+            // Updated the original entry with the new fields
+            Set<Field> jointFields = new TreeSet<>(Comparator.comparing(Field::getName));
+            jointFields.addAll(mergedEntry.get().getFields());
+            Set<Field> originalFields = new TreeSet<>(Comparator.comparing(Field::getName));
+            originalFields.addAll(originalEntry.getFields());
+            boolean edited = false;
+
+            // entry type
+            EntryType oldType = originalEntry.getType();
+            EntryType newType = mergedEntry.get().getType();
+
+            if (!oldType.equals(newType)) {
+                originalEntry.setType(newType);
+                compoundEdit.addEdit(new UndoableChangeType(originalEntry, oldType, newType));
+                edited = true;
+            }
+
+            // fields
+            for (Field field : jointFields) {
+                Optional<String> originalString = originalEntry.getField(field);
+                Optional<String> mergedString = mergedEntry.get().getField(field);
+                if (originalString.isEmpty() || !originalString.equals(mergedString)) {
+                    originalEntry.setField(field, mergedString.get()); // mergedString always present
+                    compoundEdit.addEdit(new UndoableFieldChange(originalEntry, field, originalString.orElse(null),
+                            mergedString.get()));
+                    edited = true;
+                }
+            }
+
+            // Remove fields which are not in the merged entry, unless they are internal fields
+            for (Field field : originalFields) {
+                if (!jointFields.contains(field) && !FieldFactory.isInternalField(field)) {
+                    Optional<String> originalString = originalEntry.getField(field);
+                    originalEntry.clearField(field);
+                    compoundEdit.addEdit(new UndoableFieldChange(originalEntry, field, originalString.get(), null)); // originalString always present
+                    edited = true;
+                }
+            }
Evidence
The merge view model starts with an empty merged entry (new BibEntry) and only populates it once
background-loaded sources complete. In this action, even the original entry is added as an
asynchronously-loaded source, so there is a window where the user can confirm the dialog while
mergedEntry still has no fields; the new code then treats “fields absent from mergedEntry” as
“should be removed from originalEntry”. The dialog sets OK/Cancel but does not disable the OK action
while sources are loading.

jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModel.java[28-47]
jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModel.java[98-113]
jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesView.java[91-106]
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[52-75]
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[77-130]

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 merge dialog’s result can be the default, still-empty `mergedEntry` if the user confirms before sources finish loading (the original entry is currently loaded via `BackgroundTask` too). The new apply logic then clears all original fields not present in that empty result, causing severe data loss.

## Issue Context
`MultiMergeEntriesViewModel` initializes `mergedEntry` as `new BibEntry()` and only fills it after each `EntrySource` finishes loading. `UpdateWithBibliographicInformationByWebFetchers` now applies the dialog result back to `originalEntry` and clears fields missing from the merged result.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[55-75]
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[77-130]
- jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesView.java[91-141]

## Suggested fix
1. Ensure the original entry is added synchronously so the merged result is at least initialized with the original fields immediately (e.g., use the `addSource(String, BibEntry)` overload for the original entry).
2. Additionally (recommended), disable the OK/"Merge entries" button until at least the original source has loaded and/or until `mergedEntry` has been populated (e.g., bind the OK button’s `disableProperty` to a loading indicator / mergedEntry fields emptiness).
3. As a safety net, before applying changes, guard against an empty merged result (e.g., if `mergedEntry.getFields().isEmpty()` then treat as cancel and do not mutate `originalEntry`).

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



Remediation recommended
2. UndoManager Swing import added 📘
Description
New code introduces javax.swing.undo.UndoManager usage in the GUI command, which adds Java Swing
API usage. This violates the requirement to use JavaFX-only UI technology.
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[8]

+import javax.swing.undo.UndoManager;
Evidence
PR Compliance ID 18 forbids introducing Java Swing usage. The change adds an explicit import of the
Swing UndoManager type in the GUI code.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[8-8]

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 GUI command introduces a Swing API dependency via `javax.swing.undo.UndoManager`, conflicting with the JavaFX-only UI technology requirement.

## Issue Context
This PR adds `UndoManager` as a constructor dependency and stores it in the command for undo handling.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[8-46]
- jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java[309-312]

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


3. Manual Optional isPresent() branching 📘
Description
The new code uses mergedEntry.isPresent() followed by repeated mergedEntry.get() calls instead
of ifPresentOrElse/fluent Optional control flow. This reduces clarity and violates the project's
Optional usage conventions.
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[R77-91]

+        Optional<BibEntry> mergedEntry = dialogService.showCustomDialogAndWait(mergedEntriesView);
+
+        if (mergedEntry.isPresent()) {
+            NamedCompoundEdit compoundEdit = new NamedCompoundEdit(Localization.lang("Merge entry with information"));
+
+            // Updated the original entry with the new fields
+            Set<Field> jointFields = new TreeSet<>(Comparator.comparing(Field::getName));
+            jointFields.addAll(mergedEntry.get().getFields());
+            Set<Field> originalFields = new TreeSet<>(Comparator.comparing(Field::getName));
+            originalFields.addAll(originalEntry.getFields());
+            boolean edited = false;
+
+            // entry type
+            EntryType oldType = originalEntry.getType();
+            EntryType newType = mergedEntry.get().getType();
Evidence
PR Compliance ID 10 requires using Optional’s fluent APIs (e.g., ifPresentOrElse) and discourages
manual isPresent() branching. The new code branches on isPresent() and then repeatedly calls
get() on the Optional.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[77-91]

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 code uses manual `Optional.isPresent()` + `get()` patterns rather than `Optional.ifPresentOrElse(...)`, which is discouraged by the compliance checklist.

## Issue Context
`showCustomDialogAndWait(...)` returns an `Optional<BibEntry>`. The code branches with `isPresent()` and then repeatedly accesses `mergedEntry.get()`.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[77-130]

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


ⓘ The new review experience is currently in Beta. Learn more

Qodo Logo

Comment on lines +77 to +119
Optional<BibEntry> mergedEntry = dialogService.showCustomDialogAndWait(mergedEntriesView);

if (mergedEntry.isPresent()) {
NamedCompoundEdit compoundEdit = new NamedCompoundEdit(Localization.lang("Merge entry with information"));

// Updated the original entry with the new fields
Set<Field> jointFields = new TreeSet<>(Comparator.comparing(Field::getName));
jointFields.addAll(mergedEntry.get().getFields());
Set<Field> originalFields = new TreeSet<>(Comparator.comparing(Field::getName));
originalFields.addAll(originalEntry.getFields());
boolean edited = false;

// entry type
EntryType oldType = originalEntry.getType();
EntryType newType = mergedEntry.get().getType();

if (!oldType.equals(newType)) {
originalEntry.setType(newType);
compoundEdit.addEdit(new UndoableChangeType(originalEntry, oldType, newType));
edited = true;
}

// fields
for (Field field : jointFields) {
Optional<String> originalString = originalEntry.getField(field);
Optional<String> mergedString = mergedEntry.get().getField(field);
if (originalString.isEmpty() || !originalString.equals(mergedString)) {
originalEntry.setField(field, mergedString.get()); // mergedString always present
compoundEdit.addEdit(new UndoableFieldChange(originalEntry, field, originalString.orElse(null),
mergedString.get()));
edited = true;
}
}

// Remove fields which are not in the merged entry, unless they are internal fields
for (Field field : originalFields) {
if (!jointFields.contains(field) && !FieldFactory.isInternalField(field)) {
Optional<String> originalString = originalEntry.getField(field);
originalEntry.clearField(field);
compoundEdit.addEdit(new UndoableFieldChange(originalEntry, field, originalString.get(), null)); // originalString always present
edited = true;
}
}

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. Early merge wipes fields 🐞 Bug ≡ Correctness

If the user clicks “Merge entries” before sources finish loading, the dialog can return the
still-empty default merged entry, and the new apply logic will then clear nearly all non-internal
fields on the original entry. This can cause severe, silent data loss in the selected BibEntry (only
recoverable if the user immediately uses Undo).
Agent Prompt
## Issue description
The merge dialog’s result can be the default, still-empty `mergedEntry` if the user confirms before sources finish loading (the original entry is currently loaded via `BackgroundTask` too). The new apply logic then clears all original fields not present in that empty result, causing severe data loss.

## Issue Context
`MultiMergeEntriesViewModel` initializes `mergedEntry` as `new BibEntry()` and only fills it after each `EntrySource` finishes loading. `UpdateWithBibliographicInformationByWebFetchers` now applies the dialog result back to `originalEntry` and clears fields missing from the merged result.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[55-75]
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[77-130]
- jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesView.java[91-141]

## Suggested fix
1. Ensure the original entry is added synchronously so the merged result is at least initialized with the original fields immediately (e.g., use the `addSource(String, BibEntry)` overload for the original entry).
2. Additionally (recommended), disable the OK/"Merge entries" button until at least the original source has loaded and/or until `mergedEntry` has been populated (e.g., bind the OK button’s `disableProperty` to a loading indicator / mergedEntry fields emptiness).
3. As a safety net, before applying changes, guard against an empty merged result (e.g., if `mergedEntry.getFields().isEmpty()` then treat as cancel and do not mutate `originalEntry`).

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

@github-actions github-actions Bot added the status: changes-required Pull requests that are not yet complete label Apr 15, 2026
if (edited) {
compoundEdit.end();
undoManager.addEdit(compoundEdit);
dialogService.notify(Localization.lang("Updated entry with merged information from multiple sources"));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Does this key exist in the properties?

Comment on lines +79 to +109
if (mergedEntry.isPresent()) {
NamedCompoundEdit compoundEdit = new NamedCompoundEdit(Localization.lang("Merge entry with information"));

// Updated the original entry with the new fields
Set<Field> jointFields = new TreeSet<>(Comparator.comparing(Field::getName));
jointFields.addAll(mergedEntry.get().getFields());
Set<Field> originalFields = new TreeSet<>(Comparator.comparing(Field::getName));
originalFields.addAll(originalEntry.getFields());
boolean edited = false;

// entry type
EntryType oldType = originalEntry.getType();
EntryType newType = mergedEntry.get().getType();

if (!oldType.equals(newType)) {
originalEntry.setType(newType);
compoundEdit.addEdit(new UndoableChangeType(originalEntry, oldType, newType));
edited = true;
}

// fields
for (Field field : jointFields) {
Optional<String> originalString = originalEntry.getField(field);
Optional<String> mergedString = mergedEntry.get().getField(field);
if (originalString.isEmpty() || !originalString.equals(mergedString)) {
originalEntry.setField(field, mergedString.get()); // mergedString always present
compoundEdit.addEdit(new UndoableFieldChange(originalEntry, field, originalString.orElse(null),
mergedString.get()));
edited = true;
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The merge logic is same to what's already in FetchAndMergeEntry.showMergeDialog( ). You can extract that into a shared helper method maybe.

return;
}
if (mergedEntry.get().getType() == StandardEntryType.Misc
&& entry.getType() != null

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

You can simplify this to just the Misc comparison.

@subhramit

Copy link
Copy Markdown
Member

@deepgupta6 if there are no responses to review comments for the next two days, we will be closing the PR due to inactivity.

@faneeshh thank you for reviewing!

@deepgupta6 deepgupta6 marked this pull request as draft April 19, 2026 13:37
merged result is initialized immediately.

Guard UpdateOriginalEntry against empty merged results to avoid clearing
fields when the dialog is confirmed before background sources finish
loading.

Add regression tests for immediate merged-entry initialization and empty
merge application handling.
@deepgupta6 deepgupta6 marked this pull request as ready for review April 19, 2026 20:27
@koppor koppor marked this pull request as draft April 19, 2026 20:27
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Persist merge results and improve entry type handling in bibliographic updates

✨ Enhancement 🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Refactored merge logic into reusable UpdateOriginalEntry class for consistency
• Added undo support to web-based bibliographic update flow
• Improved entry type preservation by avoiding fallback to Misc type
• Added guards against empty merge results to prevent field clearing
• Added regression tests for merge initialization and empty result handling
Diagram
flowchart LR
  A["Web Fetchers Update"] --> B["Multi-Merge Dialog"]
  C["Single Fetcher Merge"] --> B
  B --> D["UpdateOriginalEntry"]
  D --> E["Apply Changes with Undo"]
  D --> F["Notify User"]
Loading

Grey Divider

File Changes

1. jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java ✨ Enhancement +1/-1

Pass undo manager to web fetcher action

• Added undoManager parameter to UpdateWithBibliographicInformationByWebFetchers constructor
 call

jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java


2. jabgui/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java Refactoring +2/-61

Simplify merge logic by delegating to UpdateOriginalEntry

• Removed manual merge logic (60+ lines of field update code)
• Removed unused imports for undo-related classes and field utilities
• Delegated merge result application to new UpdateOriginalEntry class

jabgui/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java


3. jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java ✨ Enhancement +96/-0

New class for applying merge results to original entry

• New class that encapsulates entry update logic from merge results
• Handles entry type changes and field updates with undo support
• Guards against empty merged entries to prevent clearing fields
• Supports both single-fetcher and multi-fetcher merge scenarios

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java


View more (5)
4. jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java ✨ Enhancement +12/-3

Add undo support to multi-source merge workflow

• Added undoManager field and constructor parameter
• Changed addSource call to pass entry directly instead of lambda
• Integrated UpdateOriginalEntry to persist merge results with undo support

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java


5. jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModel.java ✨ Enhancement +5/-0

Preserve entry type during multi-way merge

• Added logic to preserve non-Misc entry types when merging
• Prevents fallback to Misc type when source entry has specific type
• Improves merge accuracy by maintaining entry type information

jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModel.java


6. jabgui/src/test/java/org/jabref/gui/mergeentries/UpdateOriginalEntryTest.java 🧪 Tests +70/-0

Add regression tests for UpdateOriginalEntry

• New test class with two regression tests
• Tests that empty merged entries don't modify original entry
• Tests that canceled merges don't modify original entry or undo manager

jabgui/src/test/java/org/jabref/gui/mergeentries/UpdateOriginalEntryTest.java


7. jabgui/src/test/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModelTest.java 🧪 Tests +31/-18

Add test for immediate merge initialization

• Reordered test methods for better organization
• Added new test for immediate entry initialization in merged entry
• Verifies that original entry fields are copied to merged entry on source addition

jabgui/src/test/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModelTest.java


8. jablib/src/main/resources/l10n/JabRef_en.properties 📝 Documentation +2/-0

Add localization strings for merge operations

• Added localization string for generic merge operation without specific fetcher
• Added localization string for multi-source merge notification

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


Grey Divider

Qodo Logo

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

qodo-free-for-open-source-projects Bot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (4) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Type-only merge skipped 🐞 Bug ≡ Correctness ⭐ New
Description
UpdateOriginalEntry.update() treats a merge result with no fields as "Canceled" and returns without
applying any changes, which drops valid type-only merges. In the 3-way merge dialog, changing only
the entry type updates the merged entry via setType (not via setField), so mergedEntry.getFields()
can remain empty even though the user confirmed the merge.
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[R36-41]

+    public void update() {
+        if (mergedEntry.isPresent() && !mergedEntry.get().getFields().isEmpty()) {
+            String editName = fetcher
+                    .map(value -> Localization.lang("Merge entry with %0 information", value.getName()))
+                    .orElseGet(() -> Localization.lang("Merge entry with information"));
+            NamedCompoundEdit compoundEdit = new NamedCompoundEdit(editName);
Evidence
UpdateOriginalEntry only proceeds when mergedEntry is present AND mergedEntry.getFields() is
non-empty; otherwise it notifies "Canceled" and performs no update. However, BibEntry.getFields() is
derived only from the fields map (it does not include the entry type), and the 3-way merge view
writes type changes via mergedEntry.setType(...) when the special TYPE_HEADER row changes.
Therefore, a user-confirmed merge that changes only the type yields a merged entry with no fields,
causing UpdateOriginalEntry to skip persisting the type change and to emit a misleading "Canceled"
notification.

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[36-94]
jablib/src/main/java/org/jabref/model/entry/BibEntry.java[430-433]
jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/FieldRowViewModel.java[80-86]
jabgui/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java[108-117]

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

### Issue description
`UpdateOriginalEntry.update()` currently short-circuits when `mergedEntry.get().getFields().isEmpty()`. This prevents persisting type-only merges coming from the 3-way merge dialog (type changes are applied via `BibEntry.setType`, which does not add anything to `getFields()`). It also misreports a confirmed merge as "Canceled".

### Issue Context
- `BibEntry.getFields()` only returns keys from the internal `fields` map; entry type is stored separately.
- 3-way merge UI sets type through `mergedEntry.setType(...)` when the `TYPE_HEADER` row changes.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[36-94]

### What to change
1. Treat only `mergedEntry.isEmpty()` as cancel.
2. Allow proceeding when the merge result contains *either* (a) field changes or (b) a type change.
3. Avoid destructive field clearing when `mergedEntry.get().getFields()` is empty:
  - Apply type change if needed.
  - Skip field update/removal loops when there are no merged fields.
4. Ensure notifications reflect the action:
  - If user confirmed but nothing changed: "No information added".
  - If type changed only: record undo + show updated notification.

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


2. Early merge wipes fields 🐞 Bug ≡ Correctness
Description
If the user clicks “Merge entries” before sources finish loading, the dialog can return the
still-empty default merged entry, and the new apply logic will then clear nearly all non-internal
fields on the original entry. This can cause severe, silent data loss in the selected BibEntry (only
recoverable if the user immediately uses Undo).
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[R77-119]

+        Optional<BibEntry> mergedEntry = dialogService.showCustomDialogAndWait(mergedEntriesView);
+
+        if (mergedEntry.isPresent()) {
+            NamedCompoundEdit compoundEdit = new NamedCompoundEdit(Localization.lang("Merge entry with information"));
+
+            // Updated the original entry with the new fields
+            Set<Field> jointFields = new TreeSet<>(Comparator.comparing(Field::getName));
+            jointFields.addAll(mergedEntry.get().getFields());
+            Set<Field> originalFields = new TreeSet<>(Comparator.comparing(Field::getName));
+            originalFields.addAll(originalEntry.getFields());
+            boolean edited = false;
+
+            // entry type
+            EntryType oldType = originalEntry.getType();
+            EntryType newType = mergedEntry.get().getType();
+
+            if (!oldType.equals(newType)) {
+                originalEntry.setType(newType);
+                compoundEdit.addEdit(new UndoableChangeType(originalEntry, oldType, newType));
+                edited = true;
+            }
+
+            // fields
+            for (Field field : jointFields) {
+                Optional<String> originalString = originalEntry.getField(field);
+                Optional<String> mergedString = mergedEntry.get().getField(field);
+                if (originalString.isEmpty() || !originalString.equals(mergedString)) {
+                    originalEntry.setField(field, mergedString.get()); // mergedString always present
+                    compoundEdit.addEdit(new UndoableFieldChange(originalEntry, field, originalString.orElse(null),
+                            mergedString.get()));
+                    edited = true;
+                }
+            }
+
+            // Remove fields which are not in the merged entry, unless they are internal fields
+            for (Field field : originalFields) {
+                if (!jointFields.contains(field) && !FieldFactory.isInternalField(field)) {
+                    Optional<String> originalString = originalEntry.getField(field);
+                    originalEntry.clearField(field);
+                    compoundEdit.addEdit(new UndoableFieldChange(originalEntry, field, originalString.get(), null)); // originalString always present
+                    edited = true;
+                }
+            }
Evidence
The merge view model starts with an empty merged entry (new BibEntry) and only populates it once
background-loaded sources complete. In this action, even the original entry is added as an
asynchronously-loaded source, so there is a window where the user can confirm the dialog while
mergedEntry still has no fields; the new code then treats “fields absent from mergedEntry” as
“should be removed from originalEntry”. The dialog sets OK/Cancel but does not disable the OK action
while sources are loading.

jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModel.java[28-47]
jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModel.java[98-113]
jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesView.java[91-106]
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[52-75]
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[77-130]

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 merge dialog’s result can be the default, still-empty `mergedEntry` if the user confirms before sources finish loading (the original entry is currently loaded via `BackgroundTask` too). The new apply logic then clears all original fields not present in that empty result, causing severe data loss.
## Issue Context
`MultiMergeEntriesViewModel` initializes `mergedEntry` as `new BibEntry()` and only fills it after each `EntrySource` finishes loading. `UpdateWithBibliographicInformationByWebFetchers` now applies the dialog result back to `originalEntry` and clears fields missing from the merged result.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[55-75]
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[77-130]
- jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesView.java[91-141]
## Suggested fix
1. Ensure the original entry is added synchronously so the merged result is at least initialized with the original fields immediately (e.g., use the `addSource(String, BibEntry)` overload for the original entry).
2. Additionally (recommended), disable the OK/"Merge entries" button until at least the original source has loaded and/or until `mergedEntry` has been populated (e.g., bind the OK button’s `disableProperty` to a loading indicator / mergedEntry fields emptiness).
3. As a safety net, before applying changes, guard against an empty merged result (e.g., if `mergedEntry.getFields().isEmpty()` then treat as cancel and do not mutate `originalEntry`).

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



Remediation recommended

3. Test uses orElse("") 📘 Rule violation ⚙ Maintainability ⭐ New
Description
The test asserts Optional values by converting absence into an empty-string sentinel via
orElse(""). This violates the Optional-idiom guideline and can weaken the intent of assertions
compared to asserting on the Optional directly or using orElseThrow().
Code

jabgui/src/test/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModelTest.java[55]

+        assertEquals("2015", viewModel.mergedEntryProperty().get().getField(StandardField.YEAR).orElse(""));
Evidence
The checklist explicitly calls out avoiding orElse("") patterns for Optional handling. The added
assertion uses orElse("") when asserting the Optional field value.

AGENTS.md
jabgui/src/test/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModelTest.java[55-55]

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 newly added unit test uses `Optional.orElse("")` as a sentinel in assertions.

## Issue Context
Project guidelines prefer `orElseThrow()` or `assertEquals(Optional.of(expected), optionalActual)` to keep Optional semantics explicit.

## Fix Focus Areas
- jabgui/src/test/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModelTest.java[51-56]

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


4. UndoManager Swing import added 📘 Rule violation ⛨ Security
Description
New code introduces javax.swing.undo.UndoManager usage in the GUI command, which adds Java Swing
API usage. This violates the requirement to use JavaFX-only UI technology.
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[8]

+import javax.swing.undo.UndoManager;
Evidence
PR Compliance ID 18 forbids introducing Java Swing usage. The change adds an explicit import of the
Swing UndoManager type in the GUI code.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[8-8]

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 GUI command introduces a Swing API dependency via `javax.swing.undo.UndoManager`, conflicting with the JavaFX-only UI technology requirement.
## Issue Context
This PR adds `UndoManager` as a constructor dependency and stores it in the command for undo handling.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[8-46]
- jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java[309-312]

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


5. Manual Optional isPresent() branching 📘 Rule violation ⚙ Maintainability
Description
The new code uses mergedEntry.isPresent() followed by repeated mergedEntry.get() calls instead
of ifPresentOrElse/fluent Optional control flow. This reduces clarity and violates the project's
Optional usage conventions.
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[R77-91]

+        Optional<BibEntry> mergedEntry = dialogService.showCustomDialogAndWait(mergedEntriesView);
+
+        if (mergedEntry.isPresent()) {
+            NamedCompoundEdit compoundEdit = new NamedCompoundEdit(Localization.lang("Merge entry with information"));
+
+            // Updated the original entry with the new fields
+            Set<Field> jointFields = new TreeSet<>(Comparator.comparing(Field::getName));
+            jointFields.addAll(mergedEntry.get().getFields());
+            Set<Field> originalFields = new TreeSet<>(Comparator.comparing(Field::getName));
+            originalFields.addAll(originalEntry.getFields());
+            boolean edited = false;
+
+            // entry type
+            EntryType oldType = originalEntry.getType();
+            EntryType newType = mergedEntry.get().getType();
Evidence
PR Compliance ID 10 requires using Optional’s fluent APIs (e.g., ifPresentOrElse) and discourages
manual isPresent() branching. The new code branches on isPresent() and then repeatedly calls
get() on the Optional.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[77-91]

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 code uses manual `Optional.isPresent()` + `get()` patterns rather than `Optional.ifPresentOrElse(...)`, which is discouraged by the compliance checklist.
## Issue Context
`showCustomDialogAndWait(...)` returns an `Optional<BibEntry>`. The code branches with `isPresent()` and then repeatedly accesses `mergedEntry.get()`.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[77-130]

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



Advisory comments

6. Redundant comment in update() 📘 Rule violation ⚙ Maintainability ⭐ New
Description
UpdateOriginalEntry.update() adds comments that restate the code (e.g., "Updated the original
entry with the new fields") without explaining rationale. This increases noise and violates the
requirement that comments explain "why" rather than "what".
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[43]

+            // Updated the original entry with the new fields
Evidence
The checklist forbids trivial comments that merely paraphrase nearby code. The added comment
provides no rationale beyond what the code already expresses.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[43-43]

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

## Issue description
New comments in `UpdateOriginalEntry.update()` restate behavior without adding rationale.

## Issue Context
Per style guidance, comments should explain intent/why, not narrate obvious steps.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[43-73]

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


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines +36 to +41
public void update() {
if (mergedEntry.isPresent() && !mergedEntry.get().getFields().isEmpty()) {
String editName = fetcher
.map(value -> Localization.lang("Merge entry with %0 information", value.getName()))
.orElseGet(() -> Localization.lang("Merge entry with information"));
NamedCompoundEdit compoundEdit = new NamedCompoundEdit(editName);

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. Type-only merge skipped 🐞 Bug ≡ Correctness

UpdateOriginalEntry.update() treats a merge result with no fields as "Canceled" and returns without
applying any changes, which drops valid type-only merges. In the 3-way merge dialog, changing only
the entry type updates the merged entry via setType (not via setField), so mergedEntry.getFields()
can remain empty even though the user confirmed the merge.
Agent Prompt
### Issue description
`UpdateOriginalEntry.update()` currently short-circuits when `mergedEntry.get().getFields().isEmpty()`. This prevents persisting type-only merges coming from the 3-way merge dialog (type changes are applied via `BibEntry.setType`, which does not add anything to `getFields()`). It also misreports a confirmed merge as "Canceled".

### Issue Context
- `BibEntry.getFields()` only returns keys from the internal `fields` map; entry type is stored separately.
- 3-way merge UI sets type through `mergedEntry.setType(...)` when the `TYPE_HEADER` row changes.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[36-94]

### What to change
1. Treat only `mergedEntry.isEmpty()` as cancel.
2. Allow proceeding when the merge result contains *either* (a) field changes or (b) a type change.
3. Avoid destructive field clearing when `mergedEntry.get().getFields()` is empty:
   - Apply type change if needed.
   - Skip field update/removal loops when there are no merged fields.
4. Ensure notifications reflect the action:
   - If user confirmed but nothing changed: "No information added".
   - If type changed only: record undo + show updated notification.

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

@jabref-machine

Copy link
Copy Markdown
Collaborator

Note that your PR will not be reviewed/accepted until you have gone through the mandatory checks in the description and marked each of them them exactly in the format of - [x] (done), - [ ] (yet to be done) or - [/] (not applicable). Please adhere to our pull request template.

@deepgupta6 deepgupta6 requested a review from faneeshh April 19, 2026 20:48
@koppor koppor removed the request for review from faneeshh April 19, 2026 20:48
@deepgupta6

Copy link
Copy Markdown
Author

@faneesh Sorry for the late response, I was out of town. now I have done the changes you suggested

@deepgupta6

Copy link
Copy Markdown
Author

@koppor I think I’ve completed the requested changes. Could you please let me know why the PR was moved back to draft? If there is something remaining to change you can suggest

@koppor

koppor commented Apr 19, 2026

Copy link
Copy Markdown
Member

This needs to be reviewed with refactoring miner

@deepgupta6

Copy link
Copy Markdown
Author

@koppor I have checked with refactoring miner and found no issue. Adding screenshot for proof
Screenshot 2026-04-20 145807

@deepgupta6

Copy link
Copy Markdown
Author

Let me know if anything else needs to be changed, I’ll take care of it.

@koppor

koppor commented Apr 20, 2026

Copy link
Copy Markdown
Member

@koppor I have checked with refactoring miner and found no issue. Adding screenshot for proof
Screenshot 2026-04-20 145807

You did not fully understand about the UI and intention of RefactoringMiner...

@deepgupta6

Copy link
Copy Markdown
Author

@koppor sorry for taking too much of your time.
Now I used RefactoringMiner on the latest commit and verified it with my understanding.
I extracted the logic from showMergeDialog, created a new class UpdateOriginalEntry, and placed the logic inside its update() method so that it can be reused in the update bibliographic information execute method.
if I missed anything Please tell me that in brief.

@deepgupta6

Copy link
Copy Markdown
Author

@Siedlerchr @calixtus @koppor
could you please review? I think i have done my work, but let me know if anything missing.

@subhramit

Copy link
Copy Markdown
Member

@Siedlerchr @calixtus @koppor could you please review? I think i have done my work, but let me know if anything missing.

Please be patient. If you are done with your part, mark the PR ready for review (move it out of draft status). We will take a look as we find time.

@deepgupta6 deepgupta6 marked this pull request as ready for review April 24, 2026 18:19
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (5) 📎 Requirement gaps (0)

Grey Divider


Action required

1. mergedEntry uses isPresent()+get() 📘 Rule violation ⚙ Maintainability ⭐ New
Description
UpdateOriginalEntry.update() uses mergedEntry.isPresent() and then calls mergedEntry.get()
multiple times, which is discouraged and less safe/idiomatic than fluent Optional handling. This
violates the Optional control-flow compliance requirement and increases the chance of future
misuse/maintenance errors.
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[R36-38]

+    public void update() {
+        if (mergedEntry.isPresent() && !mergedEntry.get().getFields().isEmpty()) {
+            String editName = fetcher
Evidence
PR Compliance ID 26 forbids using isPresent()/isEmpty() followed by get() in new or modified
code. The newly added UpdateOriginalEntry.update() method checks mergedEntry.isPresent() and
then immediately dereferences it with mergedEntry.get().

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[36-38]
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
`UpdateOriginalEntry.update()` uses `mergedEntry.isPresent()` and then dereferences with `mergedEntry.get()`. This violates the project’s Optional style rule and is less readable/robust than fluent Optional control flow.

## Issue Context
This is newly introduced code in `UpdateOriginalEntry` and should follow JabRef’s Optional conventions.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[36-95]

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


2. Early merge wipes fields 🐞 Bug ≡ Correctness
Description
If the user clicks “Merge entries” before sources finish loading, the dialog can return the
still-empty default merged entry, and the new apply logic will then clear nearly all non-internal
fields on the original entry. This can cause severe, silent data loss in the selected BibEntry (only
recoverable if the user immediately uses Undo).
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[R77-119]

+        Optional<BibEntry> mergedEntry = dialogService.showCustomDialogAndWait(mergedEntriesView);
+
+        if (mergedEntry.isPresent()) {
+            NamedCompoundEdit compoundEdit = new NamedCompoundEdit(Localization.lang("Merge entry with information"));
+
+            // Updated the original entry with the new fields
+            Set<Field> jointFields = new TreeSet<>(Comparator.comparing(Field::getName));
+            jointFields.addAll(mergedEntry.get().getFields());
+            Set<Field> originalFields = new TreeSet<>(Comparator.comparing(Field::getName));
+            originalFields.addAll(originalEntry.getFields());
+            boolean edited = false;
+
+            // entry type
+            EntryType oldType = originalEntry.getType();
+            EntryType newType = mergedEntry.get().getType();
+
+            if (!oldType.equals(newType)) {
+                originalEntry.setType(newType);
+                compoundEdit.addEdit(new UndoableChangeType(originalEntry, oldType, newType));
+                edited = true;
+            }
+
+            // fields
+            for (Field field : jointFields) {
+                Optional<String> originalString = originalEntry.getField(field);
+                Optional<String> mergedString = mergedEntry.get().getField(field);
+                if (originalString.isEmpty() || !originalString.equals(mergedString)) {
+                    originalEntry.setField(field, mergedString.get()); // mergedString always present
+                    compoundEdit.addEdit(new UndoableFieldChange(originalEntry, field, originalString.orElse(null),
+                            mergedString.get()));
+                    edited = true;
+                }
+            }
+
+            // Remove fields which are not in the merged entry, unless they are internal fields
+            for (Field field : originalFields) {
+                if (!jointFields.contains(field) && !FieldFactory.isInternalField(field)) {
+                    Optional<String> originalString = originalEntry.getField(field);
+                    originalEntry.clearField(field);
+                    compoundEdit.addEdit(new UndoableFieldChange(originalEntry, field, originalString.get(), null)); // originalString always present
+                    edited = true;
+                }
+            }
Evidence
The merge view model starts with an empty merged entry (new BibEntry) and only populates it once
background-loaded sources complete. In this action, even the original entry is added as an
asynchronously-loaded source, so there is a window where the user can confirm the dialog while
mergedEntry still has no fields; the new code then treats “fields absent from mergedEntry” as
“should be removed from originalEntry”. The dialog sets OK/Cancel but does not disable the OK action
while sources are loading.

jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModel.java[28-47]
jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModel.java[98-113]
jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesView.java[91-106]
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[52-75]
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[77-130]

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 merge dialog’s result can be the default, still-empty `mergedEntry` if the user confirms before sources finish loading (the original entry is currently loaded via `BackgroundTask` too). The new apply logic then clears all original fields not present in that empty result, causing severe data loss.
## Issue Context
`MultiMergeEntriesViewModel` initializes `mergedEntry` as `new BibEntry()` and only fills it after each `EntrySource` finishes loading. `UpdateWithBibliographicInformationByWebFetchers` now applies the dialog result back to `originalEntry` and clears fields missing from the merged result.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[55-75]
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[77-130]
- jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesView.java[91-141]
## Suggested fix
1. Ensure the original entry is added synchronously so the merged result is at least initialized with the original fields immediately (e.g., use the `addSource(String, BibEntry)` overload for the original entry).
2. Additionally (recommended), disable the OK/"Merge entries" button until at least the original source has loaded and/or until `mergedEntry` has been populated (e.g., bind the OK button’s `disableProperty` to a loading indicator / mergedEntry fields emptiness).
3. As a safety net, before applying changes, guard against an empty merged result (e.g., if `mergedEntry.getFields().isEmpty()` then treat as cancel and do not mutate `originalEntry`).

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


3. Type-only merge skipped 🐞 Bug ≡ Correctness
Description
UpdateOriginalEntry.update() treats a merge result with no fields as "Canceled" and returns without
applying any changes, which drops valid type-only merges. In the 3-way merge dialog, changing only
the entry type updates the merged entry via setType (not via setField), so mergedEntry.getFields()
can remain empty even though the user confirmed the merge.
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[R36-41]

+    public void update() {
+        if (mergedEntry.isPresent() && !mergedEntry.get().getFields().isEmpty()) {
+            String editName = fetcher
+                    .map(value -> Localization.lang("Merge entry with %0 information", value.getName()))
+                    .orElseGet(() -> Localization.lang("Merge entry with information"));
+            NamedCompoundEdit compoundEdit = new NamedCompoundEdit(editName);
Evidence
UpdateOriginalEntry only proceeds when mergedEntry is present AND mergedEntry.getFields() is
non-empty; otherwise it notifies "Canceled" and performs no update. However, BibEntry.getFields() is
derived only from the fields map (it does not include the entry type), and the 3-way merge view
writes type changes via mergedEntry.setType(...) when the special TYPE_HEADER row changes.
Therefore, a user-confirmed merge that changes only the type yields a merged entry with no fields,
causing UpdateOriginalEntry to skip persisting the type change and to emit a misleading "Canceled"
notification.

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[36-94]
jablib/src/main/java/org/jabref/model/entry/BibEntry.java[430-433]
jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/FieldRowViewModel.java[80-86]
jabgui/src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java[108-117]

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

## Issue description
`UpdateOriginalEntry.update()` currently short-circuits when `mergedEntry.get().getFields().isEmpty()`. This prevents persisting type-only merges coming from the 3-way merge dialog (type changes are applied via `BibEntry.setType`, which does not add anything to `getFields()`). It also misreports a confirmed merge as "Canceled".
### Issue Context
- `BibEntry.getFields()` only returns keys from the internal `fields` map; entry type is stored separately.
- 3-way merge UI sets type through `mergedEntry.setType(...)` when the `TYPE_HEADER` row changes.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[36-94]
### What to change
1. Treat only `mergedEntry.isEmpty()` as cancel.
2. Allow proceeding when the merge result contains *either* (a) field changes or (b) a type change.
3. Avoid destructive field clearing when `mergedEntry.get().getFields()` is empty:
 - Apply type change if needed.
 - Skip field update/removal loops when there are no merged fields.
4. Ensure notifications reflect the action:
 - If user confirmed but nothing changed: "No information added".
 - If type changed only: record undo + show updated notification.

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



Remediation recommended

4. UndoManager Swing import added 📘 Rule violation ⛨ Security
Description
New code introduces javax.swing.undo.UndoManager usage in the GUI command, which adds Java Swing
API usage. This violates the requirement to use JavaFX-only UI technology.
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[8]

+import javax.swing.undo.UndoManager;
Evidence
PR Compliance ID 18 forbids introducing Java Swing usage. The change adds an explicit import of the
Swing UndoManager type in the GUI code.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[8-8]

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 GUI command introduces a Swing API dependency via `javax.swing.undo.UndoManager`, conflicting with the JavaFX-only UI technology requirement.
## Issue Context
This PR adds `UndoManager` as a constructor dependency and stores it in the command for undo handling.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[8-46]
- jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java[309-312]

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


5. Manual Optional isPresent() branching 📘 Rule violation ⚙ Maintainability
Description
The new code uses mergedEntry.isPresent() followed by repeated mergedEntry.get() calls instead
of ifPresentOrElse/fluent Optional control flow. This reduces clarity and violates the project's
Optional usage conventions.
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[R77-91]

+        Optional<BibEntry> mergedEntry = dialogService.showCustomDialogAndWait(mergedEntriesView);
+
+        if (mergedEntry.isPresent()) {
+            NamedCompoundEdit compoundEdit = new NamedCompoundEdit(Localization.lang("Merge entry with information"));
+
+            // Updated the original entry with the new fields
+            Set<Field> jointFields = new TreeSet<>(Comparator.comparing(Field::getName));
+            jointFields.addAll(mergedEntry.get().getFields());
+            Set<Field> originalFields = new TreeSet<>(Comparator.comparing(Field::getName));
+            originalFields.addAll(originalEntry.getFields());
+            boolean edited = false;
+
+            // entry type
+            EntryType oldType = originalEntry.getType();
+            EntryType newType = mergedEntry.get().getType();
Evidence
PR Compliance ID 10 requires using Optional’s fluent APIs (e.g., ifPresentOrElse) and discourages
manual isPresent() branching. The new code branches on isPresent() and then repeatedly calls
get() on the Optional.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[77-91]

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 code uses manual `Optional.isPresent()` + `get()` patterns rather than `Optional.ifPresentOrElse(...)`, which is discouraged by the compliance checklist.
## Issue Context
`showCustomDialogAndWait(...)` returns an `Optional<BibEntry>`. The code branches with `isPresent()` and then repeatedly accesses `mergedEntry.get()`.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateWithBibliographicInformationByWebFetchers.java[77-130]

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


6. Test uses orElse("") 📘 Rule violation ⚙ Maintainability
Description
The test asserts Optional values by converting absence into an empty-string sentinel via
orElse(""). This violates the Optional-idiom guideline and can weaken the intent of assertions
compared to asserting on the Optional directly or using orElseThrow().
Code

jabgui/src/test/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModelTest.java[55]

+        assertEquals("2015", viewModel.mergedEntryProperty().get().getField(StandardField.YEAR).orElse(""));
Evidence
The checklist explicitly calls out avoiding orElse("") patterns for Optional handling. The added
assertion uses orElse("") when asserting the Optional field value.

AGENTS.md
jabgui/src/test/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModelTest.java[55-55]

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 newly added unit test uses `Optional.orElse("")` as a sentinel in assertions.
## Issue Context
Project guidelines prefer `orElseThrow()` or `assertEquals(Optional.of(expected), optionalActual)` to keep Optional semantics explicit.
## Fix Focus Areas
- jabgui/src/test/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModelTest.java[51-56]

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



Advisory comments

7. Redundant comment in update() 📘 Rule violation ⚙ Maintainability
Description
UpdateOriginalEntry.update() adds comments that restate the code (e.g., "Updated the original
entry with the new fields") without explaining rationale. This increases noise and violates the
requirement that comments explain "why" rather than "what".
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[43]

+            // Updated the original entry with the new fields
Evidence
The checklist forbids trivial comments that merely paraphrase nearby code. The added comment
provides no rationale beyond what the code already expresses.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[43-43]

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

## Issue description
New comments in `UpdateOriginalEntry.update()` restate behavior without adding rationale.
## Issue Context
Per style guidance, comments should explain intent/why, not narrate obvious steps.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[43-73]

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


Grey Divider

Qodo Logo

@github-actions

Copy link
Copy Markdown
Contributor

Do not mark a PR as ready-for-review if changes are required.
Address the changes first.

@koppor koppor marked this pull request as draft April 24, 2026 18:20
Comment on lines +36 to +38
public void update() {
if (mergedEntry.isPresent() && !mergedEntry.get().getFields().isEmpty()) {
String editName = fetcher

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. mergedentry uses ispresent()+get() 📘 Rule violation ⚙ Maintainability

UpdateOriginalEntry.update() uses mergedEntry.isPresent() and then calls mergedEntry.get()
multiple times, which is discouraged and less safe/idiomatic than fluent Optional handling. This
violates the Optional control-flow compliance requirement and increases the chance of future
misuse/maintenance errors.
Agent Prompt
## Issue description
`UpdateOriginalEntry.update()` uses `mergedEntry.isPresent()` and then dereferences with `mergedEntry.get()`. This violates the project’s Optional style rule and is less readable/robust than fluent Optional control flow.

## Issue Context
This is newly introduced code in `UpdateOriginalEntry` and should follow JabRef’s Optional conventions.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[36-95]

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

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

The requested changes were not addressed for 10 days. Please follow-up in the next 10 days or your PR will be automatically closed. You can check the contributing guidelines for hints on the pull request process.

@github-actions github-actions Bot added the status: stale Issues marked by a bot as "stale". All issues need to be investigated manually. label May 5, 2026
@deepgupta6

Copy link
Copy Markdown
Author

Please be patient. If you are done with your part, mark the PR ready for review (move it out of draft status). We will take a look as we find time.

@subhramit whenever I mark the PR as “Ready for Review,” it automatically gets converted back to Draft again and i dont know how to move further could you please suggest what might be causing this?

@subhramit subhramit marked this pull request as ready for review May 7, 2026 18:23
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit c69403b

Comment on lines +64 to +79
if (originalString.isEmpty() || !originalString.equals(mergedString)) {
originalEntry.setField(field, mergedString.get()); // mergedString always present
compoundEdit.addEdit(new UndoableFieldChange(originalEntry, field, originalString.orElse(null),
mergedString.get()));
edited = true;
}
}

// Remove fields which are not in the merged entry, unless they are internal fields
for (Field field : originalFields) {
if (!jointFields.contains(field) && !FieldFactory.isInternalField(field)) {
Optional<String> originalString = originalEntry.getField(field);
originalEntry.clearField(field);
compoundEdit.addEdit(new UndoableFieldChange(originalEntry, field, originalString.get(), null)); // originalString always present
edited = true;
}

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. Passing null to undoablefieldchange 📘 Rule violation ☼ Reliability

New code passes null as an argument to UndoableFieldChange, extending a null-based contract
instead of using explicit nullability/safer alternatives. This violates the project rule to avoid
passing null and can lead to null-related bugs and unclear contracts.
Agent Prompt
## Issue description
`UpdateOriginalEntry.update()` constructs `UndoableFieldChange` edits passing `null` for old/new values (`originalString.orElse(null)` and `..., null`). This violates the project's null-safety rule.

## Issue Context
Prefer an API that encodes absence explicitly (e.g., Optional) or a dedicated constant/sentinel, or adjust `UndoableFieldChange` usage so that `null` is not passed.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/UpdateOriginalEntry.java[64-79]

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

Comment on lines +54 to +56
if (mergedEntry.get().getType() == StandardEntryType.Misc
&& entry.getType() != StandardEntryType.Misc) {
mergedEntry.get().setType(entry.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.

Action required

2. Entrytype identity comparison 🐞 Bug ≡ Correctness

MultiMergeEntriesViewModel.updateFields() uses '=='/'!=' to compare EntryType values against
StandardEntryType.Misc, which is not reliable because EntryType is an interface and not all
implementations are enums. This can cause the merged entry type to remain Misc even when a source
entry has a more specific type, reducing merge accuracy.
Agent Prompt
### Issue description
`MultiMergeEntriesViewModel.updateFields` compares `EntryType` values using `==`/`!=` against `StandardEntryType.Misc`. Since `EntryType` is an interface and can be implemented by non-enum types, identity comparison can fail even when types are logically equal, preventing the merged entry type from being upgraded away from `Misc`.

### Issue Context
This logic is part of the PR’s goal to keep a more specific entry type when possible.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/multiwaymerge/MultiMergeEntriesViewModel.java[50-57]

### Suggested change
Replace identity checks with logical equality, e.g.:
```java
if (StandardEntryType.Misc.equals(mergedEntry.get().getType())
        && !StandardEntryType.Misc.equals(entry.getType())) {
    mergedEntry.get().setType(entry.getType());
}
```
(Equivalent `.equals(...)` forms are fine; the key is to avoid `==`/`!=` for `EntryType`.)

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

@github-actions github-actions Bot removed the status: stale Issues marked by a bot as "stale". All issues need to be investigated manually. label May 7, 2026
@github-actions

Copy link
Copy Markdown
Contributor

The requested changes were not addressed for 10 days. Please follow-up in the next 10 days or your PR will be automatically closed. You can check the contributing guidelines for hints on the pull request process.

@github-actions github-actions Bot added the status: stale Issues marked by a bot as "stale". All issues need to be investigated manually. label May 18, 2026
@deepgupta6

Copy link
Copy Markdown
Author

@koppor sorry for taking too much of your time. Now I used RefactoringMiner on the latest commit and verified it with my understanding. I extracted the logic from showMergeDialog, created a new class UpdateOriginalEntry, and placed the logic inside its update() method so that it can be reused in the update bibliographic information execute method. if I missed anything Please tell me that in brief.

The requested changes were not addressed for 10 days. Please follow-up in the next 10 days or your PR will be automatically closed. You can check the contributing guidelines for hints on the pull request process.

@koppor could you please suggest the changes? I would be very grateful if you could spend some time reviewing this PR.

@github-actions github-actions Bot removed the status: stale Issues marked by a bot as "stale". All issues need to be investigated manually. label May 18, 2026
Comment on lines +40 to 58
@BeforeEach
void setUp() {
viewModel = new MultiMergeEntriesViewModel();
}

@Test
void updateFieldsIgnoresNullEntry() {
viewModel.updateFields(null);
assertEquals(new BibEntry(), viewModel.mergedEntryProperty().get());
}

@Test
void updateFieldsSetsFieldWhenNotYetPresent() {
BibEntry source = new BibEntry().withField(StandardField.YEAR, "2015");
viewModel.updateFields(source);
assertEquals("2015", viewModel.mergedEntryProperty().get().getField(StandardField.YEAR).orElse(""));
}

@ParameterizedTest

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.

Source move is wrong - breaks coherence here.

@koppor koppor marked this pull request as draft May 18, 2026 19:36
@calixtus calixtus self-assigned this May 18, 2026
Comment on lines +60 to +79
// fields
for (Field field : jointFields) {
Optional<String> originalString = originalEntry.getField(field);
Optional<String> mergedString = mergedEntry.get().getField(field);
if (originalString.isEmpty() || !originalString.equals(mergedString)) {
originalEntry.setField(field, mergedString.get()); // mergedString always present
compoundEdit.addEdit(new UndoableFieldChange(originalEntry, field, originalString.orElse(null),
mergedString.get()));
edited = true;
}
}

// Remove fields which are not in the merged entry, unless they are internal fields
for (Field field : originalFields) {
if (!jointFields.contains(field) && !FieldFactory.isInternalField(field)) {
Optional<String> originalString = originalEntry.getField(field);
originalEntry.clearField(field);
compoundEdit.addEdit(new UndoableFieldChange(originalEntry, field, originalString.get(), null)); // originalString always present
edited = true;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I want to ask for clarification of the intended behavior here:

Because now, when a user deselects all toggles for a field (neither the old nor the new value was selected),
setField(field, "") was called, which treats "" field as removed from mergedEntry entirely.

Then in line 76 clearField(field) is called, because the field is no longer in jointFields.

Result: the original field is silently deleted from the entry.

I think, "deselect all" could mean either "don't update this field" (preserve original) or "clear this field(current behavior). The distinction isn't visible to the user in the UI

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.

I think, "deselect all" could mean either "don't update this field" (preserve original)

+1 for this behavior

@jabref-machine

Copy link
Copy Markdown
Collaborator

Hey, we noticed that you force-pushed your changes. Force pushing is a bad practice when working together on a project (mainly because it is not supported well by GitHub itself). Commits are lost and comments on commits lose their context, thus making it harder to review changes.

When the pull request is getting integrated into main, all commits will be squashed anyway. Thus, your individual commit history will not be visible in main.

In future, please avoid that. For now, you can continue working.

@jabref-machine

Copy link
Copy Markdown
Collaborator

JUnit tests of jablib are failing. You can see which checks are failing by locating the box "Some checks were not successful" on the pull request page. To see the test output, locate "Source Code Tests / Unit tests (pull_request)" and click on it.

You can then run these tests in IntelliJ to reproduce the failing tests locally. We offer a quick test running howto in the section Final build system checks in our setup guide.

@ThiloteE

ThiloteE commented Jun 4, 2026

Copy link
Copy Markdown
Member

What did you do with your force-push?

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

Labels

📌 Pinned status: changes-required Pull requests that are not yet complete

Projects

None yet

Development

Successfully merging this pull request may close these issues.

"Merge Entries" in "multiwaymerge" doesn't work

8 participants