Skip to content

Mirror directory libraries into a .bib with git-sync merge-back - #743

Open
koppor wants to merge 18 commits into
directory-pattern-renamesfrom
directory-bib-mirror
Open

koppor wants to merge 18 commits into
directory-pattern-renamesfrom
directory-bib-mirror

Conversation

@koppor

@koppor koppor commented Jul 19, 2026 •

Copy link
Copy Markdown
Member

📚 Directory-as-library stack — bottom → top, each builds on the one below:

  1. Programmatic Hayagriva writer (directory-as-library, phase 0) #736 · Hayagriva YAML writer
  2. Open folder as library (directory as library, phase 1) #737 · Open folder as library
  3. Live inbound sync for directory libraries (phase 2) #738 · Inbound file sync
  4. Write-back to Hayagriva sidecars (directory as library, phase 3) #739 · Write-back to sidecars
  5. Groups panel mirrors the directory structure (phase 4, #10930) #740 · Directory-structure groups
  6. Pattern-driven pair renames for directory libraries (phase 5a) #741 · Pattern-driven pair renames
  7. Mirror directory libraries into a .bib with git-sync merge-back #743 · .bib mirror + merge-back ← this PR
  8. Convert .bib libraries into directory libraries #744 · Convert .bib → directory
  9. Make directory libraries reachable through the REST API #745 · REST API support
  10. Split DirectoryLibrarySynchronizer into collaborators #760 · Split the synchronizer

Related issues and pull requests

Phase 6 of the "directory as library" plan (PLAN.md), stacked on #741 (base branch directory-pattern-renames). No issue is closed by this PR.

PR Description

🤖 A directory library is now continuously mirrored into a single .bib file in its root, so collaborators and plain BibTeX tools can read and edit the whole library as one file. External edits to that mirror — while JabRef is open, or found offline at open time — are merged back into the library, and genuine conflicts open JabRef's existing conflict-resolution dialog where cancelling keeps the library's version. Adopting a pre-existing .bib only ever adds entries, never deletes.

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

Analogies

Like honey poured into a jar from many combs, the mirror gathers every sidecar into one sweet, portable vessel — and what is stirred into the jar finds its way back to the combs. Like a chocolate bar's imprint, the .bib shows the same squares as the mold it was cast from; snap off or add a square and the mold is recut to match. And like the moon, the mirror shines with no light of its own — yet its pull moves the tides of the library beneath it.

Steps to test

  1. Open a folder as library (File > Open folder as library…) containing some .yml/.md sidecars and PDFs.
  2. A <folder-name>.bib appears in the folder immediately, containing every entry (plus a .jabref/mirror-base.bib snapshot).
  3. Edit that .bib in a text editor while JabRef is open — e.g. change a title: within ~2 s the entry updates in the main table and the sidecar is rewritten (and pattern-renamed, if the pattern covers the title). Screenshot after externally editing a title and externally appending a new @Article to the mirror — both landed in the open library, the added entry got its own Markdown sidecar on disk:

Main table after external .bib edits: retitled entry and externally added entry

  1. Close JabRef, edit the .bib externally (or git pull a collaborator's change), reopen: the offline change is merged in at open time.
  2. Change the same field in JabRef and (externally) in the .bib to different values: the git conflict resolution dialog opens; cancelling keeps JabRef's version.

AI usage

Claude Code (model claude-fable-5).

AI CHECKLIST.md walkthrough
  • No == null / != null checks — JSpecify annotations used instead (single getFileName() == null guard follows the file's existing precedent for JDK-nullable returns).
  • No Objects.requireNonNull(...) — nullability expressed via JSpecify annotations.
  • New classes annotated with @NullMarked — no new classes; the extended DirectoryLibrarySynchronizer already is.
  • Optional consumed with ifPresent / map / orElseGet — no isPresent() + get() blocks.
  • StringUtil.isBlank(...) used instead of s == null || s.isBlank() — no blank checks added.
  • No catch (Exception e) — only IOException is caught.
  • No throw new RuntimeException(...) / IllegalStateException(...).
  • Logged exceptions passed as the last logger argument.
  • [/] New BibEntry objects built with withers — none built in production code.
  • Modern Java used (List.of(), records, text blocks in tests).
  • [/] Regexes precompiled — no production regex added.
  • Background work rides the existing single-threaded sync executor; no new Thread().
  • No commented-out code, no trivial comments, no AI-disclosure comments.
  • Markdown Javadoc (///) with Markdown syntax ([SemanticMergeAnalyzer], backticks).
  • [/] All user-facing text localized — no new user-facing strings (log messages only; the conflict dialog is the existing localized one).
  • [/] Sentence case / no trailing ! / no : labels — no new labels.
  • [/] Variance via placeholders — no new user-facing strings.
  • [/] HTML-escaping of user-controlled data — no HTML output involved.
  • Behavior changes in org.jabref.logic have added tests — six new DirectoryLibrarySynchronizerTest cases (mirror creation, live edit, addition, deletion, cancelled conflict, adoption).
  • Tests assert contents with assertEquals, plain JUnit, no @DisplayName, no caught exceptions, @TempDir.
  • ./gradlew :jablib:test for the touched packages + full directorylibrary suite green locally (full check left to CI).
  • ./gradlew checkstyleMain checkstyleTest green for touched modules.
  • ./gradlew modernizer green.
  • ./gradlew :rewriteRun applied (no diff).
  • ./gradlew javadoc green.
  • npx markdownlint-cli2 green on the changed Markdown files.
  • [/] Docker IntelliJ formatter — not available on this machine; CI format job is the backstop.

Checklist

  • I own the copyright of the code submitted and I license it under the MIT license
  • If AI tools were used, I disclosed them in the "AI usage" section and reviewed, understood, and take full ownership of all AI-generated code
  • I manually tested my changes in running JabRef (opened a directory library on the X display: mirror + base appeared on open; an external title edit updated the entry and pattern-renamed the sidecar pair; an externally appended @Article appeared in the table and materialized its Markdown sidecar)
  • 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 added one sentence (max 20 words) to CHANGELOG.md describing the change from the user's point of view (if the change is visible to the user)
  • I checked the user documentation for up to dateness and submitted a pull request to our user documentation repository — the docs PR is the next phase-5 step, covering the whole feature

🤖 Generated with Claude Code

A directory library is continuously written to <root>/<root-name>.bib
(same debounce as the sidecar write-back), so plain BibTeX consumers
can read and edit the library as one file. External edits of the
mirror - live or while JabRef was closed - are three-way merged into
the library with the git-sync semantic merge: base is the mirror as
last written (kept under .jabref/mirror-base.bib), auto-mergeable
changes apply as local changes so the regular write-back persists them
into the sidecars, conflicts go through the git conflict resolution
dialog, and a cancelled resolution keeps the library's state. A
pre-existing .bib without a recorded base is adopted against an empty
base, which can only add entries or conflict, never delete.

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

koppor commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

This also contributes to foambubble/foam#241

koppor and others added 8 commits September 3, 2026 03:25
Resync layer 7 of the stack (no conflicts; the git-sync merge API this
layer uses still compiles against current upstream).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDcHNMt9fPWnpYaHheFvry
…the mirror

The mirror is serialized on the UI thread and written on the sync thread (no concurrent walk of the live model), tracked like any other pending file so failed writes are reported, and the UI thread no longer contends for the synchronizer's monitor on every change. The mirror carries no machine-specific file directory: a directory library resolves links against its root. The mirror uses the library's save order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vr3E1Gg5DRU4LQDDVnhPys
Fixes the CI format check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDcHNMt9fPWnpYaHheFvry
Resolve the OpenDirectoryLibraryAction import overlap; no logic change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDcHNMt9fPWnpYaHheFvry
Upstream added a 0071 ADR after this stack claimed the number, so the
MADR duplicate-ID check failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant