Skip to content

Fix merge dialog overwriting user's citation key choice during duplicate import#15406

Closed
0xRozier wants to merge 27 commits intoJabRef:mainfrom
0xRozier:fix/issue-8644-citationkey-merge-dialog-overwrite
Closed

Fix merge dialog overwriting user's citation key choice during duplicate import#15406
0xRozier wants to merge 27 commits intoJabRef:mainfrom
0xRozier:fix/issue-8644-citationkey-merge-dialog-overwrite

Conversation

@0xRozier
Copy link
Copy Markdown

@0xRozier 0xRozier commented Mar 24, 2026

Related issues and pull requests

Closes #8644

PR Description

This PR fixes the merge dialog during duplicate import so it no longer overwrites the user's citation key choice. The generated key is now shown only in the "Merged entry" column, while the "From import" column keeps the original imported key.

Steps to test

  1. Open a library with an existing entry (e.g., an article by "Smith" from 2022)
  2. Import a .bib file containing a duplicate of that entry with a different citation key
  3. The merge dialog should appear:
    • "Existing entry" column: shows the existing entry's key
    • "From import" column: shows the original imported key (not the auto-generated one)
    • "Merged entry" column: shows the auto-generated key as a suggestion
  4. Edit the citation key in the merged column to a custom value (e.g., "MyCustomKey")
  5. Click "Keep merged"
  6. Verify that the imported entry has the custom key "MyCustomKey" and not the auto-generated one

Expected behavior per resolution choice

Resolution Citation key used
Keep merged Key chosen/edited by the user in the merged column
Keep from import Original imported key (generated key applied if import had no key)
Keep both Existing keeps its key, import keeps its original key (generated key applied if import had no key)
Keep existing Imported entry discarded, nothing changes

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

I used AI to help resolve code review feedback and to troubleshoot formatting.

JabRef8644 JabRef Library

0xRozier added 10 commits March 24, 2026 10:18
Updated duplicate handling to include generated citation key when managing duplicates during import.
Refactor importCleanedEntries method to conditionally generate citation keys based on the skipKeyGeneration parameter.
Add method to set merged field value in DuplicateResolverDialog.
Add test to ensure merged citation key is preserved during import with duplicate check.
@github-actions
Copy link
Copy Markdown
Contributor

Hey @0xRozier! 👋

Thank you for contributing to JabRef!

We have automated checks in place, based on which you will soon get feedback if any of them are failing. We also use Qodo for review assistance. It will update your pull request description with a review help and offer suggestions to improve the pull request.

After all automated checks pass, a maintainer will also review your contribution. Once that happens, you can go through their comments in the "Files changed" tab and act on them, or reply to the conversation if you have further inputs. You can read about the whole pull request process in our contribution guide.

Please ensure that your pull request is in line with our AI Usage Policy and make necessary disclosures.

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Fix merge dialog overwriting user's citation key during import

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Preserve user's citation key choice in merge dialog during duplicate import
• Generate citation key before merge dialog for "From import" column display
• Apply generated key only to "Merged entry" column as suggestion
• Skip redundant key generation after merge resolution
Diagram
flowchart LR
  A["Import Entry"] --> B["Generate Key String"]
  B --> C["Find Duplicate"]
  C --> D{Duplicate Found?}
  D -->|Yes| E["Show Merge Dialog"]
  E --> F["User Edits Key"]
  F --> G["Apply User Choice"]
  D -->|No| H["Apply Generated Key"]
  G --> I["Import with skipKeyGeneration=true"]
  H --> I
  I --> J["Entry in Database"]
Loading

Grey Divider

File Changes

1. jabgui/src/main/java/org/jabref/gui/duplicationFinder/DuplicateResolverDialog.java ✨ Enhancement +5/-0

Add method to set merged field values

• Added import for Field class
• Added setMergedFieldValue() method to set merged field values in the dialog
• Delegates to ThreeWayMergeView.setMergedFieldValue()

jabgui/src/main/java/org/jabref/gui/duplicationFinder/DuplicateResolverDialog.java


2. jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeView.java ✨ Enhancement +9/-0

Add method to set merged field values in view

• Added setMergedFieldValue() method to update merged field values
• Iterates through field rows to find and update the specified field
• Allows setting merged field values before dialog display

jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeView.java


3. jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java 🐞 Bug fix +79/-4

Refactor import flow to preserve user's citation key choice

• Added import for InternalField class
• Refactored importCleanedEntries() to accept skipKeyGeneration parameter
• Added generateKeyString() method to generate keys without modifying entries
• Modified importEntryWithDuplicateCheck() to generate key before merge dialog
• Added overloaded handleDuplicates() method accepting generated key parameter
• Added overloaded getDuplicateDecision() method to set merged field value with generated key
• Generated key is shown in merge dialog but not applied until user confirms

jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java


View more (2)
4. jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java 🧪 Tests +87/-0

Add test for citation key preservation on import

• Added imports for test dependencies and preferences classes
• Added comprehensive test importWithDuplicateCheckPreservesMergedCitationKey()
• Test verifies that user-chosen citation key from merge dialog is preserved
• Mocks key generation, duplicate detection, and merge dialog handling
• Asserts that custom key is not overwritten by automatic key generation

jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java


5. CHANGELOG.md 📝 Documentation +1/-0

Document citation key merge dialog fix

• Added entry documenting fix for merge dialog overwriting user's citation key
• References issue #8644

CHANGELOG.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

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

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. KEEP_BOTH key collision unhandled📎 Requirement gap ✓ Correctness
Description
In the 'keep both entries' path, the imported entry's existing citation key is not checked for
collisions, so duplicates can be inserted without applying a uniqueness suffix. This violates the
requirement to preserve the user-selected key as the base while still enforcing uniqueness.
Code

jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[R371-375]

+            case KEEP_BOTH:
+                if (originalEntry.getCitationKey().isEmpty()) {
+                    generatedKey.ifPresent(key -> originalEntry.setCitationKey(key));
+                }
+                break;
Evidence
PR Compliance ID 2 requires collision resolution (suffixing) when keeping both entries; the new
KEEP_BOTH branch only sets generatedKey when the key is empty and never resolves collisions when
the imported entry already has a key. Additionally, BibDatabase.insertEntries does not enforce
citation key uniqueness, so collisions can persist after insertion.

When keeping both entries during merge, enforce citation key uniqueness without discarding the user-selected key
jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[371-375]
jablib/src/main/java/org/jabref/model/database/BibDatabase.java[169-188]

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

## Issue description
In the `KEEP_BOTH` decision path, if the imported entry already has a citation key that collides with an existing entry, no uniqueness adjustment is applied, so duplicate citation keys can be inserted.
## Issue Context
Compliance requires that when keeping both entries, the chosen/imported key remains the *base* key and only a minimal uniqueness suffix is applied when necessary.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[362-386]

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


2. Merged key not displayed 🐞 Bug ✓ Correctness
Description
The generated citation key is injected into the merge dialog via an internal field row that is
filtered out of the three-way merge view, so the “Merged entry” column will not show the suggested
key and the user cannot edit it there.
Code

jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[R388-391]

+    public DuplicateDecisionResult getDuplicateDecision(BibEntry originalEntry, BibEntry duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult decision, Optional<String> generatedKey) {
+        DuplicateResolverDialog dialog = new DuplicateResolverDialog(duplicateEntry, originalEntry, DuplicateResolverDialog.DuplicateResolverType.IMPORT_CHECK, stateManager, dialogService, preferences);
+        generatedKey.ifPresent(key -> dialog.setMergedFieldValue(InternalField.KEY_FIELD, key));
+        if (decision == BREAK) {
Evidence
ImportHandler sets the merged field value using InternalField.KEY_FIELD. However,
ThreeWayMergeViewModel explicitly removes internal fields from visibleFields, and BibEntry stores
the citation key under InternalField.KEY_FIELD. Therefore the citation key row is not created in
ThreeWayMergeView, and setMergedFieldValue(Field, String) will iterate fieldRows and find no
matching row, silently doing nothing.

jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[388-397]
jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeViewModel.java[42-47]
jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeViewModel.java[102-112]
jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeView.java[141-149]
jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeView.java[207-214]
jablib/src/main/java/org/jabref/model/entry/BibEntry.java[375-393]

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 key suggestion is injected via `InternalField.KEY_FIELD`, but `ThreeWayMergeViewModel` removes internal fields from `visibleFields`, so the citation key row is never rendered. As a result, `setMergedFieldValue(InternalField.KEY_FIELD, key)` does nothing and the suggested/generated key is not shown in the “Merged entry” column.
### Issue Context
- `ImportHandler.getDuplicateDecision(..., generatedKey)` calls `dialog.setMergedFieldValue(InternalField.KEY_FIELD, key)`.
- `ThreeWayMergeViewModel.setVisibleFields(...)` removes internal fields (`FieldFactory::isInternalField`).
- Citation key is stored as `InternalField.KEY_FIELD` inside `BibEntry`.
### Fix Focus Areas
- Ensure the citation key field is visible in the three-way merge dialog for IMPORT_CHECK.
- Options:
- Special-case `InternalField.KEY_FIELD` to NOT be removed from visible fields in `ThreeWayMergeViewModel`.
- Or explicitly add `InternalField.KEY_FIELD` to `visibleFields` when constructing the model/view for merge dialogs.
- Or implement a dedicated UI row for citation key outside the internal field filtering.
- Add/adjust tests to cover the case where the merge dialog is populated with a generated key and that the merged entry returns the edited key.
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[388-397]
- jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeViewModel.java[102-112]
- jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeView.java[207-214]

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



Remediation recommended

3. Duplicated handleDuplicates overload📘 Rule violation ⚙ Maintainability
Description
A new overload of handleDuplicates and getDuplicateDecision duplicates large portions of the
existing logic, increasing maintenance cost and risk of divergent fixes. This violates the
requirement to avoid straightforward code duplication.
Code

jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[R362-398]

+    public Optional<BibEntry> handleDuplicates(BibEntry originalEntry, BibEntry duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult decision, Optional<String> generatedKey) {
+        DuplicateDecisionResult decisionResult = getDuplicateDecision(originalEntry, duplicateEntry, decision, generatedKey);
+        switch (decisionResult.decision()) {
+            case KEEP_RIGHT:
+                targetBibDatabaseContext.getDatabase().removeEntry(duplicateEntry);
+                if (originalEntry.getCitationKey().isEmpty()) {
+                    generatedKey.ifPresent(key -> originalEntry.setCitationKey(key));
+                }
+                break;
+            case KEEP_BOTH:
+                if (originalEntry.getCitationKey().isEmpty()) {
+                    generatedKey.ifPresent(key -> originalEntry.setCitationKey(key));
+                }
+                break;
+            case KEEP_MERGE:
+                targetBibDatabaseContext.getDatabase().removeEntry(duplicateEntry);
+                return Optional.of(decisionResult.mergedEntry());
+            case KEEP_LEFT:
+            case AUTOREMOVE_EXACT:
+            case BREAK:
+            default:
+                return Optional.empty();
+        }
+        return Optional.of(originalEntry);
+    }
+
+    public DuplicateDecisionResult getDuplicateDecision(BibEntry originalEntry, BibEntry duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult decision, Optional<String> generatedKey) {
+        DuplicateResolverDialog dialog = new DuplicateResolverDialog(duplicateEntry, originalEntry, DuplicateResolverDialog.DuplicateResolverType.IMPORT_CHECK, stateManager, dialogService, preferences);
+        generatedKey.ifPresent(key -> dialog.setMergedFieldValue(InternalField.KEY_FIELD, key));
+        if (decision == BREAK) {
+            decision = dialogService.showCustomDialogAndWait(dialog).orElse(BREAK);
+        }
+        if (preferences.getMergeDialogPreferences().shouldMergeApplyToAllEntries()) {
+            preferences.getMergeDialogPreferences().setAllEntriesDuplicateResolverDecision(decision);
+        }
+        return new DuplicateDecisionResult(decision, dialog.getMergedEntry());
+    }
Evidence
PR Compliance ID 10 requires avoiding duplicated logic; the newly added overloads repeat the prior
switch/decision flow with only small additions for generatedKey, rather than factoring shared
behavior into a helper.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[331-360]
jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[362-398]

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 overloads of `handleDuplicates`/`getDuplicateDecision` duplicate existing decision logic, increasing future maintenance burden.
## Issue Context
Only a small additional behavior (pre-seeding a generated citation key in the dialog and applying it when empty) is needed; the control flow can be shared.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[331-399]

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


4. anyMatch weak assertion📘 Rule violation ⚙ Maintainability
Description
The new test asserts via a boolean anyMatch predicate instead of asserting the expected entry/key
content directly, which weakens diagnostics and intent. This conflicts with the guideline to prefer
content assertions over boolean predicates when values can be asserted.
Code

jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java[R303-306]

+        boolean keyPreserved = bibDatabase.getEntries().stream()
+            .anyMatch(e -> userChosenKey.equals(e.getCitationKey().orElse(null)));
+        assertTrue(keyPreserved,
+                   "The citation key chosen by the user in the merge dialog should not be overwritten by key generation");
Evidence
PR Compliance ID 36 asks to prefer content/value assertions over assertTrue on complex predicates;
the test uses anyMatch with assertTrue(...) rather than asserting the imported/merged entry's
citation key value explicitly.

AGENTS.md
jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java[303-306]

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 test checks for the expected citation key using a boolean predicate, which provides weaker failure output and can be less precise than asserting the actual stored value on the relevant entry.
## Issue Context
Prefer assertions that compare expected vs actual values (e.g., assert the citation key of the imported/merged entry directly).
## Fix Focus Areas
- jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java[230-307]

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


5. Test misses dialog injection🐞 Bug ⚙ Maintainability
Description
The added test stubs handleDuplicates and therefore does not exercise the new code path that injects
the generated key into the merge dialog (and would not catch the fact that the key row is not
shown/updated).
Code

jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java[R289-293]

+        Mockito.doReturn(Optional.of(existingEntry)).when(importHandler).findDuplicate(any());
+
+        // Mock handleDuplicates(4 params) to simulate user choosing KEEP_MERGE with their custom key
+        Mockito.doReturn(Optional.of(mergedEntry)).when(importHandler).handleDuplicates(any(), any(), any(), any());
+    
Evidence
The production behavior change includes getDuplicateDecision(..., generatedKey) calling
dialog.setMergedFieldValue(InternalField.KEY_FIELD, key) to show the suggestion in the merge UI.
The test replaces the entire duplicate-handling logic by mocking `handleDuplicates(any(), any(),
any(), any()), so it cannot fail if setMergedFieldValue` is ineffective (the main regression) and
primarily validates that ImportHandler inserts the returned merged entry.

jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java[289-306]
jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[388-397]
jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeViewModel.java[102-112]

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

## Issue description
Current test coverage does not validate the new behavior of injecting a generated citation key into the merge dialog and ensuring it is visible/editable in the merged column.
### Issue Context
The PR’s core UI behavior relies on `DuplicateResolverDialog.setMergedFieldValue(InternalField.KEY_FIELD, key)` working, but the test mocks `handleDuplicates(...)`, bypassing that path.
### Fix Focus Areas
- Add a test that does not mock `handleDuplicates` and instead mocks only `dialogService.showCustomDialogAndWait(dialog)` to return `KEEP_MERGE`, then assert the merged entry contains the expected citation key.
- Alternatively, add focused tests for `ThreeWayMergeViewModel`/`ThreeWayMergeView` ensuring the citation key field is present and `setMergedFieldValue` updates the merged entry.
- jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java[230-307]
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[388-397]
- jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeViewModel.java[102-112]

ⓘ 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

@github-actions github-actions Bot added good second issue Issues that involve a tour of two or three interweaved components in JabRef component: citationkey-generator component: duplicate-finder labels Mar 24, 2026
@0xRozier 0xRozier marked this pull request as draft March 24, 2026 21:34
@0xRozier 0xRozier changed the title Fix/issue 8644 citationkey merge dialog overwrite Fix merge dialog overwriting user's citation key choice during duplicate import Mar 24, 2026
Comment on lines +388 to +391
public DuplicateDecisionResult getDuplicateDecision(BibEntry originalEntry, BibEntry duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult decision, Optional<String> generatedKey) {
DuplicateResolverDialog dialog = new DuplicateResolverDialog(duplicateEntry, originalEntry, DuplicateResolverDialog.DuplicateResolverType.IMPORT_CHECK, stateManager, dialogService, preferences);
generatedKey.ifPresent(key -> dialog.setMergedFieldValue(InternalField.KEY_FIELD, key));
if (decision == BREAK) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

2. Merged key not displayed 🐞 Bug ✓ Correctness

The generated citation key is injected into the merge dialog via an internal field row that is
filtered out of the three-way merge view, so the “Merged entry” column will not show the suggested
key and the user cannot edit it there.
Agent Prompt
### Issue description
The merge dialog key suggestion is injected via `InternalField.KEY_FIELD`, but `ThreeWayMergeViewModel` removes internal fields from `visibleFields`, so the citation key row is never rendered. As a result, `setMergedFieldValue(InternalField.KEY_FIELD, key)` does nothing and the suggested/generated key is not shown in the “Merged entry” column.

### Issue Context
- `ImportHandler.getDuplicateDecision(..., generatedKey)` calls `dialog.setMergedFieldValue(InternalField.KEY_FIELD, key)`.
- `ThreeWayMergeViewModel.setVisibleFields(...)` removes internal fields (`FieldFactory::isInternalField`).
- Citation key is stored as `InternalField.KEY_FIELD` inside `BibEntry`.

### Fix Focus Areas
- Ensure the citation key field is visible in the three-way merge dialog for IMPORT_CHECK.
  - Options:
    - Special-case `InternalField.KEY_FIELD` to NOT be removed from visible fields in `ThreeWayMergeViewModel`.
    - Or explicitly add `InternalField.KEY_FIELD` to `visibleFields` when constructing the model/view for merge dialogs.
    - Or implement a dedicated UI row for citation key outside the internal field filtering.
- Add/adjust tests to cover the case where the merge dialog is populated with a generated key and that the merged entry returns the edited key.

- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[388-397]
- jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeViewModel.java[102-112]
- jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeView.java[207-214]

ⓘ 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 Mar 24, 2026
@testlens-app

This comment has been minimized.

@testlens-app

This comment has been minimized.

@testlens-app

This comment has been minimized.

Removed duplicate entry for merge dialog citation key fix.
@testlens-app

This comment has been minimized.

@testlens-app

This comment has been minimized.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@testlens-app

This comment has been minimized.

…nt formatter alignment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@testlens-app

This comment has been minimized.

@0xRozier 0xRozier marked this pull request as ready for review March 25, 2026 13:07
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Fix merge dialog overwriting user's citation key during duplicate import

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Preserve user's citation key choice in merge dialog during duplicate import
• Generate citation key before merge dialog so user can edit it
• Pass generated key to merge dialog for "Merged entry" column display
• Skip automatic key generation after merge to preserve user's selection
Diagram
flowchart LR
  A["Import Entry"] --> B["Generate Key String"]
  B --> C["Find Duplicate"]
  C --> D{"Duplicate Found?"}
  D -->|Yes| E["Show Merge Dialog<br/>with Generated Key"]
  D -->|No| F["Apply Generated Key"]
  E --> G["User Edits Key"]
  G --> H["Handle Duplicates<br/>with User's Key"]
  H --> I["Import Entry<br/>Skip Key Generation"]
  F --> I
Loading

Grey Divider

File Changes

1. jabgui/src/main/java/org/jabref/gui/duplicationFinder/DuplicateResolverDialog.java ✨ Enhancement +5/-0

Add method to set merged field values

• Added import for Field class
• Added setMergedFieldValue() method to set merged field values in the dialog
• Method delegates to ThreeWayMergeView to update specific field in merged entry column

jabgui/src/main/java/org/jabref/gui/duplicationFinder/DuplicateResolverDialog.java


2. jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeView.java ✨ Enhancement +9/-0

Add method to set merged field values in view

• Added setMergedFieldValue() method to update merged field values
• Iterates through field rows to find matching field and updates its merged value
• Allows external code to set merged field values before dialog is shown

jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeView.java


3. jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java 🐞 Bug fix +57/-5

Preserve citation key choice in duplicate import flow

• Added import for InternalField class
• Refactored importCleanedEntries() to accept skipKeyGeneration parameter
• Added generateKeyString() method to generate key without modifying entry
• Modified importEntryWithDuplicateCheck() to generate key before merge dialog
• Updated handleDuplicates() to accept and use generated key parameter
• Updated getDuplicateDecision() to set generated key in merge dialog
• Applied generated key to entries without duplicates
• Skip key generation after merge to preserve user's choice

jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java


View more (2)
4. jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java 🧪 Tests +88/-3

Add test for citation key preservation on import

• Added imports for Optional, LibraryPreferences, CitationKeyPatternPreferences,
 GlobalCitationKeyPatterns, ImporterPreferences, OwnerPreferences, TimestampPreferences, and
 MetaData
• Updated existing test mocks to use any() matchers for 4-parameter getDuplicateDecision() calls
• Added comprehensive test importWithDuplicateCheckPreservesMergedCitationKey() to verify user's
 custom citation key is preserved during merge

jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java


5. CHANGELOG.md 📝 Documentation +1/-0

Document citation key merge dialog fix

• Added entry documenting fix for merge dialog overwriting user's citation key during duplicate
 import
• References issue #8644

CHANGELOG.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

Code Review by Qodo

Grey Divider

Looking for bugs?

Check back in a few minutes. An AI review agent is analyzing this pull request.

Grey Divider

Qodo Logo

@github-actions
Copy link
Copy Markdown
Contributor

Your pull request conflicts with the target branch.

Please merge with your code. For a step-by-step guide to resolve merge conflicts, see https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/resolving-a-merge-conflict-using-the-command-line.

@testlens-app

This comment has been minimized.

new CurrentThreadTaskExecutor()));
// Mock the behavior of getDuplicateDecision to return KEEP_RIGHT
Mockito.doReturn(decisionResult).when(importHandler).getDuplicateDecision(testEntry, duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult.BREAK);
Mockito.doReturn(decisionResult).when(importHandler).getDuplicateDecision(any(), any(), any(), any());
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.

What is happening here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The 3-param getDuplicateDecision now delegates to the 4-param overload, so the mock needs to target the 4-param version to match the actual call chain. The test still calls handleDuplicates(3 params) which internally chains through to getDuplicateDecision(4 params).

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.

You do understand that the question was not about number of parameters changing, but the existing parameters changing to any()?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Oh yes my bad, I switched to any() to avoid adding an eq() import

I reworked the three tests "handleDuplicates" to match their original form, and add the fourth parameter
@testlens-app

This comment has been minimized.

Add a case : if imported key has already the same key as the existing key, the generated key is selected to avoid collision
@testlens-app

This comment has been minimized.

@testlens-app

This comment has been minimized.

@0xRozier 0xRozier marked this pull request as draft March 30, 2026 21:07
@testlens-app

This comment has been minimized.

@testlens-app
Copy link
Copy Markdown

testlens-app Bot commented Mar 31, 2026

✅ All tests passed ✅

🏷️ Commit: e0b521e
▶️ Tests: 10214 executed
⚪️ Checks: 65/65 completed


Learn more about TestLens at testlens.app.

@0xRozier 0xRozier marked this pull request as ready for review March 31, 2026 07:18
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Fix merge dialog overwriting user's citation key choice during duplicate import

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Preserve user's citation key choice in merge dialog during duplicate import
• Generate citation key before merge dialog so user can edit it
• Apply generated key only when no duplicate exists or user confirms merge
• Add methods to set merged field values in dialog and merge view
Diagram
flowchart LR
  A["Import Entry"] --> B["Generate Key String"]
  B --> C["Find Duplicate"]
  C --> D{Duplicate Found?}
  D -->|No| E["Apply Generated Key"]
  D -->|Yes| F["Show Merge Dialog"]
  F --> G["User Edits Key"]
  G --> H{Resolution Choice}
  H -->|Keep Merge| I["Use User's Key"]
  H -->|Keep Both| J["Apply Generated Key if Colliding"]
  H -->|Keep Import| K["Use Original Key"]
  E --> L["Import Entry"]
  I --> L
  J --> L
  K --> L
Loading

Grey Divider

File Changes

1. jabgui/src/main/java/org/jabref/gui/duplicationFinder/DuplicateResolverDialog.java ✨ Enhancement +5/-0

Add method to set merged field values

• Added import for Field class
• Added setMergedFieldValue() method to set merged field values in the three-way merge view

jabgui/src/main/java/org/jabref/gui/duplicationFinder/DuplicateResolverDialog.java


2. jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java 🐞 Bug fix +59/-5

Preserve citation key choice in merge dialog

• Added import for InternalField class
• Refactored importCleanedEntries() to accept skipKeyGeneration parameter to prevent duplicate
 key generation
• Added generateKeyString() method to generate citation key without modifying entry
• Modified importEntryWithDuplicateCheck() to generate key before merge dialog and pass it to
 duplicate handler
• Updated handleDuplicates() to accept optional generated key and apply it based on resolution
 choice
• Updated getDuplicateDecision() to set generated key in merge dialog for user preview
• Added logic to apply generated key when no duplicate exists or when keeping both entries with
 colliding keys

jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java


3. jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeView.java ✨ Enhancement +9/-0

Add method to set merged field values

• Added setMergedFieldValue() method to set merged field values by iterating through field rows

jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeView.java


View more (2)
4. jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java 🧪 Tests +132/-3

Add tests for citation key preservation

• Added imports for Optional, LibraryPreferences, CitationKeyPatternPreferences,
 GlobalCitationKeyPatterns, ImporterPreferences, OwnerPreferences, TimestampPreferences, and
 MetaData
• Updated existing test mocks to include Optional.empty() parameter for new
 getDuplicateDecision() signature
• Added test handleDuplicatesKeepBothWithCollidingKeysAppliesGeneratedKey() to verify generated
 key is applied when keeping both entries with same key
• Added test importWithDuplicateCheckPreservesMergedCitationKey() to verify user's custom citation
 key from merge dialog is preserved during import

jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java


5. CHANGELOG.md 📝 Documentation +1/-0

Document citation key merge dialog fix

• Added entry documenting fix for merge dialog overwriting user's citation key choice during
 duplicate import

CHANGELOG.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Mar 31, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. DuplicateDecisionResult constructed with null 📘 Rule violation ≡ Correctness ⭐ New
Description
The new test passes null for DuplicateDecisionResult's mergedEntry, which violates the
no-null-passing rule and can lead to NPEs if the value is accessed. Use a real BibEntry instance
(or refactor the type to model absence explicitly) instead of passing null.
Code

jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java[R244-245]

+        DuplicateDecisionResult decisionResult = new DuplicateDecisionResult(
+                DuplicateResolverDialog.DuplicateResolverResult.KEEP_BOTH, null);
Evidence
The checklist forbids passing null to methods/constructors unless explicitly intended. The new
test constructs DuplicateDecisionResult with null for mergedEntry, while the record defines
mergedEntry as a non-annotated BibEntry component (no indication it is intended to be nullable).

AGENTS.md
jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java[244-245]
jabgui/src/main/java/org/jabref/gui/externalfiles/DuplicateDecisionResult.java[6-9]

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 new test constructs `DuplicateDecisionResult` with a `null` `mergedEntry`, violating the project rule to not pass null unless explicitly intended and risking NPEs.

## Issue Context
`DuplicateDecisionResult` is a `record` with a `BibEntry mergedEntry` component (no `@Nullable`), so callers should not pass `null`.

## Fix Focus Areas
- jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java[244-245]

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


2. Duplicate key from pregen 🐞 Bug ≡ Correctness ⭐ New
Description
ImportHandler generates the citation key before inserting the entry and then skips generateKeys(),
but CitationKeyGenerator.generateKey() can suppress suffixing when the entry already has the same
key as the generated key. This can insert entries with citation keys that already exist in the
database (no suffix added), creating duplicate citation keys after import.
Code

jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[R295-314]

+                          // Generate citation key string WITHOUT modifying the entry,
+                          // so the "From import" column shows the original key
+                          Optional<String> generatedKey = generateKeyString(entryToInsert);
+
                          BibEntry finalEntry = entryToInsert;
                          if (existingDuplicateInLibrary.isPresent()) {
-                              Optional<BibEntry> duplicateHandledEntry = handleDuplicates(entryToInsert, existingDuplicateInLibrary.get(), decision);
+                              Optional<BibEntry> duplicateHandledEntry = handleDuplicates(entryToInsert, existingDuplicateInLibrary.get(), decision, generatedKey);
                              if (duplicateHandledEntry.isEmpty()) {
                                  tracker.markSkipped();
                                  return;
                              }
                              finalEntry = duplicateHandledEntry.get();
+                          } else {
+                              // No duplicate: apply the generated key directly
+                              generatedKey.ifPresent(key -> entryToInsert.setCitationKey(key));
                          }

-                          importCleanedEntries(transferInformation, List.of(finalEntry));
+                          // Skip key generation since it was already handled
+                          importCleanedEntries(transferInformation, List.of(finalEntry), true);
Evidence
The import flow computes a key string on the pre-insert entry and then calls
importCleanedEntries(..., true), which skips generateKeys(), so the precomputed value becomes final.
CitationKeyGenerator.generateKey() passes the entry’s current citation key as oldKey and decrements
the occurrence count when oldKey equals the generated key; because the entry is not inserted yet,
BibDatabase.getNumberOfCitationKeyOccurrences only counts existing DB entries, so a single existing
occurrence can be decremented to zero and prevent suffixing, returning a colliding key.

jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[284-316]
jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[432-446]
jablib/src/main/java/org/jabref/logic/citationkeypattern/CitationKeyGenerator.java[120-169]
jablib/src/main/java/org/jabref/model/database/BibDatabase.java[580-586]

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

### Issue description
`generateKeyString(entry)` calls `CitationKeyGenerator.generateKey(entry)` on an entry that may already have a citation key. `CitationKeyGenerator` uses that existing key (`oldKey`) to suppress suffixing, which is correct for *updating* existing entries but incorrect for *pre-insert* generation; combined with skipping `generateKeys(...)`, it can leave duplicate keys in the DB.

### Issue Context
Key generation is now done before insertion (to avoid overwriting the "From import" column) and post-insert key generation is skipped. That makes the pre-insert generated key authoritative and it must be collision-safe.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[284-316]
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[432-446]

### Suggested fix
In `generateKeyString`, generate based on a clone with the citation key cleared so `oldKey` is `null`:
- `BibEntry tmp = new BibEntry(entry); tmp.clearCiteKey(); return Optional.of(keyGenerator.generateKey(tmp));`

Optionally, add a safety net: only pass `skipKeyGeneration=true` if `finalEntry` already has a citation key (or the user explicitly chose one), otherwise allow the normal `generateKeys(...)` path.

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


3. KEEP_BOTH key collision unhandled📎 Requirement gap ≡ Correctness
Description
In the 'keep both entries' path, the imported entry's existing citation key is not checked for
collisions, so duplicates can be inserted without applying a uniqueness suffix. This violates the
requirement to preserve the user-selected key as the base while still enforcing uniqueness.
Code

jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[R371-375]

+            case KEEP_BOTH:
+                if (originalEntry.getCitationKey().isEmpty()) {
+                    generatedKey.ifPresent(key -> originalEntry.setCitationKey(key));
+                }
+                break;
Evidence
PR Compliance ID 2 requires collision resolution (suffixing) when keeping both entries; the new
KEEP_BOTH branch only sets generatedKey when the key is empty and never resolves collisions when
the imported entry already has a key. Additionally, BibDatabase.insertEntries does not enforce
citation key uniqueness, so collisions can persist after insertion.

When keeping both entries during merge, enforce citation key uniqueness without discarding the user-selected key
jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[371-375]
jablib/src/main/java/org/jabref/model/database/BibDatabase.java[169-188]

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

## Issue description
In the `KEEP_BOTH` decision path, if the imported entry already has a citation key that collides with an existing entry, no uniqueness adjustment is applied, so duplicate citation keys can be inserted.
## Issue Context
Compliance requires that when keeping both entries, the chosen/imported key remains the *base* key and only a minimal uniqueness suffix is applied when necessary.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[362-386]

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


View more (1)
4. Merged key not displayed 🐞 Bug ≡ Correctness
Description
The generated citation key is injected into the merge dialog via an internal field row that is
filtered out of the three-way merge view, so the “Merged entry” column will not show the suggested
key and the user cannot edit it there.
Code

jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[R388-391]

+    public DuplicateDecisionResult getDuplicateDecision(BibEntry originalEntry, BibEntry duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult decision, Optional<String> generatedKey) {
+        DuplicateResolverDialog dialog = new DuplicateResolverDialog(duplicateEntry, originalEntry, DuplicateResolverDialog.DuplicateResolverType.IMPORT_CHECK, stateManager, dialogService, preferences);
+        generatedKey.ifPresent(key -> dialog.setMergedFieldValue(InternalField.KEY_FIELD, key));
+        if (decision == BREAK) {
Evidence
ImportHandler sets the merged field value using InternalField.KEY_FIELD. However,
ThreeWayMergeViewModel explicitly removes internal fields from visibleFields, and BibEntry stores
the citation key under InternalField.KEY_FIELD. Therefore the citation key row is not created in
ThreeWayMergeView, and setMergedFieldValue(Field, String) will iterate fieldRows and find no
matching row, silently doing nothing.

jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[388-397]
jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeViewModel.java[42-47]
jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeViewModel.java[102-112]
jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeView.java[141-149]
jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeView.java[207-214]
jablib/src/main/java/org/jabref/model/entry/BibEntry.java[375-393]

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 key suggestion is injected via `InternalField.KEY_FIELD`, but `ThreeWayMergeViewModel` removes internal fields from `visibleFields`, so the citation key row is never rendered. As a result, `setMergedFieldValue(InternalField.KEY_FIELD, key)` does nothing and the suggested/generated key is not shown in the “Merged entry” column.
### Issue Context
- `ImportHandler.getDuplicateDecision(..., generatedKey)` calls `dialog.setMergedFieldValue(InternalField.KEY_FIELD, key)`.
- `ThreeWayMergeViewModel.setVisibleFields(...)` removes internal fields (`FieldFactory::isInternalField`).
- Citation key is stored as `InternalField.KEY_FIELD` inside `BibEntry`.
### Fix Focus Areas
- Ensure the citation key field is visible in the three-way merge dialog for IMPORT_CHECK.
- Options:
- Special-case `InternalField.KEY_FIELD` to NOT be removed from visible fields in `ThreeWayMergeViewModel`.
- Or explicitly add `InternalField.KEY_FIELD` to `visibleFields` when constructing the model/view for merge dialogs.
- Or implement a dedicated UI row for citation key outside the internal field filtering.
- Add/adjust tests to cover the case where the merge dialog is populated with a generated key and that the merged entry returns the edited key.
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[388-397]
- jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeViewModel.java[102-112]
- jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeView.java[207-214]

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



Remediation recommended

5. Merged key row missing 🐞 Bug ☼ Reliability ⭐ New
Description
DuplicateResolverDialog pre-fills the merged citation key via setMergedFieldValue, but
ThreeWayMergeView only applies it if a KEY_FIELD row already exists. If both entries have no
citation key field, the row won’t exist, the prefill becomes a no-op, and ImportHandler still skips
later key generation—so the merged import can be inserted without any generated citation key even
when key generation is enabled.
Code

jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeView.java[R207-214]

+    public void setMergedFieldValue(Field field, String value) {
+        for (FieldRowView row : fieldRows) {
+            if (row.viewModel.getField().equals(field)) {
+                row.viewModel.setMergedFieldValue(value);
+                break;
+            }
+        }
+    }
Evidence
ThreeWayMergeViewModel builds visibleFields from the union of left/right entry fields; if neither
entry contains KEY_FIELD, no row is created. ThreeWayMergeView.setMergedFieldValue only iterates
existing rows and does nothing if the field is absent, while ImportHandler skips post-insert key
generation for this path.

jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeViewModel.java[42-47]
jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeView.java[207-214]
jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[312-313]
jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[367-370]

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

### Issue description
`setMergedFieldValue(KEY_FIELD, ...)` is a best-effort update of an existing UI row. When the KEY_FIELD row is not present (e.g., both entries have no citation key), the generated key never gets applied, and the import path skips post-insert key generation.

### Issue Context
The PR intends to show a generated key suggestion in the "Merged entry" column without overwriting the "From import" column. That still requires reliably applying a generated key when needed.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeView.java[207-214]
- jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeViewModel.java[42-47]
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[312-313]
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[367-370]

### Suggested fix
Implement a fallback so the generated key is applied even if the row is missing:
- Change `ThreeWayMergeView.setMergedFieldValue` to return `boolean found`.
- In `ImportHandler.getDuplicateDecision`, if `setMergedFieldValue` returns false, directly set it on the merged entry model (e.g., `dialog.getMergedEntry().setCitationKey(key)`), or avoid `skipKeyGeneration` when the merged entry has no citation key.

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


6. Duplicated handleDuplicates overload📘 Rule violation ⚙ Maintainability
Description
A new overload of handleDuplicates and getDuplicateDecision duplicates large portions of the
existing logic, increasing maintenance cost and risk of divergent fixes. This violates the
requirement to avoid straightforward code duplication.
Code

jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[R362-398]

+    public Optional<BibEntry> handleDuplicates(BibEntry originalEntry, BibEntry duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult decision, Optional<String> generatedKey) {
+        DuplicateDecisionResult decisionResult = getDuplicateDecision(originalEntry, duplicateEntry, decision, generatedKey);
+        switch (decisionResult.decision()) {
+            case KEEP_RIGHT:
+                targetBibDatabaseContext.getDatabase().removeEntry(duplicateEntry);
+                if (originalEntry.getCitationKey().isEmpty()) {
+                    generatedKey.ifPresent(key -> originalEntry.setCitationKey(key));
+                }
+                break;
+            case KEEP_BOTH:
+                if (originalEntry.getCitationKey().isEmpty()) {
+                    generatedKey.ifPresent(key -> originalEntry.setCitationKey(key));
+                }
+                break;
+            case KEEP_MERGE:
+                targetBibDatabaseContext.getDatabase().removeEntry(duplicateEntry);
+                return Optional.of(decisionResult.mergedEntry());
+            case KEEP_LEFT:
+            case AUTOREMOVE_EXACT:
+            case BREAK:
+            default:
+                return Optional.empty();
+        }
+        return Optional.of(originalEntry);
+    }
+
+    public DuplicateDecisionResult getDuplicateDecision(BibEntry originalEntry, BibEntry duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult decision, Optional<String> generatedKey) {
+        DuplicateResolverDialog dialog = new DuplicateResolverDialog(duplicateEntry, originalEntry, DuplicateResolverDialog.DuplicateResolverType.IMPORT_CHECK, stateManager, dialogService, preferences);
+        generatedKey.ifPresent(key -> dialog.setMergedFieldValue(InternalField.KEY_FIELD, key));
+        if (decision == BREAK) {
+            decision = dialogService.showCustomDialogAndWait(dialog).orElse(BREAK);
+        }
+        if (preferences.getMergeDialogPreferences().shouldMergeApplyToAllEntries()) {
+            preferences.getMergeDialogPreferences().setAllEntriesDuplicateResolverDecision(decision);
+        }
+        return new DuplicateDecisionResult(decision, dialog.getMergedEntry());
+    }
Evidence
PR Compliance ID 10 requires avoiding duplicated logic; the newly added overloads repeat the prior
switch/decision flow with only small additions for generatedKey, rather than factoring shared
behavior into a helper.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[331-360]
jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[362-398]

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 overloads of `handleDuplicates`/`getDuplicateDecision` duplicate existing decision logic, increasing future maintenance burden.
## Issue Context
Only a small additional behavior (pre-seeding a generated citation key in the dialog and applying it when empty) is needed; the control flow can be shared.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[331-399]

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


7. anyMatch weak assertion📘 Rule violation ⚙ Maintainability
Description
The new test asserts via a boolean anyMatch predicate instead of asserting the expected entry/key
content directly, which weakens diagnostics and intent. This conflicts with the guideline to prefer
content assertions over boolean predicates when values can be asserted.
Code

jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java[R303-306]

+        boolean keyPreserved = bibDatabase.getEntries().stream()
+            .anyMatch(e -> userChosenKey.equals(e.getCitationKey().orElse(null)));
+        assertTrue(keyPreserved,
+                   "The citation key chosen by the user in the merge dialog should not be overwritten by key generation");
Evidence
PR Compliance ID 36 asks to prefer content/value assertions over assertTrue on complex predicates;
the test uses anyMatch with assertTrue(...) rather than asserting the imported/merged entry's
citation key value explicitly.

AGENTS.md
jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java[303-306]

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 test checks for the expected citation key using a boolean predicate, which provides weaker failure output and can be less precise than asserting the actual stored value on the relevant entry.
## Issue Context
Prefer assertions that compare expected vs actual values (e.g., assert the citation key of the imported/merged entry directly).
## Fix Focus Areas
- jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java[230-307]

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


View more (1)
8. Test misses dialog injection🐞 Bug ⚙ Maintainability
Description
The added test stubs handleDuplicates and therefore does not exercise the new code path that injects
the generated key into the merge dialog (and would not catch the fact that the key row is not
shown/updated).
Code

jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java[R289-293]

+        Mockito.doReturn(Optional.of(existingEntry)).when(importHandler).findDuplicate(any());
+
+        // Mock handleDuplicates(4 params) to simulate user choosing KEEP_MERGE with their custom key
+        Mockito.doReturn(Optional.of(mergedEntry)).when(importHandler).handleDuplicates(any(), any(), any(), any());
+    
Evidence
The production behavior change includes getDuplicateDecision(..., generatedKey) calling
dialog.setMergedFieldValue(InternalField.KEY_FIELD, key) to show the suggestion in the merge UI.
The test replaces the entire duplicate-handling logic by mocking `handleDuplicates(any(), any(),
any(), any()), so it cannot fail if setMergedFieldValue` is ineffective (the main regression) and
primarily validates that ImportHandler inserts the returned merged entry.

jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java[289-306]
jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[388-397]
jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeViewModel.java[102-112]

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

## Issue description
Current test coverage does not validate the new behavior of injecting a generated citation key into the merge dialog and ensuring it is visible/editable in the merged column.
### Issue Context
The PR’s core UI behavior relies on `DuplicateResolverDialog.setMergedFieldValue(InternalField.KEY_FIELD, key)` working, but the test mocks `handleDuplicates(...)`, bypassing that path.
### Fix Focus Areas
- Add a test that does not mock `handleDuplicates` and instead mocks only `dialogService.showCustomDialogAndWait(dialog)` to return `KEEP_MERGE`, then assert the merged entry contains the expected citation key.
- Alternatively, add focused tests for `ThreeWayMergeViewModel`/`ThreeWayMergeView` ensuring the citation key field is present and `setMergedFieldValue` updates the merged entry.
- jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java[230-307]
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[388-397]
- jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeViewModel.java[102-112]

ⓘ 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

@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.

@0xRozier
Copy link
Copy Markdown
Author

Hi @subhramit I reworked some points :

  • Replaced any() matchers with concrete values in getDuplicateDecision mocks to restore test precision
  • Added a new test handleDuplicatesKeepBothWithCollidingKeysAppliesGeneratedKey to cover the case where both entries
    have the same citation key
  • Fixed the KEEP_BOTH path in handleDuplicates to apply the generated key when the imported entry's key collides with
    the existing entry's key

Hope it'll be better, I'm waiting for your feedback

Comment on lines +244 to +245
DuplicateDecisionResult decisionResult = new DuplicateDecisionResult(
DuplicateResolverDialog.DuplicateResolverResult.KEEP_BOTH, null);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. duplicatedecisionresult constructed with null 📘 Rule violation ≡ Correctness

The new test passes null for DuplicateDecisionResult's mergedEntry, which violates the
no-null-passing rule and can lead to NPEs if the value is accessed. Use a real BibEntry instance
(or refactor the type to model absence explicitly) instead of passing null.
Agent Prompt
## Issue description
A new test constructs `DuplicateDecisionResult` with a `null` `mergedEntry`, violating the project rule to not pass null unless explicitly intended and risking NPEs.

## Issue Context
`DuplicateDecisionResult` is a `record` with a `BibEntry mergedEntry` component (no `@Nullable`), so callers should not pass `null`.

## Fix Focus Areas
- jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java[244-245]

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

Comment on lines +295 to 314
// Generate citation key string WITHOUT modifying the entry,
// so the "From import" column shows the original key
Optional<String> generatedKey = generateKeyString(entryToInsert);

BibEntry finalEntry = entryToInsert;
if (existingDuplicateInLibrary.isPresent()) {
Optional<BibEntry> duplicateHandledEntry = handleDuplicates(entryToInsert, existingDuplicateInLibrary.get(), decision);
Optional<BibEntry> duplicateHandledEntry = handleDuplicates(entryToInsert, existingDuplicateInLibrary.get(), decision, generatedKey);
if (duplicateHandledEntry.isEmpty()) {
tracker.markSkipped();
return;
}
finalEntry = duplicateHandledEntry.get();
} else {
// No duplicate: apply the generated key directly
generatedKey.ifPresent(key -> entryToInsert.setCitationKey(key));
}

importCleanedEntries(transferInformation, List.of(finalEntry));
// Skip key generation since it was already handled
importCleanedEntries(transferInformation, List.of(finalEntry), 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

2. Duplicate key from pregen 🐞 Bug ≡ Correctness

ImportHandler generates the citation key before inserting the entry and then skips generateKeys(),
but CitationKeyGenerator.generateKey() can suppress suffixing when the entry already has the same
key as the generated key. This can insert entries with citation keys that already exist in the
database (no suffix added), creating duplicate citation keys after import.
Agent Prompt
### Issue description
`generateKeyString(entry)` calls `CitationKeyGenerator.generateKey(entry)` on an entry that may already have a citation key. `CitationKeyGenerator` uses that existing key (`oldKey`) to suppress suffixing, which is correct for *updating* existing entries but incorrect for *pre-insert* generation; combined with skipping `generateKeys(...)`, it can leave duplicate keys in the DB.

### Issue Context
Key generation is now done before insertion (to avoid overwriting the "From import" column) and post-insert key generation is skipped. That makes the pre-insert generated key authoritative and it must be collision-safe.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[284-316]
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[432-446]

### Suggested fix
In `generateKeyString`, generate based on a clone with the citation key cleared so `oldKey` is `null`:
- `BibEntry tmp = new BibEntry(entry); tmp.clearCiteKey(); return Optional.of(keyGenerator.generateKey(tmp));`

Optionally, add a safety net: only pass `skipKeyGeneration=true` if `finalEntry` already has a citation key (or the user explicitly chose one), otherwise allow the normal `generateKeys(...)` path.

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

@0xRozier 0xRozier closed this by deleting the head repository May 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: citationkey-generator component: duplicate-finder first contrib good second issue Issues that involve a tour of two or three interweaved components in JabRef status: changes-required Pull requests that are not yet complete

Projects

None yet

Development

Successfully merging this pull request may close these issues.

citationkey pattern always overwrites key, regardless of chosen key in entry-merge dialogue

3 participants